From 9876313a88b45163b345cd578e83228a4096053f Mon Sep 17 00:00:00 2001 From: Tejashri Majage Date: Wed, 19 Oct 2022 18:52:40 +0530 Subject: [PATCH] Joomla update to version 4.2.3 --- administrator/logs/joomla_update.php | 4 + .../components/com_actionlogs/actionlogs.xml | 2 +- .../components/com_actionlogs/config.xml | 1 + .../com_actionlogs/services/provider.php | 46 +- .../src/Controller/ActionlogsController.php | 272 +- .../src/Controller/DisplayController.php | 21 +- .../src/Field/ExtensionField.php | 83 +- .../src/Field/LogcreatorField.php | 102 +- .../src/Field/LogsdaterangeField.php | 83 +- .../com_actionlogs/src/Field/LogtypeField.php | 74 +- .../src/Field/PlugininfoField.php | 82 +- .../src/Helper/ActionlogsHelper.php | 671 +- .../src/Model/ActionlogModel.php | 291 +- .../src/Model/ActionlogsModel.php | 755 +- .../src/Plugin/ActionLogPlugin.php | 157 +- .../tmpl/actionlogs/default.php | 1 + .../components/com_admin/admin.xml | 2 +- .../com_admin/postinstall/addnosniff.php | 7 +- .../com_admin/postinstall/behindproxy.php | 75 +- .../com_admin/postinstall/htaccesssvg.php | 7 +- .../postinstall/languageaccess340.php | 36 +- .../com_admin/postinstall/statscollection.php | 7 +- .../com_admin/postinstall/textfilter3919.php | 7 +- .../components/com_admin/script.php | 17427 ++++++++-------- .../com_admin/services/provider.php | 48 +- .../sql/updates/mysql/4.0.0-2018-03-05.sql | 37 +- .../sql/updates/mysql/4.0.0-2018-05-15.sql | 17 +- .../sql/updates/mysql/4.0.0-2018-07-29.sql | 70 +- .../sql/updates/mysql/4.0.0-2018-08-29.sql | 4 +- .../sql/updates/mysql/4.0.0-2019-03-09.sql | 5 +- .../sql/updates/mysql/4.0.0-2019-03-30.sql | 6 +- .../sql/updates/mysql/4.0.0-2019-05-20.sql | 4 +- .../sql/updates/mysql/4.0.0-2019-09-13.sql | 24 +- .../sql/updates/mysql/4.0.0-2020-03-25.sql | 4 +- .../sql/updates/mysql/4.0.0-2020-05-29.sql | 14 +- .../sql/updates/mysql/4.0.0-2020-12-20.sql | 4 +- .../sql/updates/mysql/4.0.0-2021-04-22.sql | 8 +- .../sql/updates/mysql/4.0.0-2021-05-30.sql | 4 +- .../sql/updates/mysql/4.1.0-2021-11-20.sql | 4 +- .../sql/updates/mysql/4.1.0-2022-01-24.sql | 6 +- .../sql/updates/mysql/4.2.0-2022-05-15.sql | 57 + .../sql/updates/mysql/4.2.0-2022-06-15.sql | 4 + .../sql/updates/mysql/4.2.0-2022-06-19.sql | 3 + .../sql/updates/mysql/4.2.0-2022-06-22.sql | 9 + .../sql/updates/mysql/4.2.0-2022-07-07.sql | 4 + .../sql/updates/mysql/4.2.1-2022-08-23.sql | 3 + .../sql/updates/mysql/4.2.3-2022-09-07.sql | 2 + .../updates/postgresql/4.0.0-2018-03-05.sql | 37 +- .../updates/postgresql/4.0.0-2018-05-15.sql | 81 +- .../updates/postgresql/4.0.0-2018-07-19.sql | 6 +- .../updates/postgresql/4.0.0-2018-07-29.sql | 102 +- .../updates/postgresql/4.0.0-2019-03-09.sql | 12 +- .../updates/postgresql/4.0.0-2019-03-30.sql | 6 +- .../updates/postgresql/4.0.0-2019-05-20.sql | 4 +- .../updates/postgresql/4.0.0-2019-07-13.sql | 4 +- .../updates/postgresql/4.0.0-2019-09-13.sql | 25 +- .../updates/postgresql/4.0.0-2020-03-25.sql | 4 +- .../updates/postgresql/4.0.0-2020-05-29.sql | 21 +- .../updates/postgresql/4.0.0-2020-12-20.sql | 4 +- .../updates/postgresql/4.0.0-2021-04-22.sql | 8 +- .../updates/postgresql/4.0.0-2021-05-30.sql | 4 +- .../updates/postgresql/4.1.0-2021-11-20.sql | 23 +- .../updates/postgresql/4.1.0-2022-01-24.sql | 4 +- .../updates/postgresql/4.2.0-2022-05-15.sql | 63 + .../updates/postgresql/4.2.0-2022-06-19.sql | 3 + .../updates/postgresql/4.2.0-2022-06-22.sql | 9 + .../updates/postgresql/4.2.0-2022-07-07.sql | 4 + .../updates/postgresql/4.2.1-2022-08-23.sql | 3 + .../updates/postgresql/4.2.3-2022-09-07.sql | 2 + .../src/Controller/DisplayController.php | 50 +- .../com_admin/src/Dispatcher/Dispatcher.php | 23 +- .../src/Extension/AdminComponent.php | 49 +- .../com_admin/src/Model/HelpModel.php | 336 +- .../com_admin/src/Model/SysinfoModel.php | 1418 +- .../com_admin/src/Service/HTML/Directory.php | 79 +- .../com_admin/src/Service/HTML/PhpSetting.php | 73 +- .../com_admin/src/Service/HTML/System.php | 29 +- .../com_admin/src/View/Help/HtmlView.php | 129 +- .../com_admin/src/View/Sysinfo/HtmlView.php | 177 +- .../com_admin/src/View/Sysinfo/JsonView.php | 96 +- .../com_admin/src/View/Sysinfo/TextView.php | 314 +- .../com_admin/tmpl/help/default.php | 55 +- .../com_admin/tmpl/help/langforum.php | 6 +- .../com_admin/tmpl/sysinfo/default.php | 35 +- .../com_admin/tmpl/sysinfo/default_config.php | 55 +- .../tmpl/sysinfo/default_directory.php | 55 +- .../tmpl/sysinfo/default_phpinfo.php | 3 +- .../tmpl/sysinfo/default_phpsettings.php | 355 +- .../com_admin/tmpl/sysinfo/default_system.php | 227 +- .../components/com_ajax/ajax.php | 1 + .../components/com_ajax/ajax.xml | 2 +- .../com_associations/associations.xml | 2 +- .../layouts/joomla/searchtools/default.php | 158 +- .../com_associations/services/provider.php | 46 +- .../src/Controller/AssociationController.php | 133 +- .../src/Controller/AssociationsController.php | 232 +- .../src/Controller/DisplayController.php | 23 +- .../src/Dispatcher/Dispatcher.php | 72 +- .../src/Field/ItemlanguageField.php | 160 +- .../src/Field/ItemtypeField.php | 78 +- .../src/Field/Modal/AssociationField.php | 167 +- .../src/Helper/AssociationsHelper.php | 1322 +- .../src/Model/AssociationModel.php | 39 +- .../src/Model/AssociationsModel.php | 1129 +- .../src/View/Association/HtmlView.php | 706 +- .../src/View/Associations/HtmlView.php | 426 +- .../tmpl/association/edit.php | 115 +- .../tmpl/associations/default.php | 270 +- .../tmpl/associations/modal.php | 254 +- .../components/com_banners/banners.xml | 2 +- .../com_banners/forms/filter_tracks.xml | 2 + .../com_banners/helpers/banners.php | 8 +- .../com_banners/services/provider.php | 56 +- .../src/Controller/BannerController.php | 174 +- .../src/Controller/BannersController.php | 164 +- .../src/Controller/ClientController.php | 23 +- .../src/Controller/ClientsController.php | 51 +- .../src/Controller/DisplayController.php | 94 +- .../src/Controller/TracksController.php | 295 +- .../src/Extension/BannersComponent.php | 90 +- .../src/Field/BannerclientField.php | 43 +- .../com_banners/src/Field/ClicksField.php | 51 +- .../com_banners/src/Field/ImpmadeField.php | 51 +- .../com_banners/src/Field/ImptotalField.php | 63 +- .../com_banners/src/Helper/BannersHelper.php | 306 +- .../com_banners/src/Model/BannerModel.php | 861 +- .../com_banners/src/Model/BannersModel.php | 488 +- .../com_banners/src/Model/ClientModel.php | 230 +- .../com_banners/src/Model/ClientsModel.php | 568 +- .../com_banners/src/Model/DownloadModel.php | 120 +- .../com_banners/src/Model/TracksModel.php | 988 +- .../com_banners/src/Service/Html/Banner.php | 191 +- .../com_banners/src/Table/BannerTable.php | 695 +- .../com_banners/src/Table/ClientTable.php | 152 +- .../com_banners/src/View/Banner/HtmlView.php | 235 +- .../com_banners/src/View/Banners/HtmlView.php | 372 +- .../com_banners/src/View/Client/HtmlView.php | 255 +- .../com_banners/src/View/Clients/HtmlView.php | 293 +- .../src/View/Download/HtmlView.php | 72 +- .../com_banners/src/View/Tracks/HtmlView.php | 255 +- .../com_banners/src/View/Tracks/RawView.php | 74 +- .../com_banners/tmpl/banner/edit.php | 115 +- .../com_banners/tmpl/banners/default.php | 331 +- .../tmpl/banners/default_batch_body.php | 47 +- .../tmpl/banners/default_batch_footer.php | 5 +- .../com_banners/tmpl/banners/emptystate.php | 14 +- .../com_banners/tmpl/client/edit.php | 75 +- .../com_banners/tmpl/clients/default.php | 310 +- .../com_banners/tmpl/clients/emptystate.php | 14 +- .../com_banners/tmpl/download/default.php | 35 +- .../com_banners/tmpl/tracks/default.php | 143 +- .../com_banners/tmpl/tracks/emptystate.php | 7 +- .../components/com_cache/cache.xml | 2 +- .../com_cache/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 278 +- .../com_cache/src/Model/CacheModel.php | 521 +- .../com_cache/src/View/Cache/HtmlView.php | 2 +- .../com_cache/tmpl/cache/default.php | 1 + .../components/com_categories/categories.xml | 2 +- .../com_categories/forms/category.xml | 1 - .../com_categories/helpers/categories.php | 7 +- .../joomla/form/field/categoryedit.php | 117 +- .../com_categories/services/provider.php | 48 +- .../src/Controller/AjaxController.php | 129 +- .../src/Controller/CategoriesController.php | 174 +- .../src/Controller/CategoryController.php | 446 +- .../src/Controller/DisplayController.php | 181 +- .../src/Dispatcher/Dispatcher.php | 36 +- .../src/Extension/CategoriesComponent.php | 43 +- .../src/Field/CategoryeditField.php | 725 +- .../src/Field/ComponentsCategoryField.php | 135 +- .../src/Field/Modal/CategoryField.php | 571 +- .../src/Helper/CategoriesHelper.php | 166 +- .../src/Helper/CategoryAssociationHelper.php | 78 +- .../src/Model/CategoriesModel.php | 913 +- .../src/Model/CategoryModel.php | 2605 ++- .../src/Service/HTML/AdministratorService.php | 152 +- .../src/Table/CategoryTable.php | 31 +- .../src/View/Categories/HtmlView.php | 595 +- .../src/View/Category/HtmlView.php | 532 +- .../tmpl/categories/default.php | 529 +- .../tmpl/categories/default_batch_body.php | 86 +- .../tmpl/categories/default_batch_footer.php | 6 +- .../tmpl/categories/emptystate.php | 34 +- .../com_categories/tmpl/categories/modal.php | 206 +- .../com_categories/tmpl/category/edit.php | 162 +- .../com_categories/tmpl/category/modal.php | 5 +- .../components/com_checkin/checkin.xml | 2 +- .../com_checkin/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 198 +- .../com_checkin/src/Model/CheckinModel.php | 455 +- .../com_checkin/src/View/Checkin/HtmlView.php | 215 +- .../com_checkin/tmpl/checkin/default.php | 103 +- .../com_checkin/tmpl/checkin/emptystate.php | 9 +- .../components/com_config/config.xml | 2 +- .../com_config/forms/application.xml | 14 + .../com_config/services/provider.php | 50 +- .../src/Controller/ApplicationController.php | 562 +- .../src/Controller/ComponentController.php | 379 +- .../src/Controller/DisplayController.php | 80 +- .../src/Controller/RequestController.php | 157 +- .../src/Extension/ConfigComponent.php | 9 +- .../src/Field/ConfigComponentsField.php | 102 +- .../com_config/src/Field/FiltersField.php | 301 +- .../com_config/src/Helper/ConfigHelper.php | 219 +- .../com_config/src/Model/ApplicationModel.php | 2560 ++- .../com_config/src/Model/ComponentModel.php | 431 +- .../src/View/Application/HtmlView.php | 193 +- .../src/View/Component/HtmlView.php | 239 +- .../com_config/tmpl/application/default.php | 95 +- .../tmpl/application/default_cache.php | 1 + .../tmpl/application/default_cookie.php | 1 + .../tmpl/application/default_database.php | 1 + .../tmpl/application/default_debug.php | 1 + .../tmpl/application/default_filters.php | 1 + .../tmpl/application/default_locale.php | 1 + .../tmpl/application/default_logging.php | 1 + .../application/default_logging_custom.php | 1 + .../tmpl/application/default_mail.php | 11 +- .../tmpl/application/default_metadata.php | 1 + .../tmpl/application/default_navigation.php | 27 +- .../tmpl/application/default_permissions.php | 1 + .../tmpl/application/default_proxy.php | 1 + .../tmpl/application/default_seo.php | 1 + .../tmpl/application/default_server.php | 1 + .../tmpl/application/default_session.php | 1 + .../tmpl/application/default_site.php | 1 + .../tmpl/application/default_webservices.php | 1 + .../com_config/tmpl/component/default.php | 215 +- .../tmpl/component/default_navigation.php | 36 +- .../components/com_contact/contact.xml | 2 +- .../com_contact/helpers/contact.php | 7 +- .../com_contact/services/provider.php | 60 +- .../src/Controller/AjaxController.php | 103 +- .../src/Controller/ContactController.php | 255 +- .../src/Controller/ContactsController.php | 203 +- .../src/Controller/DisplayController.php | 75 +- .../src/Extension/ContactComponent.php | 210 +- .../src/Field/Modal/ContactField.php | 564 +- .../src/Helper/AssociationsHelper.php | 373 +- .../com_contact/src/Helper/ContactHelper.php | 7 +- .../com_contact/src/Model/ContactModel.php | 1002 +- .../com_contact/src/Model/ContactsModel.php | 687 +- .../src/Service/HTML/AdministratorService.php | 240 +- .../com_contact/src/Service/HTML/Icon.php | 271 +- .../com_contact/src/Table/ContactTable.php | 479 +- .../com_contact/src/View/Contact/HtmlView.php | 327 +- .../src/View/Contacts/HtmlView.php | 386 +- .../com_contact/tmpl/contact/edit.php | 186 +- .../com_contact/tmpl/contact/modal.php | 5 +- .../com_contact/tmpl/contacts/default.php | 345 +- .../tmpl/contacts/default_batch_body.php | 68 +- .../tmpl/contacts/default_batch_footer.php | 6 +- .../com_contact/tmpl/contacts/emptystate.php | 14 +- .../com_contact/tmpl/contacts/modal.php | 226 +- .../components/com_content/content.xml | 2 +- .../com_content/helpers/content.php | 7 +- .../com_content/services/provider.php | 60 +- .../src/Controller/AjaxController.php | 103 +- .../src/Controller/ArticleController.php | 309 +- .../src/Controller/ArticlesController.php | 270 +- .../src/Controller/DisplayController.php | 75 +- .../src/Controller/FeaturedController.php | 144 +- .../src/Event/Model/FeatureEvent.php | 118 +- .../src/Extension/ContentComponent.php | 618 +- .../com_content/src/Field/AssocField.php | 64 +- .../src/Field/Modal/ArticleField.php | 556 +- .../com_content/src/Field/VotelistField.php | 64 +- .../com_content/src/Field/VoteradioField.php | 64 +- .../src/Helper/AssociationsHelper.php | 368 +- .../com_content/src/Helper/ContentHelper.php | 319 +- .../com_content/src/Model/ArticleModel.php | 2213 +- .../com_content/src/Model/ArticlesModel.php | 1316 +- .../com_content/src/Model/FeatureModel.php | 59 +- .../com_content/src/Model/FeaturedModel.php | 150 +- .../src/Service/HTML/AdministratorService.php | 148 +- .../com_content/src/Service/HTML/Icon.php | 257 +- .../com_content/src/Table/ArticleTable.php | 3 + .../com_content/src/Table/FeaturedTable.php | 29 +- .../com_content/src/View/Article/HtmlView.php | 415 +- .../src/View/Articles/HtmlView.php | 446 +- .../src/View/Featured/HtmlView.php | 375 +- .../com_content/tmpl/article/edit.php | 267 +- .../com_content/tmpl/article/modal.php | 5 +- .../com_content/tmpl/article/pagebreak.php | 47 +- .../com_content/tmpl/articles/default.php | 700 +- .../tmpl/articles/default_batch_body.php | 72 +- .../tmpl/articles/default_batch_footer.php | 6 +- .../com_content/tmpl/articles/emptystate.php | 14 +- .../com_content/tmpl/articles/modal.php | 254 +- .../com_content/tmpl/featured/default.php | 691 +- .../tmpl/featured/default_stage_body.php | 16 +- .../tmpl/featured/default_stage_footer.php | 6 +- .../com_content/tmpl/featured/emptystate.php | 12 +- .../com_contenthistory/contenthistory.xml | 2 +- .../helpers/contenthistory.php | 7 +- .../com_contenthistory/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 8 +- .../src/Controller/HistoryController.php | 132 +- .../src/Controller/PreviewController.php | 37 +- .../src/Dispatcher/Dispatcher.php | 40 +- .../src/Helper/ContenthistoryHelper.php | 698 +- .../src/Model/CompareModel.php | 316 +- .../src/Model/HistoryModel.php | 762 +- .../src/Model/PreviewModel.php | 243 +- .../src/View/Compare/HtmlView.php | 78 +- .../src/View/History/HtmlView.php | 226 +- .../src/View/Preview/HtmlView.php | 79 +- .../tmpl/compare/compare.php | 101 +- .../com_contenthistory/tmpl/history/modal.php | 159 +- .../tmpl/preview/preview.php | 88 +- .../components/com_cpanel/cpanel.xml | 2 +- .../com_cpanel/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 115 +- .../com_cpanel/src/Dispatcher/Dispatcher.php | 31 +- .../com_cpanel/src/View/Cpanel/HtmlView.php | 288 +- .../com_cpanel/tmpl/cpanel/default.php | 81 +- .../components/com_fields/fields.xml | 2 +- .../components/com_fields/helpers/fields.php | 8 +- .../com_fields/services/provider.php | 50 +- .../src/Controller/DisplayController.php | 83 +- .../src/Controller/FieldController.php | 340 +- .../src/Controller/FieldsController.php | 53 +- .../src/Controller/GroupController.php | 353 +- .../src/Controller/GroupsController.php | 53 +- .../com_fields/src/Dispatcher/Dispatcher.php | 48 +- .../src/Extension/FieldsComponent.php | 35 +- .../src/Field/ComponentsFieldgroupField.php | 188 +- .../src/Field/ComponentsFieldsField.php | 188 +- .../com_fields/src/Field/FieldLayoutField.php | 286 +- .../src/Field/FieldcontextsField.php | 76 +- .../com_fields/src/Field/FieldgroupsField.php | 99 +- .../com_fields/src/Field/SectionField.php | 87 +- .../com_fields/src/Field/SubfieldsField.php | 207 +- .../com_fields/src/Field/TypeField.php | 109 +- .../com_fields/src/Helper/FieldsHelper.php | 1369 +- .../com_fields/src/Model/FieldModel.php | 2377 +-- .../com_fields/src/Model/FieldsModel.php | 908 +- .../com_fields/src/Model/GroupModel.php | 711 +- .../com_fields/src/Model/GroupsModel.php | 444 +- .../src/Plugin/FieldsListPlugin.php | 104 +- .../com_fields/src/Plugin/FieldsPlugin.php | 588 +- .../com_fields/src/Table/FieldTable.php | 602 +- .../com_fields/src/Table/GroupTable.php | 385 +- .../com_fields/src/View/Field/HtmlView.php | 283 +- .../com_fields/src/View/Fields/HtmlView.php | 346 +- .../com_fields/src/View/Group/HtmlView.php | 333 +- .../com_fields/src/View/Groups/HtmlView.php | 350 +- .../components/com_fields/tmpl/field/edit.php | 135 +- .../com_fields/tmpl/fields/default.php | 343 +- .../tmpl/fields/default_batch_body.php | 80 +- .../tmpl/fields/default_batch_footer.php | 6 +- .../com_fields/tmpl/fields/modal.php | 175 +- .../components/com_fields/tmpl/group/edit.php | 109 +- .../com_fields/tmpl/groups/default.php | 283 +- .../tmpl/groups/default_batch_body.php | 30 +- .../tmpl/groups/default_batch_footer.php | 6 +- .../components/com_finder/config.xml | 20 +- .../components/com_finder/finder.xml | 2 +- .../com_finder/helpers/indexer/adapter.php | 1 + .../com_finder/helpers/indexer/helper.php | 1 + .../com_finder/helpers/indexer/parser.php | 1 + .../com_finder/helpers/indexer/query.php | 1 + .../com_finder/helpers/indexer/result.php | 1 + .../com_finder/helpers/indexer/taxonomy.php | 1 + .../com_finder/helpers/indexer/token.php | 1 + .../com_finder/helpers/language.php | 9 +- .../com_finder/services/provider.php | 52 +- .../src/Controller/DisplayController.php | 103 +- .../src/Controller/FilterController.php | 424 +- .../src/Controller/FiltersController.php | 51 +- .../src/Controller/IndexController.php | 133 +- .../src/Controller/IndexerController.php | 539 +- .../src/Controller/MapsController.php | 51 +- .../src/Controller/SearchesController.php | 44 +- .../src/Extension/FinderComponent.php | 60 +- .../com_finder/src/Field/BranchesField.php | 45 +- .../com_finder/src/Field/ContentmapField.php | 213 +- .../src/Field/ContenttypesField.php | 102 +- .../src/Field/SearchfilterField.php | 70 +- .../com_finder/src/Helper/FinderHelper.php | 45 +- .../com_finder/src/Helper/LanguageHelper.php | 260 +- .../com_finder/src/Indexer/Adapter.php | 1773 +- .../com_finder/src/Indexer/Helper.php | 861 +- .../com_finder/src/Indexer/Indexer.php | 1966 +- .../com_finder/src/Indexer/Language.php | 312 +- .../com_finder/src/Indexer/Language/El.php | 1907 +- .../com_finder/src/Indexer/Language/Zh.php | 116 +- .../com_finder/src/Indexer/Parser.php | 208 +- .../com_finder/src/Indexer/Parser/Html.php | 275 +- .../com_finder/src/Indexer/Parser/Rtf.php | 45 +- .../com_finder/src/Indexer/Parser/Txt.php | 33 +- .../com_finder/src/Indexer/Query.php | 2692 ++- .../com_finder/src/Indexer/Result.php | 1101 +- .../com_finder/src/Indexer/Taxonomy.php | 957 +- .../com_finder/src/Indexer/Token.php | 315 +- .../com_finder/src/Model/FilterModel.php | 261 +- .../com_finder/src/Model/FiltersModel.php | 236 +- .../com_finder/src/Model/IndexModel.php | 903 +- .../com_finder/src/Model/IndexerModel.php | 7 +- .../com_finder/src/Model/MapsModel.php | 770 +- .../com_finder/src/Model/SearchesModel.php | 306 +- .../com_finder/src/Model/StatisticsModel.php | 108 +- .../com_finder/src/Response/Response.php | 119 +- .../com_finder/src/Service/HTML/Filter.php | 934 +- .../com_finder/src/Service/HTML/Finder.php | 215 +- .../com_finder/src/Service/HTML/Query.php | 277 +- .../com_finder/src/Table/FilterTable.php | 296 +- .../com_finder/src/Table/LinkTable.php | 68 +- .../com_finder/src/Table/MapTable.php | 92 +- .../com_finder/src/View/Filter/HtmlView.php | 301 +- .../com_finder/src/View/Filters/HtmlView.php | 306 +- .../com_finder/src/View/Index/HtmlView.php | 400 +- .../com_finder/src/View/Indexer/HtmlView.php | 8 +- .../com_finder/src/View/Maps/HtmlView.php | 303 +- .../com_finder/src/View/Searches/HtmlView.php | 272 +- .../src/View/Statistics/HtmlView.php | 68 +- .../com_finder/tmpl/filter/edit.php | 117 +- .../com_finder/tmpl/filters/default.php | 198 +- .../com_finder/tmpl/filters/emptystate.php | 20 +- .../com_finder/tmpl/index/default.php | 268 +- .../com_finder/tmpl/index/emptystate.php | 63 +- .../com_finder/tmpl/indexer/default.php | 25 +- .../com_finder/tmpl/maps/default.php | 279 +- .../com_finder/tmpl/maps/emptystate.php | 11 +- .../com_finder/tmpl/searches/default.php | 109 +- .../com_finder/tmpl/searches/emptystate.php | 13 +- .../com_finder/tmpl/statistics/default.php | 75 +- .../com_installer/forms/filter_manage.xml | 12 + .../com_installer/helpers/installer.php | 8 +- .../components/com_installer/installer.xml | 2 +- .../com_installer/services/provider.php | 48 +- .../src/Controller/DatabaseController.php | 153 +- .../src/Controller/DiscoverController.php | 135 +- .../src/Controller/DisplayController.php | 149 +- .../src/Controller/InstallController.php | 180 +- .../src/Controller/ManageController.php | 307 +- .../src/Controller/UpdateController.php | 365 +- .../src/Controller/UpdatesiteController.php | 7 +- .../src/Controller/UpdatesitesController.php | 274 +- .../src/Extension/InstallerComponent.php | 47 +- .../src/Field/ExtensionstatusField.php | 45 +- .../com_installer/src/Field/FolderField.php | 45 +- .../com_installer/src/Field/LocationField.php | 45 +- .../com_installer/src/Field/PackageField.php | 41 + .../com_installer/src/Field/TypeField.php | 45 +- .../src/Helper/InstallerHelper.php | 884 +- .../com_installer/src/Model/DatabaseModel.php | 1314 +- .../com_installer/src/Model/DiscoverModel.php | 567 +- .../com_installer/src/Model/InstallModel.php | 780 +- .../src/Model/InstallerModel.php | 406 +- .../src/Model/LanguagesModel.php | 492 +- .../com_installer/src/Model/ManageModel.php | 874 +- .../com_installer/src/Model/UpdateModel.php | 1198 +- .../src/Model/UpdatesiteModel.php | 293 +- .../src/Model/UpdatesitesModel.php | 1290 +- .../com_installer/src/Model/WarningsModel.php | 334 +- .../com_installer/src/Service/HTML/Manage.php | 101 +- .../src/Service/HTML/Updatesites.php | 81 +- .../src/Table/UpdatesiteTable.php | 47 +- .../src/View/Database/HtmlView.php | 205 +- .../src/View/Discover/HtmlView.php | 146 +- .../src/View/Install/HtmlView.php | 81 +- .../src/View/Installer/HtmlView.php | 149 +- .../src/View/Languages/HtmlView.php | 102 +- .../src/View/Manage/HtmlView.php | 193 +- .../src/View/Update/HtmlView.php | 216 +- .../src/View/Updatesite/HtmlView.php | 192 +- .../src/View/Updatesites/HtmlView.php | 217 +- .../src/View/Warnings/HtmlView.php | 74 +- .../com_installer/tmpl/database/default.php | 212 +- .../com_installer/tmpl/discover/default.php | 201 +- .../tmpl/discover/emptystate.php | 17 +- .../com_installer/tmpl/install/default.php | 71 +- .../tmpl/installer/default_message.php | 13 +- .../com_installer/tmpl/languages/default.php | 198 +- .../com_installer/tmpl/manage/default.php | 298 +- .../com_installer/tmpl/update/default.php | 267 +- .../com_installer/tmpl/update/emptystate.php | 14 +- .../com_installer/tmpl/updatesite/edit.php | 7 +- .../tmpl/updatesites/default.php | 269 +- .../com_installer/tmpl/warnings/default.php | 77 +- .../tmpl/warnings/emptystate.php | 9 +- .../components/com_joomlaupdate/config.xml | 24 + .../components/com_joomlaupdate/extract.php | 3804 ++-- .../com_joomlaupdate/finalisation.php | 442 +- .../com_joomlaupdate/joomlaupdate.xml | 2 +- .../com_joomlaupdate/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 169 +- .../src/Controller/UpdateController.php | 1234 +- .../src/Dispatcher/Dispatcher.php | 32 +- .../src/Model/UpdateModel.php | 3458 ++- .../src/View/Joomlaupdate/HtmlView.php | 573 +- .../src/View/Update/HtmlView.php | 37 +- .../src/View/Upload/HtmlView.php | 166 +- .../tmpl/joomlaupdate/complete.php | 19 +- .../tmpl/joomlaupdate/noupdate.php | 37 +- .../tmpl/joomlaupdate/preupdatecheck.php | 604 +- .../tmpl/joomlaupdate/reinstall.php | 38 +- .../tmpl/joomlaupdate/selfupdate.php | 12 +- .../tmpl/joomlaupdate/update.php | 46 +- .../com_joomlaupdate/tmpl/update/default.php | 155 +- .../tmpl/update/finaliseconfirm.php | 118 +- .../com_joomlaupdate/tmpl/upload/captive.php | 118 +- .../com_joomlaupdate/tmpl/upload/default.php | 115 +- .../components/com_languages/languages.xml | 2 +- .../com_languages/services/provider.php | 48 +- .../src/Controller/DisplayController.php | 81 +- .../src/Controller/InstalledController.php | 176 +- .../src/Controller/LanguageController.php | 35 +- .../src/Controller/LanguagesController.php | 37 +- .../src/Controller/OverrideController.php | 355 +- .../src/Controller/OverridesController.php | 117 +- .../src/Controller/StringsController.php | 47 +- .../src/Extension/LanguagesComponent.php | 43 +- .../src/Field/LanguageclientField.php | 98 +- .../src/Helper/LanguagesHelper.php | 73 +- .../src/Helper/MultilangstatusHelper.php | 659 +- .../src/Model/InstalledModel.php | 741 +- .../com_languages/src/Model/LanguageModel.php | 511 +- .../src/Model/LanguagesModel.php | 423 +- .../com_languages/src/Model/OverrideModel.php | 397 +- .../src/Model/OverridesModel.php | 479 +- .../com_languages/src/Model/StringsModel.php | 323 +- .../src/Service/HTML/Languages.php | 132 +- .../src/View/Installed/HtmlView.php | 244 +- .../src/View/Language/HtmlView.php | 208 +- .../src/View/Languages/HtmlView.php | 257 +- .../src/View/Multilangstatus/HtmlView.php | 49 +- .../src/View/Override/HtmlView.php | 243 +- .../src/View/Overrides/HtmlView.php | 197 +- .../com_languages/tmpl/installed/default.php | 227 +- .../com_languages/tmpl/language/edit.php | 109 +- .../com_languages/tmpl/languages/default.php | 277 +- .../tmpl/multilangstatus/default.php | 621 +- .../com_languages/tmpl/override/edit.php | 125 +- .../com_languages/tmpl/overrides/default.php | 169 +- .../components/com_login/login.xml | 2 +- .../com_login/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 216 +- .../com_login/src/Dispatcher/Dispatcher.php | 68 +- .../com_login/src/Model/LoginModel.php | 361 +- .../com_login/src/View/Login/HtmlView.php | 8 +- .../com_login/tmpl/login/default.php | 8 +- .../components/com_mails/mails.xml | 2 +- .../com_mails/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 78 +- .../src/Controller/TemplateController.php | 544 +- .../com_mails/src/Helper/MailsHelper.php | 179 +- .../com_mails/src/Model/TemplateModel.php | 779 +- .../com_mails/src/Model/TemplatesModel.php | 397 +- .../com_mails/src/Table/TemplateTable.php | 43 +- .../com_mails/src/View/Template/HtmlView.php | 260 +- .../com_mails/src/View/Templates/HtmlView.php | 238 +- .../com_mails/tmpl/template/edit.php | 165 +- .../com_mails/tmpl/templates/default.php | 185 +- .../components/com_media/config.xml | 15 +- .../components/com_media/helpers/media.php | 65 +- .../layouts/toolbar/create-folder.php | 11 +- .../com_media/layouts/toolbar/delete.php | 11 +- .../com_media/layouts/toolbar/upload.php | 11 +- .../components/com_media/media.xml | 2 +- .../com_media/services/provider.php | 46 +- .../src/Adapter/AdapterInterface.php | 363 +- .../src/Controller/ApiController.php | 754 +- .../src/Controller/DisplayController.php | 89 +- .../src/Controller/PluginController.php | 264 +- .../com_media/src/Dispatcher/Dispatcher.php | 52 +- .../AbstractMediaItemValidationEvent.php | 204 +- .../src/Event/FetchMediaItemEvent.php | 116 +- .../src/Event/FetchMediaItemUrlEvent.php | 148 +- .../src/Event/FetchMediaItemsEvent.php | 114 +- .../src/Event/MediaProviderEvent.php | 69 +- .../src/Event/OAuthCallbackEvent.php | 135 +- .../src/Exception/FileExistsException.php | 3 + .../src/Exception/FileNotFoundException.php | 3 + .../src/Exception/InvalidPathException.php | 3 + .../com_media/src/Model/ApiModel.php | 1045 +- .../com_media/src/Model/FileModel.php | 86 +- .../com_media/src/Model/MediaModel.php | 65 +- .../src/Plugin/MediaActionPlugin.php | 139 +- .../src/Provider/ProviderInterface.php | 55 +- .../src/Provider/ProviderManager.php | 224 +- .../Provider/ProviderManagerHelperTrait.php | 281 +- .../com_media/src/View/File/HtmlView.php | 87 +- .../com_media/src/View/Media/HtmlView.php | 2 +- .../com_media/tmpl/file/default.php | 47 +- .../com_media/tmpl/media/default.php | 19 +- .../components/com_menus/forms/item.xml | 1 + .../components/com_menus/helpers/menus.php | 7 +- .../layouts/joomla/menu/edit_modules.php | 61 +- .../layouts/joomla/searchtools/default.php | 156 +- .../components/com_menus/menus.xml | 2 +- .../com_menus/services/provider.php | 60 +- .../src/Controller/AjaxController.php | 105 +- .../src/Controller/DisplayController.php | 145 +- .../src/Controller/ItemController.php | 1149 +- .../src/Controller/ItemsController.php | 466 +- .../src/Controller/MenuController.php | 409 +- .../src/Controller/MenusController.php | 371 +- .../src/Extension/MenusComponent.php | 48 +- .../src/Field/MenuItemByTypeField.php | 500 +- .../com_menus/src/Field/MenuOrderingField.php | 207 +- .../com_menus/src/Field/MenuParentField.php | 193 +- .../com_menus/src/Field/MenuPresetField.php | 62 +- .../com_menus/src/Field/MenutypeField.php | 189 +- .../com_menus/src/Field/Modal/MenuField.php | 832 +- .../src/Helper/AssociationsHelper.php | 320 +- .../com_menus/src/Helper/MenusHelper.php | 1837 +- .../com_menus/src/Model/ItemModel.php | 3646 ++-- .../com_menus/src/Model/ItemsModel.php | 1175 +- .../com_menus/src/Model/MenuModel.php | 770 +- .../com_menus/src/Model/MenusModel.php | 544 +- .../com_menus/src/Model/MenutypesModel.php | 1180 +- .../com_menus/src/Service/HTML/Menus.php | 213 +- .../com_menus/src/Table/MenuTable.php | 133 +- .../com_menus/src/Table/MenuTypeTable.php | 4 +- .../com_menus/src/View/Item/HtmlView.php | 352 +- .../com_menus/src/View/Items/HtmlView.php | 741 +- .../com_menus/src/View/Menu/HtmlView.php | 229 +- .../com_menus/src/View/Menu/XmlView.php | 285 +- .../com_menus/src/View/Menus/HtmlView.php | 233 +- .../com_menus/src/View/Menutypes/HtmlView.php | 279 +- .../components/com_menus/tmpl/item/edit.php | 290 +- .../com_menus/tmpl/item/edit_container.php | 150 +- .../com_menus/tmpl/item/edit_modules.php | 226 +- .../components/com_menus/tmpl/item/modal.php | 5 +- .../com_menus/tmpl/items/default.php | 480 +- .../tmpl/items/default_batch_body.php | 119 +- .../tmpl/items/default_batch_footer.php | 12 +- .../components/com_menus/tmpl/items/modal.php | 299 +- .../components/com_menus/tmpl/menu/edit.php | 69 +- .../com_menus/tmpl/menus/default.php | 472 +- .../com_menus/tmpl/menutypes/default.php | 41 +- .../components/com_messages/messages.xml | 2 +- .../com_messages/services/provider.php | 48 +- .../src/Controller/ConfigController.php | 156 +- .../src/Controller/DisplayController.php | 75 +- .../src/Controller/MessageController.php | 78 +- .../src/Controller/MessagesController.php | 37 +- .../src/Extension/MessagesComponent.php | 43 +- .../src/Field/MessageStatesField.php | 45 +- .../src/Field/UserMessagesField.php | 120 +- .../src/Helper/MessagesHelper.php | 39 +- .../com_messages/src/Model/ConfigModel.php | 339 +- .../com_messages/src/Model/MessageModel.php | 983 +- .../com_messages/src/Model/MessagesModel.php | 344 +- .../src/Service/HTML/Messages.php | 72 +- .../com_messages/src/Table/MessageTable.php | 138 +- .../com_messages/src/View/Config/HtmlView.php | 140 +- .../src/View/Message/HtmlView.php | 155 +- .../src/View/Messages/HtmlView.php | 292 +- .../com_messages/tmpl/config/default.php | 31 +- .../com_messages/tmpl/message/default.php | 83 +- .../com_messages/tmpl/message/edit.php | 43 +- .../com_messages/tmpl/messages/default.php | 135 +- .../com_messages/tmpl/messages/emptystate.php | 18 +- .../com_modules/helpers/modules.php | 8 +- .../joomla/form/field/modulespositionedit.php | 41 +- .../layouts/toolbar/cancelselect.php | 7 +- .../components/com_modules/modules.xml | 2 +- .../com_modules/services/provider.php | 48 +- .../src/Controller/DisplayController.php | 162 +- .../src/Controller/ModuleController.php | 674 +- .../src/Controller/ModulesController.php | 185 +- .../src/Extension/ModulesComponent.php | 43 +- .../src/Field/ModulesModuleField.php | 182 +- .../src/Field/ModulesPositionField.php | 182 +- .../src/Field/ModulesPositioneditField.php | 236 +- .../com_modules/src/Helper/ModulesHelper.php | 588 +- .../com_modules/src/Model/ModuleModel.php | 2229 +- .../com_modules/src/Model/ModulesModel.php | 863 +- .../com_modules/src/Model/PositionsModel.php | 431 +- .../com_modules/src/Model/SelectModel.php | 274 +- .../com_modules/src/Service/HTML/Modules.php | 468 +- .../com_modules/src/View/Module/HtmlView.php | 280 +- .../com_modules/src/View/Modules/HtmlView.php | 453 +- .../com_modules/src/View/Select/HtmlView.php | 149 +- .../com_modules/tmpl/module/edit.php | 297 +- .../tmpl/module/edit_assignment.php | 251 +- .../com_modules/tmpl/module/modal.php | 5 +- .../com_modules/tmpl/modules/default.php | 367 +- .../tmpl/modules/default_batch_body.php | 89 +- .../tmpl/modules/default_batch_footer.php | 6 +- .../com_modules/tmpl/modules/emptystate.php | 18 +- .../com_modules/tmpl/modules/modal.php | 195 +- .../com_modules/tmpl/select/default.php | 91 +- .../com_modules/tmpl/select/modal.php | 5 +- .../com_newsfeeds/helpers/newsfeeds.php | 7 +- .../components/com_newsfeeds/newsfeeds.xml | 2 +- .../com_newsfeeds/services/provider.php | 60 +- .../src/Controller/AjaxController.php | 103 +- .../src/Controller/DisplayController.php | 75 +- .../src/Controller/NewsfeedController.php | 185 +- .../src/Controller/NewsfeedsController.php | 37 +- .../src/Extension/NewsfeedsComponent.php | 114 +- .../src/Field/Modal/NewsfeedField.php | 566 +- .../src/Field/NewsfeedsField.php | 86 +- .../src/Helper/AssociationsHelper.php | 371 +- .../src/Helper/NewsfeedsHelper.php | 305 +- .../com_newsfeeds/src/Model/NewsfeedModel.php | 826 +- .../src/Model/NewsfeedsModel.php | 608 +- .../src/Service/HTML/AdministratorService.php | 159 +- .../com_newsfeeds/src/Table/NewsfeedTable.php | 353 +- .../src/View/Newsfeed/HtmlView.php | 267 +- .../src/View/Newsfeeds/HtmlView.php | 339 +- .../com_newsfeeds/tmpl/newsfeed/edit.php | 159 +- .../tmpl/newsfeed/edit_display.php | 9 +- .../com_newsfeeds/tmpl/newsfeed/modal.php | 5 +- .../com_newsfeeds/tmpl/newsfeeds/default.php | 333 +- .../tmpl/newsfeeds/default_batch_body.php | 58 +- .../tmpl/newsfeeds/default_batch_footer.php | 5 +- .../tmpl/newsfeeds/emptystate.php | 14 +- .../com_newsfeeds/tmpl/newsfeeds/modal.php | 196 +- .../com_plugins/helpers/plugins.php | 8 +- .../components/com_plugins/plugins.xml | 2 +- .../com_plugins/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 75 +- .../src/Controller/PluginController.php | 8 +- .../src/Controller/PluginsController.php | 73 +- .../src/Field/PluginElementField.php | 45 +- .../com_plugins/src/Field/PluginTypeField.php | 45 +- .../src/Field/PluginorderingField.php | 94 +- .../com_plugins/src/Helper/PluginsHelper.php | 206 +- .../com_plugins/src/Model/PluginModel.php | 694 +- .../com_plugins/src/Model/PluginsModel.php | 542 +- .../com_plugins/src/View/Plugin/HtmlView.php | 180 +- .../com_plugins/src/View/Plugins/HtmlView.php | 190 +- .../com_plugins/tmpl/plugin/edit.php | 211 +- .../com_plugins/tmpl/plugin/modal.php | 5 +- .../com_plugins/tmpl/plugins/default.php | 240 +- .../com_postinstall/postinstall.xml | 2 +- .../com_postinstall/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 56 +- .../src/Controller/MessageController.php | 280 +- .../src/Helper/PostinstallHelper.php | 42 +- .../src/Model/MessagesModel.php | 1412 +- .../src/View/Messages/HtmlView.php | 127 +- .../com_postinstall/tmpl/messages/default.php | 74 +- .../tmpl/messages/emptystate.php | 19 +- .../components/com_privacy/privacy.xml | 2 +- .../com_privacy/services/provider.php | 52 +- .../src/Controller/ConsentsController.php | 149 +- .../src/Controller/DisplayController.php | 196 +- .../src/Controller/RequestController.php | 764 +- .../src/Controller/RequestsController.php | 37 +- .../com_privacy/src/Dispatcher/Dispatcher.php | 32 +- .../com_privacy/src/Export/Domain.php | 93 +- .../com_privacy/src/Export/Field.php | 31 +- .../com_privacy/src/Export/Item.php | 79 +- .../src/Extension/PrivacyComponent.php | 45 +- .../src/Field/RequeststatusField.php | 45 +- .../src/Field/RequesttypeField.php | 37 +- .../com_privacy/src/Helper/PrivacyHelper.php | 109 +- .../src/Model/CapabilitiesModel.php | 150 +- .../com_privacy/src/Model/ConsentsModel.php | 432 +- .../com_privacy/src/Model/ExportModel.php | 591 +- .../com_privacy/src/Model/RemoveModel.php | 374 +- .../com_privacy/src/Model/RequestModel.php | 828 +- .../com_privacy/src/Model/RequestsModel.php | 349 +- .../com_privacy/src/Plugin/PrivacyPlugin.php | 285 +- .../com_privacy/src/Removal/Status.php | 31 +- .../com_privacy/src/Service/HTML/Privacy.php | 60 +- .../com_privacy/src/Table/ConsentTable.php | 78 +- .../com_privacy/src/Table/RequestTable.php | 99 +- .../src/View/Capabilities/HtmlView.php | 108 +- .../src/View/Consents/HtmlView.php | 263 +- .../com_privacy/src/View/Export/XmlView.php | 68 +- .../com_privacy/src/View/Request/HtmlView.php | 313 +- .../src/View/Requests/HtmlView.php | 238 +- .../com_privacy/tmpl/capabilities/default.php | 57 +- .../com_privacy/tmpl/consents/default.php | 188 +- .../com_privacy/tmpl/consents/emptystate.php | 9 +- .../com_privacy/tmpl/request/default.php | 129 +- .../com_privacy/tmpl/request/edit.php | 31 +- .../com_privacy/tmpl/requests/default.php | 184 +- .../com_privacy/tmpl/requests/emptystate.php | 14 +- .../com_redirect/helpers/redirect.php | 7 +- .../com_redirect/layouts/toolbar/batch.php | 5 +- .../components/com_redirect/redirect.xml | 2 +- .../com_redirect/services/provider.php | 48 +- .../src/Controller/DisplayController.php | 126 +- .../src/Controller/LinkController.php | 10 +- .../src/Controller/LinksController.php | 345 +- .../src/Extension/RedirectComponent.php | 43 +- .../com_redirect/src/Field/RedirectField.php | 200 +- .../src/Helper/RedirectHelper.php | 137 +- .../com_redirect/src/Model/LinkModel.php | 417 +- .../com_redirect/src/Model/LinksModel.php | 485 +- .../src/Service/HTML/Redirect.php | 79 +- .../com_redirect/src/Table/LinkTable.php | 247 +- .../com_redirect/src/View/Link/HtmlView.php | 195 +- .../com_redirect/src/View/Links/HtmlView.php | 398 +- .../com_redirect/tmpl/link/edit.php | 45 +- .../com_redirect/tmpl/links/default.php | 283 +- .../tmpl/links/default_addform.php | 75 +- .../tmpl/links/default_batch_body.php | 18 +- .../tmpl/links/default_batch_footer.php | 6 +- .../com_redirect/tmpl/links/emptystate.php | 87 +- .../components/com_scheduler/config.xml | 1 + .../layouts/form/field/webcron_link.php | 29 +- .../components/com_scheduler/scheduler.xml | 2 +- .../com_scheduler/services/provider.php | 57 +- .../src/Controller/DisplayController.php | 146 +- .../src/Controller/TaskController.php | 184 +- .../src/Controller/TasksController.php | 148 +- .../src/Event/ExecuteTaskEvent.php | 135 +- .../src/Extension/SchedulerComponent.php | 44 +- .../com_scheduler/src/Field/CronField.php | 341 +- .../src/Field/ExecutionRuleField.php | 50 +- .../com_scheduler/src/Field/IntervalField.php | 137 +- .../src/Field/TaskStateField.php | 46 +- .../com_scheduler/src/Field/TaskTypeField.php | 81 +- .../src/Field/WebcronLinkField.php | 50 +- .../src/Helper/ExecRuleHelper.php | 197 +- .../src/Helper/SchedulerHelper.php | 77 +- .../com_scheduler/src/Model/SelectModel.php | 76 +- .../com_scheduler/src/Model/TaskModel.php | 1571 +- .../com_scheduler/src/Model/TasksModel.php | 846 +- .../src/Rule/ExecutionRulesRule.php | 118 +- .../com_scheduler/src/Scheduler/Scheduler.php | 614 +- .../com_scheduler/src/Table/TaskTable.php | 547 +- .../com_scheduler/src/Task/Status.php | 165 +- .../com_scheduler/src/Task/Task.php | 1068 +- .../com_scheduler/src/Task/TaskOption.php | 161 +- .../com_scheduler/src/Task/TaskOptions.php | 94 +- .../src/Traits/TaskPluginTrait.php | 625 +- .../src/View/Select/HtmlView.php | 213 +- .../com_scheduler/src/View/Task/HtmlView.php | 243 +- .../com_scheduler/src/View/Tasks/HtmlView.php | 330 +- .../com_scheduler/tmpl/select/default.php | 105 +- .../com_scheduler/tmpl/select/modal.php | 20 +- .../com_scheduler/tmpl/task/edit.php | 310 +- .../com_scheduler/tmpl/tasks/default.php | 479 +- .../com_scheduler/tmpl/tasks/empty_state.php | 14 +- .../components/com_tags/config.xml | 1 + .../components/com_tags/services/provider.php | 50 +- .../src/Controller/DisplayController.php | 77 +- .../com_tags/src/Controller/TagController.php | 102 +- .../src/Controller/TagsController.php | 146 +- .../com_tags/src/Extension/TagsComponent.php | 9 +- .../com_tags/src/Model/TagModel.php | 794 +- .../com_tags/src/Model/TagsModel.php | 619 +- .../com_tags/src/Table/TagTable.php | 441 +- .../com_tags/src/View/Tag/HtmlView.php | 273 +- .../com_tags/src/View/Tags/HtmlView.php | 336 +- .../components/com_tags/tags.xml | 2 +- .../components/com_tags/tmpl/tag/edit.php | 85 +- .../components/com_tags/tmpl/tags/default.php | 458 +- .../com_tags/tmpl/tags/default_batch_body.php | 30 +- .../tmpl/tags/default_batch_footer.php | 6 +- .../com_tags/tmpl/tags/emptystate.php | 14 +- .../components/com_templates/forms/source.xml | 8 +- .../com_templates/helpers/template.php | 7 +- .../com_templates/helpers/templates.php | 7 +- .../com_templates/services/provider.php | 48 +- .../src/Controller/DisplayController.php | 80 +- .../src/Controller/StyleController.php | 255 +- .../src/Controller/StylesController.php | 253 +- .../src/Controller/TemplateController.php | 2003 +- .../src/Extension/TemplatesComponent.php | 43 +- .../src/Field/TemplatelocationField.php | 45 +- .../src/Field/TemplatenameField.php | 59 +- .../src/Helper/TemplateHelper.php | 305 +- .../src/Helper/TemplatesHelper.php | 259 +- .../com_templates/src/Model/StyleModel.php | 1470 +- .../com_templates/src/Model/StylesModel.php | 429 +- .../com_templates/src/Model/TemplateModel.php | 4306 ++-- .../src/Model/TemplatesModel.php | 397 +- .../src/Service/HTML/Templates.php | 308 +- .../com_templates/src/Table/StyleTable.php | 270 +- .../com_templates/src/View/Style/HtmlView.php | 247 +- .../com_templates/src/View/Style/JsonView.php | 92 +- .../src/View/Styles/HtmlView.php | 262 +- .../src/View/Template/HtmlView.php | 771 +- .../src/View/Templates/HtmlView.php | 260 +- .../components/com_templates/templates.xml | 2 +- .../com_templates/tmpl/style/edit.php | 175 +- .../tmpl/style/edit_assignment.php | 57 +- .../com_templates/tmpl/styles/default.php | 246 +- .../com_templates/tmpl/template/default.php | 832 +- .../tmpl/template/default_description.php | 17 +- .../tmpl/template/default_folders.php | 25 +- .../tmpl/template/default_media_folders.php | 30 +- .../template/default_modal_child_body.php | 123 +- .../template/default_modal_child_footer.php | 1 + .../tmpl/template/default_modal_copy_body.php | 35 +- .../template/default_modal_copy_footer.php | 1 + .../template/default_modal_delete_body.php | 11 +- .../template/default_modal_delete_footer.php | 15 +- .../tmpl/template/default_modal_file_body.php | 166 +- .../template/default_modal_file_footer.php | 1 + .../template/default_modal_folder_body.php | 80 +- .../template/default_modal_folder_footer.php | 15 +- .../template/default_modal_rename_body.php | 35 +- .../template/default_modal_rename_footer.php | 1 + .../template/default_modal_resize_body.php | 49 +- .../template/default_modal_resize_footer.php | 1 + .../tmpl/template/default_tree.php | 92 +- .../tmpl/template/default_tree_media.php | 89 +- .../tmpl/template/default_updated_files.php | 145 +- .../com_templates/tmpl/template/readonly.php | 23 +- .../com_templates/tmpl/templates/default.php | 228 +- .../components/com_users/config.xml | 146 +- .../com_users/forms/filter_users.xml | 10 + .../components/com_users/forms/group.xml | 4 +- .../components/com_users/forms/user.xml | 1 - .../components/com_users/helpers/debug.php | 9 +- .../components/com_users/helpers/users.php | 7 +- .../com_users/postinstall/multifactorauth.php | 58 + .../com_users/services/provider.php | 52 +- .../src/Controller/CallbackController.php | 82 + .../src/Controller/CaptiveController.php | 233 + .../src/Controller/DisplayController.php | 219 +- .../src/Controller/GroupController.php | 97 +- .../src/Controller/GroupsController.php | 231 +- .../src/Controller/LevelController.php | 213 +- .../src/Controller/LevelsController.php | 48 +- .../src/Controller/MailController.php | 89 +- .../src/Controller/MethodController.php | 487 + .../src/Controller/MethodsController.php | 213 + .../src/Controller/NoteController.php | 72 +- .../src/Controller/NotesController.php | 51 +- .../src/Controller/UserController.php | 271 +- .../src/Controller/UsersController.php | 302 +- .../src/DataShape/CaptiveRenderOptions.php | 200 + .../src/DataShape/DataShapeObject.php | 198 + .../src/DataShape/MethodDescriptor.php | 120 + .../src/DataShape/SetupRenderOptions.php | 243 + .../com_users/src/Dispatcher/Dispatcher.php | 82 +- .../src/Extension/UsersComponent.php | 128 +- .../com_users/src/Field/GroupparentField.php | 139 +- .../com_users/src/Field/LevelsField.php | 45 +- .../src/Field/ModulesPositionField.php | 26 + .../com_users/src/Helper/DebugHelper.php | 287 +- .../components/com_users/src/Helper/Mfa.php | 361 + .../com_users/src/Helper/UsersHelper.php | 337 +- .../com_users/src/Model/BackupcodesModel.php | 288 + .../com_users/src/Model/CaptiveModel.php | 415 + .../com_users/src/Model/DebuggroupModel.php | 495 +- .../com_users/src/Model/DebuguserModel.php | 462 +- .../com_users/src/Model/GroupModel.php | 608 +- .../com_users/src/Model/GroupsModel.php | 456 +- .../com_users/src/Model/LevelModel.php | 554 +- .../com_users/src/Model/LevelsModel.php | 423 +- .../com_users/src/Model/MailModel.php | 446 +- .../com_users/src/Model/MethodModel.php | 261 + .../com_users/src/Model/MethodsModel.php | 225 + .../com_users/src/Model/NoteModel.php | 233 +- .../com_users/src/Model/NotesModel.php | 421 +- .../com_users/src/Model/UserModel.php | 2455 +-- .../com_users/src/Model/UsersModel.php | 1132 +- .../com_users/src/Service/Encrypt.php | 132 + .../com_users/src/Service/HTML/Users.php | 820 +- .../com_users/src/Table/MfaTable.php | 423 + .../com_users/src/Table/NoteTable.php | 208 +- .../com_users/src/View/Captive/HtmlView.php | 223 + .../src/View/Debuggroup/HtmlView.php | 223 +- .../com_users/src/View/Debuguser/HtmlView.php | 221 +- .../com_users/src/View/Group/HtmlView.php | 198 +- .../com_users/src/View/Groups/HtmlView.php | 199 +- .../com_users/src/View/Level/HtmlView.php | 198 +- .../com_users/src/View/Levels/HtmlView.php | 199 +- .../com_users/src/View/Mail/HtmlView.php | 98 +- .../com_users/src/View/Method/HtmlView.php | 221 + .../com_users/src/View/Methods/HtmlView.php | 195 + .../com_users/src/View/Note/HtmlView.php | 231 +- .../com_users/src/View/Notes/HtmlView.php | 314 +- .../com_users/src/View/SiteTemplateTrait.php | 68 + .../com_users/src/View/User/HtmlView.php | 314 +- .../com_users/src/View/Users/HtmlView.php | 307 +- .../com_users/tmpl/captive/default.php | 133 + .../com_users/tmpl/captive/select.php | 78 + .../com_users/tmpl/debuggroup/default.php | 179 +- .../com_users/tmpl/debuguser/default.php | 221 +- .../components/com_users/tmpl/group/edit.php | 27 +- .../com_users/tmpl/groups/default.php | 221 +- .../components/com_users/tmpl/level/edit.php | 61 +- .../com_users/tmpl/levels/default.php | 217 +- .../com_users/tmpl/mail/default.php | 101 +- .../com_users/tmpl/method/backupcodes.php | 80 + .../components/com_users/tmpl/method/edit.php | 185 + .../com_users/tmpl/methods/default.php | 54 + .../com_users/tmpl/methods/firsttime.php | 50 + .../com_users/tmpl/methods/list.php | 144 + .../components/com_users/tmpl/note/edit.php | 45 +- .../com_users/tmpl/notes/default.php | 196 +- .../com_users/tmpl/notes/emptystate.php | 14 +- .../components/com_users/tmpl/notes/modal.php | 73 +- .../components/com_users/tmpl/user/edit.php | 137 +- .../com_users/tmpl/user/edit_groups.php | 4 +- .../com_users/tmpl/users/default.php | 395 +- .../tmpl/users/default_batch_body.php | 72 +- .../tmpl/users/default_batch_footer.php | 6 +- .../components/com_users/tmpl/users/modal.php | 169 +- .../components/com_users/users.xml | 3 +- .../com_workflow/services/provider.php | 46 +- .../src/Controller/DisplayController.php | 190 +- .../src/Controller/StageController.php | 310 +- .../src/Controller/StagesController.php | 352 +- .../src/Controller/TransitionController.php | 312 +- .../src/Controller/TransitionsController.php | 225 +- .../src/Controller/WorkflowController.php | 431 +- .../src/Controller/WorkflowsController.php | 313 +- .../src/Dispatcher/Dispatcher.php | 36 +- .../src/Field/ComponentsWorkflowField.php | 188 +- .../src/Field/WorkflowcontextsField.php | 83 +- .../com_workflow/src/Helper/StageHelper.php | 7 +- .../src/Helper/WorkflowHelper.php | 8 +- .../com_workflow/src/Model/StageModel.php | 714 +- .../com_workflow/src/Model/StagesModel.php | 359 +- .../src/Model/TransitionModel.php | 631 +- .../src/Model/TransitionsModel.php | 457 +- .../com_workflow/src/Model/WorkflowModel.php | 756 +- .../com_workflow/src/Model/WorkflowsModel.php | 508 +- .../com_workflow/src/Table/StageTable.php | 513 +- .../src/Table/TransitionTable.php | 240 +- .../com_workflow/src/Table/WorkflowTable.php | 607 +- .../com_workflow/src/View/Stage/HtmlView.php | 302 +- .../com_workflow/src/View/Stages/HtmlView.php | 388 +- .../src/View/Transition/HtmlView.php | 377 +- .../src/View/Transitions/HtmlView.php | 375 +- .../src/View/Workflow/HtmlView.php | 308 +- .../src/View/Workflows/HtmlView.php | 334 +- .../com_workflow/tmpl/stage/edit.php | 91 +- .../com_workflow/tmpl/stages/default.php | 243 +- .../com_workflow/tmpl/transition/edit.php | 62 +- .../com_workflow/tmpl/transitions/default.php | 265 +- .../com_workflow/tmpl/workflow/edit.php | 96 +- .../com_workflow/tmpl/workflows/default.php | 298 +- .../components/com_workflow/workflow.xml | 2 +- .../com_wrapper/services/provider.php | 50 +- .../src/Extension/WrapperComponent.php | 9 +- .../components/com_wrapper/wrapper.xml | 2 +- code/administrator/help/en-GB/toc.json | 2 +- code/administrator/includes/app.php | 30 +- code/administrator/includes/defines.php | 21 +- code/administrator/includes/framework.php | 124 +- code/administrator/index.php | 18 +- .../language/en-GB/com_config.ini | 2 + .../language/en-GB/com_cpanel.ini | 2 +- .../language/en-GB/com_finder.ini | 14 +- .../language/en-GB/com_installer.ini | 4 + .../language/en-GB/com_joomlaupdate.ini | 18 +- .../language/en-GB/com_login.ini | 1 - .../language/en-GB/com_mails.ini | 2 +- .../language/en-GB/com_media.ini | 4 +- .../language/en-GB/com_postinstall.ini | 2 + .../language/en-GB/com_scheduler.ini | 2 +- .../language/en-GB/com_templates.ini | 27 +- .../language/en-GB/com_users.ini | 113 +- code/administrator/language/en-GB/install.xml | 4 +- code/administrator/language/en-GB/joomla.ini | 16 +- .../language/en-GB/langmetadata.xml | 4 +- .../language/en-GB/lib_joomla.ini | 16 +- .../administrator/language/en-GB/localise.php | 140 +- .../en-GB/plg_authentication_joomla.ini | 1 - .../en-GB/plg_editors-xtd_readmore.ini | 4 +- .../en-GB/plg_editors-xtd_readmore.sys.ini | 2 +- .../language/en-GB/plg_installer_override.ini | 5 +- .../en-GB/plg_installer_packageinstaller.ini | 6 +- .../en-GB/plg_installer_webinstaller.ini | 2 +- .../en-GB/plg_multifactorauth_email.ini | 26 + .../en-GB/plg_multifactorauth_email.sys.ini | 7 + .../en-GB/plg_multifactorauth_fixed.ini | 17 + .../en-GB/plg_multifactorauth_fixed.sys.ini | 7 + .../en-GB/plg_multifactorauth_totp.ini | 21 + .../en-GB/plg_multifactorauth_totp.sys.ini | 7 + .../en-GB/plg_multifactorauth_webauthn.ini | 23 + .../plg_multifactorauth_webauthn.sys.ini | 7 + .../en-GB/plg_multifactorauth_yubikey.ini | 16 + .../en-GB/plg_multifactorauth_yubikey.sys.ini | 7 + .../en-GB/plg_quickicon_extensionupdate.ini | 4 +- .../en-GB/plg_quickicon_joomlaupdate.ini | 4 +- .../en-GB/plg_quickicon_overridecheck.ini | 2 +- .../en-GB/plg_quickicon_privacycheck.ini | 4 +- .../language/en-GB/plg_sampledata_blog.ini | 5 +- .../en-GB/plg_system_accessibility.ini | 3 + .../language/en-GB/plg_system_shortcut.ini | 12 + .../en-GB/plg_system_shortcut.sys.ini | 7 + .../language/en-GB/plg_system_webauthn.ini | 9 +- .../language/en-GB/plg_twofactorauth_totp.ini | 1 + .../en-GB/plg_twofactorauth_totp.sys.ini | 1 + .../en-GB/plg_twofactorauth_yubikey.ini | 3 +- .../en-GB/plg_twofactorauth_yubikey.sys.ini | 3 +- .../language/en-GB/plg_user_profile.ini | 5 +- .../en-GB/plg_workflow_notification.ini | 1 + code/administrator/manifests/files/joomla.xml | 4 +- .../manifests/libraries/joomla.xml | 2 +- .../manifests/libraries/phpass.xml | 2 +- .../manifests/packages/pkg_en-GB.xml | 4 +- .../modules/mod_custom/mod_custom.php | 8 +- .../modules/mod_custom/mod_custom.xml | 2 +- .../modules/mod_custom/tmpl/default.php | 3 +- .../modules/mod_feed/mod_feed.php | 1 + .../modules/mod_feed/mod_feed.xml | 2 +- .../mod_feed/src/Helper/FeedHelper.php | 67 +- .../modules/mod_feed/tmpl/default.php | 193 +- .../modules/mod_frontend/mod_frontend.php | 1 + .../modules/mod_frontend/mod_frontend.xml | 2 +- .../modules/mod_frontend/tmpl/default.php | 17 +- .../modules/mod_latest/mod_latest.php | 33 +- .../modules/mod_latest/mod_latest.xml | 2 +- .../mod_latest/src/Helper/LatestHelper.php | 224 +- .../modules/mod_latest/tmpl/default.php | 97 +- .../mod_latestactions/mod_latestactions.php | 11 +- .../mod_latestactions/mod_latestactions.xml | 2 +- .../src/Helper/LatestActionsHelper.php | 92 +- .../mod_latestactions/tmpl/default.php | 55 +- .../modules/mod_logged/mod_logged.php | 19 +- .../modules/mod_logged/mod_logged.xml | 2 +- .../mod_logged/src/Helper/LoggedHelper.php | 115 +- .../modules/mod_logged/tmpl/default.php | 95 +- .../modules/mod_logged/tmpl/disabled.php | 9 +- .../modules/mod_login/mod_login.php | 2 +- .../modules/mod_login/mod_login.xml | 2 +- .../mod_login/src/Helper/LoginHelper.php | 96 +- .../modules/mod_login/tmpl/default.php | 213 +- .../mod_loginsupport/mod_loginsupport.php | 6 +- .../mod_loginsupport/mod_loginsupport.xml | 2 +- .../modules/mod_loginsupport/tmpl/default.php | 73 +- .../modules/mod_menu/mod_menu.php | 1 + .../modules/mod_menu/mod_menu.xml | 2 +- .../modules/mod_menu/src/Menu/CssMenu.php | 1050 +- .../modules/mod_menu/tmpl/default.php | 18 +- .../modules/mod_menu/tmpl/default_submenu.php | 224 +- .../modules/mod_messages/mod_messages.php | 29 +- .../modules/mod_messages/mod_messages.xml | 2 +- .../modules/mod_messages/tmpl/default.php | 24 +- .../mod_multilangstatus.php | 1 + .../mod_multilangstatus.xml | 2 +- .../mod_multilangstatus/tmpl/default.php | 42 +- .../modules/mod_popular/mod_popular.php | 36 +- .../modules/mod_popular/mod_popular.xml | 2 +- .../mod_popular/src/Helper/PopularHelper.php | 205 +- .../modules/mod_popular/tmpl/default.php | 87 +- .../mod_post_installation_messages.php | 22 +- .../mod_post_installation_messages.xml | 2 +- .../tmpl/default.php | 32 +- .../mod_privacy_dashboard.php | 26 +- .../mod_privacy_dashboard.xml | 2 +- .../src/Helper/PrivacyDashboardHelper.php | 66 +- .../mod_privacy_dashboard/tmpl/default.php | 83 +- .../mod_privacy_status/mod_privacy_status.php | 8 +- .../mod_privacy_status/mod_privacy_status.xml | 2 +- .../src/Helper/PrivacyStatusHelper.php | 310 +- .../mod_privacy_status/tmpl/default.php | 261 +- .../modules/mod_quickicon/mod_quickicon.xml | 5 +- .../mod_quickicon/services/provider.php | 31 +- .../src/Dispatcher/Dispatcher.php | 35 +- .../src/Event/QuickIconsEvent.php | 77 +- .../src/Helper/QuickIconHelper.php | 557 +- .../modules/mod_quickicon/tmpl/default.php | 11 +- .../modules/mod_sampledata/mod_sampledata.php | 1 + .../modules/mod_sampledata/mod_sampledata.xml | 2 +- .../src/Helper/SampledataHelper.php | 53 +- .../modules/mod_sampledata/tmpl/default.php | 63 +- .../mod_stats_admin/mod_stats_admin.php | 1 + .../mod_stats_admin/mod_stats_admin.xml | 2 +- .../src/Helper/StatsAdminHelper.php | 253 +- .../modules/mod_stats_admin/tmpl/default.php | 21 +- .../modules/mod_submenu/mod_submenu.php | 35 +- .../modules/mod_submenu/mod_submenu.xml | 2 +- .../modules/mod_submenu/src/Menu/Menu.php | 424 +- .../modules/mod_submenu/tmpl/default.php | 177 +- .../modules/mod_title/mod_title.php | 6 +- .../modules/mod_title/mod_title.xml | 2 +- .../modules/mod_title/tmpl/default.php | 7 +- .../modules/mod_toolbar/mod_toolbar.php | 1 + .../modules/mod_toolbar/mod_toolbar.xml | 2 +- .../modules/mod_toolbar/tmpl/default.php | 1 + .../modules/mod_user/mod_user.php | 1 + .../modules/mod_user/mod_user.xml | 4 +- .../modules/mod_user/tmpl/default.php | 70 +- .../modules/mod_version/mod_version.php | 1 + .../modules/mod_version/mod_version.xml | 2 +- .../mod_version/src/Helper/VersionHelper.php | 27 +- .../modules/mod_version/tmpl/default.php | 11 +- .../templates/atum/component.php | 17 +- code/administrator/templates/atum/cpanel.php | 1 + code/administrator/templates/atum/error.php | 12 +- .../templates/atum/error_full.php | 241 +- .../templates/atum/error_login.php | 181 +- .../atum/html/layouts/chromes/body.php | 60 +- .../atum/html/layouts/chromes/header-item.php | 8 +- .../atum/html/layouts/chromes/title.php | 8 +- .../atum/html/layouts/chromes/well.php | 74 +- .../templates/atum/html/layouts/status.php | 70 +- code/administrator/templates/atum/index.php | 181 +- .../templates/atum/joomla.asset.json | 1 + code/administrator/templates/atum/login.php | 137 +- .../templates/atum/templateDetails.xml | 2 +- .../templates/system/component.php | 7 +- code/administrator/templates/system/error.php | 91 +- code/administrator/templates/system/index.php | 1 + .../src/Controller/BannersController.php | 35 +- .../src/Controller/ClientsController.php | 35 +- .../src/View/Banners/JsonapiView.php | 155 +- .../src/View/Clients/JsonapiView.php | 99 +- .../src/Controller/CategoriesController.php | 244 +- .../src/View/Categories/JsonapiView.php | 261 +- .../src/Controller/ApplicationController.php | 247 +- .../src/Controller/ComponentController.php | 265 +- .../src/View/Application/JsonapiView.php | 194 +- .../src/View/Component/JsonapiView.php | 220 +- .../src/Controller/ContactController.php | 455 +- .../src/Serializer/ContactSerializer.php | 214 +- .../src/View/Contacts/JsonapiView.php | 386 +- .../src/Controller/ArticlesController.php | 170 +- .../com_content/src/Helper/ContentHelper.php | 38 +- .../src/Serializer/ContentSerializer.php | 172 +- .../src/View/Articles/JsonapiView.php | 434 +- .../src/Controller/HistoryController.php | 216 +- .../src/View/History/JsonapiView.php | 83 +- .../src/Controller/FieldsController.php | 111 +- .../src/Controller/GroupsController.php | 111 +- .../src/View/Fields/JsonapiView.php | 202 +- .../src/View/Groups/JsonapiView.php | 188 +- .../src/Controller/ManageController.php | 81 +- .../src/View/Manage/JsonapiView.php | 67 +- .../src/Controller/LanguagesController.php | 35 +- .../src/Controller/OverridesController.php | 328 +- .../src/Controller/StringsController.php | 211 +- .../src/View/Languages/JsonapiView.php | 133 +- .../src/View/Overrides/JsonapiView.php | 150 +- .../src/View/Strings/JsonapiView.php | 153 +- .../src/Controller/ItemsController.php | 315 +- .../src/Controller/MenusController.php | 111 +- .../com_menus/src/View/Items/JsonapiView.php | 368 +- .../com_menus/src/View/Menus/JsonapiView.php | 67 +- .../src/Controller/MessagesController.php | 35 +- .../src/View/Messages/JsonapiView.php | 105 +- .../src/Controller/ModulesController.php | 213 +- .../src/View/Modules/JsonapiView.php | 237 +- .../src/Controller/FeedsController.php | 35 +- .../src/Serializer/NewsfeedSerializer.php | 176 +- .../src/View/Feeds/JsonapiView.php | 322 +- .../src/Controller/PluginsController.php | 198 +- .../src/View/Plugins/JsonapiView.php | 119 +- .../src/Controller/ConsentsController.php | 78 +- .../src/Controller/RequestsController.php | 122 +- .../src/View/Consents/JsonapiView.php | 189 +- .../src/View/Requests/JsonapiView.php | 78 +- .../src/Controller/RedirectController.php | 35 +- .../src/View/Redirect/JsonapiView.php | 79 +- .../src/Controller/TagsController.php | 35 +- .../com_tags/src/View/Tags/JsonapiView.php | 143 +- .../src/Controller/StylesController.php | 157 +- .../src/View/Styles/JsonapiView.php | 106 +- .../src/Controller/GroupsController.php | 35 +- .../src/Controller/LevelsController.php | 35 +- .../src/Controller/UsersController.php | 302 +- .../com_users/src/View/Groups/JsonapiView.php | 59 +- .../com_users/src/View/Levels/JsonapiView.php | 51 +- .../com_users/src/View/Users/JsonapiView.php | 191 +- code/api/includes/app.php | 21 +- code/api/includes/defines.php | 21 +- code/api/includes/framework.php | 135 +- code/api/index.php | 14 +- code/api/language/en-GB/install.xml | 4 +- code/api/language/en-GB/joomla.ini | 14 +- code/api/language/en-GB/langmetadata.xml | 4 +- code/cli/joomla.php | 58 +- code/components/com_ajax/ajax.php | 422 +- .../src/Controller/DisplayController.php | 40 +- .../com_banners/src/Helper/BannerHelper.php | 38 +- .../com_banners/src/Model/BannerModel.php | 413 +- .../com_banners/src/Model/BannersModel.php | 750 +- .../com_banners/src/Service/Category.php | 33 +- .../com_banners/src/Service/Router.php | 165 +- .../src/Controller/ConfigController.php | 228 +- .../src/Controller/DisplayController.php | 31 +- .../src/Controller/ModulesController.php | 280 +- .../src/Controller/TemplatesController.php | 187 +- .../com_config/src/Dispatcher/Dispatcher.php | 57 +- .../com_config/src/Model/ConfigModel.php | 42 +- .../com_config/src/Model/FormModel.php | 500 +- .../com_config/src/Model/ModulesModel.php | 468 +- .../com_config/src/Model/TemplatesModel.php | 200 +- .../com_config/src/Service/Router.php | 37 +- .../com_config/src/View/Config/HtmlView.php | 224 +- .../com_config/src/View/Modules/HtmlView.php | 150 +- .../src/View/Templates/HtmlView.php | 260 +- .../com_config/tmpl/config/default.php | 73 +- .../tmpl/config/default_metadata.php | 25 +- .../com_config/tmpl/config/default_seo.php | 25 +- .../com_config/tmpl/config/default_site.php | 25 +- .../com_config/tmpl/modules/default.php | 306 +- .../tmpl/modules/default_options.php | 39 +- .../com_config/tmpl/templates/default.php | 63 +- .../tmpl/templates/default_options.php | 33 +- code/components/com_contact/helpers/route.php | 9 +- .../com_contact/layouts/field/render.php | 31 +- .../com_contact/layouts/fields/render.php | 66 +- .../src/Controller/ContactController.php | 833 +- .../src/Controller/DisplayController.php | 104 +- .../com_contact/src/Dispatcher/Dispatcher.php | 51 +- .../src/Helper/AssociationHelper.php | 72 +- .../com_contact/src/Helper/RouteHelper.php | 118 +- .../com_contact/src/Model/CategoriesModel.php | 267 +- .../com_contact/src/Model/CategoryModel.php | 924 +- .../com_contact/src/Model/ContactModel.php | 850 +- .../com_contact/src/Model/FeaturedModel.php | 371 +- .../com_contact/src/Model/FormModel.php | 421 +- .../src/Rule/ContactEmailMessageRule.php | 62 +- .../com_contact/src/Rule/ContactEmailRule.php | 75 +- .../src/Rule/ContactEmailSubjectRule.php | 62 +- .../com_contact/src/Service/Category.php | 35 +- .../com_contact/src/Service/Router.php | 522 +- .../src/View/Categories/HtmlView.php | 31 +- .../src/View/Category/FeedView.php | 47 +- .../src/View/Category/HtmlView.php | 224 +- .../com_contact/src/View/Contact/HtmlView.php | 915 +- .../com_contact/src/View/Contact/VcfView.php | 174 +- .../src/View/Featured/HtmlView.php | 302 +- .../com_contact/src/View/Form/HtmlView.php | 314 +- .../com_contact/tmpl/categories/default.php | 9 +- .../tmpl/categories/default_items.php | 98 +- .../com_contact/tmpl/category/default.php | 9 +- .../tmpl/category/default_children.php | 61 +- .../tmpl/category/default_items.php | 365 +- .../com_contact/tmpl/contact/default.php | 314 +- .../tmpl/contact/default_address.php | 240 +- .../tmpl/contact/default_articles.php | 15 +- .../com_contact/tmpl/contact/default_form.php | 60 +- .../tmpl/contact/default_links.php | 49 +- .../tmpl/contact/default_profile.php | 63 +- .../contact/default_user_custom_fields.php | 45 +- .../com_contact/tmpl/featured/default.php | 25 +- .../tmpl/featured/default_items.php | 345 +- .../components/com_contact/tmpl/form/edit.php | 97 +- code/components/com_content/helpers/icon.php | 165 +- .../src/Controller/ArticleController.php | 834 +- .../src/Controller/DisplayController.php | 203 +- .../com_content/src/Dispatcher/Dispatcher.php | 71 +- .../src/Helper/AssociationHelper.php | 259 +- .../com_content/src/Helper/QueryHelper.php | 418 +- .../com_content/src/Helper/RouteHelper.php | 178 +- .../com_content/src/Model/ArchiveModel.php | 399 +- .../com_content/src/Model/ArticleModel.php | 849 +- .../com_content/src/Model/ArticlesModel.php | 1660 +- .../com_content/src/Model/CategoriesModel.php | 265 +- .../com_content/src/Model/CategoryModel.php | 947 +- .../com_content/src/Model/FeaturedModel.php | 295 +- .../com_content/src/Model/FormModel.php | 583 +- .../com_content/src/Service/Category.php | 33 +- .../com_content/src/Service/Router.php | 524 +- .../com_content/src/View/Archive/HtmlView.php | 465 +- .../com_content/src/View/Article/HtmlView.php | 649 +- .../src/View/Categories/HtmlView.php | 31 +- .../src/View/Category/FeedView.php | 95 +- .../src/View/Category/HtmlView.php | 429 +- .../src/View/Featured/FeedView.php | 179 +- .../src/View/Featured/HtmlView.php | 448 +- .../com_content/src/View/Form/HtmlView.php | 425 +- .../com_content/tmpl/archive/default.php | 57 +- .../tmpl/archive/default_items.php | 417 +- .../com_content/tmpl/article/default.php | 194 +- .../tmpl/article/default_links.php | 130 +- .../com_content/tmpl/categories/default.php | 9 +- .../tmpl/categories/default_items.php | 111 +- .../com_content/tmpl/category/blog.php | 214 +- .../com_content/tmpl/category/blog.xml | 1 + .../tmpl/category/blog_children.php | 112 +- .../com_content/tmpl/category/blog_item.php | 125 +- .../com_content/tmpl/category/blog_links.php | 13 +- .../com_content/tmpl/category/default.php | 1 + .../com_content/tmpl/category/default.xml | 1 + .../tmpl/category/default_articles.php | 593 +- .../tmpl/category/default_children.php | 109 +- .../com_content/tmpl/featured/default.php | 109 +- .../tmpl/featured/default_item.php | 153 +- .../tmpl/featured/default_links.php | 13 +- .../components/com_content/tmpl/form/edit.php | 292 +- .../src/Controller/DisplayController.php | 33 +- .../src/Dispatcher/Dispatcher.php | 100 +- .../com_fields/layouts/field/render.php | 18 +- .../com_fields/layouts/fields/render.php | 75 +- .../src/Controller/DisplayController.php | 49 +- .../com_fields/src/Dispatcher/Dispatcher.php | 57 +- code/components/com_finder/helpers/route.php | 9 +- .../src/Controller/DisplayController.php | 79 +- .../src/Controller/SuggestionsController.php | 157 +- .../com_finder/src/Helper/FinderHelper.php | 144 +- .../com_finder/src/Helper/RouteHelper.php | 273 +- .../com_finder/src/Model/SearchModel.php | 965 +- .../com_finder/src/Model/SuggestionsModel.php | 308 +- .../com_finder/src/Service/Router.php | 37 +- .../com_finder/src/View/Search/FeedView.php | 106 +- .../com_finder/src/View/Search/HtmlView.php | 610 +- .../src/View/Search/OpensearchView.php | 110 +- .../com_finder/tmpl/search/default.php | 41 +- .../com_finder/tmpl/search/default.xml | 4 +- .../com_finder/tmpl/search/default_form.php | 106 +- .../com_finder/tmpl/search/default_result.php | 164 +- .../tmpl/search/default_results.php | 113 +- .../com_media/src/Dispatcher/Dispatcher.php | 104 +- .../layouts/joomla/searchtools/default.php | 156 +- .../com_menus/src/Dispatcher/Dispatcher.php | 98 +- .../src/Controller/DisplayController.php | 46 +- .../com_modules/src/Dispatcher/Dispatcher.php | 103 +- .../com_newsfeeds/helpers/route.php | 9 +- .../src/Controller/DisplayController.php | 62 +- .../src/Helper/AssociationHelper.php | 71 +- .../com_newsfeeds/src/Helper/RouteHelper.php | 110 +- .../src/Model/CategoriesModel.php | 267 +- .../com_newsfeeds/src/Model/CategoryModel.php | 778 +- .../com_newsfeeds/src/Model/NewsfeedModel.php | 410 +- .../com_newsfeeds/src/Service/Category.php | 31 +- .../com_newsfeeds/src/Service/Router.php | 484 +- .../src/View/Categories/HtmlView.php | 31 +- .../src/View/Category/HtmlView.php | 163 +- .../src/View/Newsfeed/HtmlView.php | 581 +- .../com_newsfeeds/tmpl/categories/default.php | 5 +- .../tmpl/categories/default_items.php | 93 +- .../com_newsfeeds/tmpl/category/default.php | 81 +- .../tmpl/category/default_children.php | 69 +- .../tmpl/category/default_items.php | 145 +- .../com_newsfeeds/tmpl/newsfeed/default.php | 253 +- .../src/Controller/DisplayController.php | 69 +- .../src/Controller/RequestController.php | 306 +- .../com_privacy/src/Model/ConfirmModel.php | 417 +- .../com_privacy/src/Model/RemindModel.php | 331 +- .../com_privacy/src/Model/RequestModel.php | 464 +- .../com_privacy/src/Service/Router.php | 43 +- .../com_privacy/src/View/Confirm/HtmlView.php | 197 +- .../com_privacy/src/View/Remind/HtmlView.php | 197 +- .../com_privacy/src/View/Request/HtmlView.php | 215 +- .../com_privacy/tmpl/confirm/default.php | 51 +- .../com_privacy/tmpl/remind/default.php | 51 +- .../com_privacy/tmpl/request/default.php | 65 +- code/components/com_tags/helpers/route.php | 9 +- .../src/Controller/DisplayController.php | 78 +- .../src/Controller/TagsController.php | 75 +- .../com_tags/src/Helper/RouteHelper.php | 439 +- .../com_tags/src/Model/TagModel.php | 661 +- .../com_tags/src/Model/TagsModel.php | 291 +- .../com_tags/src/Service/Router.php | 381 +- .../com_tags/src/View/Tag/FeedView.php | 159 +- .../com_tags/src/View/Tag/HtmlView.php | 709 +- .../com_tags/src/View/Tags/FeedView.php | 105 +- .../com_tags/src/View/Tags/HtmlView.php | 305 +- code/components/com_tags/tmpl/tag/default.php | 85 +- .../com_tags/tmpl/tag/default_items.php | 153 +- code/components/com_tags/tmpl/tag/list.php | 71 +- .../com_tags/tmpl/tag/list_items.php | 210 +- .../components/com_tags/tmpl/tags/default.php | 33 +- .../com_tags/tmpl/tags/default_items.php | 223 +- code/components/com_users/forms/login.xml | 10 - code/components/com_users/forms/profile.xml | 5 - .../src/Controller/CallbackController.php | 26 + .../src/Controller/CaptiveController.php | 55 + .../src/Controller/DisplayController.php | 231 +- .../src/Controller/MethodController.php | 55 + .../src/Controller/MethodsController.php | 55 + .../src/Controller/ProfileController.php | 415 +- .../src/Controller/RegistrationController.php | 423 +- .../src/Controller/RemindController.php | 78 +- .../src/Controller/ResetController.php | 332 +- .../src/Controller/UserController.php | 580 +- .../com_users/src/Model/BackupcodesModel.php | 24 + .../com_users/src/Model/CaptiveModel.php | 24 + .../com_users/src/Model/LoginModel.php | 228 +- .../com_users/src/Model/MethodModel.php | 24 + .../com_users/src/Model/MethodsModel.php | 24 + .../com_users/src/Model/ProfileModel.php | 661 +- .../com_users/src/Model/RegistrationModel.php | 1297 +- .../com_users/src/Model/RemindModel.php | 372 +- .../com_users/src/Model/ResetModel.php | 1007 +- .../src/Rule/LoginUniqueFieldRule.php | 78 +- .../src/Rule/LogoutUniqueFieldRule.php | 78 +- .../com_users/src/Service/Router.php | 86 +- .../com_users/src/View/Captive/HtmlView.php | 24 + .../com_users/src/View/Login/HtmlView.php | 266 +- .../com_users/src/View/Method/HtmlView.php | 24 + .../com_users/src/View/Methods/HtmlView.php | 24 + .../com_users/src/View/Profile/HtmlView.php | 336 +- .../src/View/Registration/HtmlView.php | 236 +- .../com_users/src/View/Remind/HtmlView.php | 206 +- .../com_users/src/View/Reset/HtmlView.php | 221 +- .../com_users/tmpl/captive/default.php | 136 + .../com_users/tmpl/captive/select.php | 78 + .../com_users/tmpl/login/default.php | 18 +- .../com_users/tmpl/login/default_login.php | 205 +- .../com_users/tmpl/login/default_logout.php | 82 +- .../com_users/tmpl/method/backupcodes.php | 77 + .../components/com_users/tmpl/method/edit.php | 181 + .../com_users/tmpl/methods/default.php | 60 + .../com_users/tmpl/methods/firsttime.php | 50 + .../com_users/tmpl/methods/list.php | 144 + .../com_users/tmpl/profile/default.php | 39 +- .../com_users/tmpl/profile/default_core.php | 71 +- .../com_users/tmpl/profile/default_custom.php | 82 +- .../com_users/tmpl/profile/default_params.php | 47 +- .../com_users/tmpl/profile/edit.php | 150 +- .../com_users/tmpl/registration/complete.php | 11 +- .../com_users/tmpl/registration/default.php | 61 +- .../com_users/tmpl/remind/default.php | 51 +- .../com_users/tmpl/reset/complete.php | 51 +- .../com_users/tmpl/reset/confirm.php | 51 +- .../com_users/tmpl/reset/default.php | 51 +- .../src/Controller/DisplayController.php | 43 +- .../com_wrapper/src/Service/Router.php | 66 +- .../com_wrapper/src/View/Wrapper/HtmlView.php | 196 +- .../com_wrapper/tmpl/wrapper/default.php | 55 +- code/includes/app.php | 30 +- code/includes/defines.php | 21 +- code/includes/framework.php | 129 +- code/index.php | 18 +- code/language/en-GB/com_content.ini | 8 +- code/language/en-GB/com_media.ini | 2 +- code/language/en-GB/com_users.ini | 63 +- code/language/en-GB/finder_cli.ini | 4 +- code/language/en-GB/install.xml | 4 +- code/language/en-GB/joomla.ini | 16 +- code/language/en-GB/langmetadata.xml | 4 +- code/language/en-GB/lib_joomla.ini | 4 +- code/language/en-GB/localise.php | 52 +- code/language/en-GB/mod_articles_category.ini | 2 +- code/language/en-GB/mod_articles_news.ini | 4 +- code/language/en-GB/mod_finder.ini | 2 +- code/layouts/chromes/html5.php | 34 +- code/layouts/chromes/none.php | 1 + code/layouts/chromes/outline.php | 27 +- code/layouts/chromes/table.php | 28 +- code/layouts/joomla/button/action-button.php | 19 +- code/layouts/joomla/button/iconclass.php | 1 + .../joomla/button/transition-button.php | 63 +- code/layouts/joomla/content/associations.php | 27 +- .../content/blog_style_default_item_title.php | 56 +- .../joomla/content/categories_default.php | 29 +- .../content/categories_default_items.php | 6 +- .../joomla/content/category_default.php | 94 +- code/layouts/joomla/content/emptystate.php | 58 +- .../joomla/content/emptystate_module.php | 14 +- code/layouts/joomla/content/full_image.php | 20 +- code/layouts/joomla/content/icons.php | 15 +- code/layouts/joomla/content/icons/create.php | 7 +- code/layouts/joomla/content/icons/edit.php | 12 +- .../joomla/content/icons/edit_lock.php | 26 +- code/layouts/joomla/content/info_block.php | 80 +- .../content/info_block/associations.php | 29 +- .../joomla/content/info_block/author.php | 17 +- .../joomla/content/info_block/category.php | 23 +- .../joomla/content/info_block/create_date.php | 9 +- .../joomla/content/info_block/hits.php | 7 +- .../joomla/content/info_block/modify_date.php | 9 +- .../content/info_block/parent_category.php | 23 +- .../content/info_block/publish_date.php | 9 +- code/layouts/joomla/content/intro_image.php | 30 +- code/layouts/joomla/content/language.php | 24 +- .../joomla/content/options_default.php | 49 +- code/layouts/joomla/content/readmore.php | 49 +- code/layouts/joomla/content/tags.php | 27 +- code/layouts/joomla/content/text_filters.php | 47 +- code/layouts/joomla/edit/admin_modules.php | 54 +- code/layouts/joomla/edit/associations.php | 5 +- code/layouts/joomla/edit/fieldset.php | 71 +- .../joomla/edit/frontediting_modules.php | 57 +- code/layouts/joomla/edit/global.php | 68 +- code/layouts/joomla/edit/metadata.php | 42 +- code/layouts/joomla/edit/params.php | 305 +- code/layouts/joomla/edit/publishingdata.php | 49 +- code/layouts/joomla/edit/title_alias.php | 13 +- code/layouts/joomla/editors/buttons.php | 13 +- .../layouts/joomla/editors/buttons/button.php | 23 +- code/layouts/joomla/editors/buttons/modal.php | 48 +- code/layouts/joomla/error/backtrace.php | 88 +- code/layouts/joomla/form/field/calendar.php | 137 +- code/layouts/joomla/form/field/checkbox.php | 17 +- code/layouts/joomla/form/field/checkboxes.php | 53 +- .../joomla/form/field/color/advanced.php | 46 +- .../joomla/form/field/color/simple.php | 19 +- .../joomla/form/field/color/slider.php | 152 +- code/layouts/joomla/form/field/combo.php | 20 +- .../joomla/form/field/contenthistory.php | 39 +- code/layouts/joomla/form/field/email.php | 39 +- code/layouts/joomla/form/field/file.php | 25 +- .../form/field/groupedlist-fancy-select.php | 90 +- .../layouts/joomla/form/field/groupedlist.php | 75 +- code/layouts/joomla/form/field/hidden.php | 11 +- .../joomla/form/field/list-fancy-select.php | 59 +- code/layouts/joomla/form/field/list.php | 60 +- code/layouts/joomla/form/field/media.php | 189 +- code/layouts/joomla/form/field/meter.php | 17 +- .../layouts/joomla/form/field/moduleorder.php | 28 +- code/layouts/joomla/form/field/number.php | 52 +- code/layouts/joomla/form/field/password.php | 154 +- .../joomla/form/field/radio/buttons.php | 111 +- .../joomla/form/field/radio/switcher.php | 49 +- code/layouts/joomla/form/field/radiobasic.php | 59 +- code/layouts/joomla/form/field/range.php | 31 +- code/layouts/joomla/form/field/rules.php | 321 +- .../joomla/form/field/subform/default.php | 3 +- .../form/field/subform/repeatable-table.php | 143 +- .../repeatable-table/section-byfieldsets.php | 59 +- .../subform/repeatable-table/section.php | 53 +- .../joomla/form/field/subform/repeatable.php | 60 +- .../repeatable/section-byfieldsets.php | 51 +- .../form/field/subform/repeatable/section.php | 27 +- code/layouts/joomla/form/field/tag.php | 77 +- code/layouts/joomla/form/field/tel.php | 39 +- code/layouts/joomla/form/field/text.php | 86 +- code/layouts/joomla/form/field/textarea.php | 52 +- code/layouts/joomla/form/field/time.php | 35 +- code/layouts/joomla/form/field/url.php | 44 +- code/layouts/joomla/form/field/user.php | 124 +- code/layouts/joomla/form/renderfield.php | 45 +- code/layouts/joomla/form/renderlabel.php | 14 +- code/layouts/joomla/html/batch/access.php | 22 +- .../joomla/html/batch/adminlanguage.php | 7 +- code/layouts/joomla/html/batch/item.php | 35 +- code/layouts/joomla/html/batch/language.php | 7 +- code/layouts/joomla/html/batch/tag.php | 7 +- code/layouts/joomla/html/batch/user.php | 14 +- .../joomla/html/batch/workflowstage.php | 15 +- code/layouts/joomla/html/image.php | 31 +- code/layouts/joomla/html/treeprefix.php | 6 +- code/layouts/joomla/icon/iconclass.php | 52 +- code/layouts/joomla/installer/changelog.php | 93 +- code/layouts/joomla/links/groupclose.php | 1 + code/layouts/joomla/links/groupopen.php | 1 + code/layouts/joomla/links/groupsclose.php | 1 + code/layouts/joomla/links/groupseparator.php | 1 + code/layouts/joomla/links/groupsopen.php | 1 + code/layouts/joomla/links/link.php | 9 +- code/layouts/joomla/pagination/link.php | 120 +- code/layouts/joomla/pagination/links.php | 101 +- code/layouts/joomla/pagination/list.php | 19 +- code/layouts/joomla/quickicons/icon.php | 95 +- code/layouts/joomla/searchtools/default.php | 118 +- .../joomla/searchtools/default/bar.php | 57 +- .../joomla/searchtools/default/filters.php | 27 +- .../joomla/searchtools/default/list.php | 17 +- .../joomla/searchtools/default/noitems.php | 5 +- .../joomla/searchtools/default/selector.php | 9 +- code/layouts/joomla/searchtools/grid/sort.php | 47 +- code/layouts/joomla/sidebars/submenu.php | 97 +- code/layouts/joomla/system/message.php | 62 +- code/layouts/joomla/tinymce/textarea.php | 28 +- code/layouts/joomla/tinymce/togglebutton.php | 13 +- code/layouts/joomla/toolbar/base.php | 1 + code/layouts/joomla/toolbar/basic.php | 41 +- code/layouts/joomla/toolbar/batch.php | 5 +- .../layouts/joomla/toolbar/containerclose.php | 1 + code/layouts/joomla/toolbar/containeropen.php | 1 + code/layouts/joomla/toolbar/dropdown.php | 56 +- code/layouts/joomla/toolbar/iconclass.php | 1 + code/layouts/joomla/toolbar/inlinehelp.php | 3 +- code/layouts/joomla/toolbar/link.php | 19 +- code/layouts/joomla/toolbar/popup.php | 17 +- code/layouts/joomla/toolbar/separator.php | 17 +- code/layouts/joomla/toolbar/standard.php | 30 +- code/layouts/joomla/toolbar/title.php | 5 +- code/layouts/joomla/toolbar/versions.php | 63 +- .../libraries/html/bootstrap/modal/body.php | 8 +- .../libraries/html/bootstrap/modal/footer.php | 3 +- .../libraries/html/bootstrap/modal/header.php | 15 +- .../libraries/html/bootstrap/modal/iframe.php | 22 +- .../libraries/html/bootstrap/modal/main.php | 68 +- .../libraries/html/bootstrap/tab/addtab.php | 9 +- .../libraries/html/bootstrap/tab/endtab.php | 1 + .../html/bootstrap/tab/endtabset.php | 1 + .../html/bootstrap/tab/starttabset.php | 1 + .../editors/tinymce/field/tinymcebuilder.php | 226 +- .../field/tinymcebuilder/setaccess.php | 3 +- .../field/tinymcebuilder/setoptions.php | 3 +- .../plugins/system/privacyconsent/label.php | 50 +- .../plugins/system/privacyconsent/message.php | 2 +- .../plugins/system/webauthn/manage.php | 206 +- .../plugins/user/profile/fields/dob.php | 24 - code/layouts/plugins/user/terms/label.php | 50 +- code/layouts/plugins/user/terms/message.php | 1 + code/layouts/plugins/user/token/token.php | 27 +- code/libraries/bootstrap.php | 40 +- code/libraries/classmap.php | 1023 +- code/libraries/cms.php | 49 +- code/libraries/extensions.classmap.php | 21 +- code/libraries/import.legacy.php | 38 +- code/libraries/import.php | 38 +- code/libraries/loader.php | 1436 +- code/libraries/namespacemap.php | 553 +- code/libraries/src/Access/Access.php | 2199 +- .../Access/Exception/AuthenticationFailed.php | 3 + .../src/Access/Exception/NotAllowed.php | 3 + code/libraries/src/Access/Rule.php | 298 +- code/libraries/src/Access/Rules.php | 384 +- code/libraries/src/Adapter/Adapter.php | 395 +- .../libraries/src/Adapter/AdapterInstance.php | 93 +- .../Application/AdministratorApplication.php | 1021 +- .../src/Application/ApiApplication.php | 776 +- .../src/Application/ApplicationHelper.php | 380 +- .../src/Application/BaseApplication.php | 47 +- .../src/Application/CLI/CliInput.php | 27 +- .../src/Application/CLI/CliOutput.php | 126 +- .../src/Application/CLI/ColorStyle.php | 489 +- .../CLI/Output/Processor/ColorProcessor.php | 345 +- .../Output/Processor/ProcessorInterface.php | 23 +- .../src/Application/CLI/Output/Stdout.php | 39 +- .../src/Application/CLI/Output/Xml.php | 41 +- .../src/Application/CMSApplication.php | 2646 ++- .../Application/CMSApplicationInterface.php | 313 +- .../CMSWebApplicationInterface.php | 139 +- .../src/Application/CliApplication.php | 750 +- .../src/Application/ConsoleApplication.php | 964 +- .../src/Application/DaemonApplication.php | 1694 +- code/libraries/src/Application/EventAware.php | 168 +- .../src/Application/EventAwareInterface.php | 61 +- .../Application/Exception/NotAcceptable.php | 3 + .../Application/ExtensionNamespaceMapper.php | 33 +- .../src/Application/IdentityAware.php | 111 +- .../MultiFactorAuthenticationHandler.php | 516 + .../src/Application/SiteApplication.php | 1721 +- .../src/Application/WebApplication.php | 796 +- .../AssociationExtensionHelper.php | 546 +- .../AssociationExtensionInterface.php | 41 +- .../AssociationServiceInterface.php | 19 +- .../Association/AssociationServiceTrait.php | 67 +- .../src/Authentication/Authentication.php | 399 +- .../Authentication/AuthenticationResponse.php | 221 +- .../Password/Argon2iHandler.php | 33 +- .../Password/Argon2idHandler.php | 33 +- .../Authentication/Password/BCryptHandler.php | 33 +- .../Password/ChainedHandler.php | 177 +- .../CheckIfRehashNeededHandlerInterface.php | 23 +- .../Authentication/Password/MD5Handler.php | 127 +- .../Authentication/Password/PHPassHandler.php | 133 +- ...iderAwareAuthenticationPluginInterface.php | 37 +- code/libraries/src/Autoload/ClassLoader.php | 82 +- code/libraries/src/Button/ActionButton.php | 595 +- code/libraries/src/Button/FeaturedButton.php | 201 +- code/libraries/src/Button/PublishedButton.php | 195 +- .../libraries/src/Button/TransitionButton.php | 83 +- .../CacheControllerFactoryAwareInterface.php | 30 + .../CacheControllerFactoryAwareTrait.php | 66 + .../src/Cache/Storage/WincacheStorage.php | 29 +- code/libraries/src/Captcha/Captcha.php | 487 +- .../Google/HttpBridgePostRequestMethod.php | 96 +- code/libraries/src/Categories/Categories.php | 830 +- .../src/Categories/CategoryFactory.php | 93 +- .../Categories/CategoryFactoryInterface.php | 29 +- .../src/Categories/CategoryInterface.php | 25 +- .../libraries/src/Categories/CategoryNode.php | 968 +- .../Categories/CategoryServiceInterface.php | 73 +- .../src/Categories/CategoryServiceTrait.php | 195 +- .../Categories/SectionNotFoundException.php | 5 + code/libraries/src/Changelog/Changelog.php | 713 +- code/libraries/src/Client/ClientHelper.php | 411 +- code/libraries/src/Client/FtpClient.php | 3649 ++-- .../src/Component/ComponentHelper.php | 884 +- .../src/Component/ComponentRecord.php | 261 +- .../Exception/MissingComponentException.php | 33 +- .../src/Component/Router/RouterBase.php | 109 +- .../src/Component/Router/RouterFactory.php | 122 +- .../Router/RouterFactoryInterface.php | 29 +- .../src/Component/Router/RouterInterface.php | 79 +- .../src/Component/Router/RouterLegacy.php | 179 +- .../Router/RouterServiceInterface.php | 29 +- .../Component/Router/RouterServiceTrait.php | 77 +- .../src/Component/Router/RouterView.php | 531 +- .../src/Component/Router/Rules/MenuRules.php | 535 +- .../Component/Router/Rules/NomenuRules.php | 321 +- .../Component/Router/Rules/RulesInterface.php | 77 +- .../Component/Router/Rules/StandardRules.php | 557 +- code/libraries/src/Console/AddUserCommand.php | 563 +- .../src/Console/AddUserToGroupCommand.php | 543 +- .../src/Console/ChangeUserPasswordCommand.php | 287 +- .../src/Console/CheckJoomlaUpdatesCommand.php | 239 +- .../src/Console/CheckUpdatesCommand.php | 104 +- .../src/Console/CleanCacheCommand.php | 117 +- .../src/Console/DeleteUserCommand.php | 351 +- .../src/Console/ExtensionDiscoverCommand.php | 258 +- .../ExtensionDiscoverInstallCommand.php | 434 +- .../Console/ExtensionDiscoverListCommand.php | 166 +- .../src/Console/ExtensionInstallCommand.php | 374 +- .../src/Console/ExtensionRemoveCommand.php | 358 +- .../src/Console/ExtensionsListCommand.php | 450 +- .../src/Console/FinderIndexCommand.php | 948 +- .../src/Console/GetConfigurationCommand.php | 681 +- .../libraries/src/Console/ListUserCommand.php | 212 +- .../Loader/WritableContainerLoader.php | 168 +- .../Loader/WritableLoaderInterface.php | 25 +- .../src/Console/RemoveOldFilesCommand.php | 229 +- .../Console/RemoveUserFromGroupCommand.php | 548 +- .../src/Console/SessionGcCommand.php | 171 +- .../src/Console/SessionMetadataGcCommand.php | 163 +- .../src/Console/SetConfigurationCommand.php | 871 +- .../libraries/src/Console/SiteDownCommand.php | 176 +- code/libraries/src/Console/SiteUpCommand.php | 176 +- .../src/Console/TasksListCommand.php | 226 +- .../libraries/src/Console/TasksRunCommand.php | 249 +- .../src/Console/TasksStateCommand.php | 329 +- .../src/Console/UpdateCoreCommand.php | 707 +- .../src/Crypt/Cipher/CryptoCipher.php | 217 +- .../src/Crypt/Cipher/SodiumCipher.php | 242 +- code/libraries/src/Crypt/Crypt.php | 270 +- code/libraries/src/Date/Date.php | 874 +- .../Dispatcher/AbstractModuleDispatcher.php | 231 +- .../src/Dispatcher/ApiDispatcher.php | 82 +- .../src/Dispatcher/ComponentDispatcher.php | 304 +- .../Dispatcher/ComponentDispatcherFactory.php | 115 +- .../ComponentDispatcherFactoryInterface.php | 29 +- code/libraries/src/Dispatcher/Dispatcher.php | 83 +- .../src/Dispatcher/DispatcherInterface.php | 19 +- .../Dispatcher/LegacyComponentDispatcher.php | 90 +- .../src/Dispatcher/ModuleDispatcher.php | 72 +- .../Dispatcher/ModuleDispatcherFactory.php | 93 +- .../ModuleDispatcherFactoryInterface.php | 31 +- code/libraries/src/Document/Document.php | 2435 ++- .../src/Document/DocumentRenderer.php | 109 +- code/libraries/src/Document/ErrorDocument.php | 288 +- code/libraries/src/Document/Factory.php | 175 +- .../src/Document/FactoryInterface.php | 49 +- .../src/Document/Feed/FeedEnclosure.php | 57 +- .../libraries/src/Document/Feed/FeedImage.php | 111 +- code/libraries/src/Document/Feed/FeedItem.php | 273 +- code/libraries/src/Document/FeedDocument.php | 455 +- code/libraries/src/Document/HtmlDocument.php | 1705 +- code/libraries/src/Document/ImageDocument.php | 100 +- code/libraries/src/Document/JsonDocument.php | 187 +- .../src/Document/JsonapiDocument.php | 355 +- .../Document/Opensearch/OpensearchImage.php | 75 +- .../src/Document/Opensearch/OpensearchUrl.php | 57 +- .../src/Document/OpensearchDocument.php | 409 +- .../libraries/src/Document/PreloadManager.php | 296 +- .../src/Document/PreloadManagerInterface.php | 153 +- code/libraries/src/Document/RawDocument.php | 71 +- .../Document/Renderer/Feed/AtomRenderer.php | 344 +- .../Document/Renderer/Feed/RssRenderer.php | 466 +- .../Renderer/Html/ComponentRenderer.php | 37 +- .../Document/Renderer/Html/HeadRenderer.php | 45 +- .../Renderer/Html/MessageRenderer.php | 126 +- .../Document/Renderer/Html/MetasRenderer.php | 336 +- .../Document/Renderer/Html/ModuleRenderer.php | 159 +- .../Renderer/Html/ModulesRenderer.php | 81 +- .../Renderer/Html/ScriptsRenderer.php | 617 +- .../Document/Renderer/Html/StylesRenderer.php | 620 +- .../src/Document/RendererInterface.php | 27 +- code/libraries/src/Document/XmlDocument.php | 227 +- code/libraries/src/Editor/Editor.php | 580 +- .../libraries/src/Encrypt/AES/AbstractAES.php | 137 +- .../src/Encrypt/AES/AesInterface.php | 123 +- code/libraries/src/Encrypt/AES/Mcrypt.php | 353 +- code/libraries/src/Encrypt/AES/OpenSSL.php | 408 +- code/libraries/src/Encrypt/Aes.php | 527 +- code/libraries/src/Encrypt/Base32.php | 404 +- .../src/Encrypt/RandValInterface.php | 19 +- code/libraries/src/Encrypt/Randval.php | 31 +- code/libraries/src/Encrypt/Totp.php | 369 +- code/libraries/src/Environment/Browser.php | 1886 +- code/libraries/src/Error/AbstractRenderer.php | 165 +- .../AuthenticationFailedExceptionHandler.php | 82 +- .../CheckinCheckoutExceptionHandler.php | 78 +- .../InvalidParameterExceptionHandler.php | 52 +- .../JsonApi/InvalidRouteExceptionHandler.php | 82 +- .../JsonApi/NotAcceptableExceptionHandler.php | 82 +- .../JsonApi/NotAllowedExceptionHandler.php | 82 +- .../ResourceNotFoundExceptionHandler.php | 80 +- .../Error/JsonApi/SaveExceptionHandler.php | 84 +- .../JsonApi/SendEmailExceptionHandler.php | 78 +- .../src/Error/Renderer/CliRenderer.php | 91 +- .../src/Error/Renderer/FeedRenderer.php | 3 + .../src/Error/Renderer/HtmlRenderer.php | 99 +- .../src/Error/Renderer/JsonRenderer.php | 97 +- .../src/Error/Renderer/JsonapiRenderer.php | 111 +- .../src/Error/Renderer/XmlRenderer.php | 89 +- .../libraries/src/Error/RendererInterface.php | 43 +- code/libraries/src/Event/AbstractEvent.php | 280 +- .../src/Event/AbstractImmutableEvent.php | 142 +- .../src/Event/AfterExtensionBootEvent.php | 71 +- .../src/Event/BeforeExtensionBootEvent.php | 71 +- code/libraries/src/Event/CoreEventAware.php | 139 + code/libraries/src/Event/ErrorEvent.php | 77 +- code/libraries/src/Event/GenericEvent.php | 3 + .../src/Event/Model/BeforeBatchEvent.php | 49 +- .../MultiFactor/BeforeDisplayMethods.php | 58 + .../src/Event/MultiFactor/Callback.php | 55 + .../src/Event/MultiFactor/Captive.php | 67 + .../src/Event/MultiFactor/GetMethod.php | 46 + .../src/Event/MultiFactor/GetSetup.php | 67 + .../src/Event/MultiFactor/NotifyActionLog.php | 59 + .../src/Event/MultiFactor/SaveSetup.php | 88 + .../src/Event/MultiFactor/Validate.php | 102 + .../src/Event/Plugin/System/Webauthn/Ajax.php | 25 + .../Plugin/System/Webauthn/AjaxChallenge.php | 48 + .../Plugin/System/Webauthn/AjaxCreate.php | 30 + .../Plugin/System/Webauthn/AjaxDelete.php | 30 + .../Plugin/System/Webauthn/AjaxInitCreate.php | 49 + .../Plugin/System/Webauthn/AjaxLogin.php | 25 + .../Plugin/System/Webauthn/AjaxSaveLabel.php | 30 + .../src/Event/QuickIcon/GetIconEvent.php | 66 + .../src/Event/ReshapeArgumentsAware.php | 99 + .../src/Event/Result/ResultAware.php | 102 + .../src/Event/Result/ResultAwareInterface.php | 55 + .../src/Event/Result/ResultTypeArrayAware.php | 71 + .../Event/Result/ResultTypeBooleanAware.php | 57 + .../src/Event/Result/ResultTypeFloatAware.php | 71 + .../Event/Result/ResultTypeIntegerAware.php | 71 + .../src/Event/Result/ResultTypeMixedAware.php | 44 + .../Event/Result/ResultTypeNumericAware.php | 71 + .../Event/Result/ResultTypeObjectAware.php | 95 + .../Event/Result/ResultTypeStringAware.php | 71 + .../src/Event/Table/AbstractEvent.php | 77 +- .../src/Event/Table/AfterBindEvent.php | 3 + .../src/Event/Table/AfterCheckinEvent.php | 3 + .../src/Event/Table/AfterCheckoutEvent.php | 3 + .../src/Event/Table/AfterDeleteEvent.php | 46 +- .../src/Event/Table/AfterHitEvent.php | 3 + .../src/Event/Table/AfterLoadEvent.php | 115 +- .../src/Event/Table/AfterMoveEvent.php | 163 +- .../src/Event/Table/AfterPublishEvent.php | 3 + .../src/Event/Table/AfterReorderEvent.php | 87 +- .../src/Event/Table/AfterResetEvent.php | 3 + .../src/Event/Table/AfterStoreEvent.php | 73 +- .../src/Event/Table/BeforeBindEvent.php | 121 +- .../src/Event/Table/BeforeCheckinEvent.php | 46 +- .../src/Event/Table/BeforeCheckoutEvent.php | 88 +- .../src/Event/Table/BeforeDeleteEvent.php | 46 +- .../src/Event/Table/BeforeHitEvent.php | 3 + .../src/Event/Table/BeforeLoadEvent.php | 83 +- .../src/Event/Table/BeforeMoveEvent.php | 163 +- .../src/Event/Table/BeforePublishEvent.php | 164 +- .../src/Event/Table/BeforeReorderEvent.php | 121 +- .../src/Event/Table/BeforeResetEvent.php | 3 + .../src/Event/Table/BeforeStoreEvent.php | 83 +- code/libraries/src/Event/Table/CheckEvent.php | 4 +- .../src/Event/Table/ObjectCreateEvent.php | 3 + .../src/Event/Table/SetNewTagsEvent.php | 83 +- .../libraries/src/Event/View/DisplayEvent.php | 81 +- .../src/Event/WebAsset/AbstractEvent.php | 42 +- .../WebAsset/WebAssetRegistryAssetChanged.php | 161 +- .../src/Event/Workflow/AbstractEvent.php | 78 +- .../WorkflowFunctionalityUsedEvent.php | 76 +- .../Workflow/WorkflowTransitionEvent.php | 78 +- .../src/Exception/ExceptionHandler.php | 417 +- .../Extension/BootableExtensionInterface.php | 35 +- code/libraries/src/Extension/Component.php | 71 +- .../src/Extension/ComponentInterface.php | 27 +- code/libraries/src/Extension/DummyPlugin.php | 7 +- .../src/Extension/ExtensionHelper.php | 922 +- .../Extension/ExtensionManagerInterface.php | 67 +- .../src/Extension/ExtensionManagerTrait.php | 444 +- .../src/Extension/LegacyComponent.php | 495 +- code/libraries/src/Extension/MVCComponent.php | 9 +- code/libraries/src/Extension/Module.php | 130 +- .../src/Extension/ModuleInterface.php | 31 +- .../src/Extension/PluginInterface.php | 23 +- .../Service/Provider/CategoryFactory.php | 86 +- .../Provider/ComponentDispatcherFactory.php | 82 +- .../Service/Provider/HelperFactory.php | 86 +- .../Extension/Service/Provider/MVCFactory.php | 105 +- .../src/Extension/Service/Provider/Module.php | 50 +- .../Provider/ModuleDispatcherFactory.php | 82 +- .../Service/Provider/RouterFactory.php | 99 +- code/libraries/src/Factory.php | 1578 +- code/libraries/src/Feed/Feed.php | 696 +- code/libraries/src/Feed/FeedEntry.php | 437 +- code/libraries/src/Feed/FeedFactory.php | 276 +- code/libraries/src/Feed/FeedLink.php | 140 +- code/libraries/src/Feed/FeedParser.php | 535 +- code/libraries/src/Feed/FeedPerson.php | 93 +- code/libraries/src/Feed/Parser/AtomParser.php | 438 +- .../Feed/Parser/NamespaceParserInterface.php | 51 +- .../src/Feed/Parser/Rss/ItunesRssParser.php | 59 +- .../src/Feed/Parser/Rss/MediaRssParser.php | 59 +- code/libraries/src/Feed/Parser/RssParser.php | 836 +- .../src/Fields/FieldsServiceInterface.php | 43 +- code/libraries/src/Filesystem/File.php | 1217 +- .../src/Filesystem/FilesystemHelper.php | 681 +- code/libraries/src/Filesystem/Folder.php | 1330 +- code/libraries/src/Filesystem/Patcher.php | 1008 +- code/libraries/src/Filesystem/Path.php | 737 +- code/libraries/src/Filesystem/Stream.php | 2668 ++- .../src/Filesystem/Streams/StreamString.php | 474 +- .../Filesystem/Support/StringController.php | 96 +- code/libraries/src/Filter/InputFilter.php | 973 +- code/libraries/src/Filter/OutputFilter.php | 193 +- .../src/Form/Field/AccessiblemediaField.php | 341 +- .../src/Form/Field/AccesslevelField.php | 43 +- .../src/Form/Field/AliastagField.php | 103 +- code/libraries/src/Form/Field/AuthorField.php | 115 +- .../src/Form/Field/CachehandlerField.php | 64 +- .../src/Form/Field/CalendarField.php | 727 +- .../libraries/src/Form/Field/CaptchaField.php | 313 +- .../src/Form/Field/CategoryField.php | 147 +- .../src/Form/Field/CheckboxField.php | 243 +- .../src/Form/Field/CheckboxesField.php | 287 +- .../src/Form/Field/ChromestyleField.php | 454 +- code/libraries/src/Form/Field/ColorField.php | 711 +- code/libraries/src/Form/Field/ComboField.php | 94 +- .../src/Form/Field/ComponentlayoutField.php | 476 +- .../src/Form/Field/ComponentsField.php | 107 +- .../src/Form/Field/ContenthistoryField.php | 102 +- .../src/Form/Field/ContentlanguageField.php | 43 +- .../src/Form/Field/ContenttypeField.php | 181 +- .../Form/Field/DatabaseconnectionField.php | 110 +- code/libraries/src/Form/Field/EditorField.php | 621 +- code/libraries/src/Form/Field/EmailField.php | 87 +- code/libraries/src/Form/Field/FileField.php | 256 +- .../src/Form/Field/FilelistField.php | 432 +- .../src/Form/Field/FolderlistField.php | 432 +- .../src/Form/Field/FrontendlanguageField.php | 95 +- .../src/Form/Field/GroupedlistField.php | 280 +- .../src/Form/Field/HeadertagField.php | 68 +- code/libraries/src/Form/Field/HiddenField.php | 81 +- .../src/Form/Field/ImagelistField.php | 47 +- .../libraries/src/Form/Field/IntegerField.php | 110 +- .../src/Form/Field/LanguageField.php | 134 +- .../Form/Field/LastvisitdaterangeField.php | 67 +- .../src/Form/Field/LimitboxField.php | 174 +- code/libraries/src/Form/Field/ListField.php | 425 +- code/libraries/src/Form/Field/MediaField.php | 810 +- code/libraries/src/Form/Field/MenuField.php | 219 +- .../src/Form/Field/MenuitemField.php | 471 +- code/libraries/src/Form/Field/MeterField.php | 352 +- .../src/Form/Field/ModulelayoutField.php | 376 +- .../src/Form/Field/ModuleorderField.php | 240 +- .../src/Form/Field/ModulepositionField.php | 305 +- .../src/Form/Field/ModuletagField.php | 68 +- code/libraries/src/Form/Field/NoteField.php | 136 +- code/libraries/src/Form/Field/NumberField.php | 385 +- .../src/Form/Field/OrderingField.php | 339 +- .../src/Form/Field/PasswordField.php | 413 +- .../libraries/src/Form/Field/PluginsField.php | 313 +- .../src/Form/Field/PluginstatusField.php | 37 +- .../src/Form/Field/PredefinedlistField.php | 215 +- code/libraries/src/Form/Field/RadioField.php | 71 +- .../src/Form/Field/RadiobasicField.php | 85 +- code/libraries/src/Form/Field/RangeField.php | 89 +- .../src/Form/Field/RedirectStatusField.php | 43 +- .../Form/Field/RegistrationdaterangeField.php | 85 +- code/libraries/src/Form/Field/RulesField.php | 531 +- .../src/Form/Field/SessionhandlerField.php | 66 +- code/libraries/src/Form/Field/SpacerField.php | 228 +- code/libraries/src/Form/Field/SqlField.php | 578 +- code/libraries/src/Form/Field/StatusField.php | 43 +- .../libraries/src/Form/Field/SubformField.php | 894 +- code/libraries/src/Form/Field/TagField.php | 658 +- .../src/Form/Field/TelephoneField.php | 89 +- .../src/Form/Field/TemplatestyleField.php | 356 +- code/libraries/src/Form/Field/TextField.php | 557 +- .../src/Form/Field/TextareaField.php | 340 +- code/libraries/src/Form/Field/TimeField.php | 310 +- .../src/Form/Field/TimezoneField.php | 294 +- .../src/Form/Field/TransitionField.php | 312 +- code/libraries/src/Form/Field/UrlField.php | 97 +- code/libraries/src/Form/Field/UserField.php | 306 +- .../src/Form/Field/UseractiveField.php | 75 +- .../src/Form/Field/UsergrouplistField.php | 139 +- .../src/Form/Field/UserstateField.php | 37 +- .../Field/WorkflowComponentSectionsField.php | 79 +- .../src/Form/Field/WorkflowconditionField.php | 225 +- .../src/Form/Field/WorkflowstageField.php | 271 +- .../src/Form/Filter/IntarrayFilter.php | 71 +- code/libraries/src/Form/Filter/RawFilter.php | 45 +- .../libraries/src/Form/Filter/RulesFilter.php | 74 +- .../src/Form/Filter/SafehtmlFilter.php | 55 +- code/libraries/src/Form/Filter/TelFilter.php | 176 +- .../libraries/src/Form/Filter/UnsetFilter.php | 45 +- code/libraries/src/Form/Filter/UrlFilter.php | 130 +- code/libraries/src/Form/Form.php | 3850 ++-- code/libraries/src/Form/FormFactory.php | 39 +- .../src/Form/FormFactoryAwareInterface.php | 23 +- .../src/Form/FormFactoryAwareTrait.php | 84 +- .../src/Form/FormFactoryInterface.php | 25 +- code/libraries/src/Form/FormField.php | 2709 ++- .../src/Form/FormFilterInterface.php | 39 +- code/libraries/src/Form/FormHelper.php | 1078 +- code/libraries/src/Form/FormRule.php | 125 +- code/libraries/src/Form/Rule/BooleanRule.php | 35 +- code/libraries/src/Form/Rule/CalendarRule.php | 74 +- code/libraries/src/Form/Rule/CaptchaRule.php | 82 +- code/libraries/src/Form/Rule/ColorRule.php | 78 +- .../src/Form/Rule/CssIdentifierRule.php | 117 +- .../Form/Rule/CssIdentifierSubstringRule.php | 104 +- code/libraries/src/Form/Rule/EmailRule.php | 352 +- code/libraries/src/Form/Rule/EqualsRule.php | 89 +- code/libraries/src/Form/Rule/ExistsRule.php | 92 +- code/libraries/src/Form/Rule/FilePathRule.php | 102 +- .../src/Form/Rule/FolderPathExistsRule.php | 83 +- .../src/Form/Rule/ModuleLayoutRule.php | 37 +- .../libraries/src/Form/Rule/NotequalsRule.php | 80 +- code/libraries/src/Form/Rule/NumberRule.php | 88 +- code/libraries/src/Form/Rule/OptionsRule.php | 121 +- code/libraries/src/Form/Rule/PasswordRule.php | 341 +- code/libraries/src/Form/Rule/RulesRule.php | 196 +- code/libraries/src/Form/Rule/SubformRule.php | 119 +- code/libraries/src/Form/Rule/TelRule.php | 152 +- code/libraries/src/Form/Rule/TimeRule.php | 309 +- code/libraries/src/Form/Rule/UrlRule.php | 207 +- code/libraries/src/Form/Rule/UserIdRule.php | 83 +- code/libraries/src/Form/Rule/UsernameRule.php | 97 +- code/libraries/src/HTML/HTMLHelper.php | 2518 ++- .../src/HTML/HTMLRegistryAwareTrait.php | 74 +- code/libraries/src/HTML/Helpers/Access.php | 569 +- .../src/HTML/Helpers/ActionsDropdown.php | 438 +- .../src/HTML/Helpers/AdminLanguage.php | 82 +- code/libraries/src/HTML/Helpers/Behavior.php | 501 +- code/libraries/src/HTML/Helpers/Bootstrap.php | 1652 +- code/libraries/src/HTML/Helpers/Category.php | 385 +- code/libraries/src/HTML/Helpers/Content.php | 116 +- .../src/HTML/Helpers/ContentLanguage.php | 103 +- code/libraries/src/HTML/Helpers/Date.php | 139 +- code/libraries/src/HTML/Helpers/Debug.php | 93 +- .../src/HTML/Helpers/DraggableList.php | 111 +- code/libraries/src/HTML/Helpers/Dropdown.php | 598 +- code/libraries/src/HTML/Helpers/Email.php | 90 +- code/libraries/src/HTML/Helpers/Form.php | 110 +- .../src/HTML/Helpers/FormBehavior.php | 277 +- code/libraries/src/HTML/Helpers/Grid.php | 513 +- code/libraries/src/HTML/Helpers/Icons.php | 118 +- code/libraries/src/HTML/Helpers/JGrid.php | 828 +- code/libraries/src/HTML/Helpers/Jquery.php | 130 +- code/libraries/src/HTML/Helpers/Links.php | 215 +- .../libraries/src/HTML/Helpers/ListHelper.php | 513 +- code/libraries/src/HTML/Helpers/Menu.php | 855 +- code/libraries/src/HTML/Helpers/Number.php | 169 +- .../src/HTML/Helpers/SearchTools.php | 226 +- code/libraries/src/HTML/Helpers/Select.php | 1388 +- code/libraries/src/HTML/Helpers/Sidebar.php | 267 +- .../src/HTML/Helpers/SortableList.php | 45 +- .../src/HTML/Helpers/StringHelper.php | 540 +- code/libraries/src/HTML/Helpers/Tag.php | 389 +- code/libraries/src/HTML/Helpers/Telephone.php | 104 +- code/libraries/src/HTML/Helpers/UiTab.php | 182 +- code/libraries/src/HTML/Helpers/User.php | 115 +- .../src/HTML/Helpers/WorkflowStage.php | 98 +- code/libraries/src/HTML/Registry.php | 213 +- code/libraries/src/Help/Help.php | 351 +- .../src/Helper/AuthenticationHelper.php | 291 +- code/libraries/src/Helper/CMSHelper.php | 225 +- code/libraries/src/Helper/ContentHelper.php | 489 +- code/libraries/src/Helper/HelperFactory.php | 97 +- .../Helper/HelperFactoryAwareInterface.php | 33 + .../src/Helper/HelperFactoryAwareTrait.php | 63 + .../src/Helper/HelperFactoryInterface.php | 25 +- code/libraries/src/Helper/LibraryHelper.php | 316 +- code/libraries/src/Helper/MediaHelper.php | 989 +- code/libraries/src/Helper/ModuleHelper.php | 1421 +- code/libraries/src/Helper/RouteHelper.php | 538 +- code/libraries/src/Helper/TagsHelper.php | 2114 +- .../libraries/src/Helper/UserGroupsHelper.php | 652 +- code/libraries/src/Http/Http.php | 72 +- code/libraries/src/Http/HttpFactory.php | 230 +- code/libraries/src/Http/Response.php | 7 +- .../src/Http/Transport/CurlTransport.php | 562 +- .../src/Http/Transport/SocketTransport.php | 530 +- .../src/Http/Transport/StreamTransport.php | 422 +- .../libraries/src/Http/TransportInterface.php | 7 +- .../Exception/UnparsableImageException.php | 3 + .../src/Image/Filter/Backgroundfill.php | 224 +- .../libraries/src/Image/Filter/Brightness.php | 46 +- code/libraries/src/Image/Filter/Contrast.php | 46 +- .../libraries/src/Image/Filter/Edgedetect.php | 35 +- code/libraries/src/Image/Filter/Emboss.php | 36 +- code/libraries/src/Image/Filter/Grayscale.php | 35 +- code/libraries/src/Image/Filter/Negate.php | 35 +- code/libraries/src/Image/Filter/Sketchy.php | 35 +- code/libraries/src/Image/Filter/Smooth.php | 46 +- code/libraries/src/Image/Image.php | 2246 +- code/libraries/src/Image/ImageFilter.php | 87 +- code/libraries/src/Input/Cli.php | 357 +- code/libraries/src/Input/Cookie.php | 156 +- code/libraries/src/Input/Files.php | 243 +- code/libraries/src/Input/Input.php | 368 +- code/libraries/src/Input/Json.php | 104 +- .../Installer/Adapter/ComponentAdapter.php | 2981 ++- .../src/Installer/Adapter/FileAdapter.php | 1130 +- .../src/Installer/Adapter/LanguageAdapter.php | 1796 +- .../src/Installer/Adapter/LibraryAdapter.php | 970 +- .../src/Installer/Adapter/ModuleAdapter.php | 1423 +- .../src/Installer/Adapter/PackageAdapter.php | 1411 +- .../src/Installer/Adapter/PluginAdapter.php | 1216 +- .../src/Installer/Adapter/TemplateAdapter.php | 1349 +- code/libraries/src/Installer/Installer.php | 4823 +++-- .../src/Installer/InstallerAdapter.php | 2543 ++- .../src/Installer/InstallerExtension.php | 264 +- .../src/Installer/InstallerHelper.php | 691 +- .../src/Installer/InstallerScript.php | 772 +- .../Installer/InstallerScriptInterface.php | 79 + .../src/Installer/LegacyInstallerScript.php | 180 + code/libraries/src/Installer/Manifest.php | 239 +- .../Installer/Manifest/LibraryManifest.php | 163 +- .../Installer/Manifest/PackageManifest.php | 157 +- code/libraries/src/Language/Associations.php | 352 +- .../src/Language/CachingLanguageFactory.php | 52 +- code/libraries/src/Language/Language.php | 2358 +-- .../src/Language/LanguageFactory.php | 31 +- .../src/Language/LanguageFactoryInterface.php | 25 +- .../libraries/src/Language/LanguageHelper.php | 1278 +- code/libraries/src/Language/Multilanguage.php | 213 +- code/libraries/src/Language/Text.php | 745 +- code/libraries/src/Language/Transliterate.php | 483 +- code/libraries/src/Layout/BaseLayout.php | 560 +- code/libraries/src/Layout/FileLayout.php | 1213 +- code/libraries/src/Layout/LayoutHelper.php | 99 +- code/libraries/src/Layout/LayoutInterface.php | 43 +- .../libraries/src/Log/DelegatingPsrLogger.php | 162 +- code/libraries/src/Log/Log.php | 757 +- code/libraries/src/Log/LogEntry.php | 211 +- code/libraries/src/Log/Logger.php | 95 +- .../src/Log/Logger/CallbackLogger.php | 88 +- .../src/Log/Logger/DatabaseLogger.php | 283 +- code/libraries/src/Log/Logger/EchoLogger.php | 86 +- .../src/Log/Logger/FormattedtextLogger.php | 552 +- .../src/Log/Logger/InMemoryLogger.php | 119 +- .../src/Log/Logger/MessagequeueLogger.php | 72 +- .../libraries/src/Log/Logger/SyslogLogger.php | 213 +- code/libraries/src/Log/Logger/W3cLogger.php | 54 +- code/libraries/src/Log/LoggerRegistry.php | 144 +- .../src/MVC/Controller/AdminController.php | 920 +- .../src/MVC/Controller/ApiController.php | 1034 +- .../src/MVC/Controller/BaseController.php | 2150 +- .../MVC/Controller/ControllerInterface.php | 27 +- .../Controller/Exception/CheckinCheckout.php | 3 + .../Controller/Exception/ResourceNotFound.php | 3 + .../src/MVC/Controller/Exception/Save.php | 3 + .../MVC/Controller/Exception/SendEmail.php | 3 + .../src/MVC/Controller/FormController.php | 1743 +- .../src/MVC/Factory/ApiMVCFactory.php | 85 +- .../src/MVC/Factory/LegacyFactory.php | 210 +- code/libraries/src/MVC/Factory/MVCFactory.php | 617 +- .../src/MVC/Factory/MVCFactoryAwareTrait.php | 76 +- .../src/MVC/Factory/MVCFactoryInterface.php | 117 +- .../Factory/MVCFactoryServiceInterface.php | 21 +- .../MVC/Factory/MVCFactoryServiceTrait.php | 72 +- code/libraries/src/MVC/Model/AdminModel.php | 3429 ++- .../src/MVC/Model/BaseDatabaseModel.php | 680 +- code/libraries/src/MVC/Model/BaseModel.php | 252 +- .../src/MVC/Model/DatabaseAwareTrait.php | 92 +- .../src/MVC/Model/DatabaseModelInterface.php | 23 +- .../src/MVC/Model/FormBehaviorTrait.php | 322 +- code/libraries/src/MVC/Model/FormModel.php | 420 +- .../src/MVC/Model/FormModelInterface.php | 33 +- code/libraries/src/MVC/Model/ItemModel.php | 67 +- .../src/MVC/Model/ItemModelInterface.php | 25 +- .../src/MVC/Model/LegacyModelLoaderTrait.php | 303 +- code/libraries/src/MVC/Model/ListModel.php | 1385 +- .../src/MVC/Model/ListModelInterface.php | 23 +- .../src/MVC/Model/ModelInterface.php | 21 +- .../src/MVC/Model/StateBehaviorTrait.php | 148 +- .../src/MVC/Model/StatefulModelInterface.php | 47 +- .../src/MVC/Model/WorkflowBehaviorTrait.php | 792 +- .../src/MVC/Model/WorkflowModelInterface.php | 214 +- code/libraries/src/MVC/View/AbstractView.php | 467 +- .../libraries/src/MVC/View/CategoriesView.php | 227 +- .../src/MVC/View/CategoryFeedView.php | 255 +- code/libraries/src/MVC/View/CategoryView.php | 590 +- .../src/MVC/View/Event/OnGetApiFields.php | 337 +- code/libraries/src/MVC/View/FormView.php | 439 +- .../src/MVC/View/GenericDataException.php | 3 + code/libraries/src/MVC/View/HtmlView.php | 1181 +- code/libraries/src/MVC/View/JsonApiView.php | 515 +- code/libraries/src/MVC/View/JsonView.php | 145 +- code/libraries/src/MVC/View/ListView.php | 494 +- code/libraries/src/MVC/View/ViewInterface.php | 43 +- .../Mail/Exception/MailDisabledException.php | 97 +- code/libraries/src/Mail/Mail.php | 1344 +- code/libraries/src/Mail/MailHelper.php | 499 +- code/libraries/src/Mail/MailTemplate.php | 944 +- .../Mail/language/phpmailer.lang-en_gb.php | 1 + code/libraries/src/Menu/AbstractMenu.php | 798 +- code/libraries/src/Menu/AdministratorMenu.php | 25 +- .../src/Menu/AdministratorMenuItem.php | 45 +- code/libraries/src/Menu/MenuFactory.php | 68 +- .../src/Menu/MenuFactoryInterface.php | 25 +- code/libraries/src/Menu/MenuItem.php | 458 +- code/libraries/src/Menu/SiteMenu.php | 521 +- code/libraries/src/Microdata/Microdata.php | 1698 +- code/libraries/src/Object/CMSObject.php | 422 +- code/libraries/src/Pagination/Pagination.php | 1509 +- .../src/Pagination/PaginationObject.php | 91 +- code/libraries/src/Pathway/Pathway.php | 342 +- code/libraries/src/Pathway/SitePathway.php | 117 +- code/libraries/src/Plugin/CMSPlugin.php | 681 +- code/libraries/src/Plugin/PluginHelper.php | 564 +- code/libraries/src/Profiler/Profiler.php | 332 +- code/libraries/src/Response/JsonResponse.php | 183 +- .../src/Router/AdministratorRouter.php | 83 +- code/libraries/src/Router/ApiRouter.php | 378 +- .../Exception/RouteNotFoundException.php | 36 +- code/libraries/src/Router/Route.php | 339 +- code/libraries/src/Router/Router.php | 881 +- code/libraries/src/Router/SiteRouter.php | 1195 +- .../src/Router/SiteRouterAwareInterface.php | 33 + .../src/Router/SiteRouterAwareTrait.php | 60 + code/libraries/src/Schema/ChangeItem.php | 443 +- .../src/Schema/ChangeItem/MysqlChangeItem.php | 765 +- .../ChangeItem/PostgresqlChangeItem.php | 637 +- .../Schema/ChangeItem/SqlsrvChangeItem.php | 267 +- code/libraries/src/Schema/ChangeSet.php | 594 +- .../Serializer/Events/OnGetApiAttributes.php | 145 +- .../Serializer/Events/OnGetApiRelation.php | 124 +- .../src/Serializer/JoomlaSerializer.php | 182 +- .../src/Service/Provider/ApiRouter.php | 45 - .../src/Service/Provider/Application.php | 295 +- .../src/Service/Provider/Authentication.php | 223 +- .../src/Service/Provider/CacheController.php | 50 +- .../libraries/src/Service/Provider/Config.php | 70 +- .../src/Service/Provider/Console.php | 380 +- .../src/Service/Provider/Database.php | 240 +- .../src/Service/Provider/Dispatcher.php | 50 +- .../src/Service/Provider/Document.php | 54 +- code/libraries/src/Service/Provider/Form.php | 54 +- .../src/Service/Provider/HTMLRegistry.php | 46 +- .../src/Service/Provider/Language.php | 50 +- .../libraries/src/Service/Provider/Logger.php | 48 +- code/libraries/src/Service/Provider/Menu.php | 56 +- .../src/Service/Provider/Pathway.php | 73 +- .../libraries/src/Service/Provider/Router.php | 71 + .../src/Service/Provider/Session.php | 615 +- .../src/Service/Provider/Toolbar.php | 54 +- code/libraries/src/Service/Provider/User.php | 50 +- .../src/Service/Provider/WebAssetRegistry.php | 60 +- .../EventListener/MetadataManagerListener.php | 102 +- .../Exception/UnsupportedStorageException.php | 3 + .../libraries/src/Session/MetadataManager.php | 603 +- code/libraries/src/Session/Session.php | 641 +- code/libraries/src/Session/SessionFactory.php | 283 +- code/libraries/src/Session/SessionManager.php | 105 +- .../src/Session/Storage/JoomlaStorage.php | 569 +- code/libraries/src/String/PunycodeHelper.php | 469 +- code/libraries/src/Table/Asset.php | 391 +- code/libraries/src/Table/Category.php | 520 +- code/libraries/src/Table/Content.php | 715 +- code/libraries/src/Table/ContentHistory.php | 413 +- code/libraries/src/Table/ContentType.php | 280 +- code/libraries/src/Table/CoreContent.php | 628 +- code/libraries/src/Table/Extension.php | 194 +- code/libraries/src/Table/Language.php | 267 +- code/libraries/src/Table/Menu.php | 581 +- code/libraries/src/Table/MenuType.php | 595 +- code/libraries/src/Table/Module.php | 391 +- code/libraries/src/Table/Nested.php | 3465 ++- code/libraries/src/Table/Table.php | 3818 ++-- code/libraries/src/Table/TableInterface.php | 221 +- code/libraries/src/Table/Ucm.php | 25 +- code/libraries/src/Table/Update.php | 178 +- code/libraries/src/Table/UpdateSite.php | 79 +- code/libraries/src/Table/User.php | 1045 +- code/libraries/src/Table/Usergroup.php | 623 +- code/libraries/src/Table/ViewLevel.php | 172 +- .../src/Tag/TagApiSerializerTrait.php | 52 +- .../libraries/src/Tag/TagServiceInterface.php | 27 +- code/libraries/src/Tag/TagServiceTrait.php | 105 +- .../src/Tag/TaggableTableInterface.php | 87 +- code/libraries/src/Tag/TaggableTableTrait.php | 95 +- .../Toolbar/Button/AbstractGroupButton.php | 102 +- .../src/Toolbar/Button/BasicButton.php | 55 +- .../src/Toolbar/Button/ConfirmButton.php | 160 +- .../src/Toolbar/Button/CustomButton.php | 99 +- .../src/Toolbar/Button/DropdownButton.php | 191 +- .../src/Toolbar/Button/HelpButton.php | 159 +- .../src/Toolbar/Button/InlinehelpButton.php | 152 +- .../src/Toolbar/Button/LinkButton.php | 127 +- .../src/Toolbar/Button/PopupButton.php | 360 +- .../src/Toolbar/Button/SeparatorButton.php | 47 +- .../src/Toolbar/Button/StandardButton.php | 247 +- .../Toolbar/ContainerAwareToolbarFactory.php | 191 +- .../src/Toolbar/CoreButtonsTrait.php | 1034 +- code/libraries/src/Toolbar/Toolbar.php | 898 +- code/libraries/src/Toolbar/ToolbarButton.php | 968 +- .../src/Toolbar/ToolbarFactoryInterface.php | 47 +- code/libraries/src/Toolbar/ToolbarHelper.php | 1471 +- .../src/Tree/ImmutableNodeInterface.php | 107 +- .../libraries/src/Tree/ImmutableNodeTrait.php | 274 +- code/libraries/src/Tree/NodeInterface.php | 93 +- code/libraries/src/Tree/NodeTrait.php | 154 +- code/libraries/src/UCM/UCM.php | 3 + code/libraries/src/UCM/UCMBase.php | 238 +- code/libraries/src/UCM/UCMContent.php | 428 +- code/libraries/src/UCM/UCMType.php | 392 +- .../src/Updater/Adapter/CollectionAdapter.php | 450 +- .../src/Updater/Adapter/ExtensionAdapter.php | 651 +- code/libraries/src/Updater/DownloadSource.php | 129 +- code/libraries/src/Updater/Update.php | 1030 +- code/libraries/src/Updater/UpdateAdapter.php | 532 +- code/libraries/src/Updater/Updater.php | 834 +- code/libraries/src/Uri/Uri.php | 555 +- .../src/User/CurrentUserInterface.php | 33 + code/libraries/src/User/CurrentUserTrait.php | 67 + code/libraries/src/User/User.php | 1753 +- code/libraries/src/User/UserFactory.php | 105 +- .../src/User/UserFactoryInterface.php | 43 +- code/libraries/src/User/UserHelper.php | 1250 +- .../src/Utility/BufferStreamHandler.php | 458 +- code/libraries/src/Utility/Utility.php | 109 +- code/libraries/src/Version.php | 587 +- .../Versioning/VersionableControllerTrait.php | 166 +- .../src/Versioning/VersionableModelTrait.php | 126 +- .../Versioning/VersionableTableInterface.php | 29 +- code/libraries/src/Versioning/Versioning.php | 266 +- .../src/WebAsset/AssetItem/CoreAssetItem.php | 55 +- .../AssetItem/FormValidateAssetItem.php | 43 +- .../WebAsset/AssetItem/KeepaliveAssetItem.php | 77 +- .../AssetItem/LangActiveAssetItem.php | 69 +- .../AssetItem/TableColumnsAssetItem.php | 43 + .../Exception/InvalidActionException.php | 4 +- .../Exception/UnknownAssetException.php | 4 +- .../UnsatisfiedDependencyException.php | 4 +- .../Exception/WebAssetExceptionInterface.php | 3 + .../WebAssetAttachBehaviorInterface.php | 29 +- code/libraries/src/WebAsset/WebAssetItem.php | 649 +- .../src/WebAsset/WebAssetItemInterface.php | 192 +- .../src/WebAsset/WebAssetManager.php | 1994 +- .../src/WebAsset/WebAssetManagerInterface.php | 119 +- .../src/WebAsset/WebAssetRegistry.php | 855 +- .../WebAsset/WebAssetRegistryInterface.php | 100 +- code/libraries/src/Workflow/Workflow.php | 1046 +- .../src/Workflow/WorkflowPluginTrait.php | 259 +- .../src/Workflow/WorkflowServiceInterface.php | 155 +- .../src/Workflow/WorkflowServiceTrait.php | 308 +- code/libraries/vendor/autoload.php | 2 +- .../vendor/composer/InstalledVersions.php | 18 +- code/libraries/vendor/composer/LICENSE | 2 + .../vendor/composer/autoload_classmap.php | 507 +- .../vendor/composer/autoload_files.php | 5 + .../vendor/composer/autoload_psr4.php | 7 + .../vendor/composer/autoload_real.php | 14 +- .../vendor/composer/autoload_static.php | 566 +- code/libraries/vendor/composer/installed.json | 1424 +- code/libraries/vendor/composer/installed.php | 387 +- .../src/Cron/AbstractField.php | 73 +- .../src/Cron/CronExpression.php | 138 +- .../src/Cron/DayOfMonthField.php | 21 +- .../src/Cron/DayOfWeekField.php | 28 +- .../src/Cron/FieldInterface.php | 4 +- .../cron-expression/src/Cron/HoursField.php | 156 +- .../cron-expression/src/Cron/MinutesField.php | 36 +- .../cron-expression/src/Cron/MonthField.php | 12 +- .../database/src/DatabaseAwareInterface.php | 28 + .../database/src/DatabaseAwareTrait.php | 59 + .../Exception/DatabaseNotFoundException.php | 18 + .../joomla/database/src/Mysql/MysqlDriver.php | 2 +- .../database/src/Mysqli/MysqliDriver.php | 4 +- .../joomla/database/src/Pdo/PdoDriver.php | 4 +- code/libraries/vendor/lcobucci/jwt/LICENSE | 27 + .../lcobucci/jwt/compat/class-aliases.php | 4 + .../jwt/compat/json-exception-polyfill.php | 7 + .../jwt/compat/lcobucci-clock-polyfill.php | 70 + .../vendor/lcobucci/jwt/src/Builder.php | 592 + .../vendor/lcobucci/jwt/src/Claim.php | 40 + .../vendor/lcobucci/jwt/src/Claim/Basic.php | 75 + .../lcobucci/jwt/src/Claim/EqualsTo.php | 34 + .../vendor/lcobucci/jwt/src/Claim/Factory.php | 131 + .../jwt/src/Claim/GreaterOrEqualsTo.php | 34 + .../jwt/src/Claim/LesserOrEqualsTo.php | 34 + .../lcobucci/jwt/src/Claim/Validatable.php | 30 + .../vendor/lcobucci/jwt/src/Configuration.php | 178 + .../jwt/src/Encoding/CannotDecodeContent.php | 26 + .../jwt/src/Encoding/CannotEncodeContent.php | 20 + .../vendor/lcobucci/jwt/src/Exception.php | 13 + .../vendor/lcobucci/jwt/src/Parser.php | 175 + .../lcobucci/jwt/src/Parsing/Decoder.php | 70 + .../lcobucci/jwt/src/Parsing/Encoder.php | 65 + .../vendor/lcobucci/jwt/src/Signature.php | 87 + .../vendor/lcobucci/jwt/src/Signer.php | 59 + .../lcobucci/jwt/src/Signer/BaseSigner.php | 89 + .../jwt/src/Signer/CannotSignPayload.php | 19 + .../vendor/lcobucci/jwt/src/Signer/Ecdsa.php | 69 + .../jwt/src/Signer/Ecdsa/ConversionFailed.php | 27 + .../Signer/Ecdsa/MultibyteStringConverter.php | 133 + .../lcobucci/jwt/src/Signer/Ecdsa/Sha256.php | 43 + .../lcobucci/jwt/src/Signer/Ecdsa/Sha384.php | 43 + .../lcobucci/jwt/src/Signer/Ecdsa/Sha512.php | 43 + .../src/Signer/Ecdsa/SignatureConverter.php | 38 + .../vendor/lcobucci/jwt/src/Signer/Hmac.php | 46 + .../lcobucci/jwt/src/Signer/Hmac/Sha256.php | 35 + .../lcobucci/jwt/src/Signer/Hmac/Sha384.php | 35 + .../lcobucci/jwt/src/Signer/Hmac/Sha512.php | 35 + .../jwt/src/Signer/InvalidKeyProvided.php | 25 + .../vendor/lcobucci/jwt/src/Signer/Key.php | 117 + .../jwt/src/Signer/Key/FileCouldNotBeRead.php | 39 + .../lcobucci/jwt/src/Signer/Key/InMemory.php | 52 + .../jwt/src/Signer/Key/LocalFileReference.php | 32 + .../lcobucci/jwt/src/Signer/Keychain.php | 44 + .../vendor/lcobucci/jwt/src/Signer/None.php | 21 + .../lcobucci/jwt/src/Signer/OpenSSL.php | 108 + .../vendor/lcobucci/jwt/src/Signer/Rsa.php | 24 + .../lcobucci/jwt/src/Signer/Rsa/Sha256.php | 35 + .../lcobucci/jwt/src/Signer/Rsa/Sha384.php | 35 + .../lcobucci/jwt/src/Signer/Rsa/Sha512.php | 35 + .../vendor/lcobucci/jwt/src/Token.php | 430 + .../vendor/lcobucci/jwt/src/Token/DataSet.php | 56 + .../jwt/src/Token/InvalidTokenStructure.php | 35 + .../vendor/lcobucci/jwt/src/Token/Plain.php | 8 + .../jwt/src/Token/RegisteredClaimGiven.php | 24 + .../jwt/src/Token/RegisteredClaims.php | 76 + .../lcobucci/jwt/src/Token/Signature.php | 8 + .../jwt/src/Token/UnsupportedHeaderFound.php | 15 + .../jwt/src/Validation/Constraint.php | 11 + .../Validation/Constraint/IdentifiedBy.php | 28 + .../src/Validation/Constraint/IssuedBy.php | 28 + .../Constraint/LeewayCannotBeNegative.php | 15 + .../Validation/Constraint/PermittedFor.php | 27 + .../src/Validation/Constraint/RelatedTo.php | 27 + .../src/Validation/Constraint/SignedWith.php | 34 + .../jwt/src/Validation/Constraint/ValidAt.php | 72 + .../src/Validation/ConstraintViolation.php | 10 + .../jwt/src/Validation/NoConstraintsGiven.php | 10 + .../RequiredConstraintsViolated.php | 53 + .../lcobucci/jwt/src/Validation/Validator.php | 55 + .../lcobucci/jwt/src/ValidationData.php | 132 + .../vendor/lcobucci/jwt/src/Validator.php | 23 + .../libraries/vendor/nyholm/psr7/doc/final.md | 20 - .../psr7/src/Factory/HttplugFactory.php | 9 +- .../nyholm/psr7/src/Factory/Psr17Factory.php | 10 +- .../vendor/nyholm/psr7/src/ServerRequest.php | 3 + .../vendor/nyholm/psr7/src/Stream.php | 39 +- .../vendor/nyholm/psr7/src/UploadedFile.php | 29 +- .../constant_time_encoding/LICENSE.txt | 48 + .../constant_time_encoding/src/Base32.php | 519 + .../constant_time_encoding/src/Base32Hex.php | 111 + .../constant_time_encoding/src/Base64.php | 314 + .../src/Base64DotSlash.php | 88 + .../src/Base64DotSlashOrdered.php | 82 + .../src/Base64UrlSafe.php | 95 + .../constant_time_encoding/src/Binary.php | 90 + .../src/EncoderInterface.php | 52 + .../constant_time_encoding/src/Encoding.php | 262 + .../constant_time_encoding/src/Hex.php | 146 + .../constant_time_encoding/src/RFC4648.php | 186 + .../paragonie/sodium_compat/psalm-above-3.xml | 1 - .../paragonie/sodium_compat/psalm-below-3.xml | 1 - .../paragonie/sodium_compat/src/Compat.php | 4 + .../sodium_compat/src/Core/BLAKE2b.php | 19 +- .../sodium_compat/src/Core/Curve25519.php | 151 +- .../paragonie/sodium_compat/src/Core/Util.php | 8 +- .../sodium_compat/src/Core32/Curve25519.php | 57 +- .../src/Core32/Curve25519/Fe.php | 3 + .../sodium_compat/src/Core32/Int32.php | 9 +- .../sodium_compat/src/Core32/Int64.php | 6 +- .../src/Core32/Poly1305/State.php | 2 +- .../sodium_compat/src/Core32/XChaCha20.php | 23 + .../paragonie/sodium_compat/src/File.php | 15 +- .../vendor/phpmailer/phpmailer/VERSION | 2 +- .../vendor/phpmailer/phpmailer/src/OAuth.php | 2 +- .../phpmailer/src/OAuthTokenProvider.php | 44 + .../phpmailer/phpmailer/src/PHPMailer.php | 65 +- .../vendor/phpmailer/phpmailer/src/POP3.php | 2 +- .../vendor/phpmailer/phpmailer/src/SMTP.php | 5 +- .../vendor/phpseclib/bcmath_compat/LICENSE.md | 21 + .../phpseclib/bcmath_compat/lib/bcmath.php | 185 + .../phpseclib/bcmath_compat/src/BCMath.php | 494 + .../vendor/phpseclib/phpseclib/AUTHORS | 6 + .../vendor/phpseclib/phpseclib/BACKERS.md | 13 + .../vendor/phpseclib/phpseclib/LICENSE | 20 + .../phpseclib/Common/Functions/Strings.php | 428 + .../phpseclib/phpseclib/Crypt/AES.php | 123 + .../phpseclib/phpseclib/Crypt/Blowfish.php | 556 + .../phpseclib/phpseclib/Crypt/ChaCha20.php | 804 + .../phpseclib/Crypt/Common/AsymmetricKey.php | 624 + .../phpseclib/Crypt/Common/BlockCipher.php | 27 + .../Crypt/Common/Formats/Keys/OpenSSH.php | 234 + .../Crypt/Common/Formats/Keys/PKCS.php | 80 + .../Crypt/Common/Formats/Keys/PKCS1.php | 223 + .../Crypt/Common/Formats/Keys/PKCS8.php | 707 + .../Crypt/Common/Formats/Keys/PuTTY.php | 389 + .../Crypt/Common/Formats/Signature/Raw.php | 66 + .../phpseclib/Crypt/Common/PrivateKey.php | 35 + .../phpseclib/Crypt/Common/PublicKey.php | 29 + .../phpseclib/Crypt/Common/StreamCipher.php | 59 + .../phpseclib/Crypt/Common/SymmetricKey.php | 3419 +++ .../Crypt/Common/Traits/Fingerprint.php | 62 + .../Crypt/Common/Traits/PasswordProtected.php | 51 + .../phpseclib/phpseclib/Crypt/DES.php | 1411 ++ .../phpseclib/phpseclib/Crypt/DH.php | 403 + .../phpseclib/Crypt/DH/Formats/Keys/PKCS1.php | 83 + .../phpseclib/Crypt/DH/Formats/Keys/PKCS8.php | 157 + .../phpseclib/Crypt/DH/Parameters.php | 40 + .../phpseclib/Crypt/DH/PrivateKey.php | 82 + .../phpseclib/Crypt/DH/PublicKey.php | 53 + .../phpseclib/phpseclib/Crypt/DSA.php | 347 + .../Crypt/DSA/Formats/Keys/OpenSSH.php | 125 + .../Crypt/DSA/Formats/Keys/PKCS1.php | 151 + .../Crypt/DSA/Formats/Keys/PKCS8.php | 170 + .../Crypt/DSA/Formats/Keys/PuTTY.php | 118 + .../phpseclib/Crypt/DSA/Formats/Keys/Raw.php | 92 + .../phpseclib/Crypt/DSA/Formats/Keys/XML.php | 139 + .../Crypt/DSA/Formats/Signature/ASN1.php | 68 + .../Crypt/DSA/Formats/Signature/Raw.php | 29 + .../Crypt/DSA/Formats/Signature/SSH2.php | 80 + .../phpseclib/Crypt/DSA/Parameters.php | 40 + .../phpseclib/Crypt/DSA/PrivateKey.php | 159 + .../phpseclib/Crypt/DSA/PublicKey.php | 91 + .../phpseclib/phpseclib/Crypt/EC.php | 480 + .../phpseclib/Crypt/EC/BaseCurves/Base.php | 236 + .../phpseclib/Crypt/EC/BaseCurves/Binary.php | 377 + .../Crypt/EC/BaseCurves/KoblitzPrime.php | 324 + .../Crypt/EC/BaseCurves/Montgomery.php | 283 + .../phpseclib/Crypt/EC/BaseCurves/Prime.php | 786 + .../Crypt/EC/BaseCurves/TwistedEdwards.php | 219 + .../phpseclib/Crypt/EC/Curves/Curve25519.php | 83 + .../phpseclib/Crypt/EC/Curves/Curve448.php | 94 + .../phpseclib/Crypt/EC/Curves/Ed25519.php | 333 + .../phpseclib/Crypt/EC/Curves/Ed448.php | 270 + .../Crypt/EC/Curves/brainpoolP160r1.php | 36 + .../Crypt/EC/Curves/brainpoolP160t1.php | 49 + .../Crypt/EC/Curves/brainpoolP192r1.php | 36 + .../Crypt/EC/Curves/brainpoolP192t1.php | 36 + .../Crypt/EC/Curves/brainpoolP224r1.php | 36 + .../Crypt/EC/Curves/brainpoolP224t1.php | 36 + .../Crypt/EC/Curves/brainpoolP256r1.php | 36 + .../Crypt/EC/Curves/brainpoolP256t1.php | 36 + .../Crypt/EC/Curves/brainpoolP320r1.php | 42 + .../Crypt/EC/Curves/brainpoolP320t1.php | 42 + .../Crypt/EC/Curves/brainpoolP384r1.php | 60 + .../Crypt/EC/Curves/brainpoolP384t1.php | 60 + .../Crypt/EC/Curves/brainpoolP512r1.php | 60 + .../Crypt/EC/Curves/brainpoolP512t1.php | 60 + .../phpseclib/Crypt/EC/Curves/nistb233.php | 20 + .../phpseclib/Crypt/EC/Curves/nistb409.php | 20 + .../phpseclib/Crypt/EC/Curves/nistk163.php | 20 + .../phpseclib/Crypt/EC/Curves/nistk233.php | 20 + .../phpseclib/Crypt/EC/Curves/nistk283.php | 20 + .../phpseclib/Crypt/EC/Curves/nistk409.php | 20 + .../phpseclib/Crypt/EC/Curves/nistp192.php | 20 + .../phpseclib/Crypt/EC/Curves/nistp224.php | 20 + .../phpseclib/Crypt/EC/Curves/nistp256.php | 20 + .../phpseclib/Crypt/EC/Curves/nistp384.php | 20 + .../phpseclib/Crypt/EC/Curves/nistp521.php | 20 + .../phpseclib/Crypt/EC/Curves/nistt571.php | 20 + .../phpseclib/Crypt/EC/Curves/prime192v1.php | 20 + .../phpseclib/Crypt/EC/Curves/prime192v2.php | 36 + .../phpseclib/Crypt/EC/Curves/prime192v3.php | 36 + .../phpseclib/Crypt/EC/Curves/prime239v1.php | 36 + .../phpseclib/Crypt/EC/Curves/prime239v2.php | 36 + .../phpseclib/Crypt/EC/Curves/prime239v3.php | 36 + .../phpseclib/Crypt/EC/Curves/prime256v1.php | 20 + .../phpseclib/Crypt/EC/Curves/secp112r1.php | 36 + .../phpseclib/Crypt/EC/Curves/secp112r2.php | 37 + .../phpseclib/Crypt/EC/Curves/secp128r1.php | 36 + .../phpseclib/Crypt/EC/Curves/secp128r2.php | 37 + .../phpseclib/Crypt/EC/Curves/secp160k1.php | 48 + .../phpseclib/Crypt/EC/Curves/secp160r1.php | 36 + .../phpseclib/Crypt/EC/Curves/secp160r2.php | 37 + .../phpseclib/Crypt/EC/Curves/secp192k1.php | 47 + .../phpseclib/Crypt/EC/Curves/secp192r1.php | 80 + .../phpseclib/Crypt/EC/Curves/secp224k1.php | 47 + .../phpseclib/Crypt/EC/Curves/secp224r1.php | 36 + .../phpseclib/Crypt/EC/Curves/secp256k1.php | 51 + .../phpseclib/Crypt/EC/Curves/secp256r1.php | 36 + .../phpseclib/Crypt/EC/Curves/secp384r1.php | 54 + .../phpseclib/Crypt/EC/Curves/secp521r1.php | 48 + .../phpseclib/Crypt/EC/Curves/sect113r1.php | 36 + .../phpseclib/Crypt/EC/Curves/sect113r2.php | 36 + .../phpseclib/Crypt/EC/Curves/sect131r1.php | 36 + .../phpseclib/Crypt/EC/Curves/sect131r2.php | 36 + .../phpseclib/Crypt/EC/Curves/sect163k1.php | 36 + .../phpseclib/Crypt/EC/Curves/sect163r1.php | 36 + .../phpseclib/Crypt/EC/Curves/sect163r2.php | 36 + .../phpseclib/Crypt/EC/Curves/sect193r1.php | 36 + .../phpseclib/Crypt/EC/Curves/sect193r2.php | 36 + .../phpseclib/Crypt/EC/Curves/sect233k1.php | 36 + .../phpseclib/Crypt/EC/Curves/sect233r1.php | 36 + .../phpseclib/Crypt/EC/Curves/sect239k1.php | 36 + .../phpseclib/Crypt/EC/Curves/sect283k1.php | 36 + .../phpseclib/Crypt/EC/Curves/sect283r1.php | 36 + .../phpseclib/Crypt/EC/Curves/sect409k1.php | 40 + .../phpseclib/Crypt/EC/Curves/sect409r1.php | 40 + .../phpseclib/Crypt/EC/Curves/sect571k1.php | 44 + .../phpseclib/Crypt/EC/Curves/sect571r1.php | 44 + .../Crypt/EC/Formats/Keys/Common.php | 554 + .../EC/Formats/Keys/MontgomeryPrivate.php | 108 + .../EC/Formats/Keys/MontgomeryPublic.php | 78 + .../Crypt/EC/Formats/Keys/OpenSSH.php | 215 + .../phpseclib/Crypt/EC/Formats/Keys/PKCS1.php | 201 + .../phpseclib/Crypt/EC/Formats/Keys/PKCS8.php | 248 + .../phpseclib/Crypt/EC/Formats/Keys/PuTTY.php | 145 + .../phpseclib/Crypt/EC/Formats/Keys/XML.php | 491 + .../Crypt/EC/Formats/Keys/libsodium.php | 121 + .../Crypt/EC/Formats/Signature/ASN1.php | 68 + .../Crypt/EC/Formats/Signature/Raw.php | 29 + .../Crypt/EC/Formats/Signature/SSH2.php | 100 + .../phpseclib/Crypt/EC/Parameters.php | 40 + .../phpseclib/Crypt/EC/PrivateKey.php | 257 + .../phpseclib/Crypt/EC/PublicKey.php | 177 + .../phpseclib/phpseclib/Crypt/Hash.php | 1487 ++ .../phpseclib/Crypt/PublicKeyLoader.php | 119 + .../phpseclib/phpseclib/Crypt/RC2.php | 669 + .../phpseclib/phpseclib/Crypt/RC4.php | 309 + .../phpseclib/phpseclib/Crypt/RSA.php | 965 + .../Crypt/RSA/Formats/Keys/MSBLOB.php | 243 + .../Crypt/RSA/Formats/Keys/OpenSSH.php | 139 + .../Crypt/RSA/Formats/Keys/PKCS1.php | 167 + .../Crypt/RSA/Formats/Keys/PKCS8.php | 149 + .../phpseclib/Crypt/RSA/Formats/Keys/PSS.php | 250 + .../Crypt/RSA/Formats/Keys/PuTTY.php | 130 + .../phpseclib/Crypt/RSA/Formats/Keys/Raw.php | 191 + .../phpseclib/Crypt/RSA/Formats/Keys/XML.php | 179 + .../phpseclib/Crypt/RSA/PrivateKey.php | 552 + .../phpseclib/Crypt/RSA/PublicKey.php | 527 + .../phpseclib/phpseclib/Crypt/Random.php | 223 + .../phpseclib/phpseclib/Crypt/Rijndael.php | 1061 + .../phpseclib/phpseclib/Crypt/Salsa20.php | 531 + .../phpseclib/phpseclib/Crypt/TripleDES.php | 457 + .../phpseclib/phpseclib/Crypt/Twofish.php | 826 + .../Exception/BadConfigurationException.php | 26 + .../Exception/BadDecryptionException.php | 26 + .../phpseclib/Exception/BadModeException.php | 26 + .../Exception/ConnectionClosedException.php | 26 + .../Exception/FileNotFoundException.php | 26 + .../Exception/InconsistentSetupException.php | 26 + .../Exception/InsufficientSetupException.php | 26 + .../Exception/NoKeyLoadedException.php | 26 + .../NoSupportedAlgorithmsException.php | 26 + .../Exception/UnableToConnectException.php | 26 + .../UnsupportedAlgorithmException.php | 26 + .../Exception/UnsupportedCurveException.php | 26 + .../Exception/UnsupportedFormatException.php | 26 + .../UnsupportedOperationException.php | 26 + .../phpseclib/phpseclib/File/ANSI.php | 581 + .../phpseclib/phpseclib/File/ASN1.php | 1540 ++ .../phpseclib/phpseclib/File/ASN1/Element.php | 49 + .../File/ASN1/Maps/AccessDescription.php | 36 + .../ASN1/Maps/AdministrationDomainName.php | 40 + .../File/ASN1/Maps/AlgorithmIdentifier.php | 39 + .../phpseclib/File/ASN1/Maps/AnotherName.php | 41 + .../phpseclib/File/ASN1/Maps/Attribute.php | 41 + .../File/ASN1/Maps/AttributeType.php | 30 + .../File/ASN1/Maps/AttributeTypeAndValue.php | 36 + .../File/ASN1/Maps/AttributeValue.php | 30 + .../phpseclib/File/ASN1/Maps/Attributes.php | 35 + .../ASN1/Maps/AuthorityInfoAccessSyntax.php | 35 + .../File/ASN1/Maps/AuthorityKeyIdentifier.php | 49 + .../phpseclib/File/ASN1/Maps/BaseDistance.php | 30 + .../File/ASN1/Maps/BasicConstraints.php | 43 + .../Maps/BuiltInDomainDefinedAttribute.php | 36 + .../Maps/BuiltInDomainDefinedAttributes.php | 35 + .../ASN1/Maps/BuiltInStandardAttributes.php | 71 + .../phpseclib/File/ASN1/Maps/CPSuri.php | 30 + .../File/ASN1/Maps/CRLDistributionPoints.php | 35 + .../phpseclib/File/ASN1/Maps/CRLNumber.php | 30 + .../phpseclib/File/ASN1/Maps/CRLReason.php | 45 + .../phpseclib/File/ASN1/Maps/CertPolicyId.php | 30 + .../phpseclib/File/ASN1/Maps/Certificate.php | 37 + .../File/ASN1/Maps/CertificateIssuer.php | 30 + .../File/ASN1/Maps/CertificateList.php | 37 + .../File/ASN1/Maps/CertificatePolicies.php | 35 + .../ASN1/Maps/CertificateSerialNumber.php | 30 + .../File/ASN1/Maps/CertificationRequest.php | 37 + .../ASN1/Maps/CertificationRequestInfo.php | 45 + .../File/ASN1/Maps/Characteristic_two.php | 40 + .../phpseclib/File/ASN1/Maps/CountryName.php | 40 + .../phpseclib/File/ASN1/Maps/Curve.php | 40 + .../phpseclib/File/ASN1/Maps/DHParameter.php | 42 + .../phpseclib/File/ASN1/Maps/DSAParams.php | 37 + .../File/ASN1/Maps/DSAPrivateKey.php | 40 + .../phpseclib/File/ASN1/Maps/DSAPublicKey.php | 30 + .../phpseclib/File/ASN1/Maps/DigestInfo.php | 38 + .../File/ASN1/Maps/DirectoryString.php | 39 + .../phpseclib/File/ASN1/Maps/DisplayText.php | 38 + .../File/ASN1/Maps/DistributionPoint.php | 49 + .../File/ASN1/Maps/DistributionPointName.php | 44 + .../phpseclib/File/ASN1/Maps/DssSigValue.php | 36 + .../phpseclib/File/ASN1/Maps/ECParameters.php | 49 + .../phpseclib/File/ASN1/Maps/ECPoint.php | 30 + .../phpseclib/File/ASN1/Maps/ECPrivateKey.php | 52 + .../phpseclib/File/ASN1/Maps/EDIPartyName.php | 46 + .../File/ASN1/Maps/EcdsaSigValue.php | 36 + .../File/ASN1/Maps/EncryptedData.php | 30 + .../ASN1/Maps/EncryptedPrivateKeyInfo.php | 36 + .../File/ASN1/Maps/ExtKeyUsageSyntax.php | 35 + .../phpseclib/File/ASN1/Maps/Extension.php | 47 + .../File/ASN1/Maps/ExtensionAttribute.php | 46 + .../File/ASN1/Maps/ExtensionAttributes.php | 35 + .../phpseclib/File/ASN1/Maps/Extensions.php | 37 + .../phpseclib/File/ASN1/Maps/FieldElement.php | 30 + .../phpseclib/File/ASN1/Maps/FieldID.php | 39 + .../phpseclib/File/ASN1/Maps/GeneralName.php | 84 + .../phpseclib/File/ASN1/Maps/GeneralNames.php | 35 + .../File/ASN1/Maps/GeneralSubtree.php | 46 + .../File/ASN1/Maps/GeneralSubtrees.php | 35 + .../File/ASN1/Maps/HashAlgorithm.php | 30 + .../File/ASN1/Maps/HoldInstructionCode.php | 30 + .../File/ASN1/Maps/InvalidityDate.php | 30 + .../File/ASN1/Maps/IssuerAltName.php | 30 + .../ASN1/Maps/IssuingDistributionPoint.php | 72 + .../File/ASN1/Maps/KeyIdentifier.php | 30 + .../phpseclib/File/ASN1/Maps/KeyPurposeId.php | 30 + .../phpseclib/File/ASN1/Maps/KeyUsage.php | 43 + .../File/ASN1/Maps/MaskGenAlgorithm.php | 30 + .../phpseclib/File/ASN1/Maps/Name.php | 35 + .../File/ASN1/Maps/NameConstraints.php | 44 + .../File/ASN1/Maps/NetworkAddress.php | 30 + .../File/ASN1/Maps/NoticeReference.php | 41 + .../File/ASN1/Maps/NumericUserIdentifier.php | 30 + .../phpseclib/File/ASN1/Maps/ORAddress.php | 37 + .../File/ASN1/Maps/OneAsymmetricKey.php | 52 + .../File/ASN1/Maps/OrganizationName.php | 30 + .../ASN1/Maps/OrganizationalUnitNames.php | 35 + .../File/ASN1/Maps/OtherPrimeInfo.php | 38 + .../File/ASN1/Maps/OtherPrimeInfos.php | 36 + .../phpseclib/File/ASN1/Maps/PBEParameter.php | 38 + .../phpseclib/File/ASN1/Maps/PBES2params.php | 38 + .../phpseclib/File/ASN1/Maps/PBKDF2params.php | 45 + .../phpseclib/File/ASN1/Maps/PBMAC1params.php | 38 + .../phpseclib/File/ASN1/Maps/PKCS9String.php | 36 + .../phpseclib/File/ASN1/Maps/Pentanomial.php | 37 + .../phpseclib/File/ASN1/Maps/PersonalName.php | 58 + .../File/ASN1/Maps/PolicyInformation.php | 42 + .../File/ASN1/Maps/PolicyMappings.php | 41 + .../File/ASN1/Maps/PolicyQualifierId.php | 30 + .../File/ASN1/Maps/PolicyQualifierInfo.php | 36 + .../File/ASN1/Maps/PostalAddress.php | 36 + .../phpseclib/File/ASN1/Maps/Prime_p.php | 30 + .../File/ASN1/Maps/PrivateDomainName.php | 36 + .../phpseclib/File/ASN1/Maps/PrivateKey.php | 30 + .../File/ASN1/Maps/PrivateKeyInfo.php | 45 + .../File/ASN1/Maps/PrivateKeyUsagePeriod.php | 44 + .../phpseclib/File/ASN1/Maps/PublicKey.php | 30 + .../File/ASN1/Maps/PublicKeyAndChallenge.php | 36 + .../File/ASN1/Maps/PublicKeyInfo.php | 39 + .../File/ASN1/Maps/RC2CBCParameter.php | 41 + .../phpseclib/File/ASN1/Maps/RDNSequence.php | 42 + .../File/ASN1/Maps/RSAPrivateKey.php | 48 + .../phpseclib/File/ASN1/Maps/RSAPublicKey.php | 36 + .../File/ASN1/Maps/RSASSA_PSS_params.php | 62 + .../phpseclib/File/ASN1/Maps/ReasonFlags.php | 43 + .../ASN1/Maps/RelativeDistinguishedName.php | 41 + .../File/ASN1/Maps/RevokedCertificate.php | 39 + .../ASN1/Maps/SignedPublicKeyAndChallenge.php | 37 + .../File/ASN1/Maps/SpecifiedECDomain.php | 49 + .../File/ASN1/Maps/SubjectAltName.php | 30 + .../ASN1/Maps/SubjectDirectoryAttributes.php | 35 + .../ASN1/Maps/SubjectInfoAccessSyntax.php | 35 + .../File/ASN1/Maps/SubjectPublicKeyInfo.php | 36 + .../phpseclib/File/ASN1/Maps/TBSCertList.php | 58 + .../File/ASN1/Maps/TBSCertificate.php | 69 + .../File/ASN1/Maps/TerminalIdentifier.php | 30 + .../phpseclib/File/ASN1/Maps/Time.php | 36 + .../phpseclib/File/ASN1/Maps/Trinomial.php | 30 + .../File/ASN1/Maps/UniqueIdentifier.php | 30 + .../phpseclib/File/ASN1/Maps/UserNotice.php | 42 + .../phpseclib/File/ASN1/Maps/Validity.php | 36 + .../File/ASN1/Maps/netscape_ca_policy_url.php | 30 + .../File/ASN1/Maps/netscape_cert_type.php | 44 + .../File/ASN1/Maps/netscape_comment.php | 30 + .../phpseclib/phpseclib/File/X509.php | 4105 ++++ .../phpseclib/phpseclib/Math/BigInteger.php | 895 + .../Math/BigInteger/Engines/BCMath.php | 703 + .../Math/BigInteger/Engines/BCMath/Base.php | 116 + .../BigInteger/Engines/BCMath/BuiltIn.php | 44 + .../Engines/BCMath/DefaultEngine.php | 29 + .../BigInteger/Engines/BCMath/OpenSSL.php | 29 + .../Engines/BCMath/Reductions/Barrett.php | 193 + .../Engines/BCMath/Reductions/EvalBarrett.php | 112 + .../Math/BigInteger/Engines/Engine.php | 1283 ++ .../phpseclib/Math/BigInteger/Engines/GMP.php | 702 + .../BigInteger/Engines/GMP/DefaultEngine.php | 44 + .../Math/BigInteger/Engines/OpenSSL.php | 72 + .../phpseclib/Math/BigInteger/Engines/PHP.php | 1337 ++ .../Math/BigInteger/Engines/PHP/Base.php | 149 + .../BigInteger/Engines/PHP/DefaultEngine.php | 29 + .../BigInteger/Engines/PHP/Montgomery.php | 93 + .../Math/BigInteger/Engines/PHP/OpenSSL.php | 29 + .../Engines/PHP/Reductions/Barrett.php | 285 + .../Engines/PHP/Reductions/Classic.php | 46 + .../Engines/PHP/Reductions/EvalBarrett.php | 488 + .../Engines/PHP/Reductions/Montgomery.php | 130 + .../Engines/PHP/Reductions/MontgomeryMult.php | 81 + .../Engines/PHP/Reductions/PowerOfTwo.php | 63 + .../Math/BigInteger/Engines/PHP32.php | 373 + .../Math/BigInteger/Engines/PHP64.php | 377 + .../phpseclib/phpseclib/Math/BinaryField.php | 198 + .../phpseclib/Math/BinaryField/Integer.php | 522 + .../phpseclib/Math/Common/FiniteField.php | 26 + .../Math/Common/FiniteField/Integer.php | 46 + .../phpseclib/phpseclib/Math/PrimeField.php | 124 + .../phpseclib/Math/PrimeField/Integer.php | 422 + .../phpseclib/phpseclib/Net/SFTP.php | 3571 ++++ .../phpseclib/phpseclib/Net/SFTP/Stream.php | 796 + .../phpseclib/phpseclib/Net/SSH2.php | 5290 +++++ .../phpseclib/phpseclib/System/SSH/Agent.php | 296 + .../phpseclib/System/SSH/Agent/Identity.php | 337 + .../System/SSH/Common/Traits/ReadBytes.php | 42 + .../phpseclib/phpseclib/bootstrap.php | 22 + .../phpseclib/phpseclib/phpseclib/openssl.cnf | 6 + .../vendor/symfony/console/Application.php | 12 +- .../console/Command/DumpCompletionCommand.php | 2 +- .../console/Formatter/OutputFormatter.php | 22 +- .../symfony/console/Helper/QuestionHelper.php | 17 +- .../vendor/symfony/console/Helper/Table.php | 4 +- .../symfony/console/Input/StringInput.php | 24 +- code/libraries/vendor/symfony/console/LICENSE | 2 +- .../symfony/console/Output/ConsoleOutput.php | 10 +- .../console/Question/ChoiceQuestion.php | 6 +- .../symfony/deprecation-contracts/LICENSE | 2 +- .../error-handler/DebugClassLoader.php | 7 + .../vendor/symfony/error-handler/LICENSE | 2 +- .../Resources/assets/css/exception.css | 2 +- .../ldap/Adapter/ExtLdap/EntryManager.php | 2 +- code/libraries/vendor/symfony/ldap/LICENSE | 2 +- .../vendor/symfony/options-resolver/LICENSE | 2 +- .../vendor/symfony/polyfill-ctype/Ctype.php | 53 +- .../symfony/polyfill-iconv/bootstrap.php | 2 +- .../symfony/polyfill-iconv/bootstrap80.php | 2 +- .../polyfill-intl-grapheme/Grapheme.php | 2 +- .../symfony/polyfill-mbstring/Mbstring.php | 5 +- .../Resources/stubs/JsonException.php | 6 +- .../vendor/symfony/polyfill-php80/Php80.php | 12 +- .../symfony/polyfill-php80/PhpToken.php | 103 + .../Resources/stubs/PhpToken.php | 7 + .../Resources/stubs/UnhandledMatchError.php | 6 +- .../Resources/stubs/ValueError.php | 6 +- .../vendor/symfony/service-contracts/LICENSE | 2 +- .../ServiceSubscriberTrait.php | 4 +- code/libraries/vendor/symfony/string/LICENSE | 2 +- .../var-dumper/Caster/MysqliCaster.php | 33 + .../var-dumper/Cloner/AbstractCloner.php | 2 + .../symfony/var-dumper/Dumper/HtmlDumper.php | 2 +- .../vendor/symfony/var-dumper/LICENSE | 2 +- .../libraries/vendor/symfony/web-link/LICENSE | 2 +- code/libraries/vendor/symfony/yaml/Inline.php | 13 +- code/libraries/vendor/symfony/yaml/LICENSE | 2 +- code/libraries/vendor/symfony/yaml/Parser.php | 2 + .../vendor/web-token/jwt-core/Algorithm.php | 29 + .../web-token/jwt-core/AlgorithmManager.php | 80 + .../jwt-core/AlgorithmManagerFactory.php | 80 + .../vendor/web-token/jwt-core/JWK.php | 146 + .../vendor/web-token/jwt-core/JWKSet.php | 340 + .../vendor/web-token/jwt-core/JWT.php | 23 + .../vendor/web-token/jwt-core/LICENSE | 21 + .../web-token/jwt-core/Util/BigInteger.php | 231 + .../vendor/web-token/jwt-core/Util/ECKey.php | 366 + .../web-token/jwt-core/Util/ECSignature.php | 143 + .../vendor/web-token/jwt-core/Util/Hash.php | 103 + .../web-token/jwt-core/Util/JsonConverter.php | 55 + .../web-token/jwt-core/Util/KeyChecker.php | 121 + .../vendor/web-token/jwt-core/Util/RSAKey.php | 315 + .../jwt-signature-algorithm-ecdsa/ECDSA.php | 80 + .../jwt-signature-algorithm-ecdsa/ES256.php | 32 + .../jwt-signature-algorithm-ecdsa/ES384.php | 32 + .../jwt-signature-algorithm-ecdsa/ES512.php | 32 + .../jwt-signature-algorithm-ecdsa/LICENSE | 21 + .../jwt-signature-algorithm-eddsa/EdDSA.php | 107 + .../jwt-signature-algorithm-eddsa/LICENSE | 21 + .../ES256K.php | 32 + .../HS1.php | 27 + .../HS256_64.php | 36 + .../LICENSE | 21 + .../RS1.php | 27 + .../jwt-signature-algorithm-hmac/HMAC.php | 61 + .../jwt-signature-algorithm-hmac/HS256.php | 43 + .../jwt-signature-algorithm-hmac/HS384.php | 43 + .../jwt-signature-algorithm-hmac/HS512.php | 43 + .../jwt-signature-algorithm-hmac/LICENSE | 21 + .../jwt-signature-algorithm-none/LICENSE | 21 + .../jwt-signature-algorithm-none/None.php | 53 + .../jwt-signature-algorithm-rsa/LICENSE | 21 + .../jwt-signature-algorithm-rsa/PS256.php | 27 + .../jwt-signature-algorithm-rsa/PS384.php | 27 + .../jwt-signature-algorithm-rsa/PS512.php | 27 + .../jwt-signature-algorithm-rsa/RS256.php | 27 + .../jwt-signature-algorithm-rsa/RS384.php | 27 + .../jwt-signature-algorithm-rsa/RS512.php | 27 + .../jwt-signature-algorithm-rsa/RSA.php | 74 + .../jwt-signature-algorithm-rsa/RSAPKCS1.php | 75 + .../jwt-signature-algorithm-rsa/RSAPSS.php | 69 + .../jwt-signature-algorithm-rsa/Util/RSA.php | 251 + .../jwt-signature/Algorithm/MacAlgorithm.php | 37 + .../Algorithm/SignatureAlgorithm.php | 37 + .../vendor/web-token/jwt-signature/JWS.php | 150 + .../web-token/jwt-signature/JWSBuilder.php | 235 + .../jwt-signature/JWSBuilderFactory.php | 41 + .../web-token/jwt-signature/JWSLoader.php | 124 + .../jwt-signature/JWSLoaderFactory.php | 59 + .../jwt-signature/JWSTokenSupport.php | 42 + .../web-token/jwt-signature/JWSVerifier.php | 170 + .../jwt-signature/JWSVerifierFactory.php | 41 + .../vendor/web-token/jwt-signature/LICENSE | 21 + .../Serializer/CompactSerializer.php | 96 + .../Serializer/JSONFlattenedSerializer.php | 110 + .../Serializer/JSONGeneralSerializer.php | 167 + .../Serializer/JWSSerializer.php | 38 + .../Serializer/JWSSerializerManager.php | 86 + .../JWSSerializerManagerFactory.php | 63 + .../jwt-signature/Serializer/Serializer.php | 24 + .../web-token/jwt-signature/Signature.php | 134 + .../src/Negotiation/AbstractNegotiator.php | 2 +- .../src/Negotiation/BaseAccept.php | 6 +- .../src/Negotiation/LanguageNegotiator.php | 6 +- code/media/com_actionlogs/joomla.asset.json | 22 +- code/media/com_banners/joomla.asset.json | 22 +- code/media/com_content/joomla.asset.json | 25 + .../com_content/js/articles-status-es5.js | 19 + .../com_content/js/articles-status-es5.min.js | 1 + code/media/com_content/js/articles-status.js | 15 + .../com_content/js/articles-status.min.js | 1 + code/media/com_cpanel/joomla.asset.json | 4 +- .../com_cpanel/js/admin-system-loader-es5.js | 56 +- .../js/admin-system-loader-es5.min.js | 2 +- .../com_cpanel/js/admin-system-loader.js | 56 +- .../com_cpanel/js/admin-system-loader.min.js | 2 +- code/media/com_fields/joomla.asset.json | 50 +- code/media/com_finder/css/dates.css | 1 - code/media/com_finder/css/dates.min.css | 2 +- code/media/com_finder/css/finder.css | 4 +- code/media/com_finder/css/finder.min.css | 2 +- code/media/com_finder/joomla.asset.json | 39 +- code/media/com_finder/js/index-es5.js | 29 - code/media/com_finder/js/index-es5.min.js | 1 - code/media/com_finder/js/index.js | 25 - code/media/com_finder/js/index.min.js | 1 - code/media/com_installer/joomla.asset.json | 34 +- code/media/com_joomlaupdate/joomla.asset.json | 22 +- .../js/admin-update-default-es5.js | 69 +- .../js/admin-update-default-es5.min.js | 2 +- .../js/admin-update-default.js | 69 +- .../js/admin-update-default.min.js | 2 +- code/media/com_joomlaupdate/js/default-es5.js | 173 +- .../com_joomlaupdate/js/default-es5.min.js | 2 +- code/media/com_joomlaupdate/js/default.js | 176 +- code/media/com_joomlaupdate/js/default.min.js | 2 +- code/media/com_languages/css/overrider.css | 2 +- .../media/com_languages/css/overrider.min.css | 2 +- code/media/com_languages/joomla.asset.json | 2 +- code/media/com_media/css/media-manager.css | 22 +- .../media/com_media/css/media-manager.min.css | 2 +- code/media/com_media/joomla.asset.json | 28 +- code/media/com_media/js/media-manager-es5.js | 701 +- .../com_media/js/media-manager-es5.min.js | 2 +- code/media/com_media/js/media-manager.js | 670 +- code/media/com_media/js/media-manager.min.js | 2 +- .../scss/components/_media-browser.scss | 18 +- .../scss/components/_media-edit.scss | 1 + .../scss/components/_media-toolbar.scss | 2 +- code/media/com_modules/joomla.asset.json | 44 +- code/media/com_scheduler/joomla.asset.json | 56 +- .../js/admin-view-select-task-search-es5.js | 1 - .../js/admin-view-select-task-search.js | 1 - code/media/com_tags/joomla.asset.json | 30 +- code/media/com_templates/joomla.asset.json | 22 +- code/media/com_users/images/emergency.svg | 1 + code/media/com_users/joomla.asset.json | 39 +- .../com_users/js/two-factor-focus-es5.js | 56 + .../com_users/js/two-factor-focus-es5.min.js | 1 + code/media/com_users/js/two-factor-focus.js | 52 + .../com_users/js/two-factor-focus.min.js | 1 + .../media/com_users/js/two-factor-list-es5.js | 20 + .../com_users/js/two-factor-list-es5.min.js | 1 + code/media/com_users/js/two-factor-list.js | 16 + .../media/com_users/js/two-factor-list.min.js | 1 + .../com_users/js/two-factor-switcher-es5.js | 32 - .../js/two-factor-switcher-es5.min.js | 1 - .../media/com_users/js/two-factor-switcher.js | 30 - .../com_users/js/two-factor-switcher.min.js | 1 - code/media/legacy/joomla.asset.json | 18 +- .../js/packageinstaller-es5.js | 7 + .../js/packageinstaller-es5.min.js | 2 +- .../js/packageinstaller.js | 7 + .../js/packageinstaller.min.js | 2 +- .../plg_installer_webinstaller/css/client.css | 3 +- .../css/client.min.css | 2 +- .../scss/client.scss | 2 +- .../images/email.svg | 1 + .../images/fixed.svg | 1 + .../plg_multifactorauth_totp/images/totp.svg | 1 + .../joomla.asset.json | 37 + .../plg_multifactorauth_totp/js/setup-es5.js | 27 + .../js/setup-es5.min.js | 1 + .../plg_multifactorauth_totp/js/setup.js | 23 + .../plg_multifactorauth_totp/js/setup.min.js | 1 + .../images/webauthn.svg | 1 + .../joomla.asset.json | 35 + .../js/webauthn-es5.js | 179 + .../js/webauthn-es5.min.js | 1 + .../js/webauthn.js | 163 + .../js/webauthn.min.js | 1 + .../images/yubikey.svg | 1 + .../js/extensionupdatecheck-es5.js | 38 +- .../js/extensionupdatecheck-es5.min.js | 2 +- .../js/extensionupdatecheck.js | 38 +- .../js/extensionupdatecheck.min.js | 2 +- .../js/jupdatecheck-es5.js | 46 +- .../js/jupdatecheck-es5.min.js | 2 +- .../js/jupdatecheck.js | 46 +- .../js/jupdatecheck.min.js | 2 +- .../js/overridecheck-es5.js | 47 +- .../js/overridecheck-es5.min.js | 2 +- .../js/overridecheck.js | 47 +- .../js/overridecheck.min.js | 2 +- .../js/privacycheck-es5.js | 77 +- .../js/privacycheck-es5.min.js | 2 +- .../js/privacycheck.js | 77 +- .../js/privacycheck.min.js | 2 +- .../plg_system_jooa11y/joomla.asset.json | 22 +- .../plg_system_shortcut/js/shortcut-es5.js | 156 + .../js/shortcut-es5.min.js | 1 + code/media/plg_system_shortcut/js/shortcut.js | 171 + .../plg_system_shortcut/js/shortcut.min.js | 1 + code/media/plg_system_webauthn/css/button.css | 3 +- .../plg_system_webauthn/css/button.min.css | 2 +- .../media/plg_system_webauthn/images/fido.png | Bin 0 -> 3501 bytes .../media/plg_system_webauthn/js/login-es5.js | 19 +- .../plg_system_webauthn/js/login-es5.min.js | 2 +- code/media/plg_system_webauthn/js/login.js | 19 +- .../media/plg_system_webauthn/js/login.min.js | 2 +- .../plg_system_webauthn/js/management-es5.js | 137 +- .../js/management-es5.min.js | 2 +- .../plg_system_webauthn/js/management.js | 135 +- .../plg_system_webauthn/js/management.min.js | 2 +- .../plg_system_webauthn/scss/button.scss | 2 +- code/media/system/html/noxml.html | 2 +- code/media/system/joomla.asset.json | 52 +- code/media/system/js/core-es5.js | 75 +- code/media/system/js/core-es5.min.js | 2 +- code/media/system/js/core.js | 75 +- code/media/system/js/core.min.js | 2 +- code/media/system/js/fields/calendar.js | 2 +- code/media/system/js/fields/calendar.min.js | 2 +- .../js/fields/joomla-field-media-es5.js | 199 +- .../js/fields/joomla-field-media-es5.min.js | 2 +- .../system/js/fields/joomla-field-media.js | 115 +- .../js/fields/joomla-field-media.min.js | 2 +- .../js/fields/joomla-media-select-es5.js | 8 +- .../js/fields/joomla-media-select-es5.min.js | 2 +- .../system/js/fields/joomla-media-select.js | 8 +- .../js/fields/joomla-media-select.min.js | 2 +- code/media/system/js/multiselect-es5.js | 22 +- code/media/system/js/multiselect-es5.min.js | 2 +- code/media/system/js/multiselect.js | 22 +- code/media/system/js/multiselect.min.js | 2 +- code/media/system/js/table-columns-es5.js | 204 + code/media/system/js/table-columns-es5.min.js | 1 + code/media/system/js/table-columns.js | 188 + code/media/system/js/table-columns.min.js | 1 + .../administrator/atum/css/template-rtl.css | 4 + .../atum/css/template-rtl.min.css | 2 +- .../scss/vendor/bootstrap/_bootstrap-rtl.scss | 4 + .../site/cassiopeia/css/template-rtl.css | 8 +- .../site/cassiopeia/css/template-rtl.min.css | 2 +- .../site/cassiopeia/css/template.css | 8 +- .../site/cassiopeia/css/template.min.css | 2 +- .../site/cassiopeia/scss/blocks/_global.scss | 4 +- .../vendor/accessibility/js/accessibility.js | 2 +- .../accessibility/js/accessibility.min.js | 2 +- .../codemirror/addon/comment/comment.js | 4 +- .../codemirror/addon/comment/comment.min.js | 2 +- .../addon/comment/continuecomment.js | 2 +- .../vendor/codemirror/addon/dialog/dialog.js | 2 +- .../codemirror/addon/display/autorefresh.js | 2 +- .../codemirror/addon/display/fullscreen.js | 2 +- .../vendor/codemirror/addon/display/panel.js | 2 +- .../codemirror/addon/display/placeholder.js | 2 +- .../vendor/codemirror/addon/display/rulers.js | 2 +- .../codemirror/addon/edit/closebrackets.js | 2 +- .../vendor/codemirror/addon/edit/closetag.js | 2 +- .../codemirror/addon/edit/continuelist.js | 2 +- .../codemirror/addon/edit/matchbrackets.js | 2 +- .../vendor/codemirror/addon/edit/matchtags.js | 2 +- .../codemirror/addon/edit/trailingspace.js | 2 +- .../codemirror/addon/fold/brace-fold.js | 2 +- .../codemirror/addon/fold/comment-fold.js | 2 +- .../vendor/codemirror/addon/fold/foldcode.js | 2 +- .../codemirror/addon/fold/foldgutter.js | 2 +- .../codemirror/addon/fold/indent-fold.js | 2 +- .../codemirror/addon/fold/markdown-fold.js | 2 +- .../vendor/codemirror/addon/fold/xml-fold.js | 2 +- .../codemirror/addon/hint/anyword-hint.js | 2 +- .../vendor/codemirror/addon/hint/css-hint.js | 2 +- .../vendor/codemirror/addon/hint/html-hint.js | 2 +- .../codemirror/addon/hint/javascript-hint.js | 2 +- .../vendor/codemirror/addon/hint/show-hint.js | 4 +- .../codemirror/addon/hint/show-hint.min.js | 2 +- .../vendor/codemirror/addon/hint/sql-hint.js | 2 +- .../vendor/codemirror/addon/hint/xml-hint.js | 2 +- .../addon/lint/coffeescript-lint.js | 2 +- .../vendor/codemirror/addon/lint/css-lint.js | 2 +- .../vendor/codemirror/addon/lint/html-lint.js | 2 +- .../codemirror/addon/lint/javascript-lint.js | 2 +- .../vendor/codemirror/addon/lint/json-lint.js | 2 +- .../vendor/codemirror/addon/lint/lint.js | 2 +- .../vendor/codemirror/addon/lint/yaml-lint.js | 2 +- .../vendor/codemirror/addon/merge/merge.js | 15 +- .../codemirror/addon/merge/merge.min.js | 2 +- .../vendor/codemirror/addon/mode/loadmode.js | 2 +- .../vendor/codemirror/addon/mode/multiplex.js | 2 +- .../codemirror/addon/mode/multiplex_test.js | 2 +- .../vendor/codemirror/addon/mode/overlay.js | 2 +- .../vendor/codemirror/addon/mode/simple.js | 2 +- .../codemirror/addon/runmode/colorize.js | 2 +- .../addon/runmode/runmode-standalone.js | 2 +- .../codemirror/addon/runmode/runmode.js | 2 +- .../codemirror/addon/runmode/runmode.node.js | 2 +- .../addon/scroll/annotatescrollbar.js | 2 +- .../codemirror/addon/scroll/scrollpastend.js | 2 +- .../addon/scroll/simplescrollbars.js | 2 +- .../codemirror/addon/search/jump-to-line.js | 2 +- .../addon/search/match-highlighter.js | 2 +- .../addon/search/matchesonscrollbar.js | 2 +- .../vendor/codemirror/addon/search/search.js | 15 +- .../codemirror/addon/search/search.min.js | 2 +- .../codemirror/addon/search/searchcursor.js | 2 +- .../codemirror/addon/selection/active-line.js | 2 +- .../addon/selection/mark-selection.js | 2 +- .../addon/selection/selection-pointer.js | 2 +- .../vendor/codemirror/addon/tern/tern.js | 2 +- .../vendor/codemirror/addon/tern/worker.js | 2 +- .../vendor/codemirror/addon/wrap/hardwrap.js | 2 +- code/media/vendor/codemirror/keymap/emacs.js | 2 +- .../media/vendor/codemirror/keymap/sublime.js | 2 +- code/media/vendor/codemirror/keymap/vim.js | 2 +- code/media/vendor/codemirror/lib/addons.js | 53 +- .../media/vendor/codemirror/lib/addons.min.js | 2 +- .../media/vendor/codemirror/lib/codemirror.js | 32 +- .../vendor/codemirror/lib/codemirror.min.js | 2 +- code/media/vendor/codemirror/mode/apl/apl.js | 2 +- .../codemirror/mode/asciiarmor/asciiarmor.js | 2 +- .../vendor/codemirror/mode/asn.1/asn.1.js | 2 +- .../codemirror/mode/asterisk/asterisk.js | 2 +- .../codemirror/mode/brainfuck/brainfuck.js | 2 +- .../vendor/codemirror/mode/clike/clike.js | 4 +- .../vendor/codemirror/mode/clike/clike.min.js | 2 +- .../vendor/codemirror/mode/clojure/clojure.js | 2 +- .../vendor/codemirror/mode/cmake/cmake.js | 2 +- .../vendor/codemirror/mode/cobol/cobol.js | 2 +- .../mode/coffeescript/coffeescript.js | 2 +- .../codemirror/mode/commonlisp/commonlisp.js | 2 +- .../vendor/codemirror/mode/crystal/crystal.js | 2 +- code/media/vendor/codemirror/mode/css/css.js | 16 +- .../vendor/codemirror/mode/css/css.min.js | 2 +- .../vendor/codemirror/mode/cypher/cypher.js | 2 +- code/media/vendor/codemirror/mode/d/d.js | 2 +- .../media/vendor/codemirror/mode/dart/dart.js | 2 +- .../media/vendor/codemirror/mode/diff/diff.js | 2 +- .../vendor/codemirror/mode/django/django.js | 2 +- .../codemirror/mode/dockerfile/dockerfile.js | 2 +- code/media/vendor/codemirror/mode/dtd/dtd.js | 2 +- .../vendor/codemirror/mode/dylan/dylan.js | 2 +- .../media/vendor/codemirror/mode/ebnf/ebnf.js | 2 +- code/media/vendor/codemirror/mode/ecl/ecl.js | 2 +- .../vendor/codemirror/mode/eiffel/eiffel.js | 2 +- code/media/vendor/codemirror/mode/elm/elm.js | 2 +- .../vendor/codemirror/mode/erlang/erlang.js | 2 +- .../vendor/codemirror/mode/factor/factor.js | 2 +- code/media/vendor/codemirror/mode/fcl/fcl.js | 2 +- .../vendor/codemirror/mode/forth/forth.js | 2 +- .../vendor/codemirror/mode/fortran/fortran.js | 2 +- code/media/vendor/codemirror/mode/gas/gas.js | 2 +- code/media/vendor/codemirror/mode/gfm/gfm.js | 2 +- .../vendor/codemirror/mode/gherkin/gherkin.js | 2 +- code/media/vendor/codemirror/mode/go/go.js | 2 +- .../vendor/codemirror/mode/groovy/groovy.js | 22 +- .../codemirror/mode/groovy/groovy.min.js | 2 +- .../media/vendor/codemirror/mode/haml/haml.js | 2 +- .../codemirror/mode/handlebars/handlebars.js | 2 +- .../mode/haskell-literate/haskell-literate.js | 2 +- .../vendor/codemirror/mode/haskell/haskell.js | 2 +- .../media/vendor/codemirror/mode/haxe/haxe.js | 2 +- .../mode/htmlembedded/htmlembedded.js | 2 +- .../codemirror/mode/htmlmixed/htmlmixed.js | 2 +- .../media/vendor/codemirror/mode/http/http.js | 2 +- code/media/vendor/codemirror/mode/idl/idl.js | 2 +- .../codemirror/mode/javascript/javascript.js | 2 +- .../vendor/codemirror/mode/jinja2/jinja2.js | 2 +- code/media/vendor/codemirror/mode/jsx/jsx.js | 2 +- .../vendor/codemirror/mode/julia/julia.js | 4 +- .../vendor/codemirror/mode/julia/julia.min.js | 2 +- .../codemirror/mode/livescript/livescript.js | 2 +- code/media/vendor/codemirror/mode/lua/lua.js | 2 +- .../codemirror/mode/markdown/markdown.js | 2 +- .../mode/mathematica/mathematica.js | 2 +- .../media/vendor/codemirror/mode/mbox/mbox.js | 2 +- code/media/vendor/codemirror/mode/meta.js | 2 +- .../media/vendor/codemirror/mode/mirc/mirc.js | 2 +- .../vendor/codemirror/mode/mllike/mllike.js | 2 +- .../codemirror/mode/modelica/modelica.js | 2 +- .../vendor/codemirror/mode/mscgen/mscgen.js | 2 +- .../vendor/codemirror/mode/mumps/mumps.js | 2 +- .../vendor/codemirror/mode/nginx/nginx.js | 2 +- .../media/vendor/codemirror/mode/nsis/nsis.js | 2 +- .../codemirror/mode/ntriples/ntriples.js | 2 +- .../vendor/codemirror/mode/octave/octave.js | 2 +- code/media/vendor/codemirror/mode/oz/oz.js | 2 +- .../vendor/codemirror/mode/pascal/pascal.js | 2 +- .../vendor/codemirror/mode/pegjs/pegjs.js | 2 +- .../media/vendor/codemirror/mode/perl/perl.js | 2 +- code/media/vendor/codemirror/mode/php/php.js | 2 +- code/media/vendor/codemirror/mode/pig/pig.js | 2 +- .../codemirror/mode/powershell/powershell.js | 2 +- .../codemirror/mode/properties/properties.js | 2 +- .../codemirror/mode/protobuf/protobuf.js | 2 +- code/media/vendor/codemirror/mode/pug/pug.js | 2 +- .../vendor/codemirror/mode/puppet/puppet.js | 2 +- .../vendor/codemirror/mode/python/python.js | 2 +- code/media/vendor/codemirror/mode/q/q.js | 2 +- code/media/vendor/codemirror/mode/r/r.js | 2 +- .../codemirror/mode/rpm/changes/index.html | 4 +- code/media/vendor/codemirror/mode/rpm/rpm.js | 2 +- code/media/vendor/codemirror/mode/rst/rst.js | 2 +- .../media/vendor/codemirror/mode/ruby/ruby.js | 4 +- .../vendor/codemirror/mode/ruby/ruby.min.js | 2 +- .../media/vendor/codemirror/mode/rust/rust.js | 2 +- code/media/vendor/codemirror/mode/sas/sas.js | 2 +- .../media/vendor/codemirror/mode/sass/sass.js | 2 +- .../vendor/codemirror/mode/scheme/scheme.js | 2 +- .../vendor/codemirror/mode/shell/shell.js | 2 +- .../vendor/codemirror/mode/sieve/sieve.js | 2 +- .../media/vendor/codemirror/mode/slim/slim.js | 2 +- .../codemirror/mode/smalltalk/smalltalk.js | 2 +- .../vendor/codemirror/mode/smarty/smarty.js | 2 +- .../media/vendor/codemirror/mode/solr/solr.js | 2 +- code/media/vendor/codemirror/mode/soy/soy.js | 2 +- .../vendor/codemirror/mode/sparql/sparql.js | 9 +- .../codemirror/mode/sparql/sparql.min.js | 2 +- .../mode/spreadsheet/spreadsheet.js | 2 +- code/media/vendor/codemirror/mode/sql/sql.js | 2 +- .../media/vendor/codemirror/mode/stex/stex.js | 2 +- .../vendor/codemirror/mode/stylus/stylus.js | 4 +- .../codemirror/mode/stylus/stylus.min.js | 2 +- .../vendor/codemirror/mode/swift/swift.js | 2 +- code/media/vendor/codemirror/mode/tcl/tcl.js | 2 +- .../vendor/codemirror/mode/textile/textile.js | 2 +- .../codemirror/mode/tiddlywiki/tiddlywiki.js | 2 +- .../media/vendor/codemirror/mode/tiki/tiki.js | 2 +- .../media/vendor/codemirror/mode/toml/toml.js | 2 +- .../vendor/codemirror/mode/tornado/tornado.js | 2 +- .../vendor/codemirror/mode/troff/troff.js | 2 +- .../codemirror/mode/ttcn-cfg/ttcn-cfg.js | 2 +- .../media/vendor/codemirror/mode/ttcn/ttcn.js | 2 +- .../vendor/codemirror/mode/turtle/turtle.js | 2 +- .../media/vendor/codemirror/mode/twig/twig.js | 2 +- code/media/vendor/codemirror/mode/vb/vb.js | 2 +- .../codemirror/mode/vbscript/vbscript.js | 2 +- .../codemirror/mode/velocity/velocity.js | 2 +- .../vendor/codemirror/mode/verilog/verilog.js | 2 +- .../media/vendor/codemirror/mode/vhdl/vhdl.js | 2 +- code/media/vendor/codemirror/mode/vue/vue.js | 2 +- .../media/vendor/codemirror/mode/wast/wast.js | 2 +- .../vendor/codemirror/mode/webidl/webidl.js | 2 +- code/media/vendor/codemirror/mode/xml/xml.js | 2 +- .../vendor/codemirror/mode/xquery/xquery.js | 2 +- .../vendor/codemirror/mode/yacas/yacas.js | 2 +- .../mode/yaml-frontmatter/yaml-frontmatter.js | 2 +- .../media/vendor/codemirror/mode/yaml/yaml.js | 2 +- code/media/vendor/codemirror/mode/z80/z80.js | 2 +- code/media/vendor/hotkeysjs/LICENSE | 23 + code/media/vendor/hotkeysjs/js/hotkeys.js | 606 + code/media/vendor/hotkeysjs/js/hotkeys.min.js | 2 + code/media/vendor/joomla.asset.json | 56 +- code/media/vendor/skipto/js/skipto.js | 26 +- code/media/vendor/skipto/js/skipto.min.js | 6 +- .../vendor/tinymce/plugins/advlist/plugin.js | 2 +- .../tinymce/plugins/advlist/plugin.min.js | 2 +- .../vendor/tinymce/plugins/anchor/plugin.js | 2 +- .../tinymce/plugins/anchor/plugin.min.js | 2 +- .../vendor/tinymce/plugins/autolink/plugin.js | 2 +- .../tinymce/plugins/autolink/plugin.min.js | 2 +- .../tinymce/plugins/autoresize/plugin.js | 2 +- .../tinymce/plugins/autoresize/plugin.min.js | 2 +- .../vendor/tinymce/plugins/autosave/plugin.js | 2 +- .../tinymce/plugins/autosave/plugin.min.js | 2 +- .../vendor/tinymce/plugins/bbcode/plugin.js | 2 +- .../tinymce/plugins/bbcode/plugin.min.js | 2 +- .../vendor/tinymce/plugins/charmap/plugin.js | 2 +- .../tinymce/plugins/charmap/plugin.min.js | 2 +- .../vendor/tinymce/plugins/code/plugin.js | 2 +- .../vendor/tinymce/plugins/code/plugin.min.js | 2 +- .../tinymce/plugins/codesample/plugin.js | 2 +- .../tinymce/plugins/codesample/plugin.min.js | 2 +- .../tinymce/plugins/colorpicker/plugin.js | 2 +- .../tinymce/plugins/colorpicker/plugin.min.js | 2 +- .../tinymce/plugins/contextmenu/plugin.js | 2 +- .../tinymce/plugins/contextmenu/plugin.min.js | 2 +- .../tinymce/plugins/directionality/plugin.js | 2 +- .../plugins/directionality/plugin.min.js | 2 +- .../tinymce/plugins/emoticons/plugin.js | 2 +- .../tinymce/plugins/emoticons/plugin.min.js | 2 +- .../vendor/tinymce/plugins/fullpage/plugin.js | 2 +- .../tinymce/plugins/fullpage/plugin.min.js | 2 +- .../tinymce/plugins/fullscreen/plugin.js | 2 +- .../tinymce/plugins/fullscreen/plugin.min.js | 2 +- .../vendor/tinymce/plugins/help/plugin.js | 2 +- .../vendor/tinymce/plugins/help/plugin.min.js | 2 +- .../media/vendor/tinymce/plugins/hr/plugin.js | 2 +- .../vendor/tinymce/plugins/hr/plugin.min.js | 2 +- .../vendor/tinymce/plugins/image/plugin.js | 2 +- .../tinymce/plugins/image/plugin.min.js | 2 +- .../tinymce/plugins/imagetools/plugin.js | 2 +- .../tinymce/plugins/imagetools/plugin.min.js | 2 +- .../tinymce/plugins/importcss/plugin.js | 2 +- .../tinymce/plugins/importcss/plugin.min.js | 2 +- .../tinymce/plugins/insertdatetime/plugin.js | 2 +- .../plugins/insertdatetime/plugin.min.js | 2 +- .../tinymce/plugins/legacyoutput/plugin.js | 2 +- .../plugins/legacyoutput/plugin.min.js | 2 +- .../vendor/tinymce/plugins/link/plugin.js | 2 +- .../vendor/tinymce/plugins/link/plugin.min.js | 2 +- .../vendor/tinymce/plugins/lists/plugin.js | 2 +- .../tinymce/plugins/lists/plugin.min.js | 2 +- .../vendor/tinymce/plugins/media/plugin.js | 2 +- .../tinymce/plugins/media/plugin.min.js | 2 +- .../tinymce/plugins/nonbreaking/plugin.js | 2 +- .../tinymce/plugins/nonbreaking/plugin.min.js | 2 +- .../tinymce/plugins/noneditable/plugin.js | 2 +- .../tinymce/plugins/noneditable/plugin.min.js | 2 +- .../tinymce/plugins/pagebreak/plugin.js | 2 +- .../tinymce/plugins/pagebreak/plugin.min.js | 2 +- .../vendor/tinymce/plugins/paste/plugin.js | 2 +- .../tinymce/plugins/paste/plugin.min.js | 2 +- .../vendor/tinymce/plugins/preview/plugin.js | 2 +- .../tinymce/plugins/preview/plugin.min.js | 2 +- .../vendor/tinymce/plugins/print/plugin.js | 2 +- .../tinymce/plugins/print/plugin.min.js | 2 +- .../tinymce/plugins/quickbars/plugin.js | 2 +- .../tinymce/plugins/quickbars/plugin.min.js | 2 +- .../vendor/tinymce/plugins/save/plugin.js | 2 +- .../vendor/tinymce/plugins/save/plugin.min.js | 2 +- .../tinymce/plugins/searchreplace/plugin.js | 2 +- .../plugins/searchreplace/plugin.min.js | 2 +- .../tinymce/plugins/spellchecker/plugin.js | 2 +- .../plugins/spellchecker/plugin.min.js | 2 +- .../vendor/tinymce/plugins/tabfocus/plugin.js | 2 +- .../tinymce/plugins/tabfocus/plugin.min.js | 2 +- .../vendor/tinymce/plugins/table/plugin.js | 2 +- .../tinymce/plugins/table/plugin.min.js | 2 +- .../vendor/tinymce/plugins/template/plugin.js | 2 +- .../tinymce/plugins/template/plugin.min.js | 2 +- .../tinymce/plugins/textcolor/plugin.js | 2 +- .../tinymce/plugins/textcolor/plugin.min.js | 2 +- .../tinymce/plugins/textpattern/plugin.js | 2 +- .../tinymce/plugins/textpattern/plugin.min.js | 2 +- .../vendor/tinymce/plugins/toc/plugin.js | 2 +- .../vendor/tinymce/plugins/toc/plugin.min.js | 2 +- .../tinymce/plugins/visualblocks/plugin.js | 2 +- .../plugins/visualblocks/plugin.min.js | 2 +- .../tinymce/plugins/visualchars/plugin.js | 2 +- .../tinymce/plugins/visualchars/plugin.min.js | 2 +- .../tinymce/plugins/wordcount/plugin.js | 2 +- .../tinymce/plugins/wordcount/plugin.min.js | 2 +- .../vendor/tinymce/themes/mobile/theme.js | 2 +- .../vendor/tinymce/themes/mobile/theme.min.js | 2 +- .../vendor/tinymce/themes/silver/theme.js | 9 +- .../vendor/tinymce/themes/silver/theme.min.js | 4 +- code/media/vendor/tinymce/tinymce.js | 122 +- code/media/vendor/tinymce/tinymce.min.js | 4 +- .../mod_articles_archive.php | 1 + .../mod_articles_archive.xml | 2 +- .../src/Helper/ArticlesArchiveHelper.php | 142 +- .../mod_articles_archive/tmpl/default.php | 20 +- .../mod_articles_categories.php | 4 +- .../mod_articles_categories.xml | 2 +- .../src/Helper/ArticlesCategoriesHelper.php | 67 +- .../mod_articles_categories/tmpl/default.php | 6 +- .../tmpl/default_items.php | 48 +- .../mod_articles_category.php | 91 +- .../mod_articles_category.xml | 2 +- .../src/Helper/ArticlesCategoryHelper.php | 990 +- .../mod_articles_category/tmpl/default.php | 32 +- .../tmpl/default_items.php | 113 +- .../mod_articles_latest.php | 18 - .../mod_articles_latest.xml | 4 +- .../mod_articles_latest/services/provider.php | 42 + .../src/Dispatcher/Dispatcher.php | 45 + .../src/Helper/ArticlesLatestHelper.php | 260 +- .../mod_articles_latest/tmpl/default.php | 20 +- .../mod_articles_news/mod_articles_news.php | 17 - .../mod_articles_news/mod_articles_news.xml | 7 +- .../mod_articles_news/services/provider.php | 42 + .../src/Dispatcher/Dispatcher.php | 45 + .../src/Helper/ArticlesNewsHelper.php | 350 +- code/modules/mod_articles_news/tmpl/_item.php | 56 +- .../mod_articles_news/tmpl/default.php | 16 +- .../mod_articles_news/tmpl/horizontal.php | 16 +- .../mod_articles_news/tmpl/vertical.php | 24 +- .../mod_articles_popular.php | 8 +- .../mod_articles_popular.xml | 2 +- .../src/Helper/ArticlesPopularHelper.php | 152 +- .../mod_articles_popular/tmpl/default.php | 20 +- code/modules/mod_banners/mod_banners.php | 1 + code/modules/mod_banners/mod_banners.xml | 2 +- .../mod_banners/src/Helper/BannersHelper.php | 75 +- code/modules/mod_banners/tmpl/default.php | 167 +- .../mod_breadcrumbs/mod_breadcrumbs.php | 6 + .../mod_breadcrumbs/mod_breadcrumbs.xml | 2 +- .../src/Helper/BreadcrumbsHelper.php | 174 +- code/modules/mod_breadcrumbs/tmpl/default.php | 175 +- code/modules/mod_custom/mod_custom.php | 8 +- code/modules/mod_custom/mod_custom.xml | 2 +- code/modules/mod_custom/tmpl/default.php | 12 +- code/modules/mod_feed/mod_feed.php | 1 + code/modules/mod_feed/mod_feed.xml | 2 +- .../mod_feed/src/Helper/FeedHelper.php | 68 +- code/modules/mod_feed/tmpl/default.php | 207 +- code/modules/mod_finder/mod_finder.php | 27 +- code/modules/mod_finder/mod_finder.xml | 2 +- .../mod_finder/src/Helper/FinderHelper.php | 101 +- code/modules/mod_finder/tmpl/default.php | 53 +- code/modules/mod_footer/mod_footer.php | 23 +- code/modules/mod_footer/mod_footer.xml | 2 +- code/modules/mod_footer/tmpl/default.php | 5 +- code/modules/mod_languages/mod_languages.php | 1 + code/modules/mod_languages/mod_languages.xml | 2 +- .../src/Helper/LanguagesHelper.php | 247 +- code/modules/mod_languages/tmpl/default.php | 175 +- code/modules/mod_login/mod_login.php | 7 +- code/modules/mod_login/mod_login.xml | 2 +- .../mod_login/src/Helper/LoginHelper.php | 146 +- code/modules/mod_login/tmpl/default.php | 240 +- .../modules/mod_login/tmpl/default_logout.php | 41 +- code/modules/mod_menu/mod_menu.php | 6 +- code/modules/mod_menu/mod_menu.xml | 2 +- .../mod_menu/src/Helper/MenuHelper.php | 451 +- .../mod_menu/tmpl/collapse-default.php | 13 +- code/modules/mod_menu/tmpl/default.php | 147 +- .../mod_menu/tmpl/default_component.php | 93 +- .../modules/mod_menu/tmpl/default_heading.php | 53 +- .../mod_menu/tmpl/default_separator.php | 53 +- code/modules/mod_menu/tmpl/default_url.php | 90 +- .../mod_random_image/mod_random_image.php | 1 + .../mod_random_image/mod_random_image.xml | 2 +- .../src/Helper/RandomImageHelper.php | 255 +- .../modules/mod_random_image/tmpl/default.php | 10 +- .../mod_related_items/mod_related_items.php | 8 +- .../mod_related_items/mod_related_items.xml | 2 +- .../src/Helper/RelatedItemsHelper.php | 300 +- .../mod_related_items/tmpl/default.php | 9 +- code/modules/mod_stats/mod_stats.php | 1 + code/modules/mod_stats/mod_stats.xml | 2 +- .../mod_stats/src/Helper/StatsHelper.php | 287 +- code/modules/mod_stats/tmpl/default.php | 9 +- code/modules/mod_syndicate/mod_syndicate.php | 8 +- code/modules/mod_syndicate/mod_syndicate.xml | 2 +- .../src/Helper/SyndicateHelper.php | 49 +- code/modules/mod_syndicate/tmpl/default.php | 3 +- .../mod_tags_popular/mod_tags_popular.php | 8 +- .../mod_tags_popular/mod_tags_popular.xml | 2 +- .../src/Helper/TagsPopularHelper.php | 343 +- code/modules/mod_tags_popular/tmpl/cloud.php | 66 +- .../modules/mod_tags_popular/tmpl/default.php | 31 +- .../mod_tags_similar/mod_tags_similar.php | 3 +- .../mod_tags_similar/mod_tags_similar.xml | 2 +- .../src/Helper/TagsSimilarHelper.php | 397 +- .../modules/mod_tags_similar/tmpl/default.php | 36 +- .../mod_users_latest/mod_users_latest.php | 1 + .../mod_users_latest/mod_users_latest.xml | 2 +- .../src/Helper/UsersLatestHelper.php | 84 +- .../modules/mod_users_latest/tmpl/default.php | 15 +- .../modules/mod_whosonline/mod_whosonline.php | 28 +- .../modules/mod_whosonline/mod_whosonline.xml | 2 +- .../src/Helper/WhosonlineHelper.php | 207 +- code/modules/mod_whosonline/tmpl/default.php | 35 +- code/modules/mod_whosonline/tmpl/disabled.php | 3 +- code/modules/mod_wrapper/mod_wrapper.php | 1 + code/modules/mod_wrapper/mod_wrapper.xml | 2 +- .../mod_wrapper/src/Helper/WrapperHelper.php | 86 +- code/modules/mod_wrapper/tmpl/default.php | 19 +- code/plugins/actionlog/joomla/joomla.php | 1173 -- code/plugins/actionlog/joomla/joomla.xml | 8 +- .../actionlog/joomla/services/provider.php | 49 + .../actionlog/joomla/src/Extension/Joomla.php | 1130 + .../api-authentication/basic/basic.php | 120 - .../api-authentication/basic/basic.xml | 10 +- .../basic/services/provider.php | 51 + .../basic/src/Extension/Basic.php | 125 + .../token/services/provider.php | 53 + .../token/src/Extension/Token.php | 388 + .../api-authentication/token/token.php | 402 - .../api-authentication/token/token.xml | 6 +- code/plugins/authentication/cookie/cookie.php | 802 +- code/plugins/authentication/cookie/cookie.xml | 2 +- code/plugins/authentication/joomla/joomla.php | 303 +- code/plugins/authentication/joomla/joomla.xml | 2 +- code/plugins/authentication/ldap/ldap.php | 382 +- code/plugins/authentication/ldap/ldap.xml | 2 +- .../behaviour/taggable/services/provider.php | 46 + .../taggable/src/Extension/Taggable.php | 344 + code/plugins/behaviour/taggable/taggable.php | 357 - code/plugins/behaviour/taggable/taggable.xml | 10 +- .../versionable/services/provider.php | 52 + .../versionable/src/Extension/Versionable.php | 157 + .../behaviour/versionable/versionable.php | 126 - .../behaviour/versionable/versionable.xml | 10 +- code/plugins/captcha/recaptcha/recaptcha.php | 359 +- code/plugins/captcha/recaptcha/recaptcha.xml | 2 +- .../recaptcha_invisible.php | 363 +- .../recaptcha_invisible.xml | 2 +- .../content/confirmconsent/confirmconsent.php | 109 +- .../content/confirmconsent/confirmconsent.xml | 2 +- .../src/Field/ConsentBoxField.php | 632 +- code/plugins/content/contact/contact.php | 261 +- code/plugins/content/contact/contact.xml | 2 +- .../plugins/content/emailcloak/emailcloak.php | 820 +- .../plugins/content/emailcloak/emailcloak.xml | 2 +- code/plugins/content/fields/fields.php | 282 +- code/plugins/content/fields/fields.xml | 2 +- code/plugins/content/finder/finder.php | 195 +- code/plugins/content/finder/finder.xml | 2 +- code/plugins/content/joomla/joomla.php | 1217 +- code/plugins/content/joomla/joomla.xml | 2 +- .../plugins/content/loadmodule/loadmodule.php | 451 +- .../plugins/content/loadmodule/loadmodule.xml | 2 +- code/plugins/content/pagebreak/pagebreak.php | 696 +- code/plugins/content/pagebreak/pagebreak.xml | 2 +- .../content/pagebreak/tmpl/navigation.php | 45 +- code/plugins/content/pagebreak/tmpl/toc.php | 31 +- .../content/pagenavigation/pagenavigation.php | 460 +- .../content/pagenavigation/pagenavigation.xml | 2 +- .../content/pagenavigation/tmpl/default.php | 45 +- code/plugins/content/vote/tmpl/rating.php | 56 +- code/plugins/content/vote/tmpl/vote.php | 24 +- code/plugins/content/vote/vote.php | 250 +- code/plugins/content/vote/vote.xml | 2 +- code/plugins/editors-xtd/article/article.php | 106 +- code/plugins/editors-xtd/article/article.xml | 2 +- code/plugins/editors-xtd/contact/contact.php | 104 +- code/plugins/editors-xtd/contact/contact.xml | 2 +- code/plugins/editors-xtd/fields/fields.php | 115 +- code/plugins/editors-xtd/fields/fields.xml | 2 +- code/plugins/editors-xtd/image/image.php | 308 +- code/plugins/editors-xtd/image/image.xml | 2 +- code/plugins/editors-xtd/menu/menu.php | 106 +- code/plugins/editors-xtd/menu/menu.xml | 2 +- code/plugins/editors-xtd/module/module.php | 106 +- code/plugins/editors-xtd/module/module.xml | 2 +- .../editors-xtd/pagebreak/pagebreak.php | 108 +- .../editors-xtd/pagebreak/pagebreak.xml | 2 +- .../plugins/editors-xtd/readmore/readmore.php | 99 +- .../plugins/editors-xtd/readmore/readmore.xml | 2 +- .../plugins/editors/codemirror/codemirror.php | 603 +- .../plugins/editors/codemirror/codemirror.xml | 2 +- .../layouts/editors/codemirror/element.php | 35 +- .../layouts/editors/codemirror/styles.php | 1 + .../codemirror/src/Field/FontsField.php | 63 +- code/plugins/editors/none/none.php | 161 +- code/plugins/editors/none/none.xml | 2 +- .../tinymce/src/Field/TemplateslistField.php | 138 +- .../tinymce/src/Field/TinymcebuilderField.php | 310 +- .../tinymce/src/Field/UploaddirsField.php | 118 +- .../src/PluginTraits/ActiveSiteTemplate.php | 71 +- .../tinymce/src/PluginTraits/DisplayTrait.php | 1028 +- .../src/PluginTraits/GlobalFilters.php | 343 +- .../tinymce/src/PluginTraits/KnownButtons.php | 172 +- .../tinymce/src/PluginTraits/ResolveFiles.php | 190 +- .../src/PluginTraits/ToolbarPresets.php | 134 +- .../tinymce/src/PluginTraits/XTDButtons.php | 103 +- code/plugins/editors/tinymce/tinymce.php | 83 +- code/plugins/editors/tinymce/tinymce.xml | 4 +- code/plugins/extension/finder/finder.php | 395 +- code/plugins/extension/finder/finder.xml | 2 +- code/plugins/extension/joomla/joomla.php | 563 +- code/plugins/extension/joomla/joomla.xml | 2 +- .../extension/namespacemap/namespacemap.php | 164 +- .../extension/namespacemap/namespacemap.xml | 2 +- code/plugins/fields/calendar/calendar.php | 66 +- code/plugins/fields/calendar/calendar.xml | 2 +- .../plugins/fields/calendar/tmpl/calendar.php | 11 +- code/plugins/fields/checkboxes/checkboxes.php | 88 +- code/plugins/fields/checkboxes/checkboxes.xml | 2 +- .../fields/checkboxes/tmpl/checkboxes.php | 17 +- code/plugins/fields/color/color.php | 56 +- code/plugins/fields/color/color.xml | 2 +- code/plugins/fields/color/tmpl/color.php | 11 +- code/plugins/fields/editor/editor.php | 58 +- code/plugins/fields/editor/editor.xml | 2 +- code/plugins/fields/editor/tmpl/editor.php | 7 +- code/plugins/fields/imagelist/imagelist.php | 58 +- code/plugins/fields/imagelist/imagelist.xml | 2 +- .../fields/imagelist/tmpl/imagelist.php | 75 +- code/plugins/fields/integer/integer.php | 7 +- code/plugins/fields/integer/integer.xml | 2 +- code/plugins/fields/integer/tmpl/integer.php | 17 +- code/plugins/fields/list/list.php | 101 +- code/plugins/fields/list/list.xml | 2 +- code/plugins/fields/list/tmpl/list.php | 17 +- code/plugins/fields/media/media.xml | 2 +- code/plugins/fields/radio/radio.php | 53 +- code/plugins/fields/radio/radio.xml | 2 +- code/plugins/fields/radio/tmpl/radio.php | 17 +- code/plugins/fields/sql/sql.php | 104 +- code/plugins/fields/sql/sql.xml | 2 +- code/plugins/fields/sql/tmpl/sql.php | 34 +- code/plugins/fields/subform/subform.php | 795 +- code/plugins/fields/subform/subform.xml | 2 +- code/plugins/fields/subform/tmpl/subform.php | 78 +- code/plugins/fields/text/text.php | 7 +- code/plugins/fields/text/text.xml | 2 +- code/plugins/fields/text/tmpl/text.php | 11 +- code/plugins/fields/textarea/textarea.php | 7 +- code/plugins/fields/textarea/textarea.xml | 2 +- .../plugins/fields/textarea/tmpl/textarea.php | 7 +- code/plugins/fields/url/tmpl/url.php | 34 +- code/plugins/fields/url/url.php | 65 +- code/plugins/fields/url/url.xml | 2 +- code/plugins/fields/user/tmpl/user.php | 34 +- code/plugins/fields/user/user.php | 48 +- code/plugins/fields/user/user.xml | 2 +- .../usergrouplist/tmpl/usergrouplist.php | 17 +- .../fields/usergrouplist/usergrouplist.php | 7 +- .../fields/usergrouplist/usergrouplist.xml | 2 +- code/plugins/filesystem/local/local.php | 153 +- code/plugins/filesystem/local/local.xml | 2 +- .../local/src/Adapter/LocalAdapter.php | 1619 +- code/plugins/finder/categories/categories.php | 892 +- code/plugins/finder/categories/categories.xml | 2 +- code/plugins/finder/contacts/contacts.php | 833 +- code/plugins/finder/contacts/contacts.xml | 2 +- code/plugins/finder/content/content.php | 728 +- code/plugins/finder/content/content.xml | 2 +- code/plugins/finder/newsfeeds/newsfeeds.php | 685 +- code/plugins/finder/newsfeeds/newsfeeds.xml | 2 +- code/plugins/finder/tags/tags.php | 661 +- code/plugins/finder/tags/tags.xml | 2 +- .../folderinstaller/folderinstaller.php | 70 +- .../folderinstaller/folderinstaller.xml | 2 +- .../folderinstaller/tmpl/default.php | 39 +- code/plugins/installer/override/override.php | 781 +- code/plugins/installer/override/override.xml | 2 +- .../packageinstaller/packageinstaller.php | 71 +- .../packageinstaller/packageinstaller.xml | 2 +- .../packageinstaller/tmpl/default.php | 139 +- .../installer/urlinstaller/tmpl/default.php | 25 +- .../installer/urlinstaller/urlinstaller.php | 70 +- .../installer/urlinstaller/urlinstaller.xml | 2 +- .../installer/webinstaller/tmpl/default.php | 45 +- .../installer/webinstaller/webinstaller.php | 298 +- .../installer/webinstaller/webinstaller.xml | 2 +- code/plugins/media-action/crop/crop.php | 63 +- code/plugins/media-action/crop/crop.xml | 2 +- code/plugins/media-action/resize/resize.php | 110 +- code/plugins/media-action/resize/resize.xml | 2 +- code/plugins/media-action/rotate/rotate.php | 7 +- code/plugins/media-action/rotate/rotate.xml | 2 +- code/plugins/multifactorauth/email/email.xml | 52 + .../email/services/provider.php | 47 + .../email/src/Extension/Email.php | 582 + code/plugins/multifactorauth/fixed/fixed.xml | 21 + .../fixed/services/provider.php | 43 + .../fixed/src/Extension/Fixed.php | 308 + .../totp/services/provider.php | 47 + .../totp/src/Extension/Totp.php | 384 + code/plugins/multifactorauth/totp/totp.xml | 21 + .../webauthn/services/provider.php | 47 + .../webauthn/src/CredentialRepository.php | 257 + .../webauthn/src/Extension/Webauthn.php | 440 + .../webauthn/src/Helper/Credentials.php | 336 + .../AndroidKeyAttestationStatementSupport.php | 265 + .../FidoU2FAttestationStatementSupport.php | 224 + .../webauthn/src/Hotfix/Server.php | 449 + .../multifactorauth/webauthn/tmpl/default.php | 47 + .../multifactorauth/webauthn/webauthn.xml | 22 + .../yubikey/services/provider.php | 47 + .../yubikey/src/Extension/Yubikey.php | 614 + .../multifactorauth/yubikey/yubikey.xml | 21 + .../plugins/privacy/actionlogs/actionlogs.xml | 2 +- code/plugins/privacy/consents/consents.php | 83 +- code/plugins/privacy/consents/consents.xml | 2 +- code/plugins/privacy/contact/contact.php | 94 +- code/plugins/privacy/contact/contact.xml | 2 +- code/plugins/privacy/content/content.php | 79 +- code/plugins/privacy/content/content.xml | 2 +- code/plugins/privacy/message/message.php | 85 +- code/plugins/privacy/message/message.xml | 2 +- code/plugins/privacy/user/user.php | 436 +- code/plugins/privacy/user/user.xml | 2 +- .../quickicon/downloadkey/downloadkey.php | 224 +- .../quickicon/downloadkey/downloadkey.xml | 2 +- .../extensionupdate/extensionupdate.php | 136 +- .../extensionupdate/extensionupdate.xml | 2 +- .../quickicon/joomlaupdate/joomlaupdate.xml | 2 +- .../joomlaupdate/services/provider.php | 53 +- .../src/Extension/Joomlaupdate.php | 231 +- .../quickicon/overridecheck/overridecheck.php | 205 +- .../quickicon/overridecheck/overridecheck.xml | 2 +- .../phpversioncheck/phpversioncheck.php | 444 +- .../phpversioncheck/phpversioncheck.xml | 2 +- .../quickicon/privacycheck/privacycheck.php | 126 +- .../quickicon/privacycheck/privacycheck.xml | 2 +- code/plugins/sampledata/blog/blog.php | 3785 ++-- code/plugins/sampledata/blog/blog.xml | 2 +- .../sampledata/multilang/multilang.php | 2667 ++- .../sampledata/multilang/multilang.xml | 2 +- .../system/accessibility/accessibility.php | 182 +- .../system/accessibility/accessibility.xml | 10 + code/plugins/system/actionlogs/actionlogs.xml | 2 +- .../system/actionlogs/forms/actionlogs.xml | 2 + code/plugins/system/cache/cache.php | 279 - code/plugins/system/cache/cache.xml | 6 +- .../system/cache/services/provider.php | 53 + .../system/cache/src/Extension/Cache.php | 391 + code/plugins/system/debug/debug.php | 1362 +- code/plugins/system/debug/debug.xml | 2 +- .../debug/src/AbstractDataCollector.php | 161 +- .../debug/src/DataCollector/InfoCollector.php | 372 +- .../DataCollector/LanguageErrorsCollector.php | 238 +- .../DataCollector/LanguageFilesCollector.php | 192 +- .../LanguageStringsCollector.php | 328 +- .../src/DataCollector/ProfileCollector.php | 592 +- .../src/DataCollector/QueryCollector.php | 469 +- .../src/DataCollector/SessionCollector.php | 112 +- .../system/debug/src/DataFormatter.php | 126 +- .../system/debug/src/JavascriptRenderer.php | 228 +- .../system/debug/src/JoomlaHttpDriver.php | 273 +- .../system/debug/src/Storage/FileStorage.php | 243 +- code/plugins/system/fields/fields.php | 1030 +- code/plugins/system/fields/fields.xml | 2 +- code/plugins/system/highlight/highlight.php | 229 +- code/plugins/system/highlight/highlight.xml | 2 +- .../system/httpheaders/httpheaders.php | 894 +- .../system/httpheaders/httpheaders.xml | 2 +- .../httpheaders/postinstall/introduction.php | 55 +- code/plugins/system/jooa11y/jooa11y.php | 473 +- code/plugins/system/jooa11y/jooa11y.xml | 2 +- .../system/languagecode/languagecode.php | 243 +- .../system/languagecode/languagecode.xml | 2 +- .../system/languagefilter/languagefilter.php | 1710 +- .../system/languagefilter/languagefilter.xml | 2 +- code/plugins/system/log/log.php | 92 +- code/plugins/system/log/log.xml | 2 +- code/plugins/system/logout/logout.php | 142 +- code/plugins/system/logout/logout.xml | 2 +- .../system/logrotation/logrotation.php | 474 +- .../system/logrotation/logrotation.xml | 2 +- .../system/privacyconsent/privacyconsent.php | 1390 +- .../system/privacyconsent/privacyconsent.xml | 2 +- .../privacyconsent/src/Field/PrivacyField.php | 216 +- code/plugins/system/redirect/redirect.php | 529 +- code/plugins/system/redirect/redirect.xml | 2 +- code/plugins/system/remember/remember.php | 243 +- code/plugins/system/remember/remember.xml | 2 +- .../system/schedulerunner/schedulerunner.php | 686 +- .../system/schedulerunner/schedulerunner.xml | 2 +- code/plugins/system/sef/sef.php | 426 +- code/plugins/system/sef/sef.xml | 2 +- code/plugins/system/sessiongc/sessiongc.php | 97 +- code/plugins/system/sessiongc/sessiongc.xml | 2 +- .../system/shortcut/services/provider.php | 48 + code/plugins/system/shortcut/shortcut.xml | 40 + .../shortcut/src/Extension/Shortcut.php | 140 + code/plugins/system/skipto/skipto.php | 180 +- code/plugins/system/skipto/skipto.xml | 2 +- .../system/stats/layouts/field/data.php | 7 +- .../system/stats/layouts/field/uniqueid.php | 3 +- code/plugins/system/stats/layouts/message.php | 39 +- code/plugins/system/stats/layouts/stats.php | 43 +- .../stats/src/Field/AbstractStatsField.php | 39 +- .../system/stats/src/Field/DataField.php | 77 +- .../system/stats/src/Field/UniqueidField.php | 31 +- code/plugins/system/stats/stats.php | 1234 +- code/plugins/system/stats/stats.xml | 2 +- .../tasknotification/tasknotification.php | 582 +- .../tasknotification/tasknotification.xml | 2 +- .../postinstall/updatecachetime.php | 49 +- .../updatenotification/updatenotification.php | 725 +- .../updatenotification/updatenotification.xml | 2 +- code/plugins/system/webauthn/fido.jwt | 1 + .../system/webauthn/services/provider.php | 89 + .../system/webauthn/src/Authentication.php | 551 + .../webauthn/src/CredentialRepository.php | 1078 +- .../src/Exception/AjaxNonCmsAppException.php | 24 - .../webauthn/src/Extension/Webauthn.php | 184 + .../webauthn/src/Field/WebauthnField.php | 96 +- .../src/Helper/CredentialsCreation.php | 358 - .../system/webauthn/src/Helper/Joomla.php | 744 - .../AndroidKeyAttestationStatementSupport.php | 261 + .../FidoU2FAttestationStatementSupport.php | 220 + .../system/webauthn/src/Hotfix/Server.php | 445 + .../webauthn/src/MetadataRepository.php | 184 + .../PluginTraits/AdditionalLoginButtons.php | 367 +- .../webauthn/src/PluginTraits/AjaxHandler.php | 307 +- .../src/PluginTraits/AjaxHandlerChallenge.php | 217 +- .../src/PluginTraits/AjaxHandlerCreate.php | 180 +- .../src/PluginTraits/AjaxHandlerDelete.php | 137 +- .../PluginTraits/AjaxHandlerInitCreate.php | 62 + .../src/PluginTraits/AjaxHandlerLogin.php | 516 +- .../src/PluginTraits/AjaxHandlerSaveLabel.php | 150 +- .../src/PluginTraits/EventReturnAware.php | 47 + .../src/PluginTraits/UserDeletion.php | 86 +- .../src/PluginTraits/UserProfileFields.php | 377 +- code/plugins/system/webauthn/webauthn.php | 78 - code/plugins/system/webauthn/webauthn.xml | 21 +- code/plugins/task/checkfiles/checkfiles.php | 146 - code/plugins/task/checkfiles/checkfiles.xml | 7 +- .../task/checkfiles/services/provider.php | 48 + .../checkfiles/src/Extension/Checkfiles.php | 169 + code/plugins/task/demotasks/demotasks.php | 232 - code/plugins/task/demotasks/demotasks.xml | 7 +- .../task/demotasks/services/provider.php | 49 + .../demotasks/src/Extension/DemoTasks.php | 223 + code/plugins/task/requests/requests.php | 141 - code/plugins/task/requests/requests.xml | 7 +- .../task/requests/services/provider.php | 50 + .../task/requests/src/Extension/Requests.php | 170 + .../task/sitestatus/services/provider.php | 50 + code/plugins/task/sitestatus/sitestatus.php | 172 - code/plugins/task/sitestatus/sitestatus.xml | 8 +- .../sitestatus/src/Extension/SiteStatus.php | 193 + .../totp/postinstall/actions.php | 68 - code/plugins/twofactorauth/totp/tmpl/form.php | 130 - code/plugins/twofactorauth/totp/totp.php | 299 - code/plugins/twofactorauth/totp/totp.xml | 39 - .../twofactorauth/yubikey/tmpl/form.php | 47 - .../plugins/twofactorauth/yubikey/yubikey.php | 367 - .../plugins/twofactorauth/yubikey/yubikey.xml | 38 - .../user/contactcreator/contactcreator.php | 318 +- .../user/contactcreator/contactcreator.xml | 2 +- code/plugins/user/joomla/joomla.php | 891 +- code/plugins/user/joomla/joomla.xml | 2 +- code/plugins/user/profile/forms/profile.xml | 5 +- code/plugins/user/profile/profile.php | 905 +- code/plugins/user/profile/profile.xml | 2 +- .../user/profile/src/Field/DobField.php | 67 - .../user/profile/src/Field/TosField.php | 239 +- .../user/terms/src/Field/TermsField.php | 201 +- code/plugins/user/terms/terms.php | 276 +- code/plugins/user/terms/terms.xml | 2 +- .../user/token/src/Field/JoomlatokenField.php | 279 +- code/plugins/user/token/token.php | 1249 +- code/plugins/user/token/token.xml | 2 +- code/plugins/webservices/banners/banners.php | 127 +- code/plugins/webservices/banners/banners.xml | 2 +- code/plugins/webservices/config/config.php | 71 +- code/plugins/webservices/config/config.xml | 2 +- code/plugins/webservices/contact/contact.php | 249 +- code/plugins/webservices/contact/contact.xml | 2 +- code/plugins/webservices/content/content.php | 205 +- code/plugins/webservices/content/content.xml | 2 +- .../webservices/installer/installer.php | 63 +- .../webservices/installer/installer.xml | 2 +- .../webservices/languages/languages.php | 222 +- .../webservices/languages/languages.xml | 2 +- code/plugins/webservices/media/media.xml | 2 +- code/plugins/webservices/menus/menus.php | 115 +- code/plugins/webservices/menus/menus.xml | 2 +- .../plugins/webservices/messages/messages.php | 57 +- .../plugins/webservices/messages/messages.xml | 2 +- code/plugins/webservices/modules/modules.php | 95 +- code/plugins/webservices/modules/modules.xml | 2 +- .../webservices/newsfeeds/newsfeeds.php | 67 +- .../webservices/newsfeeds/newsfeeds.xml | 2 +- code/plugins/webservices/plugins/plugins.php | 69 +- code/plugins/webservices/plugins/plugins.xml | 2 +- code/plugins/webservices/privacy/privacy.php | 87 +- code/plugins/webservices/privacy/privacy.xml | 2 +- .../plugins/webservices/redirect/redirect.php | 57 +- .../plugins/webservices/redirect/redirect.xml | 2 +- code/plugins/webservices/tags/tags.php | 57 +- code/plugins/webservices/tags/tags.xml | 2 +- .../webservices/templates/templates.php | 67 +- .../webservices/templates/templates.xml | 2 +- code/plugins/webservices/users/users.php | 123 +- code/plugins/webservices/users/users.xml | 2 +- code/plugins/workflow/featuring/featuring.php | 997 +- code/plugins/workflow/featuring/featuring.xml | 2 +- .../workflow/notification/notification.php | 634 +- .../workflow/notification/notification.xml | 2 +- .../workflow/publishing/publishing.php | 1021 +- .../workflow/publishing/publishing.xml | 2 +- code/templates/cassiopeia/component.php | 52 +- code/templates/cassiopeia/error.php | 241 +- .../cassiopeia/html/layouts/chromes/card.php | 41 +- .../html/layouts/chromes/noCard.php | 34 +- .../cassiopeia/html/mod_custom/banner.php | 16 +- .../html/mod_menu/collapse-metismenu.php | 13 +- .../html/mod_menu/dropdown-metismenu.php | 170 +- .../mod_menu/dropdown-metismenu_component.php | 98 +- .../mod_menu/dropdown-metismenu_heading.php | 75 +- .../mod_menu/dropdown-metismenu_separator.php | 75 +- .../html/mod_menu/dropdown-metismenu_url.php | 95 +- code/templates/cassiopeia/index.php | 311 +- code/templates/cassiopeia/joomla.asset.json | 1 + code/templates/cassiopeia/offline.php | 231 +- code/templates/cassiopeia/templateDetails.xml | 2 +- code/templates/system/build_incomplete.html | 2 +- code/templates/system/component.php | 7 +- code/templates/system/error.php | 129 +- code/templates/system/fatal-error.html | 2 +- code/templates/system/fatal.php | 19 +- code/templates/system/incompatible.html | 2 +- code/templates/system/index.php | 1 + code/templates/system/offline.php | 92 +- 3972 files changed, 422636 insertions(+), 342231 deletions(-) create mode 100644 code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-05-15.sql create mode 100644 code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-06-15.sql create mode 100644 code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-06-19.sql create mode 100644 code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-06-22.sql create mode 100644 code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-07-07.sql create mode 100644 code/administrator/components/com_admin/sql/updates/mysql/4.2.1-2022-08-23.sql create mode 100644 code/administrator/components/com_admin/sql/updates/mysql/4.2.3-2022-09-07.sql create mode 100644 code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-05-15.sql create mode 100644 code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-06-19.sql create mode 100644 code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-06-22.sql create mode 100644 code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-07-07.sql create mode 100644 code/administrator/components/com_admin/sql/updates/postgresql/4.2.1-2022-08-23.sql create mode 100644 code/administrator/components/com_admin/sql/updates/postgresql/4.2.3-2022-09-07.sql create mode 100644 code/administrator/components/com_installer/src/Field/PackageField.php create mode 100644 code/administrator/components/com_users/postinstall/multifactorauth.php create mode 100644 code/administrator/components/com_users/src/Controller/CallbackController.php create mode 100644 code/administrator/components/com_users/src/Controller/CaptiveController.php create mode 100644 code/administrator/components/com_users/src/Controller/MethodController.php create mode 100644 code/administrator/components/com_users/src/Controller/MethodsController.php create mode 100644 code/administrator/components/com_users/src/DataShape/CaptiveRenderOptions.php create mode 100644 code/administrator/components/com_users/src/DataShape/DataShapeObject.php create mode 100644 code/administrator/components/com_users/src/DataShape/MethodDescriptor.php create mode 100644 code/administrator/components/com_users/src/DataShape/SetupRenderOptions.php create mode 100644 code/administrator/components/com_users/src/Field/ModulesPositionField.php create mode 100644 code/administrator/components/com_users/src/Helper/Mfa.php create mode 100644 code/administrator/components/com_users/src/Model/BackupcodesModel.php create mode 100644 code/administrator/components/com_users/src/Model/CaptiveModel.php create mode 100644 code/administrator/components/com_users/src/Model/MethodModel.php create mode 100644 code/administrator/components/com_users/src/Model/MethodsModel.php create mode 100644 code/administrator/components/com_users/src/Service/Encrypt.php create mode 100644 code/administrator/components/com_users/src/Table/MfaTable.php create mode 100644 code/administrator/components/com_users/src/View/Captive/HtmlView.php create mode 100644 code/administrator/components/com_users/src/View/Method/HtmlView.php create mode 100644 code/administrator/components/com_users/src/View/Methods/HtmlView.php create mode 100644 code/administrator/components/com_users/src/View/SiteTemplateTrait.php create mode 100644 code/administrator/components/com_users/tmpl/captive/default.php create mode 100644 code/administrator/components/com_users/tmpl/captive/select.php create mode 100644 code/administrator/components/com_users/tmpl/method/backupcodes.php create mode 100644 code/administrator/components/com_users/tmpl/method/edit.php create mode 100644 code/administrator/components/com_users/tmpl/methods/default.php create mode 100644 code/administrator/components/com_users/tmpl/methods/firsttime.php create mode 100644 code/administrator/components/com_users/tmpl/methods/list.php create mode 100644 code/administrator/language/en-GB/plg_multifactorauth_email.ini create mode 100644 code/administrator/language/en-GB/plg_multifactorauth_email.sys.ini create mode 100644 code/administrator/language/en-GB/plg_multifactorauth_fixed.ini create mode 100644 code/administrator/language/en-GB/plg_multifactorauth_fixed.sys.ini create mode 100644 code/administrator/language/en-GB/plg_multifactorauth_totp.ini create mode 100644 code/administrator/language/en-GB/plg_multifactorauth_totp.sys.ini create mode 100644 code/administrator/language/en-GB/plg_multifactorauth_webauthn.ini create mode 100644 code/administrator/language/en-GB/plg_multifactorauth_webauthn.sys.ini create mode 100644 code/administrator/language/en-GB/plg_multifactorauth_yubikey.ini create mode 100644 code/administrator/language/en-GB/plg_multifactorauth_yubikey.sys.ini create mode 100644 code/administrator/language/en-GB/plg_system_shortcut.ini create mode 100644 code/administrator/language/en-GB/plg_system_shortcut.sys.ini create mode 100644 code/components/com_users/src/Controller/CallbackController.php create mode 100644 code/components/com_users/src/Controller/CaptiveController.php create mode 100644 code/components/com_users/src/Controller/MethodController.php create mode 100644 code/components/com_users/src/Controller/MethodsController.php create mode 100644 code/components/com_users/src/Model/BackupcodesModel.php create mode 100644 code/components/com_users/src/Model/CaptiveModel.php create mode 100644 code/components/com_users/src/Model/MethodModel.php create mode 100644 code/components/com_users/src/Model/MethodsModel.php create mode 100644 code/components/com_users/src/View/Captive/HtmlView.php create mode 100644 code/components/com_users/src/View/Method/HtmlView.php create mode 100644 code/components/com_users/src/View/Methods/HtmlView.php create mode 100644 code/components/com_users/tmpl/captive/default.php create mode 100644 code/components/com_users/tmpl/captive/select.php create mode 100644 code/components/com_users/tmpl/method/backupcodes.php create mode 100644 code/components/com_users/tmpl/method/edit.php create mode 100644 code/components/com_users/tmpl/methods/default.php create mode 100644 code/components/com_users/tmpl/methods/firsttime.php create mode 100644 code/components/com_users/tmpl/methods/list.php delete mode 100644 code/layouts/plugins/user/profile/fields/dob.php create mode 100644 code/libraries/src/Application/MultiFactorAuthenticationHandler.php create mode 100644 code/libraries/src/Cache/CacheControllerFactoryAwareInterface.php create mode 100644 code/libraries/src/Cache/CacheControllerFactoryAwareTrait.php create mode 100644 code/libraries/src/Event/CoreEventAware.php create mode 100644 code/libraries/src/Event/MultiFactor/BeforeDisplayMethods.php create mode 100644 code/libraries/src/Event/MultiFactor/Callback.php create mode 100644 code/libraries/src/Event/MultiFactor/Captive.php create mode 100644 code/libraries/src/Event/MultiFactor/GetMethod.php create mode 100644 code/libraries/src/Event/MultiFactor/GetSetup.php create mode 100644 code/libraries/src/Event/MultiFactor/NotifyActionLog.php create mode 100644 code/libraries/src/Event/MultiFactor/SaveSetup.php create mode 100644 code/libraries/src/Event/MultiFactor/Validate.php create mode 100644 code/libraries/src/Event/Plugin/System/Webauthn/Ajax.php create mode 100644 code/libraries/src/Event/Plugin/System/Webauthn/AjaxChallenge.php create mode 100644 code/libraries/src/Event/Plugin/System/Webauthn/AjaxCreate.php create mode 100644 code/libraries/src/Event/Plugin/System/Webauthn/AjaxDelete.php create mode 100644 code/libraries/src/Event/Plugin/System/Webauthn/AjaxInitCreate.php create mode 100644 code/libraries/src/Event/Plugin/System/Webauthn/AjaxLogin.php create mode 100644 code/libraries/src/Event/Plugin/System/Webauthn/AjaxSaveLabel.php create mode 100644 code/libraries/src/Event/QuickIcon/GetIconEvent.php create mode 100644 code/libraries/src/Event/ReshapeArgumentsAware.php create mode 100644 code/libraries/src/Event/Result/ResultAware.php create mode 100644 code/libraries/src/Event/Result/ResultAwareInterface.php create mode 100644 code/libraries/src/Event/Result/ResultTypeArrayAware.php create mode 100644 code/libraries/src/Event/Result/ResultTypeBooleanAware.php create mode 100644 code/libraries/src/Event/Result/ResultTypeFloatAware.php create mode 100644 code/libraries/src/Event/Result/ResultTypeIntegerAware.php create mode 100644 code/libraries/src/Event/Result/ResultTypeMixedAware.php create mode 100644 code/libraries/src/Event/Result/ResultTypeNumericAware.php create mode 100644 code/libraries/src/Event/Result/ResultTypeObjectAware.php create mode 100644 code/libraries/src/Event/Result/ResultTypeStringAware.php create mode 100644 code/libraries/src/Helper/HelperFactoryAwareInterface.php create mode 100644 code/libraries/src/Helper/HelperFactoryAwareTrait.php create mode 100644 code/libraries/src/Installer/InstallerScriptInterface.php create mode 100644 code/libraries/src/Installer/LegacyInstallerScript.php create mode 100644 code/libraries/src/Router/SiteRouterAwareInterface.php create mode 100644 code/libraries/src/Router/SiteRouterAwareTrait.php delete mode 100644 code/libraries/src/Service/Provider/ApiRouter.php create mode 100644 code/libraries/src/Service/Provider/Router.php create mode 100644 code/libraries/src/User/CurrentUserInterface.php create mode 100644 code/libraries/src/User/CurrentUserTrait.php create mode 100644 code/libraries/src/WebAsset/AssetItem/TableColumnsAssetItem.php create mode 100644 code/libraries/vendor/joomla/database/src/DatabaseAwareInterface.php create mode 100644 code/libraries/vendor/joomla/database/src/DatabaseAwareTrait.php create mode 100644 code/libraries/vendor/joomla/database/src/Exception/DatabaseNotFoundException.php create mode 100644 code/libraries/vendor/lcobucci/jwt/LICENSE create mode 100644 code/libraries/vendor/lcobucci/jwt/compat/class-aliases.php create mode 100644 code/libraries/vendor/lcobucci/jwt/compat/json-exception-polyfill.php create mode 100644 code/libraries/vendor/lcobucci/jwt/compat/lcobucci-clock-polyfill.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Builder.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Claim.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Claim/Basic.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Claim/EqualsTo.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Claim/Factory.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Claim/GreaterOrEqualsTo.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Claim/LesserOrEqualsTo.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Claim/Validatable.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Configuration.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Encoding/CannotDecodeContent.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Encoding/CannotEncodeContent.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Exception.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Parser.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Parsing/Decoder.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Parsing/Encoder.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signature.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/BaseSigner.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/CannotSignPayload.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Ecdsa.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Ecdsa/ConversionFailed.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Ecdsa/MultibyteStringConverter.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Ecdsa/Sha256.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Ecdsa/Sha384.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Ecdsa/Sha512.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Ecdsa/SignatureConverter.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Hmac.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Hmac/Sha256.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Hmac/Sha384.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Hmac/Sha512.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/InvalidKeyProvided.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Key.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Key/FileCouldNotBeRead.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Key/InMemory.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Key/LocalFileReference.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Keychain.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/None.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/OpenSSL.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Rsa.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Rsa/Sha256.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Rsa/Sha384.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Signer/Rsa/Sha512.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Token.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Token/DataSet.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Token/InvalidTokenStructure.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Token/Plain.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Token/RegisteredClaimGiven.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Token/RegisteredClaims.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Token/Signature.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Token/UnsupportedHeaderFound.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validation/Constraint.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validation/Constraint/IdentifiedBy.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validation/Constraint/IssuedBy.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validation/Constraint/LeewayCannotBeNegative.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validation/Constraint/PermittedFor.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validation/Constraint/RelatedTo.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validation/Constraint/SignedWith.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validation/Constraint/ValidAt.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validation/ConstraintViolation.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validation/NoConstraintsGiven.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validation/RequiredConstraintsViolated.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validation/Validator.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/ValidationData.php create mode 100644 code/libraries/vendor/lcobucci/jwt/src/Validator.php delete mode 100644 code/libraries/vendor/nyholm/psr7/doc/final.md create mode 100644 code/libraries/vendor/paragonie/constant_time_encoding/LICENSE.txt create mode 100644 code/libraries/vendor/paragonie/constant_time_encoding/src/Base32.php create mode 100644 code/libraries/vendor/paragonie/constant_time_encoding/src/Base32Hex.php create mode 100644 code/libraries/vendor/paragonie/constant_time_encoding/src/Base64.php create mode 100644 code/libraries/vendor/paragonie/constant_time_encoding/src/Base64DotSlash.php create mode 100644 code/libraries/vendor/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php create mode 100644 code/libraries/vendor/paragonie/constant_time_encoding/src/Base64UrlSafe.php create mode 100644 code/libraries/vendor/paragonie/constant_time_encoding/src/Binary.php create mode 100644 code/libraries/vendor/paragonie/constant_time_encoding/src/EncoderInterface.php create mode 100644 code/libraries/vendor/paragonie/constant_time_encoding/src/Encoding.php create mode 100644 code/libraries/vendor/paragonie/constant_time_encoding/src/Hex.php create mode 100644 code/libraries/vendor/paragonie/constant_time_encoding/src/RFC4648.php create mode 100644 code/libraries/vendor/phpmailer/phpmailer/src/OAuthTokenProvider.php create mode 100644 code/libraries/vendor/phpseclib/bcmath_compat/LICENSE.md create mode 100644 code/libraries/vendor/phpseclib/bcmath_compat/lib/bcmath.php create mode 100644 code/libraries/vendor/phpseclib/bcmath_compat/src/BCMath.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/AUTHORS create mode 100644 code/libraries/vendor/phpseclib/phpseclib/BACKERS.md create mode 100644 code/libraries/vendor/phpseclib/phpseclib/LICENSE create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Common/Functions/Strings.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/AES.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Blowfish.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/ChaCha20.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/AsymmetricKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/BlockCipher.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/OpenSSH.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PKCS.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PKCS1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PKCS8.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PuTTY.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Signature/Raw.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/PrivateKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/PublicKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/StreamCipher.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/SymmetricKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Traits/Fingerprint.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Traits/PasswordProtected.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DES.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DH.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DH/Formats/Keys/PKCS1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DH/Formats/Keys/PKCS8.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DH/Parameters.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DH/PrivateKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DH/PublicKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/OpenSSH.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/PKCS1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/PKCS8.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/PuTTY.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/Raw.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/XML.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Signature/ASN1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Signature/Raw.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Signature/SSH2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/Parameters.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/PrivateKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/DSA/PublicKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/Base.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/Binary.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/KoblitzPrime.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/Montgomery.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/Prime.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/TwistedEdwards.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/Curve25519.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/Curve448.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/Ed25519.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/Ed448.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP160r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP160t1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP192r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP192t1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP224r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP224t1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP256r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP256t1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP320r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP320t1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP384r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP384t1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP512r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP512t1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistb233.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistb409.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistk163.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistk233.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistk283.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistk409.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp192.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp224.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp256.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp384.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp521.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistt571.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime192v1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime192v2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime192v3.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime239v1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime239v2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime239v3.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime256v1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp112r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp112r2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp128r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp128r2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp160k1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp160r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp160r2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp192k1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp192r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp224k1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp224r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp256k1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp256r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp384r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp521r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect113r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect113r2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect131r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect131r2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect163k1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect163r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect163r2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect193r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect193r2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect233k1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect233r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect239k1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect283k1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect283r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect409k1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect409r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect571k1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect571r1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/Common.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/MontgomeryPrivate.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/MontgomeryPublic.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/OpenSSH.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/PKCS1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/PKCS8.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/PuTTY.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/XML.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/libsodium.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/ASN1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/Raw.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/SSH2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Parameters.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/PrivateKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/PublicKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Hash.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/PublicKeyLoader.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RC2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RC4.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/MSBLOB.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/OpenSSH.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PKCS1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PKCS8.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PSS.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PuTTY.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/Raw.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/XML.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/PrivateKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA/PublicKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Random.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Rijndael.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Salsa20.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/TripleDES.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Crypt/Twofish.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/BadConfigurationException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/BadDecryptionException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/BadModeException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/ConnectionClosedException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/FileNotFoundException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/InconsistentSetupException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/InsufficientSetupException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/NoKeyLoadedException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/NoSupportedAlgorithmsException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/UnableToConnectException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/UnsupportedAlgorithmException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/UnsupportedCurveException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/UnsupportedFormatException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Exception/UnsupportedOperationException.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ANSI.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Element.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AccessDescription.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AdministrationDomainName.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AlgorithmIdentifier.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AnotherName.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Attribute.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AttributeType.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AttributeTypeAndValue.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AttributeValue.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Attributes.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AuthorityInfoAccessSyntax.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AuthorityKeyIdentifier.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BaseDistance.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BasicConstraints.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BuiltInDomainDefinedAttribute.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BuiltInDomainDefinedAttributes.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BuiltInStandardAttributes.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CPSuri.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CRLDistributionPoints.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CRLNumber.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CRLReason.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertPolicyId.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Certificate.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificateIssuer.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificateList.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificatePolicies.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificateSerialNumber.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificationRequest.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificationRequestInfo.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Characteristic_two.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CountryName.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Curve.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DHParameter.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DSAParams.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DSAPrivateKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DSAPublicKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DigestInfo.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DirectoryString.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DisplayText.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DistributionPoint.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DistributionPointName.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DssSigValue.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ECParameters.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ECPoint.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ECPrivateKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/EDIPartyName.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/EcdsaSigValue.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/EncryptedData.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/EncryptedPrivateKeyInfo.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ExtKeyUsageSyntax.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Extension.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ExtensionAttribute.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ExtensionAttributes.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Extensions.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/FieldElement.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/FieldID.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/GeneralName.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/GeneralNames.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/GeneralSubtree.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/GeneralSubtrees.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/HashAlgorithm.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/HoldInstructionCode.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/InvalidityDate.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/IssuerAltName.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/IssuingDistributionPoint.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/KeyIdentifier.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/KeyPurposeId.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/KeyUsage.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/MaskGenAlgorithm.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Name.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/NameConstraints.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/NetworkAddress.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/NoticeReference.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/NumericUserIdentifier.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ORAddress.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OneAsymmetricKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OrganizationName.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OrganizationalUnitNames.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OtherPrimeInfo.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OtherPrimeInfos.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PBEParameter.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PBES2params.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PBKDF2params.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PBMAC1params.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PKCS9String.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Pentanomial.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PersonalName.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PolicyInformation.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PolicyMappings.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PolicyQualifierId.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PolicyQualifierInfo.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PostalAddress.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Prime_p.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PrivateDomainName.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PrivateKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PrivateKeyInfo.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PrivateKeyUsagePeriod.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PublicKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PublicKeyAndChallenge.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PublicKeyInfo.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RC2CBCParameter.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RDNSequence.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RSAPrivateKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RSAPublicKey.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RSASSA_PSS_params.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ReasonFlags.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RelativeDistinguishedName.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RevokedCertificate.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SignedPublicKeyAndChallenge.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SpecifiedECDomain.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SubjectAltName.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SubjectDirectoryAttributes.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SubjectInfoAccessSyntax.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SubjectPublicKeyInfo.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/TBSCertList.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/TBSCertificate.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/TerminalIdentifier.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Time.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Trinomial.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/UniqueIdentifier.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/UserNotice.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Validity.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/netscape_ca_policy_url.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/netscape_cert_type.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/netscape_comment.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/File/X509.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/Base.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/BuiltIn.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/DefaultEngine.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/OpenSSL.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/Reductions/Barrett.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/Reductions/EvalBarrett.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/Engine.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/GMP.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/GMP/DefaultEngine.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/OpenSSL.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Base.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/DefaultEngine.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Montgomery.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/OpenSSL.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/Barrett.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/Classic.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/EvalBarrett.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/Montgomery.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/MontgomeryMult.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/PowerOfTwo.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP32.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP64.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BinaryField.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/BinaryField/Integer.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/Common/FiniteField.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/Common/FiniteField/Integer.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/PrimeField.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Math/PrimeField/Integer.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Net/SFTP.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Net/SFTP/Stream.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/Net/SSH2.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/System/SSH/Agent.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/System/SSH/Agent/Identity.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/System/SSH/Common/Traits/ReadBytes.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/bootstrap.php create mode 100644 code/libraries/vendor/phpseclib/phpseclib/phpseclib/openssl.cnf create mode 100644 code/libraries/vendor/symfony/polyfill-php80/PhpToken.php create mode 100644 code/libraries/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php create mode 100644 code/libraries/vendor/symfony/var-dumper/Caster/MysqliCaster.php create mode 100644 code/libraries/vendor/web-token/jwt-core/Algorithm.php create mode 100644 code/libraries/vendor/web-token/jwt-core/AlgorithmManager.php create mode 100644 code/libraries/vendor/web-token/jwt-core/AlgorithmManagerFactory.php create mode 100644 code/libraries/vendor/web-token/jwt-core/JWK.php create mode 100644 code/libraries/vendor/web-token/jwt-core/JWKSet.php create mode 100644 code/libraries/vendor/web-token/jwt-core/JWT.php create mode 100644 code/libraries/vendor/web-token/jwt-core/LICENSE create mode 100644 code/libraries/vendor/web-token/jwt-core/Util/BigInteger.php create mode 100644 code/libraries/vendor/web-token/jwt-core/Util/ECKey.php create mode 100644 code/libraries/vendor/web-token/jwt-core/Util/ECSignature.php create mode 100644 code/libraries/vendor/web-token/jwt-core/Util/Hash.php create mode 100644 code/libraries/vendor/web-token/jwt-core/Util/JsonConverter.php create mode 100644 code/libraries/vendor/web-token/jwt-core/Util/KeyChecker.php create mode 100644 code/libraries/vendor/web-token/jwt-core/Util/RSAKey.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-ecdsa/ECDSA.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-ecdsa/ES256.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-ecdsa/ES384.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-ecdsa/ES512.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-ecdsa/LICENSE create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-eddsa/EdDSA.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-eddsa/LICENSE create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-experimental/ES256K.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-experimental/HS1.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-experimental/HS256_64.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-experimental/LICENSE create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-experimental/RS1.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-hmac/HMAC.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-hmac/HS256.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-hmac/HS384.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-hmac/HS512.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-hmac/LICENSE create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-none/LICENSE create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-none/None.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-rsa/LICENSE create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-rsa/PS256.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-rsa/PS384.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-rsa/PS512.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-rsa/RS256.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-rsa/RS384.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-rsa/RS512.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-rsa/RSA.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-rsa/RSAPKCS1.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-rsa/RSAPSS.php create mode 100644 code/libraries/vendor/web-token/jwt-signature-algorithm-rsa/Util/RSA.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/Algorithm/MacAlgorithm.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/Algorithm/SignatureAlgorithm.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/JWS.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/JWSBuilder.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/JWSBuilderFactory.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/JWSLoader.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/JWSLoaderFactory.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/JWSTokenSupport.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/JWSVerifier.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/JWSVerifierFactory.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/LICENSE create mode 100644 code/libraries/vendor/web-token/jwt-signature/Serializer/CompactSerializer.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/Serializer/JSONFlattenedSerializer.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/Serializer/JSONGeneralSerializer.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/Serializer/JWSSerializer.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/Serializer/JWSSerializerManager.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/Serializer/JWSSerializerManagerFactory.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/Serializer/Serializer.php create mode 100644 code/libraries/vendor/web-token/jwt-signature/Signature.php create mode 100644 code/media/com_content/js/articles-status-es5.js create mode 100644 code/media/com_content/js/articles-status-es5.min.js create mode 100644 code/media/com_content/js/articles-status.js create mode 100644 code/media/com_content/js/articles-status.min.js delete mode 100644 code/media/com_finder/js/index-es5.js delete mode 100644 code/media/com_finder/js/index-es5.min.js delete mode 100644 code/media/com_finder/js/index.js delete mode 100644 code/media/com_finder/js/index.min.js create mode 100644 code/media/com_users/images/emergency.svg create mode 100644 code/media/com_users/js/two-factor-focus-es5.js create mode 100644 code/media/com_users/js/two-factor-focus-es5.min.js create mode 100644 code/media/com_users/js/two-factor-focus.js create mode 100644 code/media/com_users/js/two-factor-focus.min.js create mode 100644 code/media/com_users/js/two-factor-list-es5.js create mode 100644 code/media/com_users/js/two-factor-list-es5.min.js create mode 100644 code/media/com_users/js/two-factor-list.js create mode 100644 code/media/com_users/js/two-factor-list.min.js delete mode 100644 code/media/com_users/js/two-factor-switcher-es5.js delete mode 100644 code/media/com_users/js/two-factor-switcher-es5.min.js delete mode 100644 code/media/com_users/js/two-factor-switcher.js delete mode 100644 code/media/com_users/js/two-factor-switcher.min.js create mode 100644 code/media/plg_multifactorauth_email/images/email.svg create mode 100644 code/media/plg_multifactorauth_fixed/images/fixed.svg create mode 100644 code/media/plg_multifactorauth_totp/images/totp.svg create mode 100644 code/media/plg_multifactorauth_totp/joomla.asset.json create mode 100644 code/media/plg_multifactorauth_totp/js/setup-es5.js create mode 100644 code/media/plg_multifactorauth_totp/js/setup-es5.min.js create mode 100644 code/media/plg_multifactorauth_totp/js/setup.js create mode 100644 code/media/plg_multifactorauth_totp/js/setup.min.js create mode 100644 code/media/plg_multifactorauth_webauthn/images/webauthn.svg create mode 100644 code/media/plg_multifactorauth_webauthn/joomla.asset.json create mode 100644 code/media/plg_multifactorauth_webauthn/js/webauthn-es5.js create mode 100644 code/media/plg_multifactorauth_webauthn/js/webauthn-es5.min.js create mode 100644 code/media/plg_multifactorauth_webauthn/js/webauthn.js create mode 100644 code/media/plg_multifactorauth_webauthn/js/webauthn.min.js create mode 100644 code/media/plg_multifactorauth_yubikey/images/yubikey.svg create mode 100644 code/media/plg_system_shortcut/js/shortcut-es5.js create mode 100644 code/media/plg_system_shortcut/js/shortcut-es5.min.js create mode 100644 code/media/plg_system_shortcut/js/shortcut.js create mode 100644 code/media/plg_system_shortcut/js/shortcut.min.js create mode 100644 code/media/plg_system_webauthn/images/fido.png create mode 100644 code/media/system/js/table-columns-es5.js create mode 100644 code/media/system/js/table-columns-es5.min.js create mode 100644 code/media/system/js/table-columns.js create mode 100644 code/media/system/js/table-columns.min.js create mode 100644 code/media/vendor/hotkeysjs/LICENSE create mode 100644 code/media/vendor/hotkeysjs/js/hotkeys.js create mode 100644 code/media/vendor/hotkeysjs/js/hotkeys.min.js delete mode 100644 code/modules/mod_articles_latest/mod_articles_latest.php create mode 100644 code/modules/mod_articles_latest/services/provider.php create mode 100644 code/modules/mod_articles_latest/src/Dispatcher/Dispatcher.php delete mode 100644 code/modules/mod_articles_news/mod_articles_news.php create mode 100644 code/modules/mod_articles_news/services/provider.php create mode 100644 code/modules/mod_articles_news/src/Dispatcher/Dispatcher.php delete mode 100644 code/plugins/actionlog/joomla/joomla.php create mode 100644 code/plugins/actionlog/joomla/services/provider.php create mode 100644 code/plugins/actionlog/joomla/src/Extension/Joomla.php delete mode 100644 code/plugins/api-authentication/basic/basic.php create mode 100644 code/plugins/api-authentication/basic/services/provider.php create mode 100644 code/plugins/api-authentication/basic/src/Extension/Basic.php create mode 100644 code/plugins/api-authentication/token/services/provider.php create mode 100644 code/plugins/api-authentication/token/src/Extension/Token.php delete mode 100644 code/plugins/api-authentication/token/token.php create mode 100644 code/plugins/behaviour/taggable/services/provider.php create mode 100644 code/plugins/behaviour/taggable/src/Extension/Taggable.php delete mode 100644 code/plugins/behaviour/taggable/taggable.php create mode 100644 code/plugins/behaviour/versionable/services/provider.php create mode 100644 code/plugins/behaviour/versionable/src/Extension/Versionable.php delete mode 100644 code/plugins/behaviour/versionable/versionable.php create mode 100644 code/plugins/multifactorauth/email/email.xml create mode 100644 code/plugins/multifactorauth/email/services/provider.php create mode 100644 code/plugins/multifactorauth/email/src/Extension/Email.php create mode 100644 code/plugins/multifactorauth/fixed/fixed.xml create mode 100644 code/plugins/multifactorauth/fixed/services/provider.php create mode 100644 code/plugins/multifactorauth/fixed/src/Extension/Fixed.php create mode 100644 code/plugins/multifactorauth/totp/services/provider.php create mode 100644 code/plugins/multifactorauth/totp/src/Extension/Totp.php create mode 100644 code/plugins/multifactorauth/totp/totp.xml create mode 100644 code/plugins/multifactorauth/webauthn/services/provider.php create mode 100644 code/plugins/multifactorauth/webauthn/src/CredentialRepository.php create mode 100644 code/plugins/multifactorauth/webauthn/src/Extension/Webauthn.php create mode 100644 code/plugins/multifactorauth/webauthn/src/Helper/Credentials.php create mode 100644 code/plugins/multifactorauth/webauthn/src/Hotfix/AndroidKeyAttestationStatementSupport.php create mode 100644 code/plugins/multifactorauth/webauthn/src/Hotfix/FidoU2FAttestationStatementSupport.php create mode 100644 code/plugins/multifactorauth/webauthn/src/Hotfix/Server.php create mode 100644 code/plugins/multifactorauth/webauthn/tmpl/default.php create mode 100644 code/plugins/multifactorauth/webauthn/webauthn.xml create mode 100644 code/plugins/multifactorauth/yubikey/services/provider.php create mode 100644 code/plugins/multifactorauth/yubikey/src/Extension/Yubikey.php create mode 100644 code/plugins/multifactorauth/yubikey/yubikey.xml delete mode 100644 code/plugins/system/cache/cache.php create mode 100644 code/plugins/system/cache/services/provider.php create mode 100644 code/plugins/system/cache/src/Extension/Cache.php create mode 100644 code/plugins/system/shortcut/services/provider.php create mode 100644 code/plugins/system/shortcut/shortcut.xml create mode 100644 code/plugins/system/shortcut/src/Extension/Shortcut.php create mode 100644 code/plugins/system/webauthn/fido.jwt create mode 100644 code/plugins/system/webauthn/services/provider.php create mode 100644 code/plugins/system/webauthn/src/Authentication.php delete mode 100644 code/plugins/system/webauthn/src/Exception/AjaxNonCmsAppException.php create mode 100644 code/plugins/system/webauthn/src/Extension/Webauthn.php delete mode 100644 code/plugins/system/webauthn/src/Helper/CredentialsCreation.php delete mode 100644 code/plugins/system/webauthn/src/Helper/Joomla.php create mode 100644 code/plugins/system/webauthn/src/Hotfix/AndroidKeyAttestationStatementSupport.php create mode 100644 code/plugins/system/webauthn/src/Hotfix/FidoU2FAttestationStatementSupport.php create mode 100644 code/plugins/system/webauthn/src/Hotfix/Server.php create mode 100644 code/plugins/system/webauthn/src/MetadataRepository.php create mode 100644 code/plugins/system/webauthn/src/PluginTraits/AjaxHandlerInitCreate.php create mode 100644 code/plugins/system/webauthn/src/PluginTraits/EventReturnAware.php delete mode 100644 code/plugins/system/webauthn/webauthn.php delete mode 100644 code/plugins/task/checkfiles/checkfiles.php create mode 100644 code/plugins/task/checkfiles/services/provider.php create mode 100644 code/plugins/task/checkfiles/src/Extension/Checkfiles.php delete mode 100644 code/plugins/task/demotasks/demotasks.php create mode 100644 code/plugins/task/demotasks/services/provider.php create mode 100644 code/plugins/task/demotasks/src/Extension/DemoTasks.php delete mode 100644 code/plugins/task/requests/requests.php create mode 100644 code/plugins/task/requests/services/provider.php create mode 100644 code/plugins/task/requests/src/Extension/Requests.php create mode 100644 code/plugins/task/sitestatus/services/provider.php delete mode 100644 code/plugins/task/sitestatus/sitestatus.php create mode 100644 code/plugins/task/sitestatus/src/Extension/SiteStatus.php delete mode 100644 code/plugins/twofactorauth/totp/postinstall/actions.php delete mode 100644 code/plugins/twofactorauth/totp/tmpl/form.php delete mode 100644 code/plugins/twofactorauth/totp/totp.php delete mode 100644 code/plugins/twofactorauth/totp/totp.xml delete mode 100644 code/plugins/twofactorauth/yubikey/tmpl/form.php delete mode 100644 code/plugins/twofactorauth/yubikey/yubikey.php delete mode 100644 code/plugins/twofactorauth/yubikey/yubikey.xml delete mode 100644 code/plugins/user/profile/src/Field/DobField.php diff --git a/administrator/logs/joomla_update.php b/administrator/logs/joomla_update.php index fee69ced..ce9bb80f 100644 --- a/administrator/logs/joomla_update.php +++ b/administrator/logs/joomla_update.php @@ -6,3 +6,7 @@ #Fields: datetime priority clientip category message 2022-10-19T12:56:39+00:00 INFO 127.0.0.1 update Starting installation of new version. 2022-10-19T12:57:49+00:00 INFO 127.0.0.1 update Starting installation of new version. +2022-10-19T13:14:08+00:00 INFO 127.0.0.1 update Update started by user Tejashree (3). Old version is 4.1.5. +2022-10-19T13:14:11+00:00 INFO 127.0.0.1 update Downloading update file from https://s3-us-west-2.amazonaws.com/joomla-official-downloads/joomladownloads/joomla4/Joomla_4.2.3-Stable-Update_Package.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA6LXDJLNUINX2AVMH%2F20221019%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20221019T131404Z&X-Amz-Expires=60&X-Amz-SignedHeaders=host&X-Amz-Signature=c41c1152f5c9c47f27f397297419879e50192b1870b55828b62a232bb8a9e290. +2022-10-19T13:16:12+00:00 INFO 127.0.0.1 update Starting installation of new version. +2022-10-19T13:16:45+00:00 INFO 127.0.0.1 update File Joomla_4.2.3-Stable-Update_Package.zip downloaded. diff --git a/code/administrator/components/com_actionlogs/actionlogs.xml b/code/administrator/components/com_actionlogs/actionlogs.xml index b2ca09ca..bece92cf 100644 --- a/code/administrator/components/com_actionlogs/actionlogs.xml +++ b/code/administrator/components/com_actionlogs/actionlogs.xml @@ -2,7 +2,7 @@ com_actionlogs Joomla! Project - May 2018 + 2018-05 (C) 2018 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_actionlogs/config.xml b/code/administrator/components/com_actionlogs/config.xml index 50859022..c58768db 100644 --- a/code/administrator/components/com_actionlogs/config.xml +++ b/code/administrator/components/com_actionlogs/config.xml @@ -29,6 +29,7 @@ type="logtype" label="COM_ACTIONLOGS_LOG_EXTENSIONS_LABEL" multiple="true" + layout="joomla.form.field.list-fancy-select" default="com_banners,com_cache,com_categories,com_checkin,com_config,com_contact,com_content,com_installer,com_media,com_menus,com_messages,com_modules,com_newsfeeds,com_plugins,com_redirect,com_scheduler,com_tags,com_templates,com_users" /> registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Actionlogs')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Actionlogs')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Actionlogs')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Actionlogs')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_actionlogs/src/Controller/ActionlogsController.php b/code/administrator/components/com_actionlogs/src/Controller/ActionlogsController.php index 6b429f54..54eb9594 100644 --- a/code/administrator/components/com_actionlogs/src/Controller/ActionlogsController.php +++ b/code/administrator/components/com_actionlogs/src/Controller/ActionlogsController.php @@ -1,4 +1,5 @@ registerTask('exportSelectedLogs', 'exportLogs'); - } - - /** - * Method to export logs - * - * @return void - * - * @since 3.9.0 - * - * @throws Exception - */ - public function exportLogs() - { - // Check for request forgeries. - $this->checkToken(); - - $task = $this->getTask(); - - $pks = array(); - - if ($task == 'exportSelectedLogs') - { - // Get selected logs - $pks = ArrayHelper::toInteger(explode(',', $this->input->post->getString('cids'))); - } - - /** @var ActionlogsModel $model */ - $model = $this->getModel(); - - // Get the logs data - $data = $model->getLogDataAsIterator($pks); - - if (\count($data)) - { - try - { - $rows = ActionlogsHelper::getCsvData($data); - } - catch (InvalidArgumentException $exception) - { - $this->setMessage(Text::_('COM_ACTIONLOGS_ERROR_COULD_NOT_EXPORT_DATA'), 'error'); - $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false)); - - return; - } - - // Destroy the iterator now - unset($data); - - $date = new Date('now', new DateTimeZone('UTC')); - $filename = 'logs_' . $date->format('Y-m-d_His_T'); - - $csvDelimiter = ComponentHelper::getComponent('com_actionlogs')->getParams()->get('csv_delimiter', ','); - - $this->app->setHeader('Content-Type', 'application/csv', true) - ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '.csv"', true) - ->setHeader('Cache-Control', 'must-revalidate', true) - ->sendHeaders(); - - $output = fopen("php://output", "w"); - - foreach ($rows as $row) - { - fputcsv($output, $row, $csvDelimiter); - } - - fclose($output); - $this->app->triggerEvent('onAfterLogExport', array()); - $this->app->close(); - } - else - { - $this->setMessage(Text::_('COM_ACTIONLOGS_NO_LOGS_TO_EXPORT')); - $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false)); - } - } - - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 3.9.0 - */ - public function getModel($name = 'Actionlogs', $prefix = 'Administrator', $config = ['ignore_request' => true]) - { - // Return the model - return parent::getModel($name, $prefix, $config); - } - - /** - * Clean out the logs - * - * @return void - * - * @since 3.9.0 - */ - public function purge() - { - // Check for request forgeries. - $this->checkToken(); - - $model = $this->getModel(); - - if ($model->purge()) - { - $message = Text::_('COM_ACTIONLOGS_PURGE_SUCCESS'); - } - else - { - $message = Text::_('COM_ACTIONLOGS_PURGE_FAIL'); - } - - $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false), $message); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.9.0 + * + * @throws Exception + */ + public function __construct($config = [], MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('exportSelectedLogs', 'exportLogs'); + } + + /** + * Method to export logs + * + * @return void + * + * @since 3.9.0 + * + * @throws Exception + */ + public function exportLogs() + { + // Check for request forgeries. + $this->checkToken(); + + $task = $this->getTask(); + + $pks = array(); + + if ($task == 'exportSelectedLogs') { + // Get selected logs + $pks = ArrayHelper::toInteger(explode(',', $this->input->post->getString('cids'))); + } + + /** @var ActionlogsModel $model */ + $model = $this->getModel(); + + // Get the logs data + $data = $model->getLogDataAsIterator($pks); + + if (\count($data)) { + try { + $rows = ActionlogsHelper::getCsvData($data); + } catch (InvalidArgumentException $exception) { + $this->setMessage(Text::_('COM_ACTIONLOGS_ERROR_COULD_NOT_EXPORT_DATA'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false)); + + return; + } + + // Destroy the iterator now + unset($data); + + $date = new Date('now', new DateTimeZone('UTC')); + $filename = 'logs_' . $date->format('Y-m-d_His_T'); + + $csvDelimiter = ComponentHelper::getComponent('com_actionlogs')->getParams()->get('csv_delimiter', ','); + + $this->app->setHeader('Content-Type', 'application/csv', true) + ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '.csv"', true) + ->setHeader('Cache-Control', 'must-revalidate', true) + ->sendHeaders(); + + $output = fopen("php://output", "w"); + + foreach ($rows as $row) { + fputcsv($output, $row, $csvDelimiter); + } + + fclose($output); + $this->app->triggerEvent('onAfterLogExport', array()); + $this->app->close(); + } else { + $this->setMessage(Text::_('COM_ACTIONLOGS_NO_LOGS_TO_EXPORT')); + $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false)); + } + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 3.9.0 + */ + public function getModel($name = 'Actionlogs', $prefix = 'Administrator', $config = ['ignore_request' => true]) + { + // Return the model + return parent::getModel($name, $prefix, $config); + } + + /** + * Clean out the logs + * + * @return void + * + * @since 3.9.0 + */ + public function purge() + { + // Check for request forgeries. + $this->checkToken(); + + $model = $this->getModel(); + + if ($model->purge()) { + $message = Text::_('COM_ACTIONLOGS_PURGE_SUCCESS'); + } else { + $message = Text::_('COM_ACTIONLOGS_PURGE_FAIL'); + } + + $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false), $message); + } } diff --git a/code/administrator/components/com_actionlogs/src/Controller/DisplayController.php b/code/administrator/components/com_actionlogs/src/Controller/DisplayController.php index 4c51bfe7..3ff880b1 100644 --- a/code/administrator/components/com_actionlogs/src/Controller/DisplayController.php +++ b/code/administrator/components/com_actionlogs/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT ' . $db->quoteName('extension')) - ->from($db->quoteName('#__action_logs')) - ->order($db->quoteName('extension')); + /** + * Method to get the options to populate list + * + * @return array The field option objects. + * + * @since 3.9.0 + */ + public function getOptions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('extension')) + ->from($db->quoteName('#__action_logs')) + ->order($db->quoteName('extension')); - $db->setQuery($query); - $context = $db->loadColumn(); + $db->setQuery($query); + $context = $db->loadColumn(); - $options = array(); + $options = array(); - if (\count($context) > 0) - { - foreach ($context as $item) - { - $extensions[] = strtok($item, '.'); - } + if (\count($context) > 0) { + foreach ($context as $item) { + $extensions[] = strtok($item, '.'); + } - $extensions = array_unique($extensions); + $extensions = array_unique($extensions); - foreach ($extensions as $extension) - { - ActionlogsHelper::loadTranslationFiles($extension); - $options[] = HTMLHelper::_('select.option', $extension, Text::_($extension)); - } - } + foreach ($extensions as $extension) { + ActionlogsHelper::loadTranslationFiles($extension); + $options[] = HTMLHelper::_('select.option', $extension, Text::_($extension)); + } + } - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/code/administrator/components/com_actionlogs/src/Field/LogcreatorField.php b/code/administrator/components/com_actionlogs/src/Field/LogcreatorField.php index ba25bcbf..66f3b921 100644 --- a/code/administrator/components/com_actionlogs/src/Field/LogcreatorField.php +++ b/code/administrator/components/com_actionlogs/src/Field/LogcreatorField.php @@ -1,4 +1,5 @@ element); + /** + * Method to get the options to populate list + * + * @return array The field option objects. + * + * @since 3.9.0 + */ + protected function getOptions() + { + // Accepted modifiers + $hash = md5($this->element); - if (!isset(static::$options[$hash])) - { - static::$options[$hash] = parent::getOptions(); + if (!isset(static::$options[$hash])) { + static::$options[$hash] = parent::getOptions(); - $db = Factory::getDbo(); + $db = $this->getDatabase(); - // Construct the query - $query = $db->getQuery(true) - ->select($db->quoteName('u.id', 'value')) - ->select($db->quoteName('u.username', 'text')) - ->from($db->quoteName('#__users', 'u')) - ->join('INNER', $db->quoteName('#__action_logs', 'c') . ' ON ' . $db->quoteName('c.user_id') . ' = ' . $db->quoteName('u.id')) - ->group($db->quoteName('u.id')) - ->group($db->quoteName('u.username')) - ->order($db->quoteName('u.username')); + // Construct the query + $query = $db->getQuery(true) + ->select($db->quoteName('u.id', 'value')) + ->select($db->quoteName('u.username', 'text')) + ->from($db->quoteName('#__users', 'u')) + ->join('INNER', $db->quoteName('#__action_logs', 'c') . ' ON ' . $db->quoteName('c.user_id') . ' = ' . $db->quoteName('u.id')) + ->group($db->quoteName('u.id')) + ->group($db->quoteName('u.username')) + ->order($db->quoteName('u.username')); - // Setup the query - $db->setQuery($query); + // Setup the query + $db->setQuery($query); - // Return the result - if ($options = $db->loadObjectList()) - { - static::$options[$hash] = array_merge(static::$options[$hash], $options); - } - } + // Return the result + if ($options = $db->loadObjectList()) { + static::$options[$hash] = array_merge(static::$options[$hash], $options); + } + } - return static::$options[$hash]; - } + return static::$options[$hash]; + } } diff --git a/code/administrator/components/com_actionlogs/src/Field/LogsdaterangeField.php b/code/administrator/components/com_actionlogs/src/Field/LogsdaterangeField.php index 89f94ac6..b01571e8 100644 --- a/code/administrator/components/com_actionlogs/src/Field/LogsdaterangeField.php +++ b/code/administrator/components/com_actionlogs/src/Field/LogsdaterangeField.php @@ -1,4 +1,5 @@ 'COM_ACTIONLOGS_OPTION_RANGE_TODAY', - 'past_week' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_WEEK', - 'past_1month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_1MONTH', - 'past_3month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_3MONTH', - 'past_6month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_6MONTH', - 'past_year' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_YEAR', - ); - - /** - * Method to instantiate the form field object. - * - * @param Form $form The form to attach to the form field object. - * - * @since 3.9.0 - */ - public function __construct($form = null) - { - parent::__construct($form); - - // Load the required language - $lang = Factory::getLanguage(); - $lang->load('com_actionlogs', JPATH_ADMINISTRATOR); - } + /** + * The form field type. + * + * @var string + * @since 3.9.0 + */ + protected $type = 'logsdaterange'; + + /** + * Available options + * + * @var array + * @since 3.9.0 + */ + protected $predefinedOptions = array( + 'today' => 'COM_ACTIONLOGS_OPTION_RANGE_TODAY', + 'past_week' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_WEEK', + 'past_1month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_1MONTH', + 'past_3month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_3MONTH', + 'past_6month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_6MONTH', + 'past_year' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_YEAR', + ); + + /** + * Method to instantiate the form field object. + * + * @param Form $form The form to attach to the form field object. + * + * @since 3.9.0 + */ + public function __construct($form = null) + { + parent::__construct($form); + + // Load the required language + $lang = Factory::getLanguage(); + $lang->load('com_actionlogs', JPATH_ADMINISTRATOR); + } } diff --git a/code/administrator/components/com_actionlogs/src/Field/LogtypeField.php b/code/administrator/components/com_actionlogs/src/Field/LogtypeField.php index 88c6d8f5..b3ae4c96 100644 --- a/code/administrator/components/com_actionlogs/src/Field/LogtypeField.php +++ b/code/administrator/components/com_actionlogs/src/Field/LogtypeField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select($db->quoteName('extension')) - ->from($db->quoteName('#__action_logs_extensions')); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.9.0 + */ + public function getOptions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension')) + ->from($db->quoteName('#__action_logs_extensions')); - $extensions = $db->setQuery($query)->loadColumn(); + $extensions = $db->setQuery($query)->loadColumn(); - $options = array(); - $tmp = array('checked' => true); + $options = []; - foreach ($extensions as $extension) - { - ActionlogsHelper::loadTranslationFiles($extension); - $option = HTMLHelper::_('select.option', $extension, Text::_($extension)); - $options[ApplicationHelper::stringURLSafe(Text::_($extension)) . '_' . $extension] = (object) array_merge($tmp, (array) $option); - } + foreach ($extensions as $extension) { + ActionlogsHelper::loadTranslationFiles($extension); + $extensionName = Text::_($extension); + $options[ApplicationHelper::stringURLSafe($extensionName) . '_' . $extension] = HTMLHelper::_('select.option', $extension, $extensionName); + } - ksort($options); + ksort($options); - return array_merge(parent::getOptions(), array_values($options)); - } + return array_merge(parent::getOptions(), array_values($options)); + } } diff --git a/code/administrator/components/com_actionlogs/src/Field/PlugininfoField.php b/code/administrator/components/com_actionlogs/src/Field/PlugininfoField.php index 84b78f2d..f5adfe9c 100644 --- a/code/administrator/components/com_actionlogs/src/Field/PlugininfoField.php +++ b/code/administrator/components/com_actionlogs/src/Field/PlugininfoField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog')) - ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); - $db->setQuery($query); + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 3.9.2 + */ + protected function getInput() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog')) + ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); + $db->setQuery($query); - $result = (int) $db->loadResult(); + $result = (int) $db->loadResult(); - $link = HTMLHelper::_( - 'link', - Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $result), - Text::_('PLG_SYSTEM_ACTIONLOGS_JOOMLA_ACTIONLOG_DISABLED'), - array('class' => 'alert-link') - ); + $link = HTMLHelper::_( + 'link', + Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $result), + Text::_('PLG_SYSTEM_ACTIONLOGS_JOOMLA_ACTIONLOG_DISABLED'), + array('class' => 'alert-link') + ); - return '
' - . '' - . Text::_('INFO') - . '' - . Text::sprintf('PLG_SYSTEM_ACTIONLOGS_JOOMLA_ACTIONLOG_DISABLED_REDIRECT', $link) - . '
'; - } + return '
' + . '' + . Text::_('INFO') + . '' + . Text::sprintf('PLG_SYSTEM_ACTIONLOGS_JOOMLA_ACTIONLOG_DISABLED_REDIRECT', $link) + . '
'; + } } diff --git a/code/administrator/components/com_actionlogs/src/Helper/ActionlogsHelper.php b/code/administrator/components/com_actionlogs/src/Helper/ActionlogsHelper.php index 6a53818f..0b50d154 100644 --- a/code/administrator/components/com_actionlogs/src/Helper/ActionlogsHelper.php +++ b/code/administrator/components/com_actionlogs/src/Helper/ActionlogsHelper.php @@ -1,4 +1,5 @@ extension, '.'); - - static::loadTranslationFiles($extension); - - yield array( - 'id' => $log->id, - 'message' => self::escapeCsvFormula(strip_tags(static::getHumanReadableLogMessage($log, false))), - 'extension' => self::escapeCsvFormula(Text::_($extension)), - 'date' => (new Date($log->log_date, new \DateTimeZone('UTC')))->format('Y-m-d H:i:s T'), - 'name' => self::escapeCsvFormula($log->name), - 'ip_address' => self::escapeCsvFormula($log->ip_address === 'COM_ACTIONLOGS_DISABLED' ? $disabledText : $log->ip_address) - ); - } - } - - /** - * Load the translation files for an extension - * - * @param string $extension Extension name - * - * @return void - * - * @since 3.9.0 - */ - public static function loadTranslationFiles($extension) - { - static $cache = array(); - $extension = strtolower($extension); - - if (isset($cache[$extension])) - { - return; - } - - $lang = Factory::getLanguage(); - $source = ''; - - switch (substr($extension, 0, 3)) - { - case 'com': - default: - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - break; - - case 'lib': - $source = JPATH_LIBRARIES . '/' . substr($extension, 4); - break; - - case 'mod': - $source = JPATH_SITE . '/modules/' . $extension; - break; - - case 'plg': - $parts = explode('_', $extension, 3); - - if (\count($parts) > 2) - { - $source = JPATH_PLUGINS . '/' . $parts[1] . '/' . $parts[2]; - } - break; - - case 'pkg': - $source = JPATH_SITE; - break; - - case 'tpl': - $source = JPATH_BASE . '/templates/' . substr($extension, 4); - break; - - } - - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, $source); - - if (!$lang->hasKey(strtoupper($extension))) - { - $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($extension . '.sys', $source); - } - - $cache[$extension] = true; - } - - /** - * Get parameters to be - * - * @param string $context The context of the content - * - * @return mixed An object contains content type parameters, or null if not found - * - * @since 3.9.0 - */ - public static function getLogContentTypeParams($context) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('a.*') - ->from($db->quoteName('#__action_log_config', 'a')) - ->where($db->quoteName('a.type_alias') . ' = :context') - ->bind(':context', $context); - - $db->setQuery($query); - - return $db->loadObject(); - } - - /** - * Get human readable log message for a User Action Log - * - * @param \stdClass $log A User Action log message record - * @param boolean $generateLinks Flag to disable link generation when creating a message - * - * @return string - * - * @since 3.9.0 - */ - public static function getHumanReadableLogMessage($log, $generateLinks = true) - { - static $links = array(); - - $message = Text::_($log->message_language_key); - $messageData = json_decode($log->message, true); - - // Special handling for translation extension name - if (isset($messageData['extension_name'])) - { - static::loadTranslationFiles($messageData['extension_name']); - $messageData['extension_name'] = Text::_($messageData['extension_name']); - } - - // Translating application - if (isset($messageData['app'])) - { - $messageData['app'] = Text::_($messageData['app']); - } - - // Translating type - if (isset($messageData['type'])) - { - $messageData['type'] = Text::_($messageData['type']); - } - - $linkMode = Factory::getApplication()->get('force_ssl', 0) >= 1 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - foreach ($messageData as $key => $value) - { - // Escape any markup in the values to prevent XSS attacks - $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); - - // Convert relative url to absolute url so that it is clickable in action logs notification email - if ($generateLinks && StringHelper::strpos($value, 'index.php?') === 0) - { - if (!isset($links[$value])) - { - $links[$value] = Route::link('administrator', $value, false, $linkMode, true); - } - - $value = $links[$value]; - } - - $message = str_replace('{' . $key . '}', $value, $message); - } - - return $message; - } - - /** - * Get link to an item of given content type - * - * @param string $component - * @param string $contentType - * @param integer $id - * @param string $urlVar - * @param CMSObject $object - * - * @return string Link to the content item - * - * @since 3.9.0 - */ - public static function getContentTypeLink($component, $contentType, $id, $urlVar = 'id', $object = null) - { - // Try to find the component helper. - $eName = str_replace('com_', '', $component); - $file = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/helpers/' . $eName . '.php'); - - if (file_exists($file)) - { - $prefix = ucfirst(str_replace('com_', '', $component)); - $cName = $prefix . 'Helper'; - - \JLoader::register($cName, $file); - - if (class_exists($cName) && \is_callable(array($cName, 'getContentTypeLink'))) - { - return $cName::getContentTypeLink($contentType, $id, $object); - } - } - - if (empty($urlVar)) - { - $urlVar = 'id'; - } - - // Return default link to avoid having to implement getContentTypeLink in most of our components - return 'index.php?option=' . $component . '&task=' . $contentType . '.edit&' . $urlVar . '=' . $id; - } - - /** - * Load both enabled and disabled actionlog plugins language file. - * - * It is used to make sure actions log is displayed properly instead of only language items displayed when a plugin is disabled. - * - * @return void - * - * @since 3.9.0 - */ - public static function loadActionLogPluginsLanguage() - { - $lang = Factory::getLanguage(); - $db = Factory::getDbo(); - - // Get all (both enabled and disabled) actionlog plugins - $query = $db->getQuery(true) - ->select( - $db->quoteName( - array( - 'folder', - 'element', - 'params', - 'extension_id' - ), - array( - 'type', - 'name', - 'params', - 'id' - ) - ) - ) - ->from('#__extensions') - ->where('type = ' . $db->quote('plugin')) - ->where('folder = ' . $db->quote('actionlog')) - ->where('state IN (0,1)') - ->order('ordering'); - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $rows = array(); - } - - if (empty($rows)) - { - return; - } - - foreach ($rows as $row) - { - $name = $row->name; - $type = $row->type; - $extension = 'Plg_' . $type . '_' . $name; - $extension = strtolower($extension); - - // If language already loaded, don't load it again. - if ($lang->getPaths($extension)) - { - continue; - } - - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, JPATH_PLUGINS . '/' . $type . '/' . $name); - } - - // Load plg_system_actionlogs too - $lang->load('plg_system_actionlogs', JPATH_ADMINISTRATOR); - - // Load com_privacy too. - $lang->load('com_privacy', JPATH_ADMINISTRATOR); - } - - /** - * Escapes potential characters that start a formula in a CSV value to prevent injection attacks - * - * @param mixed $value csv field value - * - * @return mixed - * - * @since 3.9.7 - */ - protected static function escapeCsvFormula($value) - { - if ($value == '') - { - return $value; - } - - if (\in_array($value[0], self::$characters, true)) - { - $value = ' ' . $value; - } - - return $value; - } + /** + * Array of characters starting a formula + * + * @var array + * + * @since 3.9.7 + */ + private static $characters = array('=', '+', '-', '@'); + + /** + * Method to convert logs objects array to an iterable type for use with a CSV export + * + * @param array|\Traversable $data The logs data objects to be exported + * + * @return Generator + * + * @since 3.9.0 + * + * @throws \InvalidArgumentException + */ + public static function getCsvData($data): Generator + { + if (!is_iterable($data)) { + throw new \InvalidArgumentException( + sprintf( + '%s() requires an array or object implementing the Traversable interface, a %s was given.', + __METHOD__, + \gettype($data) === 'object' ? \get_class($data) : \gettype($data) + ) + ); + } + + $disabledText = Text::_('COM_ACTIONLOGS_DISABLED'); + + // Header row + yield ['Id', 'Action', 'Extension', 'Date', 'Name', 'IP Address']; + + foreach ($data as $log) { + $extension = strtok($log->extension, '.'); + + static::loadTranslationFiles($extension); + + yield array( + 'id' => $log->id, + 'message' => self::escapeCsvFormula(strip_tags(static::getHumanReadableLogMessage($log, false))), + 'extension' => self::escapeCsvFormula(Text::_($extension)), + 'date' => (new Date($log->log_date, new \DateTimeZone('UTC')))->format('Y-m-d H:i:s T'), + 'name' => self::escapeCsvFormula($log->name), + 'ip_address' => self::escapeCsvFormula($log->ip_address === 'COM_ACTIONLOGS_DISABLED' ? $disabledText : $log->ip_address) + ); + } + } + + /** + * Load the translation files for an extension + * + * @param string $extension Extension name + * + * @return void + * + * @since 3.9.0 + */ + public static function loadTranslationFiles($extension) + { + static $cache = array(); + $extension = strtolower($extension); + + if (isset($cache[$extension])) { + return; + } + + $lang = Factory::getLanguage(); + $source = ''; + + switch (substr($extension, 0, 3)) { + case 'com': + default: + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + break; + + case 'lib': + $source = JPATH_LIBRARIES . '/' . substr($extension, 4); + break; + + case 'mod': + $source = JPATH_SITE . '/modules/' . $extension; + break; + + case 'plg': + $parts = explode('_', $extension, 3); + + if (\count($parts) > 2) { + $source = JPATH_PLUGINS . '/' . $parts[1] . '/' . $parts[2]; + } + break; + + case 'pkg': + $source = JPATH_SITE; + break; + + case 'tpl': + $source = JPATH_BASE . '/templates/' . substr($extension, 4); + break; + } + + $lang->load($extension, JPATH_ADMINISTRATOR) + || $lang->load($extension, $source); + + if (!$lang->hasKey(strtoupper($extension))) { + $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($extension . '.sys', $source); + } + + $cache[$extension] = true; + } + + /** + * Get parameters to be + * + * @param string $context The context of the content + * + * @return mixed An object contains content type parameters, or null if not found + * + * @since 3.9.0 + * + * @deprecated 5.0 Use the action log config model instead + */ + public static function getLogContentTypeParams($context) + { + return Factory::getApplication()->bootComponent('actionlogs')->getMVCFactory() + ->createModel('ActionlogConfig', 'Administrator')->getLogContentTypeParams($context); + } + + /** + * Get human readable log message for a User Action Log + * + * @param \stdClass $log A User Action log message record + * @param boolean $generateLinks Flag to disable link generation when creating a message + * + * @return string + * + * @since 3.9.0 + */ + public static function getHumanReadableLogMessage($log, $generateLinks = true) + { + static $links = array(); + + $message = Text::_($log->message_language_key); + $messageData = json_decode($log->message, true); + + // Special handling for translation extension name + if (isset($messageData['extension_name'])) { + static::loadTranslationFiles($messageData['extension_name']); + $messageData['extension_name'] = Text::_($messageData['extension_name']); + } + + // Translating application + if (isset($messageData['app'])) { + $messageData['app'] = Text::_($messageData['app']); + } + + // Translating type + if (isset($messageData['type'])) { + $messageData['type'] = Text::_($messageData['type']); + } + + $linkMode = Factory::getApplication()->get('force_ssl', 0) >= 1 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + foreach ($messageData as $key => $value) { + // Escape any markup in the values to prevent XSS attacks + $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + + // Convert relative url to absolute url so that it is clickable in action logs notification email + if ($generateLinks && StringHelper::strpos($value, 'index.php?') === 0) { + if (!isset($links[$value])) { + $links[$value] = Route::link('administrator', $value, false, $linkMode, true); + } + + $value = $links[$value]; + } + + $message = str_replace('{' . $key . '}', $value, $message); + } + + return $message; + } + + /** + * Get link to an item of given content type + * + * @param string $component + * @param string $contentType + * @param integer $id + * @param string $urlVar + * @param CMSObject $object + * + * @return string Link to the content item + * + * @since 3.9.0 + */ + public static function getContentTypeLink($component, $contentType, $id, $urlVar = 'id', $object = null) + { + // Try to find the component helper. + $eName = str_replace('com_', '', $component); + $file = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/helpers/' . $eName . '.php'); + + if (file_exists($file)) { + $prefix = ucfirst(str_replace('com_', '', $component)); + $cName = $prefix . 'Helper'; + + \JLoader::register($cName, $file); + + if (class_exists($cName) && \is_callable(array($cName, 'getContentTypeLink'))) { + return $cName::getContentTypeLink($contentType, $id, $object); + } + } + + if (empty($urlVar)) { + $urlVar = 'id'; + } + + // Return default link to avoid having to implement getContentTypeLink in most of our components + return 'index.php?option=' . $component . '&task=' . $contentType . '.edit&' . $urlVar . '=' . $id; + } + + /** + * Load both enabled and disabled actionlog plugins language file. + * + * It is used to make sure actions log is displayed properly instead of only language items displayed when a plugin is disabled. + * + * @return void + * + * @since 3.9.0 + */ + public static function loadActionLogPluginsLanguage() + { + $lang = Factory::getLanguage(); + $db = Factory::getDbo(); + + // Get all (both enabled and disabled) actionlog plugins + $query = $db->getQuery(true) + ->select( + $db->quoteName( + array( + 'folder', + 'element', + 'params', + 'extension_id' + ), + array( + 'type', + 'name', + 'params', + 'id' + ) + ) + ) + ->from('#__extensions') + ->where('type = ' . $db->quote('plugin')) + ->where('folder = ' . $db->quote('actionlog')) + ->where('state IN (0,1)') + ->order('ordering'); + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $rows = array(); + } + + if (empty($rows)) { + return; + } + + foreach ($rows as $row) { + $name = $row->name; + $type = $row->type; + $extension = 'Plg_' . $type . '_' . $name; + $extension = strtolower($extension); + + // If language already loaded, don't load it again. + if ($lang->getPaths($extension)) { + continue; + } + + $lang->load($extension, JPATH_ADMINISTRATOR) + || $lang->load($extension, JPATH_PLUGINS . '/' . $type . '/' . $name); + } + + // Load plg_system_actionlogs too + $lang->load('plg_system_actionlogs', JPATH_ADMINISTRATOR); + + // Load com_privacy too. + $lang->load('com_privacy', JPATH_ADMINISTRATOR); + } + + /** + * Escapes potential characters that start a formula in a CSV value to prevent injection attacks + * + * @param mixed $value csv field value + * + * @return mixed + * + * @since 3.9.7 + */ + protected static function escapeCsvFormula($value) + { + if ($value == '') { + return $value; + } + + if (\in_array($value[0], self::$characters, true)) { + $value = ' ' . $value; + } + + return $value; + } } diff --git a/code/administrator/components/com_actionlogs/src/Model/ActionlogModel.php b/code/administrator/components/com_actionlogs/src/Model/ActionlogModel.php index 3c4a7b4d..6914842c 100644 --- a/code/administrator/components/com_actionlogs/src/Model/ActionlogModel.php +++ b/code/administrator/components/com_actionlogs/src/Model/ActionlogModel.php @@ -1,4 +1,5 @@ getDbo(); - $date = Factory::getDate(); - $params = ComponentHelper::getComponent('com_actionlogs')->getParams(); - - if ($params->get('ip_logging', 0)) - { - $ip = IpHelper::getIp(); - - if (!filter_var($ip, FILTER_VALIDATE_IP)) - { - $ip = 'COM_ACTIONLOGS_IP_INVALID'; - } - } - else - { - $ip = 'COM_ACTIONLOGS_DISABLED'; - } - - $loggedMessages = array(); - - foreach ($messages as $message) - { - $logMessage = new \stdClass; - $logMessage->message_language_key = $messageLanguageKey; - $logMessage->message = json_encode($message); - $logMessage->log_date = (string) $date; - $logMessage->extension = $context; - $logMessage->user_id = $user->id; - $logMessage->ip_address = $ip; - $logMessage->item_id = isset($message['id']) ? (int) $message['id'] : 0; - - try - { - $db->insertObject('#__action_logs', $logMessage); - $loggedMessages[] = $logMessage; - } - catch (\RuntimeException $e) - { - // Ignore it - } - } - - try - { - // Send notification email to users who choose to be notified about the action logs - $this->sendNotificationEmails($loggedMessages, $user->name, $context); - } - catch (MailDisabledException | phpMailerException $e) - { - // Ignore it - } - } - - /** - * Send notification emails about the action log - * - * @param array $messages The logged messages - * @param string $username The username - * @param string $context The Context - * - * @return void - * - * @since 3.9.0 - * - * @throws MailDisabledException if mail is disabled - * @throws phpmailerException if sending mail failed - */ - protected function sendNotificationEmails($messages, $username, $context) - { - $app = Factory::getApplication(); - $lang = $app->getLanguage(); - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query - ->select($db->quoteName(array('u.email', 'l.extensions'))) - ->from($db->quoteName('#__users', 'u')) - ->where($db->quoteName('u.block') . ' = 0') - ->join( - 'INNER', - $db->quoteName('#__action_logs_users', 'l') . ' ON ( ' . $db->quoteName('l.notify') . ' = 1 AND ' - . $db->quoteName('l.user_id') . ' = ' . $db->quoteName('u.id') . ')' - ); - - $db->setQuery($query); - - $users = $db->loadObjectList(); - - $recipients = array(); - - foreach ($users as $user) - { - $extensions = json_decode($user->extensions, true); - - if ($extensions && \in_array(strtok($context, '.'), $extensions)) - { - $recipients[] = $user->email; - } - } - - if (empty($recipients)) - { - return; - } - - $extension = strtok($context, '.'); - $lang->load('com_actionlogs', JPATH_ADMINISTRATOR); - ActionlogsHelper::loadTranslationFiles($extension); - $temp = []; - - foreach ($messages as $message) - { - $m = []; - $m['extension'] = Text::_($extension); - $m['message'] = ActionlogsHelper::getHumanReadableLogMessage($message); - $m['date'] = HTMLHelper::_('date', $message->log_date, 'Y-m-d H:i:s T', 'UTC'); - $m['username'] = $username; - $temp[] = $m; - } - - $templateData = [ - 'messages' => $temp - ]; - - $mailer = new MailTemplate('com_actionlogs.notification', $app->getLanguage()->getTag()); - $mailer->addTemplateData($templateData); - - foreach ($recipients as $recipient) - { - $mailer->addRecipient($recipient); - } - - $mailer->send(); - } + /** + * Function to add logs to the database + * This method adds a record to #__action_logs contains (message_language_key, message, date, context, user) + * + * @param array $messages The contents of the messages to be logged + * @param string $messageLanguageKey The language key of the message + * @param string $context The context of the content passed to the plugin + * @param integer $userId ID of user perform the action, usually ID of current logged in user + * + * @return void + * + * @since 3.9.0 + */ + public function addLog($messages, $messageLanguageKey, $context, $userId = null) + { + $user = Factory::getUser($userId); + $db = $this->getDatabase(); + $date = Factory::getDate(); + $params = ComponentHelper::getComponent('com_actionlogs')->getParams(); + + if ($params->get('ip_logging', 0)) { + $ip = IpHelper::getIp(); + + if (!filter_var($ip, FILTER_VALIDATE_IP)) { + $ip = 'COM_ACTIONLOGS_IP_INVALID'; + } + } else { + $ip = 'COM_ACTIONLOGS_DISABLED'; + } + + $loggedMessages = array(); + + foreach ($messages as $message) { + $logMessage = new \stdClass(); + $logMessage->message_language_key = $messageLanguageKey; + $logMessage->message = json_encode($message); + $logMessage->log_date = (string) $date; + $logMessage->extension = $context; + $logMessage->user_id = $user->id; + $logMessage->ip_address = $ip; + $logMessage->item_id = isset($message['id']) ? (int) $message['id'] : 0; + + try { + $db->insertObject('#__action_logs', $logMessage); + $loggedMessages[] = $logMessage; + } catch (\RuntimeException $e) { + // Ignore it + } + } + + try { + // Send notification email to users who choose to be notified about the action logs + $this->sendNotificationEmails($loggedMessages, $user->name, $context); + } catch (MailDisabledException | phpMailerException $e) { + // Ignore it + } + } + + /** + * Send notification emails about the action log + * + * @param array $messages The logged messages + * @param string $username The username + * @param string $context The Context + * + * @return void + * + * @since 3.9.0 + * + * @throws MailDisabledException if mail is disabled + * @throws phpmailerException if sending mail failed + */ + protected function sendNotificationEmails($messages, $username, $context) + { + $app = Factory::getApplication(); + $lang = $app->getLanguage(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName(array('u.email', 'l.extensions'))) + ->from($db->quoteName('#__users', 'u')) + ->where($db->quoteName('u.block') . ' = 0') + ->join( + 'INNER', + $db->quoteName('#__action_logs_users', 'l') . ' ON ( ' . $db->quoteName('l.notify') . ' = 1 AND ' + . $db->quoteName('l.user_id') . ' = ' . $db->quoteName('u.id') . ')' + ); + + $db->setQuery($query); + + $users = $db->loadObjectList(); + + $recipients = array(); + + foreach ($users as $user) { + $extensions = json_decode($user->extensions, true); + + if ($extensions && \in_array(strtok($context, '.'), $extensions)) { + $recipients[] = $user->email; + } + } + + if (empty($recipients)) { + return; + } + + $extension = strtok($context, '.'); + $lang->load('com_actionlogs', JPATH_ADMINISTRATOR); + ActionlogsHelper::loadTranslationFiles($extension); + $temp = []; + + foreach ($messages as $message) { + $m = []; + $m['extension'] = Text::_($extension); + $m['message'] = ActionlogsHelper::getHumanReadableLogMessage($message); + $m['date'] = HTMLHelper::_('date', $message->log_date, 'Y-m-d H:i:s T', 'UTC'); + $m['username'] = $username; + $temp[] = $m; + } + + $templateData = [ + 'messages' => $temp + ]; + + $mailer = new MailTemplate('com_actionlogs.notification', $app->getLanguage()->getTag()); + $mailer->addTemplateData($templateData); + + foreach ($recipients as $recipient) { + $mailer->addRecipient($recipient); + } + + $mailer->send(); + } } diff --git a/code/administrator/components/com_actionlogs/src/Model/ActionlogsModel.php b/code/administrator/components/com_actionlogs/src/Model/ActionlogsModel.php index 1aa71f8b..a04acdb6 100644 --- a/code/administrator/components/com_actionlogs/src/Model/ActionlogsModel.php +++ b/code/administrator/components/com_actionlogs/src/Model/ActionlogsModel.php @@ -1,4 +1,5 @@ getDbo(); - $query = $db->getQuery(true) - ->select('a.*') - ->select($db->quoteName('u.name')) - ->from($db->quoteName('#__action_logs', 'a')) - ->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')); - - // Get ordering - $fullorderCol = $this->state->get('list.fullordering', 'a.id DESC'); - - // Apply ordering - if (!empty($fullorderCol)) - { - $query->order($db->escape($fullorderCol)); - } - - // Get filter by user - $user = $this->getState('filter.user'); - - // Apply filter by user - if (!empty($user)) - { - $user = (int) $user; - $query->where($db->quoteName('a.user_id') . ' = :userid') - ->bind(':userid', $user, ParameterType::INTEGER); - } - - // Get filter by extension - $extension = $this->getState('filter.extension'); - - // Apply filter by extension - if (!empty($extension)) - { - $extension = $extension . '%'; - $query->where($db->quoteName('a.extension') . ' LIKE :extension') - ->bind(':extension', $extension); - } - - // Get filter by date range - $dateRange = $this->getState('filter.dateRange'); - - // Apply filter by date range - if (!empty($dateRange)) - { - $date = $this->buildDateRange($dateRange); - - // If the chosen range is not more than a year ago - if ($date['dNow'] !== false && $date['dStart'] !== false) - { - $dStart = $date['dStart']->format('Y-m-d H:i:s'); - $dNow = $date['dNow']->format('Y-m-d H:i:s'); - $query->where( - $db->quoteName('a.log_date') . ' BETWEEN :dstart AND :dnow' - ); - $query->bind(':dstart', $dStart); - $query->bind(':dnow', $dNow); - } - } - - // Filter the items over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - elseif (stripos($search, 'item_id:') === 0) - { - $ids = (int) substr($search, 8); - $query->where($db->quoteName('a.item_id') . ' = :itemid') - ->bind(':itemid', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . $search . '%'; - $query->where($db->quoteName('a.message') . ' LIKE :message') - ->bind(':message', $search); - } - } - - return $query; - } - - /** - * Construct the date range to filter on. - * - * @param string $range The textual range to construct the filter for. - * - * @return array The date range to filter on. - * - * @since 3.9.0 - * - * @throws Exception - */ - private function buildDateRange($range) - { - // Get UTC for now. - $dNow = new Date; - $dStart = clone $dNow; - - switch ($range) - { - case 'past_week': - $dStart->modify('-7 day'); - break; - - case 'past_1month': - $dStart->modify('-1 month'); - break; - - case 'past_3month': - $dStart->modify('-3 month'); - break; - - case 'past_6month': - $dStart->modify('-6 month'); - break; - - case 'past_year': - $dStart->modify('-1 year'); - break; - - case 'today': - // Ranges that need to align with local 'days' need special treatment. - $offset = Factory::getApplication()->get('offset'); - - // Reset the start time to be the beginning of today, local time. - $dStart = new Date('now', $offset); - $dStart->setTime(0, 0, 0); - - // Now change the timezone back to UTC. - $tz = new DateTimeZone('GMT'); - $dStart->setTimezone($tz); - break; - } - - return array('dNow' => $dNow, 'dStart' => $dStart); - } - - /** - * Get all log entries for an item - * - * @param string $extension The extension the item belongs to - * @param integer $itemId The item ID - * - * @return array - * - * @since 3.9.0 - */ - public function getLogsForItem($extension, $itemId) - { - $itemId = (int) $itemId; - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('a.*') - ->select($db->quoteName('u.name')) - ->from($db->quoteName('#__action_logs', 'a')) - ->join('INNER', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')) - ->where($db->quoteName('a.extension') . ' = :extension') - ->where($db->quoteName('a.item_id') . ' = :itemid') - ->bind(':extension', $extension) - ->bind(':itemid', $itemId, ParameterType::INTEGER); - - // Get ordering - $fullorderCol = $this->getState('list.fullordering', 'a.id DESC'); - - // Apply ordering - if (!empty($fullorderCol)) - { - $query->order($db->escape($fullorderCol)); - } - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Get logs data into Table object - * - * @param integer[]|null $pks An optional array of log record IDs to load - * - * @return array All logs in the table - * - * @since 3.9.0 - */ - public function getLogsData($pks = null) - { - $db = $this->getDbo(); - $query = $this->getLogDataQuery($pks); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Get logs data as a database iterator - * - * @param integer[]|null $pks An optional array of log record IDs to load - * - * @return DatabaseIterator - * - * @since 3.9.0 - */ - public function getLogDataAsIterator($pks = null) - { - $db = $this->getDbo(); - $query = $this->getLogDataQuery($pks); - - $db->setQuery($query); - - return $db->getIterator(); - } - - /** - * Get the query for loading logs data - * - * @param integer[]|null $pks An optional array of log record IDs to load - * - * @return DatabaseQuery - * - * @since 3.9.0 - */ - private function getLogDataQuery($pks = null) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('a.*') - ->select($db->quoteName('u.name')) - ->from($db->quoteName('#__action_logs', 'a')) - ->join('INNER', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')); - - if (\is_array($pks) && \count($pks) > 0) - { - $pks = ArrayHelper::toInteger($pks); - $query->whereIn($db->quoteName('a.id'), $pks); - } - - return $query; - } - - /** - * Delete logs - * - * @param array $pks Primary keys of logs - * - * @return boolean - * - * @since 3.9.0 - */ - public function delete(&$pks) - { - $keys = ArrayHelper::toInteger($pks); - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__action_logs')) - ->whereIn($db->quoteName('id'), $keys); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - Factory::getApplication()->triggerEvent('onAfterLogPurge', array()); - - return true; - } - - /** - * Removes all of logs from the table. - * - * @return boolean result of operation - * - * @since 3.9.0 - */ - public function purge() - { - try - { - $this->getDbo()->truncateTable('#__action_logs'); - } - catch (Exception $e) - { - return false; - } - - Factory::getApplication()->triggerEvent('onAfterLogPurge', array()); - - return true; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return Form|boolean The Form object or false on error - * - * @since 3.9.0 - */ - public function getFilterForm($data = array(), $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - $params = ComponentHelper::getParams('com_actionlogs'); - $ipLogging = (bool) $params->get('ip_logging', 0); - - // Add ip sort options to sort dropdown - if ($form && $ipLogging) - { - /* @var \Joomla\CMS\Form\Field\ListField $field */ - $field = $form->getField('fullordering', 'list'); - $field->addOption(Text::_('COM_ACTIONLOGS_IP_ADDRESS_ASC'), array('value' => 'a.ip_address ASC')); - $field->addOption(Text::_('COM_ACTIONLOGS_IP_ADDRESS_DESC'), array('value' => 'a.ip_address DESC')); - } - - return $form; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 3.9.0 + * + * @throws Exception + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'a.id', 'id', + 'a.extension', 'extension', + 'a.user_id', 'user', + 'a.message', 'message', + 'a.log_date', 'log_date', + 'a.ip_address', 'ip_address', + 'dateRange', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.9.0 + * + * @throws Exception + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + parent::populateState($ordering, $direction); + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 3.9.0 + * + * @throws Exception + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('a.*') + ->select($db->quoteName('u.name')) + ->from($db->quoteName('#__action_logs', 'a')) + ->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')); + + // Get ordering + $fullorderCol = $this->state->get('list.fullordering', 'a.id DESC'); + + // Apply ordering + if (!empty($fullorderCol)) { + $query->order($db->escape($fullorderCol)); + } + + // Get filter by user + $user = $this->getState('filter.user'); + + // Apply filter by user + if (!empty($user)) { + $user = (int) $user; + $query->where($db->quoteName('a.user_id') . ' = :userid') + ->bind(':userid', $user, ParameterType::INTEGER); + } + + // Get filter by extension + $extension = $this->getState('filter.extension'); + + // Apply filter by extension + if (!empty($extension)) { + $extension = $extension . '%'; + $query->where($db->quoteName('a.extension') . ' LIKE :extension') + ->bind(':extension', $extension); + } + + // Get filter by date range + $dateRange = $this->getState('filter.dateRange'); + + // Apply filter by date range + if (!empty($dateRange)) { + $date = $this->buildDateRange($dateRange); + + // If the chosen range is not more than a year ago + if ($date['dNow'] !== false && $date['dStart'] !== false) { + $dStart = $date['dStart']->format('Y-m-d H:i:s'); + $dNow = $date['dNow']->format('Y-m-d H:i:s'); + $query->where( + $db->quoteName('a.log_date') . ' BETWEEN :dstart AND :dnow' + ); + $query->bind(':dstart', $dStart); + $query->bind(':dnow', $dNow); + } + } + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } elseif (stripos($search, 'item_id:') === 0) { + $ids = (int) substr($search, 8); + $query->where($db->quoteName('a.item_id') . ' = :itemid') + ->bind(':itemid', $ids, ParameterType::INTEGER); + } else { + $search = '%' . $search . '%'; + $query->where($db->quoteName('a.message') . ' LIKE :message') + ->bind(':message', $search); + } + } + + return $query; + } + + /** + * Construct the date range to filter on. + * + * @param string $range The textual range to construct the filter for. + * + * @return array The date range to filter on. + * + * @since 3.9.0 + * + * @throws Exception + */ + private function buildDateRange($range) + { + // Get UTC for now. + $dNow = new Date(); + $dStart = clone $dNow; + + switch ($range) { + case 'past_week': + $dStart->modify('-7 day'); + break; + + case 'past_1month': + $dStart->modify('-1 month'); + break; + + case 'past_3month': + $dStart->modify('-3 month'); + break; + + case 'past_6month': + $dStart->modify('-6 month'); + break; + + case 'past_year': + $dStart->modify('-1 year'); + break; + + case 'today': + // Ranges that need to align with local 'days' need special treatment. + $offset = Factory::getApplication()->get('offset'); + + // Reset the start time to be the beginning of today, local time. + $dStart = new Date('now', $offset); + $dStart->setTime(0, 0, 0); + + // Now change the timezone back to UTC. + $tz = new DateTimeZone('GMT'); + $dStart->setTimezone($tz); + break; + } + + return array('dNow' => $dNow, 'dStart' => $dStart); + } + + /** + * Get all log entries for an item + * + * @param string $extension The extension the item belongs to + * @param integer $itemId The item ID + * + * @return array + * + * @since 3.9.0 + */ + public function getLogsForItem($extension, $itemId) + { + $itemId = (int) $itemId; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('a.*') + ->select($db->quoteName('u.name')) + ->from($db->quoteName('#__action_logs', 'a')) + ->join('INNER', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')) + ->where($db->quoteName('a.extension') . ' = :extension') + ->where($db->quoteName('a.item_id') . ' = :itemid') + ->bind(':extension', $extension) + ->bind(':itemid', $itemId, ParameterType::INTEGER); + + // Get ordering + $fullorderCol = $this->getState('list.fullordering', 'a.id DESC'); + + // Apply ordering + if (!empty($fullorderCol)) { + $query->order($db->escape($fullorderCol)); + } + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Get logs data into Table object + * + * @param integer[]|null $pks An optional array of log record IDs to load + * + * @return array All logs in the table + * + * @since 3.9.0 + */ + public function getLogsData($pks = null) + { + $db = $this->getDatabase(); + $query = $this->getLogDataQuery($pks); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Get logs data as a database iterator + * + * @param integer[]|null $pks An optional array of log record IDs to load + * + * @return DatabaseIterator + * + * @since 3.9.0 + */ + public function getLogDataAsIterator($pks = null) + { + $db = $this->getDatabase(); + $query = $this->getLogDataQuery($pks); + + $db->setQuery($query); + + return $db->getIterator(); + } + + /** + * Get the query for loading logs data + * + * @param integer[]|null $pks An optional array of log record IDs to load + * + * @return DatabaseQuery + * + * @since 3.9.0 + */ + private function getLogDataQuery($pks = null) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('a.*') + ->select($db->quoteName('u.name')) + ->from($db->quoteName('#__action_logs', 'a')) + ->join('INNER', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')); + + if (\is_array($pks) && \count($pks) > 0) { + $pks = ArrayHelper::toInteger($pks); + $query->whereIn($db->quoteName('a.id'), $pks); + } + + return $query; + } + + /** + * Delete logs + * + * @param array $pks Primary keys of logs + * + * @return boolean + * + * @since 3.9.0 + */ + public function delete(&$pks) + { + $keys = ArrayHelper::toInteger($pks); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__action_logs')) + ->whereIn($db->quoteName('id'), $keys); + $db->setQuery($query); + + try { + $db->execute(); + } catch (RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + Factory::getApplication()->triggerEvent('onAfterLogPurge', array()); + + return true; + } + + /** + * Removes all of logs from the table. + * + * @return boolean result of operation + * + * @since 3.9.0 + */ + public function purge() + { + try { + $this->getDatabase()->truncateTable('#__action_logs'); + } catch (Exception $e) { + return false; + } + + Factory::getApplication()->triggerEvent('onAfterLogPurge', array()); + + return true; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return Form|boolean The Form object or false on error + * + * @since 3.9.0 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + $params = ComponentHelper::getParams('com_actionlogs'); + $ipLogging = (bool) $params->get('ip_logging', 0); + + // Add ip sort options to sort dropdown + if ($form && $ipLogging) { + /* @var \Joomla\CMS\Form\Field\ListField $field */ + $field = $form->getField('fullordering', 'list'); + $field->addOption(Text::_('COM_ACTIONLOGS_IP_ADDRESS_ASC'), array('value' => 'a.ip_address ASC')); + $field->addOption(Text::_('COM_ACTIONLOGS_IP_ADDRESS_DESC'), array('value' => 'a.ip_address DESC')); + } + + return $form; + } } diff --git a/code/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php b/code/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php index c6520f13..87ad75ea 100644 --- a/code/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php +++ b/code/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php @@ -1,4 +1,5 @@ $message) - { - if (!\array_key_exists('userid', $message)) - { - $message['userid'] = $user->id; - } - - if (!\array_key_exists('username', $message)) - { - $message['username'] = $user->username; - } - - if (!\array_key_exists('accountlink', $message)) - { - $message['accountlink'] = 'index.php?option=com_users&task=user.edit&id=' . $user->id; - } - - if (\array_key_exists('type', $message)) - { - $message['type'] = strtoupper($message['type']); - } - - if (\array_key_exists('app', $message)) - { - $message['app'] = strtoupper($message['app']); - } - - $messages[$index] = $message; - } - - /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogModel $model */ - $model = $this->app->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - - $model->addLog($messages, strtoupper($messageLanguageKey), $context, $userId); - } + /** + * Application object. + * + * @var \Joomla\CMS\Application\CMSApplication + * @since 3.9.0 + */ + protected $app; + + /** + * Database object. + * + * @var \Joomla\Database\DatabaseDriver + * @since 3.9.0 + */ + protected $db; + + /** + * Load plugin language file automatically so that it can be used inside component + * + * @var boolean + * @since 3.9.0 + */ + protected $autoloadLanguage = true; + + /** + * Proxy for ActionlogsModelUserlog addLog method + * + * This method adds a record to #__action_logs contains (message_language_key, message, date, context, user) + * + * @param array $messages The contents of the messages to be logged + * @param string $messageLanguageKey The language key of the message + * @param string $context The context of the content passed to the plugin + * @param int $userId ID of user perform the action, usually ID of current logged in user + * + * @return void + * + * @since 3.9.0 + */ + protected function addLog($messages, $messageLanguageKey, $context, $userId = null) + { + $user = Factory::getUser(); + + foreach ($messages as $index => $message) { + if (!\array_key_exists('userid', $message)) { + $message['userid'] = $user->id; + } + + if (!\array_key_exists('username', $message)) { + $message['username'] = $user->username; + } + + if (!\array_key_exists('accountlink', $message)) { + $message['accountlink'] = 'index.php?option=com_users&task=user.edit&id=' . $user->id; + } + + if (\array_key_exists('type', $message)) { + $message['type'] = strtoupper($message['type']); + } + + if (\array_key_exists('app', $message)) { + $message['app'] = strtoupper($message['app']); + } + + $messages[$index] = $message; + } + + /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogModel $model */ + $model = $this->app->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + + $model->addLog($messages, strtoupper($messageLanguageKey), $context, $userId); + } } diff --git a/code/administrator/components/com_actionlogs/tmpl/actionlogs/default.php b/code/administrator/components/com_actionlogs/tmpl/actionlogs/default.php index c3230bce..aad8da2a 100644 --- a/code/administrator/components/com_actionlogs/tmpl/actionlogs/default.php +++ b/code/administrator/components/com_actionlogs/tmpl/actionlogs/default.php @@ -24,6 +24,7 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive') + ->useScript('table.columns') ->useScript('multiselect') ->useScript('com_actionlogs.admin-actionlogs'); diff --git a/code/administrator/components/com_admin/admin.xml b/code/administrator/components/com_admin/admin.xml index e0299ee4..edddf167 100644 --- a/code/administrator/components/com_admin/admin.xml +++ b/code/administrator/components/com_admin/admin.xml @@ -2,7 +2,7 @@ com_admin Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_admin/postinstall/addnosniff.php b/code/administrator/components/com_admin/postinstall/addnosniff.php index b8dcab69..95d2f5ca 100644 --- a/code/administrator/components/com_admin/postinstall/addnosniff.php +++ b/code/administrator/components/com_admin/postinstall/addnosniff.php @@ -1,4 +1,5 @@ get('behind_loadbalancer', '0')) - { - return false; - } + if ($app->get('behind_loadbalancer', '0')) { + return false; + } - if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) - { - return true; - } + if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + return true; + } - if (array_key_exists('HTTP_CLIENT_IP', $_SERVER) && !empty($_SERVER['HTTP_CLIENT_IP'])) - { - return true; - } + if (array_key_exists('HTTP_CLIENT_IP', $_SERVER) && !empty($_SERVER['HTTP_CLIENT_IP'])) { + return true; + } - return false; + return false; } @@ -55,35 +55,32 @@ function admin_postinstall_behindproxy_condition() */ function behindproxy_postinstall_action() { - $prev = ArrayHelper::fromObject(new JConfig); - $data = array_merge($prev, array('behind_loadbalancer' => '1')); + $prev = ArrayHelper::fromObject(new JConfig()); + $data = array_merge($prev, array('behind_loadbalancer' => '1')); - $config = new Registry($data); + $config = new Registry($data); - // Set the configuration file path. - $file = JPATH_CONFIGURATION . '/configuration.php'; + // Set the configuration file path. + $file = JPATH_CONFIGURATION . '/configuration.php'; - // Attempt to make the file writeable - if (Path::isOwner($file) && !Path::setPermissions($file, '0644')) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'error'); + // Attempt to make the file writeable + if (Path::isOwner($file) && !Path::setPermissions($file, '0644')) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'error'); - return; - } + return; + } - // Attempt to write the configuration file as a PHP class named JConfig. - $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); + // Attempt to write the configuration file as a PHP class named JConfig. + $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); - if (!File::write($file, $configuration)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_WRITE_FAILED'), 'error'); + if (!File::write($file, $configuration)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_WRITE_FAILED'), 'error'); - return; - } + return; + } - // Attempt to make the file unwriteable - if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'error'); - } + // Attempt to make the file unwriteable + if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'error'); + } } diff --git a/code/administrator/components/com_admin/postinstall/htaccesssvg.php b/code/administrator/components/com_admin/postinstall/htaccesssvg.php index 91645751..73b2c375 100644 --- a/code/administrator/components/com_admin/postinstall/htaccesssvg.php +++ b/code/administrator/components/com_admin/postinstall/htaccesssvg.php @@ -1,4 +1,5 @@ getQuery(true) - ->select($db->quoteName('access')) - ->from($db->quoteName('#__languages')) - ->where($db->quoteName('access') . ' = ' . $db->quote('0')); - $db->setQuery($query); - $db->execute(); - $numRows = $db->getNumRows(); + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('access')) + ->from($db->quoteName('#__languages')) + ->where($db->quoteName('access') . ' = ' . $db->quote('0')); + $db->setQuery($query); + $db->execute(); + $numRows = $db->getNumRows(); - if (isset($numRows) && $numRows != 0) - { - // We have rows here so we have at minimum one row with access set to 0 - return true; - } + if (isset($numRows) && $numRows != 0) { + // We have rows here so we have at minimum one row with access set to 0 + return true; + } - // All good the query return nothing. - return false; + // All good the query return nothing. + return false; } diff --git a/code/administrator/components/com_admin/postinstall/statscollection.php b/code/administrator/components/com_admin/postinstall/statscollection.php index c1cf1a3a..d0a8dbae 100644 --- a/code/administrator/components/com_admin/postinstall/statscollection.php +++ b/code/administrator/components/com_admin/postinstall/statscollection.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\CMS\Extension\ExtensionHelper; use Joomla\CMS\Factory; @@ -20,6 +21,10 @@ use Joomla\Component\Fields\Administrator\Model\FieldModel; use Joomla\Database\ParameterType; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Script file of Joomla CMS * @@ -27,8746 +32,8680 @@ */ class JoomlaInstallerScript { - /** - * The Joomla Version we are updating from - * - * @var string - * @since 3.7 - */ - protected $fromVersion = null; - - /** - * Function to act prior to installation process begins - * - * @param string $action Which action is happening (install|uninstall|discover_install|update) - * @param Installer $installer The class calling this method - * - * @return boolean True on success - * - * @since 3.7.0 - */ - public function preflight($action, $installer) - { - if ($action === 'update') - { - // Get the version we are updating from - if (!empty($installer->extension->manifest_cache)) - { - $manifestValues = json_decode($installer->extension->manifest_cache, true); - - if (array_key_exists('version', $manifestValues)) - { - $this->fromVersion = $manifestValues['version']; - - // Ensure templates are moved to the correct mode - $this->fixTemplateMode(); - - return true; - } - } - - return false; - } - - return true; - } - - /** - * Method to update Joomla! - * - * @param Installer $installer The class calling this method - * - * @return void - */ - public function update($installer) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - - try - { - Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_DELETE_FILES'), Log::INFO, 'Update'); - } - catch (RuntimeException $exception) - { - // Informational log only - } - - // Uninstall plugins before removing their files and folders - $this->uninstallRepeatableFieldsPlugin(); - $this->uninstallEosPlugin(); - - // This needs to stay for 2.5 update compatibility - $this->deleteUnexistingFiles(); - $this->updateManifestCaches(); - $this->updateDatabase(); - $this->updateAssets($installer); - $this->clearStatsCache(); - $this->convertTablesToUtf8mb4(true); - $this->addUserAuthProviderColumn(); - $this->cleanJoomlaCache(); - } - - /** - * Method to clear our stats plugin cache to ensure we get fresh data on Joomla Update - * - * @return void - * - * @since 3.5 - */ - protected function clearStatsCache() - { - $db = Factory::getDbo(); - - try - { - // Get the params for the stats plugin - $params = $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('params')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('stats')) - )->loadResult(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - - $params = json_decode($params, true); - - // Reset the last run parameter - if (isset($params['lastrun'])) - { - $params['lastrun'] = ''; - } - - $params = json_encode($params); - - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('params') . ' = ' . $db->quote($params)) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('stats')); - - try - { - $db->setQuery($query)->execute(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - } - - /** - * Method to update Database - * - * @return void - */ - protected function updateDatabase() - { - if (Factory::getDbo()->getServerType() === 'mysql') - { - $this->updateDatabaseMysql(); - } - } - - /** - * Method to update MySQL Database - * - * @return void - */ - protected function updateDatabaseMysql() - { - $db = Factory::getDbo(); - - $db->setQuery('SHOW ENGINES'); - - try - { - $results = $db->loadObjectList(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - - foreach ($results as $result) - { - if ($result->Support != 'DEFAULT') - { - continue; - } - - $db->setQuery('ALTER TABLE #__update_sites_extensions ENGINE = ' . $result->Engine); - - try - { - $db->execute(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - - break; - } - } - - /** - * Uninstalls the plg_fields_repeatable plugin and transforms its custom field instances - * to instances of the plg_fields_subfields plugin. - * - * @return void - * - * @since 4.0.0 - */ - protected function uninstallRepeatableFieldsPlugin() - { - $app = Factory::getApplication(); - $db = Factory::getDbo(); - - // Check if the plg_fields_repeatable plugin is present - $extensionId = $db->setQuery( - $db->getQuery(true) - ->select('extension_id') - ->from('#__extensions') - ->where('name = ' . $db->quote('plg_fields_repeatable')) - )->loadResult(); - - // Skip uninstalling when it doesn't exist - if (!$extensionId) - { - return; - } - - // Ensure the FieldsHelper class is loaded for the Repeatable fields plugin we're about to remove - \JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php'); - - try - { - $db->transactionStart(); - - // Get the FieldsModelField, we need it in a sec - $fieldModel = $app->bootComponent('com_fields')->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); - /** @var FieldModel $fieldModel */ - - // Now get a list of all `repeatable` custom field instances - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from('#__fields') - ->where($db->quoteName('type') . ' = ' . $db->quote('repeatable')) - ); - - // Execute the query and iterate over the `repeatable` instances - foreach ($db->loadObjectList() as $row) - { - // Skip broken rows - just a security measure, should not happen - if (!isset($row->fieldparams) || !($oldFieldparams = json_decode($row->fieldparams)) || !is_object($oldFieldparams)) - { - continue; - } - - /** - * We basically want to transform this `repeatable` type into a `subfields` type. While $oldFieldparams - * holds the `fieldparams` of the `repeatable` type, $newFieldparams shall hold the `fieldparams` - * of the `subfields` type. - */ - $newFieldparams = [ - 'repeat' => '1', - 'options' => [], - ]; - - /** - * This array is used to store the mapping between the name of form fields from Repeatable field - * with ID of the child-fields. It will then be used to migrate data later - */ - $mapping = []; - - /** - * Store name of media fields which we need to convert data from old format (string) to new - * format (json) during the migration - */ - $mediaFields = []; - - // If this repeatable fields actually had child-fields (normally this is always the case) - if (isset($oldFieldparams->fields) && is_object($oldFieldparams->fields)) - { - // Small counter for the child-fields (aka sub fields) - $newFieldCount = 0; - - // Iterate over the sub fields - foreach (get_object_vars($oldFieldparams->fields) as $oldField) - { - // Used for field name collision prevention - $fieldname_prefix = ''; - $fieldname_suffix = 0; - - // Try to save the new sub field in a loop because of field name collisions - while (true) - { - /** - * We basically want to create a completely new custom fields instance for every sub field - * of the `repeatable` instance. This is what we use $data for, we create a new custom field - * for each of the sub fields of the `repeatable` instance. - */ - $data = [ - 'context' => $row->context, - 'group_id' => $row->group_id, - 'title' => $oldField->fieldname, - 'name' => ( - $fieldname_prefix - . $oldField->fieldname - . ($fieldname_suffix > 0 ? ('_' . $fieldname_suffix) : '') - ), - 'label' => $oldField->fieldname, - 'default_value' => $row->default_value, - 'type' => $oldField->fieldtype, - 'description' => $row->description, - 'state' => '1', - 'params' => $row->params, - 'language' => '*', - 'assigned_cat_ids' => [-1], - 'only_use_in_subform' => 1, - ]; - - // `number` is not a valid custom field type, so use `text` instead. - if ($data['type'] == 'number') - { - $data['type'] = 'text'; - } - - if ($data['type'] == 'media') - { - $mediaFields[] = $oldField->fieldname; - } - - // Reset the state because else \Joomla\CMS\MVC\Model\AdminModel will take an already - // existing value (e.g. from previous save) and do an UPDATE instead of INSERT. - $fieldModel->setState('field.id', 0); - - // If an error occurred when trying to save this. - if (!$fieldModel->save($data)) - { - // If the error is, that the name collided, increase the collision prevention - $error = $fieldModel->getError(); - - if ($error == 'COM_FIELDS_ERROR_UNIQUE_NAME') - { - // If this is the first time this error occurs, set only the prefix - if ($fieldname_prefix == '') - { - $fieldname_prefix = ($row->name . '_'); - } - else - { - // Else increase the suffix - $fieldname_suffix++; - } - - // And start again with the while loop. - continue 1; - } - - // Else bail out with the error. Something is totally wrong. - throw new \Exception($error); - } - - // Break out of the while loop, saving was successful. - break 1; - } - - // Get the newly created id - $subfield_id = $fieldModel->getState('field.id'); - - // Really check that it is valid - if (!is_numeric($subfield_id) || $subfield_id < 1) - { - throw new \Exception('Something went wrong.'); - } - - // And tell our new `subfields` field about his child - $newFieldparams['options'][('option' . $newFieldCount)] = [ - 'customfield' => $subfield_id, - 'render_values' => '1', - ]; - - $newFieldCount++; - - $mapping[$oldField->fieldname] = 'field' . $subfield_id; - } - } - - // Write back the changed stuff to the database - $db->setQuery( - $db->getQuery(true) - ->update('#__fields') - ->set($db->quoteName('type') . ' = ' . $db->quote('subform')) - ->set($db->quoteName('fieldparams') . ' = ' . $db->quote(json_encode($newFieldparams))) - ->where($db->quoteName('id') . ' = ' . $db->quote($row->id)) - )->execute(); - - // Migrate data for this field - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__fields_values')) - ->where($db->quoteName('field_id') . ' = ' . $row->id); - $db->setQuery($query); - - foreach ($db->loadObjectList() as $rowFieldValue) - { - // Do not do the version if no data is entered for the custom field this item - if (!$rowFieldValue->value) - { - continue; - } - - /** - * Here we will have to update the stored value of the field to new format - * The key for each row changes from repeatable to row, for example repeatable0 to row0, and so on - * The key for each sub-field change from name of field to field + ID of the new sub-field - * Example data format stored in J3: {"repeatable0":{"id":"1","username":"admin"}} - * Example data format stored in J4: {"row0":{"field1":"1","field2":"admin"}} - */ - $newFieldValue = []; - - // Convert to array to change key - $fieldValue = json_decode($rowFieldValue->value, true); - - // If data could not be decoded for some reason, ignore - if (!$fieldValue) - { - continue; - } - - $rowIndex = 0; - - foreach ($fieldValue as $rowKey => $rowValue) - { - $rowKey = 'row' . ($rowIndex++); - $newFieldValue[$rowKey] = []; - - foreach ($rowValue as $subFieldName => $subFieldValue) - { - // This is a media field, so we need to convert data to new format required in Joomla! 4 - if (in_array($subFieldName, $mediaFields)) - { - $subFieldValue = ['imagefile' => $subFieldValue, 'alt_text' => '']; - } - - if (isset($mapping[$subFieldName])) - { - $newFieldValue[$rowKey][$mapping[$subFieldName]] = $subFieldValue; - } - else - { - // Not found, use the old key to avoid data lost - $newFieldValue[$subFieldName] = $subFieldValue; - } - } - } - - $query->clear() - ->update($db->quoteName('#__fields_values')) - ->set($db->quoteName('value') . ' = ' . $db->quote(json_encode($newFieldValue))) - ->where($db->quoteName('field_id') . ' = ' . $rowFieldValue->field_id) - ->where($db->quoteName('item_id') . ' =' . $rowFieldValue->item_id); - $db->setQuery($query) - ->execute(); - } - } - - // Now, unprotect the plugin so we can uninstall it - $db->setQuery( - $db->getQuery(true) - ->update('#__extensions') - ->set('protected = 0') - ->where($db->quoteName('extension_id') . ' = ' . $extensionId) - )->execute(); - - // And now uninstall the plugin - $installer = new Installer; - $installer->uninstall('plugin', $extensionId); - - $db->transactionCommit(); - } - catch (\Exception $e) - { - $db->transactionRollback(); - throw $e; - } - } - - /** - * Uninstall the 3.10 EOS plugin - * - * @return void - * - * @since 4.0.0 - */ - protected function uninstallEosPlugin() - { - $db = Factory::getDbo(); - - // Check if the plg_quickicon_eos310 plugin is present - $extensionId = $db->setQuery( - $db->getQuery(true) - ->select('extension_id') - ->from('#__extensions') - ->where('name = ' . $db->quote('plg_quickicon_eos310')) - )->loadResult(); - - // Skip uninstalling if it doesn't exist - if (!$extensionId) - { - return; - } - - try - { - $db->transactionStart(); - - // Unprotect the plugin so we can uninstall it - $db->setQuery( - $db->getQuery(true) - ->update('#__extensions') - ->set('protected = 0') - ->where($db->quoteName('extension_id') . ' = ' . $extensionId) - )->execute(); - - // Uninstall the plugin - $installer = new Installer; - $installer->uninstall('plugin', $extensionId); - - $db->transactionCommit(); - } - catch (\Exception $e) - { - $db->transactionRollback(); - throw $e; - } - } - - /** - * Update the manifest caches - * - * @return void - */ - protected function updateManifestCaches() - { - $extensions = ExtensionHelper::getCoreExtensions(); - - // If we have the search package around, it may not have a manifest cache entry after upgrades from 3.x, so add it to the list - if (File::exists(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml')) - { - $extensions[] = array('package', 'pkg_search', '', 0); - } - - // Attempt to refresh manifest caches - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from('#__extensions'); - - foreach ($extensions as $extension) - { - $query->where( - 'type=' . $db->quote($extension[0]) - . ' AND element=' . $db->quote($extension[1]) - . ' AND folder=' . $db->quote($extension[2]) - . ' AND client_id=' . $extension[3], 'OR' - ); - } - - $db->setQuery($query); - - try - { - $extensions = $db->loadObjectList(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - - $installer = new Installer; - - foreach ($extensions as $extension) - { - if (!$installer->refreshManifestCache($extension->extension_id)) - { - echo Text::sprintf('FILES_JOOMLA_ERROR_MANIFEST', $extension->type, $extension->element, $extension->name, $extension->client_id) . '
'; - } - } - } - - /** - * Delete files that should not exist - * - * @param bool $dryRun If set to true, will not actually delete files, but just report their status for use in CLI - * @param bool $suppressOutput Set to true to suppress echoing any errors, and just return the $status array - * - * @return array - */ - public function deleteUnexistingFiles($dryRun = false, $suppressOutput = false) - { - $status = [ - 'files_exist' => [], - 'folders_exist' => [], - 'files_deleted' => [], - 'folders_deleted' => [], - 'files_errors' => [], - 'folders_errors' => [], - 'folders_checked' => [], - 'files_checked' => [], - ]; - - $files = array( - // From 3.10 to 4.1 - '/administrator/components/com_actionlogs/actionlogs.php', - '/administrator/components/com_actionlogs/controller.php', - '/administrator/components/com_actionlogs/controllers/actionlogs.php', - '/administrator/components/com_actionlogs/helpers/actionlogs.php', - '/administrator/components/com_actionlogs/helpers/actionlogsphp55.php', - '/administrator/components/com_actionlogs/layouts/logstable.php', - '/administrator/components/com_actionlogs/libraries/actionlogplugin.php', - '/administrator/components/com_actionlogs/models/actionlog.php', - '/administrator/components/com_actionlogs/models/actionlogs.php', - '/administrator/components/com_actionlogs/models/fields/extension.php', - '/administrator/components/com_actionlogs/models/fields/logcreator.php', - '/administrator/components/com_actionlogs/models/fields/logsdaterange.php', - '/administrator/components/com_actionlogs/models/fields/logtype.php', - '/administrator/components/com_actionlogs/models/fields/plugininfo.php', - '/administrator/components/com_actionlogs/models/forms/filter_actionlogs.xml', - '/administrator/components/com_actionlogs/views/actionlogs/tmpl/default.php', - '/administrator/components/com_actionlogs/views/actionlogs/tmpl/default.xml', - '/administrator/components/com_actionlogs/views/actionlogs/view.html.php', - '/administrator/components/com_admin/admin.php', - '/administrator/components/com_admin/controller.php', - '/administrator/components/com_admin/controllers/profile.php', - '/administrator/components/com_admin/helpers/html/directory.php', - '/administrator/components/com_admin/helpers/html/phpsetting.php', - '/administrator/components/com_admin/helpers/html/system.php', - '/administrator/components/com_admin/models/forms/profile.xml', - '/administrator/components/com_admin/models/help.php', - '/administrator/components/com_admin/models/profile.php', - '/administrator/components/com_admin/models/sysinfo.php', - '/administrator/components/com_admin/postinstall/eaccelerator.php', - '/administrator/components/com_admin/postinstall/htaccess.php', - '/administrator/components/com_admin/postinstall/joomla40checks.php', - '/administrator/components/com_admin/postinstall/updatedefaultsettings.php', - '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-01.sql', - '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-02.sql', - '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-06.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-21-1.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-21-2.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-23.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2012-01-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2012-01-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.1-2012-01-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.2-2012-03-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.3-2012-03-13.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.4-2012-03-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.4-2012-03-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.5.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.6.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.7.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.0.0.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.0.1.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.0.2.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.0.3.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.0.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.1.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.2.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.3.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.4.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.5.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.10.0-2020-08-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.10.0-2021-05-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.10.7-2022-02-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.10.7-2022-03-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.0.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.1.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2013-12-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2013-12-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-23.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.3-2014-02-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.3.0-2014-02-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.3.0-2014-04-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.3.4-2014-08-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.3.6-2014-09-30.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-08-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-09-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-09-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-10-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-12-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2015-01-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2015-02-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-07-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-13.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-30.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-11-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-11-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2016-02-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2016-03-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.1-2016-03-25.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.1-2016-03-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-06.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-05-06.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-06-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-06-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.3-2016-08-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.3-2016-08-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-06.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-09-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-10-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-27.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-17.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-31.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-17.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-04-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-04-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.3-2017-06-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.4-2017-07-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.0-2017-07-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.0-2017-07-31.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.2-2017-10-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.4-2018-01-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.6-2018-02-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.8-2018-05-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.9-2018-06-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-27.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-12.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-13.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-17.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-12.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-09-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.10-2019-07-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.16-2020-02-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.16-2020-03-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.19-2020-05-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.19-2020-06-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.21-2020-08-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.22-2020-09-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.26-2021-04-07.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.27-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.3-2019-01-12.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.3-2019-02-07.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-04-23.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-04-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-05-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.8-2019-06-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.8-2019-06-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.0.0.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.0.1.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.0.2.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.0.3.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.0.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.1.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.2.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.3.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.4.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.5.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.0-2020-08-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.0-2021-05-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-02-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-02-20.sql.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-03-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.0.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.1.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2013-12-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2013-12-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-23.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.3-2014-02-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2013-12-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2014-02-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2014-04-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.4-2014-08-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.6-2014-09-30.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-08-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-09-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-09-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-10-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-12-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2015-01-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2015-02-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.4-2015-07-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-13.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-30.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-11-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-11-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2016-03-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-05-06.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-06-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-06-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-08-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-08-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-10-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-06.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-09-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-10-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-17.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-31.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-17.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-03-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-03-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-04-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-04-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.4-2017-07-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.0-2017-07-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.0-2017-07-31.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.2-2017-10-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.4-2018-01-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.6-2018-02-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.8-2018-05-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.9-2018-06-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-27.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-12.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-13.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-17.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-12.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-09-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.10-2019-07-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.15-2020-01-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.16-2020-02-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.16-2020-03-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.19-2020-06-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.21-2020-08-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.22-2020-09-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.26-2021-04-07.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.27-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.3-2019-01-12.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.3-2019-02-07.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-04-23.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-04-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-05-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.8-2019-06-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.8-2019-06-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.2-2012-03-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.3-2012-03-13.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.4-2012-03-18.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.4-2012-03-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.5.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.6.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.7.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.0.0.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.0.1.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.0.2.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.0.3.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.0.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.1.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.2.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.3.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.4.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.5.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.0-2021-05-28.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.1-2021-08-17.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-02-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-02-20.sql.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-03-18.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.0.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.1.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2013-12-22.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2013-12-28.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-08.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-18.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-23.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.3-2014-02-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.3.0-2014-02-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.3.0-2014-04-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.3.4-2014-08-03.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.3.6-2014-09-30.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-08-24.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-09-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-09-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-10-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-12-03.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2015-01-21.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2015-02-26.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.4-2015-07-11.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-13.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-26.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-30.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-11-04.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-11-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2016-03-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-06.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-08.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-05-06.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-06-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-06-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.3-2016-08-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.3-2016-08-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-06.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-22.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-29.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-09-29.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-10-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-04.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-24.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-08.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-17.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-31.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-17.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-03-03.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-03-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-04-10.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-04-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.4-2017-07-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.0-2017-07-28.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.0-2017-07-31.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.2-2017-10-14.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.4-2018-01-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.6-2018-02-14.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.8-2018-05-18.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.9-2018-06-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-03.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-24.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-27.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-12.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-13.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-14.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-17.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-10.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-11.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-12.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-28.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-29.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-09-04.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-21.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.10-2019-07-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.16-2020-03-04.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.19-2020-06-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.21-2020-08-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.22-2020-09-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.26-2021-04-07.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.27-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.3-2019-01-12.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.3-2019-02-07.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.4-2019-03-06.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-04-23.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-04-26.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-05-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.8-2019-06-11.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.8-2019-06-15.sql', - '/administrator/components/com_admin/views/help/tmpl/default.php', - '/administrator/components/com_admin/views/help/tmpl/default.xml', - '/administrator/components/com_admin/views/help/tmpl/langforum.php', - '/administrator/components/com_admin/views/help/view.html.php', - '/administrator/components/com_admin/views/profile/tmpl/edit.php', - '/administrator/components/com_admin/views/profile/view.html.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default.xml', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_config.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_directory.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_phpinfo.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_phpsettings.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_system.php', - '/administrator/components/com_admin/views/sysinfo/view.html.php', - '/administrator/components/com_admin/views/sysinfo/view.json.php', - '/administrator/components/com_admin/views/sysinfo/view.text.php', - '/administrator/components/com_associations/associations.php', - '/administrator/components/com_associations/controller.php', - '/administrator/components/com_associations/controllers/association.php', - '/administrator/components/com_associations/controllers/associations.php', - '/administrator/components/com_associations/helpers/associations.php', - '/administrator/components/com_associations/layouts/joomla/searchtools/default/bar.php', - '/administrator/components/com_associations/models/association.php', - '/administrator/components/com_associations/models/associations.php', - '/administrator/components/com_associations/models/fields/itemlanguage.php', - '/administrator/components/com_associations/models/fields/itemtype.php', - '/administrator/components/com_associations/models/fields/modalassociation.php', - '/administrator/components/com_associations/models/forms/association.xml', - '/administrator/components/com_associations/models/forms/filter_associations.xml', - '/administrator/components/com_associations/views/association/tmpl/edit.php', - '/administrator/components/com_associations/views/association/view.html.php', - '/administrator/components/com_associations/views/associations/tmpl/default.php', - '/administrator/components/com_associations/views/associations/tmpl/default.xml', - '/administrator/components/com_associations/views/associations/tmpl/modal.php', - '/administrator/components/com_associations/views/associations/view.html.php', - '/administrator/components/com_banners/banners.php', - '/administrator/components/com_banners/controller.php', - '/administrator/components/com_banners/controllers/banner.php', - '/administrator/components/com_banners/controllers/banners.php', - '/administrator/components/com_banners/controllers/client.php', - '/administrator/components/com_banners/controllers/clients.php', - '/administrator/components/com_banners/controllers/tracks.php', - '/administrator/components/com_banners/controllers/tracks.raw.php', - '/administrator/components/com_banners/helpers/html/banner.php', - '/administrator/components/com_banners/models/banner.php', - '/administrator/components/com_banners/models/banners.php', - '/administrator/components/com_banners/models/client.php', - '/administrator/components/com_banners/models/clients.php', - '/administrator/components/com_banners/models/download.php', - '/administrator/components/com_banners/models/fields/bannerclient.php', - '/administrator/components/com_banners/models/fields/clicks.php', - '/administrator/components/com_banners/models/fields/impmade.php', - '/administrator/components/com_banners/models/fields/imptotal.php', - '/administrator/components/com_banners/models/forms/banner.xml', - '/administrator/components/com_banners/models/forms/client.xml', - '/administrator/components/com_banners/models/forms/download.xml', - '/administrator/components/com_banners/models/forms/filter_banners.xml', - '/administrator/components/com_banners/models/forms/filter_clients.xml', - '/administrator/components/com_banners/models/forms/filter_tracks.xml', - '/administrator/components/com_banners/models/tracks.php', - '/administrator/components/com_banners/tables/banner.php', - '/administrator/components/com_banners/tables/client.php', - '/administrator/components/com_banners/views/banner/tmpl/edit.php', - '/administrator/components/com_banners/views/banner/view.html.php', - '/administrator/components/com_banners/views/banners/tmpl/default.php', - '/administrator/components/com_banners/views/banners/tmpl/default_batch_body.php', - '/administrator/components/com_banners/views/banners/tmpl/default_batch_footer.php', - '/administrator/components/com_banners/views/banners/view.html.php', - '/administrator/components/com_banners/views/client/tmpl/edit.php', - '/administrator/components/com_banners/views/client/view.html.php', - '/administrator/components/com_banners/views/clients/tmpl/default.php', - '/administrator/components/com_banners/views/clients/view.html.php', - '/administrator/components/com_banners/views/download/tmpl/default.php', - '/administrator/components/com_banners/views/download/view.html.php', - '/administrator/components/com_banners/views/tracks/tmpl/default.php', - '/administrator/components/com_banners/views/tracks/view.html.php', - '/administrator/components/com_banners/views/tracks/view.raw.php', - '/administrator/components/com_cache/cache.php', - '/administrator/components/com_cache/controller.php', - '/administrator/components/com_cache/helpers/cache.php', - '/administrator/components/com_cache/models/cache.php', - '/administrator/components/com_cache/models/forms/filter_cache.xml', - '/administrator/components/com_cache/views/cache/tmpl/default.php', - '/administrator/components/com_cache/views/cache/tmpl/default.xml', - '/administrator/components/com_cache/views/cache/view.html.php', - '/administrator/components/com_cache/views/purge/tmpl/default.php', - '/administrator/components/com_cache/views/purge/tmpl/default.xml', - '/administrator/components/com_cache/views/purge/view.html.php', - '/administrator/components/com_categories/categories.php', - '/administrator/components/com_categories/controller.php', - '/administrator/components/com_categories/controllers/ajax.json.php', - '/administrator/components/com_categories/controllers/categories.php', - '/administrator/components/com_categories/controllers/category.php', - '/administrator/components/com_categories/helpers/association.php', - '/administrator/components/com_categories/helpers/html/categoriesadministrator.php', - '/administrator/components/com_categories/models/categories.php', - '/administrator/components/com_categories/models/category.php', - '/administrator/components/com_categories/models/fields/categoryedit.php', - '/administrator/components/com_categories/models/fields/categoryparent.php', - '/administrator/components/com_categories/models/fields/modal/category.php', - '/administrator/components/com_categories/models/forms/category.xml', - '/administrator/components/com_categories/models/forms/filter_categories.xml', - '/administrator/components/com_categories/tables/category.php', - '/administrator/components/com_categories/views/categories/tmpl/default.php', - '/administrator/components/com_categories/views/categories/tmpl/default.xml', - '/administrator/components/com_categories/views/categories/tmpl/default_batch_body.php', - '/administrator/components/com_categories/views/categories/tmpl/default_batch_footer.php', - '/administrator/components/com_categories/views/categories/tmpl/modal.php', - '/administrator/components/com_categories/views/categories/view.html.php', - '/administrator/components/com_categories/views/category/tmpl/edit.php', - '/administrator/components/com_categories/views/category/tmpl/edit.xml', - '/administrator/components/com_categories/views/category/tmpl/edit_associations.php', - '/administrator/components/com_categories/views/category/tmpl/edit_metadata.php', - '/administrator/components/com_categories/views/category/tmpl/modal.php', - '/administrator/components/com_categories/views/category/tmpl/modal_associations.php', - '/administrator/components/com_categories/views/category/tmpl/modal_extrafields.php', - '/administrator/components/com_categories/views/category/tmpl/modal_metadata.php', - '/administrator/components/com_categories/views/category/tmpl/modal_options.php', - '/administrator/components/com_categories/views/category/view.html.php', - '/administrator/components/com_checkin/checkin.php', - '/administrator/components/com_checkin/controller.php', - '/administrator/components/com_checkin/models/checkin.php', - '/administrator/components/com_checkin/models/forms/filter_checkin.xml', - '/administrator/components/com_checkin/views/checkin/tmpl/default.php', - '/administrator/components/com_checkin/views/checkin/tmpl/default.xml', - '/administrator/components/com_checkin/views/checkin/view.html.php', - '/administrator/components/com_config/config.php', - '/administrator/components/com_config/controller.php', - '/administrator/components/com_config/controller/application/cancel.php', - '/administrator/components/com_config/controller/application/display.php', - '/administrator/components/com_config/controller/application/removeroot.php', - '/administrator/components/com_config/controller/application/save.php', - '/administrator/components/com_config/controller/application/sendtestmail.php', - '/administrator/components/com_config/controller/application/store.php', - '/administrator/components/com_config/controller/component/cancel.php', - '/administrator/components/com_config/controller/component/display.php', - '/administrator/components/com_config/controller/component/save.php', - '/administrator/components/com_config/controllers/application.php', - '/administrator/components/com_config/controllers/component.php', - '/administrator/components/com_config/helper/config.php', - '/administrator/components/com_config/model/application.php', - '/administrator/components/com_config/model/component.php', - '/administrator/components/com_config/model/field/configcomponents.php', - '/administrator/components/com_config/model/field/filters.php', - '/administrator/components/com_config/model/form/application.xml', - '/administrator/components/com_config/models/application.php', - '/administrator/components/com_config/models/component.php', - '/administrator/components/com_config/view/application/html.php', - '/administrator/components/com_config/view/application/json.php', - '/administrator/components/com_config/view/application/tmpl/default.php', - '/administrator/components/com_config/view/application/tmpl/default.xml', - '/administrator/components/com_config/view/application/tmpl/default_cache.php', - '/administrator/components/com_config/view/application/tmpl/default_cookie.php', - '/administrator/components/com_config/view/application/tmpl/default_database.php', - '/administrator/components/com_config/view/application/tmpl/default_debug.php', - '/administrator/components/com_config/view/application/tmpl/default_filters.php', - '/administrator/components/com_config/view/application/tmpl/default_ftp.php', - '/administrator/components/com_config/view/application/tmpl/default_ftplogin.php', - '/administrator/components/com_config/view/application/tmpl/default_locale.php', - '/administrator/components/com_config/view/application/tmpl/default_mail.php', - '/administrator/components/com_config/view/application/tmpl/default_metadata.php', - '/administrator/components/com_config/view/application/tmpl/default_navigation.php', - '/administrator/components/com_config/view/application/tmpl/default_permissions.php', - '/administrator/components/com_config/view/application/tmpl/default_proxy.php', - '/administrator/components/com_config/view/application/tmpl/default_seo.php', - '/administrator/components/com_config/view/application/tmpl/default_server.php', - '/administrator/components/com_config/view/application/tmpl/default_session.php', - '/administrator/components/com_config/view/application/tmpl/default_site.php', - '/administrator/components/com_config/view/application/tmpl/default_system.php', - '/administrator/components/com_config/view/component/html.php', - '/administrator/components/com_config/view/component/tmpl/default.php', - '/administrator/components/com_config/view/component/tmpl/default.xml', - '/administrator/components/com_config/view/component/tmpl/default_navigation.php', - '/administrator/components/com_contact/contact.php', - '/administrator/components/com_contact/controller.php', - '/administrator/components/com_contact/controllers/ajax.json.php', - '/administrator/components/com_contact/controllers/contact.php', - '/administrator/components/com_contact/controllers/contacts.php', - '/administrator/components/com_contact/helpers/associations.php', - '/administrator/components/com_contact/helpers/html/contact.php', - '/administrator/components/com_contact/models/contact.php', - '/administrator/components/com_contact/models/contacts.php', - '/administrator/components/com_contact/models/fields/modal/contact.php', - '/administrator/components/com_contact/models/forms/contact.xml', - '/administrator/components/com_contact/models/forms/fields/mail.xml', - '/administrator/components/com_contact/models/forms/filter_contacts.xml', - '/administrator/components/com_contact/tables/contact.php', - '/administrator/components/com_contact/views/contact/tmpl/edit.php', - '/administrator/components/com_contact/views/contact/tmpl/edit_associations.php', - '/administrator/components/com_contact/views/contact/tmpl/edit_metadata.php', - '/administrator/components/com_contact/views/contact/tmpl/edit_params.php', - '/administrator/components/com_contact/views/contact/tmpl/modal.php', - '/administrator/components/com_contact/views/contact/tmpl/modal_associations.php', - '/administrator/components/com_contact/views/contact/tmpl/modal_metadata.php', - '/administrator/components/com_contact/views/contact/tmpl/modal_params.php', - '/administrator/components/com_contact/views/contact/view.html.php', - '/administrator/components/com_contact/views/contacts/tmpl/default.php', - '/administrator/components/com_contact/views/contacts/tmpl/default_batch.php', - '/administrator/components/com_contact/views/contacts/tmpl/default_batch_body.php', - '/administrator/components/com_contact/views/contacts/tmpl/default_batch_footer.php', - '/administrator/components/com_contact/views/contacts/tmpl/modal.php', - '/administrator/components/com_contact/views/contacts/view.html.php', - '/administrator/components/com_content/content.php', - '/administrator/components/com_content/controller.php', - '/administrator/components/com_content/controllers/ajax.json.php', - '/administrator/components/com_content/controllers/article.php', - '/administrator/components/com_content/controllers/articles.php', - '/administrator/components/com_content/controllers/featured.php', - '/administrator/components/com_content/helpers/associations.php', - '/administrator/components/com_content/helpers/html/contentadministrator.php', - '/administrator/components/com_content/models/article.php', - '/administrator/components/com_content/models/articles.php', - '/administrator/components/com_content/models/feature.php', - '/administrator/components/com_content/models/featured.php', - '/administrator/components/com_content/models/fields/modal/article.php', - '/administrator/components/com_content/models/fields/voteradio.php', - '/administrator/components/com_content/models/forms/article.xml', - '/administrator/components/com_content/models/forms/filter_articles.xml', - '/administrator/components/com_content/models/forms/filter_featured.xml', - '/administrator/components/com_content/tables/featured.php', - '/administrator/components/com_content/views/article/tmpl/edit.php', - '/administrator/components/com_content/views/article/tmpl/edit.xml', - '/administrator/components/com_content/views/article/tmpl/edit_associations.php', - '/administrator/components/com_content/views/article/tmpl/edit_metadata.php', - '/administrator/components/com_content/views/article/tmpl/modal.php', - '/administrator/components/com_content/views/article/tmpl/modal_associations.php', - '/administrator/components/com_content/views/article/tmpl/modal_metadata.php', - '/administrator/components/com_content/views/article/tmpl/pagebreak.php', - '/administrator/components/com_content/views/article/view.html.php', - '/administrator/components/com_content/views/articles/tmpl/default.php', - '/administrator/components/com_content/views/articles/tmpl/default.xml', - '/administrator/components/com_content/views/articles/tmpl/default_batch_body.php', - '/administrator/components/com_content/views/articles/tmpl/default_batch_footer.php', - '/administrator/components/com_content/views/articles/tmpl/modal.php', - '/administrator/components/com_content/views/articles/view.html.php', - '/administrator/components/com_content/views/featured/tmpl/default.php', - '/administrator/components/com_content/views/featured/tmpl/default.xml', - '/administrator/components/com_content/views/featured/view.html.php', - '/administrator/components/com_contenthistory/contenthistory.php', - '/administrator/components/com_contenthistory/controller.php', - '/administrator/components/com_contenthistory/controllers/history.php', - '/administrator/components/com_contenthistory/controllers/preview.php', - '/administrator/components/com_contenthistory/helpers/html/textdiff.php', - '/administrator/components/com_contenthistory/models/compare.php', - '/administrator/components/com_contenthistory/models/history.php', - '/administrator/components/com_contenthistory/models/preview.php', - '/administrator/components/com_contenthistory/views/compare/tmpl/compare.php', - '/administrator/components/com_contenthistory/views/compare/view.html.php', - '/administrator/components/com_contenthistory/views/history/tmpl/modal.php', - '/administrator/components/com_contenthistory/views/history/view.html.php', - '/administrator/components/com_contenthistory/views/preview/tmpl/preview.php', - '/administrator/components/com_contenthistory/views/preview/view.html.php', - '/administrator/components/com_cpanel/controller.php', - '/administrator/components/com_cpanel/cpanel.php', - '/administrator/components/com_cpanel/views/cpanel/tmpl/default.php', - '/administrator/components/com_cpanel/views/cpanel/tmpl/default.xml', - '/administrator/components/com_cpanel/views/cpanel/view.html.php', - '/administrator/components/com_fields/controller.php', - '/administrator/components/com_fields/controllers/field.php', - '/administrator/components/com_fields/controllers/fields.php', - '/administrator/components/com_fields/controllers/group.php', - '/administrator/components/com_fields/controllers/groups.php', - '/administrator/components/com_fields/fields.php', - '/administrator/components/com_fields/libraries/fieldslistplugin.php', - '/administrator/components/com_fields/libraries/fieldsplugin.php', - '/administrator/components/com_fields/models/field.php', - '/administrator/components/com_fields/models/fields.php', - '/administrator/components/com_fields/models/fields/fieldcontexts.php', - '/administrator/components/com_fields/models/fields/fieldgroups.php', - '/administrator/components/com_fields/models/fields/fieldlayout.php', - '/administrator/components/com_fields/models/fields/section.php', - '/administrator/components/com_fields/models/fields/type.php', - '/administrator/components/com_fields/models/forms/field.xml', - '/administrator/components/com_fields/models/forms/filter_fields.xml', - '/administrator/components/com_fields/models/forms/filter_groups.xml', - '/administrator/components/com_fields/models/forms/group.xml', - '/administrator/components/com_fields/models/group.php', - '/administrator/components/com_fields/models/groups.php', - '/administrator/components/com_fields/tables/field.php', - '/administrator/components/com_fields/tables/group.php', - '/administrator/components/com_fields/views/field/tmpl/edit.php', - '/administrator/components/com_fields/views/field/view.html.php', - '/administrator/components/com_fields/views/fields/tmpl/default.php', - '/administrator/components/com_fields/views/fields/tmpl/default_batch_body.php', - '/administrator/components/com_fields/views/fields/tmpl/default_batch_footer.php', - '/administrator/components/com_fields/views/fields/tmpl/modal.php', - '/administrator/components/com_fields/views/fields/view.html.php', - '/administrator/components/com_fields/views/group/tmpl/edit.php', - '/administrator/components/com_fields/views/group/view.html.php', - '/administrator/components/com_fields/views/groups/tmpl/default.php', - '/administrator/components/com_fields/views/groups/tmpl/default_batch_body.php', - '/administrator/components/com_fields/views/groups/tmpl/default_batch_footer.php', - '/administrator/components/com_fields/views/groups/view.html.php', - '/administrator/components/com_finder/controller.php', - '/administrator/components/com_finder/controllers/filter.php', - '/administrator/components/com_finder/controllers/filters.php', - '/administrator/components/com_finder/controllers/index.php', - '/administrator/components/com_finder/controllers/indexer.json.php', - '/administrator/components/com_finder/controllers/maps.php', - '/administrator/components/com_finder/finder.php', - '/administrator/components/com_finder/helpers/finder.php', - '/administrator/components/com_finder/helpers/html/finder.php', - '/administrator/components/com_finder/helpers/indexer/driver/mysql.php', - '/administrator/components/com_finder/helpers/indexer/driver/postgresql.php', - '/administrator/components/com_finder/helpers/indexer/driver/sqlsrv.php', - '/administrator/components/com_finder/helpers/indexer/indexer.php', - '/administrator/components/com_finder/helpers/indexer/parser/html.php', - '/administrator/components/com_finder/helpers/indexer/parser/rtf.php', - '/administrator/components/com_finder/helpers/indexer/parser/txt.php', - '/administrator/components/com_finder/helpers/indexer/stemmer.php', - '/administrator/components/com_finder/helpers/indexer/stemmer/fr.php', - '/administrator/components/com_finder/helpers/indexer/stemmer/porter_en.php', - '/administrator/components/com_finder/helpers/indexer/stemmer/snowball.php', - '/administrator/components/com_finder/models/fields/branches.php', - '/administrator/components/com_finder/models/fields/contentmap.php', - '/administrator/components/com_finder/models/fields/contenttypes.php', - '/administrator/components/com_finder/models/fields/directories.php', - '/administrator/components/com_finder/models/fields/searchfilter.php', - '/administrator/components/com_finder/models/filter.php', - '/administrator/components/com_finder/models/filters.php', - '/administrator/components/com_finder/models/forms/filter.xml', - '/administrator/components/com_finder/models/forms/filter_filters.xml', - '/administrator/components/com_finder/models/forms/filter_index.xml', - '/administrator/components/com_finder/models/forms/filter_maps.xml', - '/administrator/components/com_finder/models/index.php', - '/administrator/components/com_finder/models/indexer.php', - '/administrator/components/com_finder/models/maps.php', - '/administrator/components/com_finder/models/statistics.php', - '/administrator/components/com_finder/tables/filter.php', - '/administrator/components/com_finder/tables/link.php', - '/administrator/components/com_finder/tables/map.php', - '/administrator/components/com_finder/views/filter/tmpl/edit.php', - '/administrator/components/com_finder/views/filter/view.html.php', - '/administrator/components/com_finder/views/filters/tmpl/default.php', - '/administrator/components/com_finder/views/filters/view.html.php', - '/administrator/components/com_finder/views/index/tmpl/default.php', - '/administrator/components/com_finder/views/index/view.html.php', - '/administrator/components/com_finder/views/indexer/tmpl/default.php', - '/administrator/components/com_finder/views/indexer/view.html.php', - '/administrator/components/com_finder/views/maps/tmpl/default.php', - '/administrator/components/com_finder/views/maps/view.html.php', - '/administrator/components/com_finder/views/statistics/tmpl/default.php', - '/administrator/components/com_finder/views/statistics/view.html.php', - '/administrator/components/com_installer/controller.php', - '/administrator/components/com_installer/controllers/database.php', - '/administrator/components/com_installer/controllers/discover.php', - '/administrator/components/com_installer/controllers/install.php', - '/administrator/components/com_installer/controllers/manage.php', - '/administrator/components/com_installer/controllers/update.php', - '/administrator/components/com_installer/controllers/updatesites.php', - '/administrator/components/com_installer/helpers/html/manage.php', - '/administrator/components/com_installer/helpers/html/updatesites.php', - '/administrator/components/com_installer/installer.php', - '/administrator/components/com_installer/models/database.php', - '/administrator/components/com_installer/models/discover.php', - '/administrator/components/com_installer/models/extension.php', - '/administrator/components/com_installer/models/fields/extensionstatus.php', - '/administrator/components/com_installer/models/fields/folder.php', - '/administrator/components/com_installer/models/fields/location.php', - '/administrator/components/com_installer/models/fields/type.php', - '/administrator/components/com_installer/models/forms/filter_discover.xml', - '/administrator/components/com_installer/models/forms/filter_languages.xml', - '/administrator/components/com_installer/models/forms/filter_manage.xml', - '/administrator/components/com_installer/models/forms/filter_update.xml', - '/administrator/components/com_installer/models/forms/filter_updatesites.xml', - '/administrator/components/com_installer/models/install.php', - '/administrator/components/com_installer/models/languages.php', - '/administrator/components/com_installer/models/manage.php', - '/administrator/components/com_installer/models/update.php', - '/administrator/components/com_installer/models/updatesites.php', - '/administrator/components/com_installer/models/warnings.php', - '/administrator/components/com_installer/views/database/tmpl/default.php', - '/administrator/components/com_installer/views/database/tmpl/default.xml', - '/administrator/components/com_installer/views/database/view.html.php', - '/administrator/components/com_installer/views/default/tmpl/default_ftp.php', - '/administrator/components/com_installer/views/default/tmpl/default_message.php', - '/administrator/components/com_installer/views/default/view.php', - '/administrator/components/com_installer/views/discover/tmpl/default.php', - '/administrator/components/com_installer/views/discover/tmpl/default.xml', - '/administrator/components/com_installer/views/discover/tmpl/default_item.php', - '/administrator/components/com_installer/views/discover/view.html.php', - '/administrator/components/com_installer/views/install/tmpl/default.php', - '/administrator/components/com_installer/views/install/tmpl/default.xml', - '/administrator/components/com_installer/views/install/view.html.php', - '/administrator/components/com_installer/views/languages/tmpl/default.php', - '/administrator/components/com_installer/views/languages/tmpl/default.xml', - '/administrator/components/com_installer/views/languages/view.html.php', - '/administrator/components/com_installer/views/manage/tmpl/default.php', - '/administrator/components/com_installer/views/manage/tmpl/default.xml', - '/administrator/components/com_installer/views/manage/view.html.php', - '/administrator/components/com_installer/views/update/tmpl/default.php', - '/administrator/components/com_installer/views/update/tmpl/default.xml', - '/administrator/components/com_installer/views/update/view.html.php', - '/administrator/components/com_installer/views/updatesites/tmpl/default.php', - '/administrator/components/com_installer/views/updatesites/tmpl/default.xml', - '/administrator/components/com_installer/views/updatesites/view.html.php', - '/administrator/components/com_installer/views/warnings/tmpl/default.php', - '/administrator/components/com_installer/views/warnings/tmpl/default.xml', - '/administrator/components/com_installer/views/warnings/view.html.php', - '/administrator/components/com_joomlaupdate/controller.php', - '/administrator/components/com_joomlaupdate/controllers/update.php', - '/administrator/components/com_joomlaupdate/helpers/joomlaupdate.php', - '/administrator/components/com_joomlaupdate/helpers/select.php', - '/administrator/components/com_joomlaupdate/joomlaupdate.php', - '/administrator/components/com_joomlaupdate/models/default.php', - '/administrator/components/com_joomlaupdate/restore.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/complete.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default.xml', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_nodownload.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_noupdate.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_preupdatecheck.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_reinstall.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_update.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_updatemefirst.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_upload.php', - '/administrator/components/com_joomlaupdate/views/default/view.html.php', - '/administrator/components/com_joomlaupdate/views/update/tmpl/default.php', - '/administrator/components/com_joomlaupdate/views/update/tmpl/finaliseconfirm.php', - '/administrator/components/com_joomlaupdate/views/update/view.html.php', - '/administrator/components/com_joomlaupdate/views/upload/tmpl/captive.php', - '/administrator/components/com_joomlaupdate/views/upload/view.html.php', - '/administrator/components/com_languages/controller.php', - '/administrator/components/com_languages/controllers/installed.php', - '/administrator/components/com_languages/controllers/language.php', - '/administrator/components/com_languages/controllers/languages.php', - '/administrator/components/com_languages/controllers/override.php', - '/administrator/components/com_languages/controllers/overrides.php', - '/administrator/components/com_languages/controllers/strings.json.php', - '/administrator/components/com_languages/helpers/html/languages.php', - '/administrator/components/com_languages/helpers/jsonresponse.php', - '/administrator/components/com_languages/helpers/languages.php', - '/administrator/components/com_languages/helpers/multilangstatus.php', - '/administrator/components/com_languages/languages.php', - '/administrator/components/com_languages/layouts/joomla/searchtools/default/bar.php', - '/administrator/components/com_languages/models/fields/languageclient.php', - '/administrator/components/com_languages/models/forms/filter_installed.xml', - '/administrator/components/com_languages/models/forms/filter_languages.xml', - '/administrator/components/com_languages/models/forms/filter_overrides.xml', - '/administrator/components/com_languages/models/forms/language.xml', - '/administrator/components/com_languages/models/forms/override.xml', - '/administrator/components/com_languages/models/installed.php', - '/administrator/components/com_languages/models/language.php', - '/administrator/components/com_languages/models/languages.php', - '/administrator/components/com_languages/models/override.php', - '/administrator/components/com_languages/models/overrides.php', - '/administrator/components/com_languages/models/strings.php', - '/administrator/components/com_languages/views/installed/tmpl/default.php', - '/administrator/components/com_languages/views/installed/tmpl/default.xml', - '/administrator/components/com_languages/views/installed/view.html.php', - '/administrator/components/com_languages/views/language/tmpl/edit.php', - '/administrator/components/com_languages/views/language/view.html.php', - '/administrator/components/com_languages/views/languages/tmpl/default.php', - '/administrator/components/com_languages/views/languages/tmpl/default.xml', - '/administrator/components/com_languages/views/languages/view.html.php', - '/administrator/components/com_languages/views/multilangstatus/tmpl/default.php', - '/administrator/components/com_languages/views/multilangstatus/view.html.php', - '/administrator/components/com_languages/views/override/tmpl/edit.php', - '/administrator/components/com_languages/views/override/view.html.php', - '/administrator/components/com_languages/views/overrides/tmpl/default.php', - '/administrator/components/com_languages/views/overrides/tmpl/default.xml', - '/administrator/components/com_languages/views/overrides/view.html.php', - '/administrator/components/com_login/controller.php', - '/administrator/components/com_login/login.php', - '/administrator/components/com_login/models/login.php', - '/administrator/components/com_login/views/login/tmpl/default.php', - '/administrator/components/com_login/views/login/view.html.php', - '/administrator/components/com_media/controller.php', - '/administrator/components/com_media/controllers/file.json.php', - '/administrator/components/com_media/controllers/file.php', - '/administrator/components/com_media/controllers/folder.php', - '/administrator/components/com_media/layouts/toolbar/deletemedia.php', - '/administrator/components/com_media/layouts/toolbar/newfolder.php', - '/administrator/components/com_media/layouts/toolbar/uploadmedia.php', - '/administrator/components/com_media/media.php', - '/administrator/components/com_media/models/list.php', - '/administrator/components/com_media/models/manager.php', - '/administrator/components/com_media/views/images/tmpl/default.php', - '/administrator/components/com_media/views/images/view.html.php', - '/administrator/components/com_media/views/imageslist/tmpl/default.php', - '/administrator/components/com_media/views/imageslist/tmpl/default_folder.php', - '/administrator/components/com_media/views/imageslist/tmpl/default_image.php', - '/administrator/components/com_media/views/imageslist/view.html.php', - '/administrator/components/com_media/views/media/tmpl/default.php', - '/administrator/components/com_media/views/media/tmpl/default.xml', - '/administrator/components/com_media/views/media/tmpl/default_folders.php', - '/administrator/components/com_media/views/media/tmpl/default_navigation.php', - '/administrator/components/com_media/views/media/view.html.php', - '/administrator/components/com_media/views/medialist/tmpl/default.php', - '/administrator/components/com_media/views/medialist/tmpl/details.php', - '/administrator/components/com_media/views/medialist/tmpl/details_doc.php', - '/administrator/components/com_media/views/medialist/tmpl/details_docs.php', - '/administrator/components/com_media/views/medialist/tmpl/details_folder.php', - '/administrator/components/com_media/views/medialist/tmpl/details_folders.php', - '/administrator/components/com_media/views/medialist/tmpl/details_img.php', - '/administrator/components/com_media/views/medialist/tmpl/details_imgs.php', - '/administrator/components/com_media/views/medialist/tmpl/details_up.php', - '/administrator/components/com_media/views/medialist/tmpl/details_video.php', - '/administrator/components/com_media/views/medialist/tmpl/details_videos.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_docs.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_folders.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_imgs.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_up.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_videos.php', - '/administrator/components/com_media/views/medialist/view.html.php', - '/administrator/components/com_menus/controller.php', - '/administrator/components/com_menus/controllers/ajax.json.php', - '/administrator/components/com_menus/controllers/item.php', - '/administrator/components/com_menus/controllers/items.php', - '/administrator/components/com_menus/controllers/menu.php', - '/administrator/components/com_menus/controllers/menus.php', - '/administrator/components/com_menus/helpers/associations.php', - '/administrator/components/com_menus/helpers/html/menus.php', - '/administrator/components/com_menus/layouts/joomla/searchtools/default/bar.php', - '/administrator/components/com_menus/menus.php', - '/administrator/components/com_menus/models/fields/componentscategory.php', - '/administrator/components/com_menus/models/fields/menuitembytype.php', - '/administrator/components/com_menus/models/fields/menuordering.php', - '/administrator/components/com_menus/models/fields/menuparent.php', - '/administrator/components/com_menus/models/fields/menupreset.php', - '/administrator/components/com_menus/models/fields/menutype.php', - '/administrator/components/com_menus/models/fields/modal/menu.php', - '/administrator/components/com_menus/models/forms/filter_items.xml', - '/administrator/components/com_menus/models/forms/filter_itemsadmin.xml', - '/administrator/components/com_menus/models/forms/filter_menus.xml', - '/administrator/components/com_menus/models/forms/item.xml', - '/administrator/components/com_menus/models/forms/item_alias.xml', - '/administrator/components/com_menus/models/forms/item_component.xml', - '/administrator/components/com_menus/models/forms/item_heading.xml', - '/administrator/components/com_menus/models/forms/item_separator.xml', - '/administrator/components/com_menus/models/forms/item_url.xml', - '/administrator/components/com_menus/models/forms/itemadmin.xml', - '/administrator/components/com_menus/models/forms/itemadmin_alias.xml', - '/administrator/components/com_menus/models/forms/itemadmin_component.xml', - '/administrator/components/com_menus/models/forms/itemadmin_container.xml', - '/administrator/components/com_menus/models/forms/itemadmin_heading.xml', - '/administrator/components/com_menus/models/forms/itemadmin_separator.xml', - '/administrator/components/com_menus/models/forms/itemadmin_url.xml', - '/administrator/components/com_menus/models/forms/menu.xml', - '/administrator/components/com_menus/models/item.php', - '/administrator/components/com_menus/models/items.php', - '/administrator/components/com_menus/models/menu.php', - '/administrator/components/com_menus/models/menus.php', - '/administrator/components/com_menus/models/menutypes.php', - '/administrator/components/com_menus/presets/joomla.xml', - '/administrator/components/com_menus/presets/modern.xml', - '/administrator/components/com_menus/tables/menu.php', - '/administrator/components/com_menus/views/item/tmpl/edit.php', - '/administrator/components/com_menus/views/item/tmpl/edit.xml', - '/administrator/components/com_menus/views/item/tmpl/edit_associations.php', - '/administrator/components/com_menus/views/item/tmpl/edit_container.php', - '/administrator/components/com_menus/views/item/tmpl/edit_modules.php', - '/administrator/components/com_menus/views/item/tmpl/edit_options.php', - '/administrator/components/com_menus/views/item/tmpl/modal.php', - '/administrator/components/com_menus/views/item/tmpl/modal_associations.php', - '/administrator/components/com_menus/views/item/tmpl/modal_options.php', - '/administrator/components/com_menus/views/item/view.html.php', - '/administrator/components/com_menus/views/items/tmpl/default.php', - '/administrator/components/com_menus/views/items/tmpl/default.xml', - '/administrator/components/com_menus/views/items/tmpl/default_batch_body.php', - '/administrator/components/com_menus/views/items/tmpl/default_batch_footer.php', - '/administrator/components/com_menus/views/items/tmpl/modal.php', - '/administrator/components/com_menus/views/items/view.html.php', - '/administrator/components/com_menus/views/menu/tmpl/edit.php', - '/administrator/components/com_menus/views/menu/tmpl/edit.xml', - '/administrator/components/com_menus/views/menu/view.html.php', - '/administrator/components/com_menus/views/menu/view.xml.php', - '/administrator/components/com_menus/views/menus/tmpl/default.php', - '/administrator/components/com_menus/views/menus/tmpl/default.xml', - '/administrator/components/com_menus/views/menus/view.html.php', - '/administrator/components/com_menus/views/menutypes/tmpl/default.php', - '/administrator/components/com_menus/views/menutypes/view.html.php', - '/administrator/components/com_messages/controller.php', - '/administrator/components/com_messages/controllers/config.php', - '/administrator/components/com_messages/controllers/message.php', - '/administrator/components/com_messages/controllers/messages.php', - '/administrator/components/com_messages/helpers/html/messages.php', - '/administrator/components/com_messages/helpers/messages.php', - '/administrator/components/com_messages/messages.php', - '/administrator/components/com_messages/models/config.php', - '/administrator/components/com_messages/models/fields/messagestates.php', - '/administrator/components/com_messages/models/fields/usermessages.php', - '/administrator/components/com_messages/models/forms/config.xml', - '/administrator/components/com_messages/models/forms/filter_messages.xml', - '/administrator/components/com_messages/models/forms/message.xml', - '/administrator/components/com_messages/models/message.php', - '/administrator/components/com_messages/models/messages.php', - '/administrator/components/com_messages/tables/message.php', - '/administrator/components/com_messages/views/config/tmpl/default.php', - '/administrator/components/com_messages/views/config/view.html.php', - '/administrator/components/com_messages/views/message/tmpl/default.php', - '/administrator/components/com_messages/views/message/tmpl/edit.php', - '/administrator/components/com_messages/views/message/view.html.php', - '/administrator/components/com_messages/views/messages/tmpl/default.php', - '/administrator/components/com_messages/views/messages/view.html.php', - '/administrator/components/com_modules/controller.php', - '/administrator/components/com_modules/controllers/module.php', - '/administrator/components/com_modules/controllers/modules.php', - '/administrator/components/com_modules/helpers/html/modules.php', - '/administrator/components/com_modules/helpers/xml.php', - '/administrator/components/com_modules/layouts/toolbar/newmodule.php', - '/administrator/components/com_modules/models/fields/modulesmodule.php', - '/administrator/components/com_modules/models/fields/modulesposition.php', - '/administrator/components/com_modules/models/forms/advanced.xml', - '/administrator/components/com_modules/models/forms/filter_modules.xml', - '/administrator/components/com_modules/models/forms/filter_modulesadmin.xml', - '/administrator/components/com_modules/models/forms/module.xml', - '/administrator/components/com_modules/models/forms/moduleadmin.xml', - '/administrator/components/com_modules/models/module.php', - '/administrator/components/com_modules/models/modules.php', - '/administrator/components/com_modules/models/positions.php', - '/administrator/components/com_modules/models/select.php', - '/administrator/components/com_modules/modules.php', - '/administrator/components/com_modules/views/module/tmpl/edit.php', - '/administrator/components/com_modules/views/module/tmpl/edit_assignment.php', - '/administrator/components/com_modules/views/module/tmpl/edit_options.php', - '/administrator/components/com_modules/views/module/tmpl/edit_positions.php', - '/administrator/components/com_modules/views/module/tmpl/modal.php', - '/administrator/components/com_modules/views/module/view.html.php', - '/administrator/components/com_modules/views/module/view.json.php', - '/administrator/components/com_modules/views/modules/tmpl/default.php', - '/administrator/components/com_modules/views/modules/tmpl/default.xml', - '/administrator/components/com_modules/views/modules/tmpl/default_batch_body.php', - '/administrator/components/com_modules/views/modules/tmpl/default_batch_footer.php', - '/administrator/components/com_modules/views/modules/tmpl/modal.php', - '/administrator/components/com_modules/views/modules/view.html.php', - '/administrator/components/com_modules/views/positions/tmpl/modal.php', - '/administrator/components/com_modules/views/positions/view.html.php', - '/administrator/components/com_modules/views/preview/tmpl/default.php', - '/administrator/components/com_modules/views/preview/view.html.php', - '/administrator/components/com_modules/views/select/tmpl/default.php', - '/administrator/components/com_modules/views/select/view.html.php', - '/administrator/components/com_newsfeeds/controller.php', - '/administrator/components/com_newsfeeds/controllers/ajax.json.php', - '/administrator/components/com_newsfeeds/controllers/newsfeed.php', - '/administrator/components/com_newsfeeds/controllers/newsfeeds.php', - '/administrator/components/com_newsfeeds/helpers/associations.php', - '/administrator/components/com_newsfeeds/helpers/html/newsfeed.php', - '/administrator/components/com_newsfeeds/models/fields/modal/newsfeed.php', - '/administrator/components/com_newsfeeds/models/fields/newsfeeds.php', - '/administrator/components/com_newsfeeds/models/forms/filter_newsfeeds.xml', - '/administrator/components/com_newsfeeds/models/forms/newsfeed.xml', - '/administrator/components/com_newsfeeds/models/newsfeed.php', - '/administrator/components/com_newsfeeds/models/newsfeeds.php', - '/administrator/components/com_newsfeeds/newsfeeds.php', - '/administrator/components/com_newsfeeds/tables/newsfeed.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_associations.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_display.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_metadata.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_params.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_associations.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_display.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_metadata.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_params.php', - '/administrator/components/com_newsfeeds/views/newsfeed/view.html.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default_batch_body.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default_batch_footer.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/modal.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/view.html.php', - '/administrator/components/com_plugins/controller.php', - '/administrator/components/com_plugins/controllers/plugin.php', - '/administrator/components/com_plugins/controllers/plugins.php', - '/administrator/components/com_plugins/models/fields/pluginelement.php', - '/administrator/components/com_plugins/models/fields/pluginordering.php', - '/administrator/components/com_plugins/models/fields/plugintype.php', - '/administrator/components/com_plugins/models/forms/filter_plugins.xml', - '/administrator/components/com_plugins/models/forms/plugin.xml', - '/administrator/components/com_plugins/models/plugin.php', - '/administrator/components/com_plugins/models/plugins.php', - '/administrator/components/com_plugins/plugins.php', - '/administrator/components/com_plugins/views/plugin/tmpl/edit.php', - '/administrator/components/com_plugins/views/plugin/tmpl/edit_options.php', - '/administrator/components/com_plugins/views/plugin/tmpl/modal.php', - '/administrator/components/com_plugins/views/plugin/view.html.php', - '/administrator/components/com_plugins/views/plugins/tmpl/default.php', - '/administrator/components/com_plugins/views/plugins/tmpl/default.xml', - '/administrator/components/com_plugins/views/plugins/view.html.php', - '/administrator/components/com_postinstall/controllers/message.php', - '/administrator/components/com_postinstall/fof.xml', - '/administrator/components/com_postinstall/models/messages.php', - '/administrator/components/com_postinstall/postinstall.php', - '/administrator/components/com_postinstall/toolbar.php', - '/administrator/components/com_postinstall/views/messages/tmpl/default.php', - '/administrator/components/com_postinstall/views/messages/tmpl/default.xml', - '/administrator/components/com_postinstall/views/messages/view.html.php', - '/administrator/components/com_privacy/controller.php', - '/administrator/components/com_privacy/controllers/consents.php', - '/administrator/components/com_privacy/controllers/request.php', - '/administrator/components/com_privacy/controllers/request.xml.php', - '/administrator/components/com_privacy/controllers/requests.php', - '/administrator/components/com_privacy/helpers/export/domain.php', - '/administrator/components/com_privacy/helpers/export/field.php', - '/administrator/components/com_privacy/helpers/export/item.php', - '/administrator/components/com_privacy/helpers/html/helper.php', - '/administrator/components/com_privacy/helpers/plugin.php', - '/administrator/components/com_privacy/helpers/privacy.php', - '/administrator/components/com_privacy/helpers/removal/status.php', - '/administrator/components/com_privacy/models/capabilities.php', - '/administrator/components/com_privacy/models/consents.php', - '/administrator/components/com_privacy/models/dashboard.php', - '/administrator/components/com_privacy/models/export.php', - '/administrator/components/com_privacy/models/fields/requeststatus.php', - '/administrator/components/com_privacy/models/fields/requesttype.php', - '/administrator/components/com_privacy/models/forms/filter_consents.xml', - '/administrator/components/com_privacy/models/forms/filter_requests.xml', - '/administrator/components/com_privacy/models/forms/request.xml', - '/administrator/components/com_privacy/models/remove.php', - '/administrator/components/com_privacy/models/request.php', - '/administrator/components/com_privacy/models/requests.php', - '/administrator/components/com_privacy/privacy.php', - '/administrator/components/com_privacy/tables/consent.php', - '/administrator/components/com_privacy/tables/request.php', - '/administrator/components/com_privacy/views/capabilities/tmpl/default.php', - '/administrator/components/com_privacy/views/capabilities/view.html.php', - '/administrator/components/com_privacy/views/consents/tmpl/default.php', - '/administrator/components/com_privacy/views/consents/tmpl/default.xml', - '/administrator/components/com_privacy/views/consents/view.html.php', - '/administrator/components/com_privacy/views/dashboard/tmpl/default.php', - '/administrator/components/com_privacy/views/dashboard/tmpl/default.xml', - '/administrator/components/com_privacy/views/dashboard/view.html.php', - '/administrator/components/com_privacy/views/export/view.xml.php', - '/administrator/components/com_privacy/views/request/tmpl/default.php', - '/administrator/components/com_privacy/views/request/tmpl/edit.php', - '/administrator/components/com_privacy/views/request/view.html.php', - '/administrator/components/com_privacy/views/requests/tmpl/default.php', - '/administrator/components/com_privacy/views/requests/tmpl/default.xml', - '/administrator/components/com_privacy/views/requests/view.html.php', - '/administrator/components/com_redirect/controller.php', - '/administrator/components/com_redirect/controllers/link.php', - '/administrator/components/com_redirect/controllers/links.php', - '/administrator/components/com_redirect/helpers/html/redirect.php', - '/administrator/components/com_redirect/models/fields/redirect.php', - '/administrator/components/com_redirect/models/forms/filter_links.xml', - '/administrator/components/com_redirect/models/forms/link.xml', - '/administrator/components/com_redirect/models/link.php', - '/administrator/components/com_redirect/models/links.php', - '/administrator/components/com_redirect/redirect.php', - '/administrator/components/com_redirect/tables/link.php', - '/administrator/components/com_redirect/views/link/tmpl/edit.php', - '/administrator/components/com_redirect/views/link/view.html.php', - '/administrator/components/com_redirect/views/links/tmpl/default.php', - '/administrator/components/com_redirect/views/links/tmpl/default.xml', - '/administrator/components/com_redirect/views/links/tmpl/default_addform.php', - '/administrator/components/com_redirect/views/links/tmpl/default_batch_body.php', - '/administrator/components/com_redirect/views/links/tmpl/default_batch_footer.php', - '/administrator/components/com_redirect/views/links/view.html.php', - '/administrator/components/com_tags/controller.php', - '/administrator/components/com_tags/controllers/tag.php', - '/administrator/components/com_tags/controllers/tags.php', - '/administrator/components/com_tags/helpers/tags.php', - '/administrator/components/com_tags/models/forms/filter_tags.xml', - '/administrator/components/com_tags/models/forms/tag.xml', - '/administrator/components/com_tags/models/tag.php', - '/administrator/components/com_tags/models/tags.php', - '/administrator/components/com_tags/tables/tag.php', - '/administrator/components/com_tags/tags.php', - '/administrator/components/com_tags/views/tag/tmpl/edit.php', - '/administrator/components/com_tags/views/tag/tmpl/edit_metadata.php', - '/administrator/components/com_tags/views/tag/tmpl/edit_options.php', - '/administrator/components/com_tags/views/tag/view.html.php', - '/administrator/components/com_tags/views/tags/tmpl/default.php', - '/administrator/components/com_tags/views/tags/tmpl/default.xml', - '/administrator/components/com_tags/views/tags/tmpl/default_batch_body.php', - '/administrator/components/com_tags/views/tags/tmpl/default_batch_footer.php', - '/administrator/components/com_tags/views/tags/view.html.php', - '/administrator/components/com_templates/controller.php', - '/administrator/components/com_templates/controllers/style.php', - '/administrator/components/com_templates/controllers/styles.php', - '/administrator/components/com_templates/controllers/template.php', - '/administrator/components/com_templates/helpers/html/templates.php', - '/administrator/components/com_templates/models/fields/templatelocation.php', - '/administrator/components/com_templates/models/fields/templatename.php', - '/administrator/components/com_templates/models/forms/filter_styles.xml', - '/administrator/components/com_templates/models/forms/filter_templates.xml', - '/administrator/components/com_templates/models/forms/source.xml', - '/administrator/components/com_templates/models/forms/style.xml', - '/administrator/components/com_templates/models/forms/style_administrator.xml', - '/administrator/components/com_templates/models/forms/style_site.xml', - '/administrator/components/com_templates/models/style.php', - '/administrator/components/com_templates/models/styles.php', - '/administrator/components/com_templates/models/template.php', - '/administrator/components/com_templates/models/templates.php', - '/administrator/components/com_templates/tables/style.php', - '/administrator/components/com_templates/templates.php', - '/administrator/components/com_templates/views/style/tmpl/edit.php', - '/administrator/components/com_templates/views/style/tmpl/edit_assignment.php', - '/administrator/components/com_templates/views/style/tmpl/edit_options.php', - '/administrator/components/com_templates/views/style/view.html.php', - '/administrator/components/com_templates/views/style/view.json.php', - '/administrator/components/com_templates/views/styles/tmpl/default.php', - '/administrator/components/com_templates/views/styles/tmpl/default.xml', - '/administrator/components/com_templates/views/styles/view.html.php', - '/administrator/components/com_templates/views/template/tmpl/default.php', - '/administrator/components/com_templates/views/template/tmpl/default_description.php', - '/administrator/components/com_templates/views/template/tmpl/default_folders.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_copy_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_copy_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_delete_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_delete_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_file_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_file_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_folder_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_folder_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_rename_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_rename_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_resize_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_resize_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_tree.php', - '/administrator/components/com_templates/views/template/tmpl/readonly.php', - '/administrator/components/com_templates/views/template/view.html.php', - '/administrator/components/com_templates/views/templates/tmpl/default.php', - '/administrator/components/com_templates/views/templates/tmpl/default.xml', - '/administrator/components/com_templates/views/templates/view.html.php', - '/administrator/components/com_users/controller.php', - '/administrator/components/com_users/controllers/group.php', - '/administrator/components/com_users/controllers/groups.php', - '/administrator/components/com_users/controllers/level.php', - '/administrator/components/com_users/controllers/levels.php', - '/administrator/components/com_users/controllers/mail.php', - '/administrator/components/com_users/controllers/note.php', - '/administrator/components/com_users/controllers/notes.php', - '/administrator/components/com_users/controllers/user.php', - '/administrator/components/com_users/controllers/users.php', - '/administrator/components/com_users/helpers/html/users.php', - '/administrator/components/com_users/models/debuggroup.php', - '/administrator/components/com_users/models/debuguser.php', - '/administrator/components/com_users/models/fields/groupparent.php', - '/administrator/components/com_users/models/fields/levels.php', - '/administrator/components/com_users/models/forms/config_domain.xml', - '/administrator/components/com_users/models/forms/fields/user.xml', - '/administrator/components/com_users/models/forms/filter_debuggroup.xml', - '/administrator/components/com_users/models/forms/filter_debuguser.xml', - '/administrator/components/com_users/models/forms/filter_groups.xml', - '/administrator/components/com_users/models/forms/filter_levels.xml', - '/administrator/components/com_users/models/forms/filter_notes.xml', - '/administrator/components/com_users/models/forms/filter_users.xml', - '/administrator/components/com_users/models/forms/group.xml', - '/administrator/components/com_users/models/forms/level.xml', - '/administrator/components/com_users/models/forms/mail.xml', - '/administrator/components/com_users/models/forms/note.xml', - '/administrator/components/com_users/models/forms/user.xml', - '/administrator/components/com_users/models/group.php', - '/administrator/components/com_users/models/groups.php', - '/administrator/components/com_users/models/level.php', - '/administrator/components/com_users/models/levels.php', - '/administrator/components/com_users/models/mail.php', - '/administrator/components/com_users/models/note.php', - '/administrator/components/com_users/models/notes.php', - '/administrator/components/com_users/models/user.php', - '/administrator/components/com_users/models/users.php', - '/administrator/components/com_users/tables/note.php', - '/administrator/components/com_users/users.php', - '/administrator/components/com_users/views/debuggroup/tmpl/default.php', - '/administrator/components/com_users/views/debuggroup/view.html.php', - '/administrator/components/com_users/views/debuguser/tmpl/default.php', - '/administrator/components/com_users/views/debuguser/view.html.php', - '/administrator/components/com_users/views/group/tmpl/edit.php', - '/administrator/components/com_users/views/group/tmpl/edit.xml', - '/administrator/components/com_users/views/group/view.html.php', - '/administrator/components/com_users/views/groups/tmpl/default.php', - '/administrator/components/com_users/views/groups/tmpl/default.xml', - '/administrator/components/com_users/views/groups/view.html.php', - '/administrator/components/com_users/views/level/tmpl/edit.php', - '/administrator/components/com_users/views/level/tmpl/edit.xml', - '/administrator/components/com_users/views/level/view.html.php', - '/administrator/components/com_users/views/levels/tmpl/default.php', - '/administrator/components/com_users/views/levels/tmpl/default.xml', - '/administrator/components/com_users/views/levels/view.html.php', - '/administrator/components/com_users/views/mail/tmpl/default.php', - '/administrator/components/com_users/views/mail/tmpl/default.xml', - '/administrator/components/com_users/views/mail/view.html.php', - '/administrator/components/com_users/views/note/tmpl/edit.php', - '/administrator/components/com_users/views/note/tmpl/edit.xml', - '/administrator/components/com_users/views/note/view.html.php', - '/administrator/components/com_users/views/notes/tmpl/default.php', - '/administrator/components/com_users/views/notes/tmpl/default.xml', - '/administrator/components/com_users/views/notes/tmpl/modal.php', - '/administrator/components/com_users/views/notes/view.html.php', - '/administrator/components/com_users/views/user/tmpl/edit.php', - '/administrator/components/com_users/views/user/tmpl/edit.xml', - '/administrator/components/com_users/views/user/tmpl/edit_groups.php', - '/administrator/components/com_users/views/user/view.html.php', - '/administrator/components/com_users/views/users/tmpl/default.php', - '/administrator/components/com_users/views/users/tmpl/default.xml', - '/administrator/components/com_users/views/users/tmpl/default_batch_body.php', - '/administrator/components/com_users/views/users/tmpl/default_batch_footer.php', - '/administrator/components/com_users/views/users/tmpl/modal.php', - '/administrator/components/com_users/views/users/view.html.php', - '/administrator/help/helpsites.xml', - '/administrator/includes/helper.php', - '/administrator/includes/subtoolbar.php', - '/administrator/language/en-GB/en-GB.com_actionlogs.ini', - '/administrator/language/en-GB/en-GB.com_actionlogs.sys.ini', - '/administrator/language/en-GB/en-GB.com_admin.ini', - '/administrator/language/en-GB/en-GB.com_admin.sys.ini', - '/administrator/language/en-GB/en-GB.com_ajax.ini', - '/administrator/language/en-GB/en-GB.com_ajax.sys.ini', - '/administrator/language/en-GB/en-GB.com_associations.ini', - '/administrator/language/en-GB/en-GB.com_associations.sys.ini', - '/administrator/language/en-GB/en-GB.com_banners.ini', - '/administrator/language/en-GB/en-GB.com_banners.sys.ini', - '/administrator/language/en-GB/en-GB.com_cache.ini', - '/administrator/language/en-GB/en-GB.com_cache.sys.ini', - '/administrator/language/en-GB/en-GB.com_categories.ini', - '/administrator/language/en-GB/en-GB.com_categories.sys.ini', - '/administrator/language/en-GB/en-GB.com_checkin.ini', - '/administrator/language/en-GB/en-GB.com_checkin.sys.ini', - '/administrator/language/en-GB/en-GB.com_config.ini', - '/administrator/language/en-GB/en-GB.com_config.sys.ini', - '/administrator/language/en-GB/en-GB.com_contact.ini', - '/administrator/language/en-GB/en-GB.com_contact.sys.ini', - '/administrator/language/en-GB/en-GB.com_content.ini', - '/administrator/language/en-GB/en-GB.com_content.sys.ini', - '/administrator/language/en-GB/en-GB.com_contenthistory.ini', - '/administrator/language/en-GB/en-GB.com_contenthistory.sys.ini', - '/administrator/language/en-GB/en-GB.com_cpanel.ini', - '/administrator/language/en-GB/en-GB.com_cpanel.sys.ini', - '/administrator/language/en-GB/en-GB.com_fields.ini', - '/administrator/language/en-GB/en-GB.com_fields.sys.ini', - '/administrator/language/en-GB/en-GB.com_finder.ini', - '/administrator/language/en-GB/en-GB.com_finder.sys.ini', - '/administrator/language/en-GB/en-GB.com_installer.ini', - '/administrator/language/en-GB/en-GB.com_installer.sys.ini', - '/administrator/language/en-GB/en-GB.com_joomlaupdate.ini', - '/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini', - '/administrator/language/en-GB/en-GB.com_languages.ini', - '/administrator/language/en-GB/en-GB.com_languages.sys.ini', - '/administrator/language/en-GB/en-GB.com_login.ini', - '/administrator/language/en-GB/en-GB.com_login.sys.ini', - '/administrator/language/en-GB/en-GB.com_mailto.sys.ini', - '/administrator/language/en-GB/en-GB.com_media.ini', - '/administrator/language/en-GB/en-GB.com_media.sys.ini', - '/administrator/language/en-GB/en-GB.com_menus.ini', - '/administrator/language/en-GB/en-GB.com_menus.sys.ini', - '/administrator/language/en-GB/en-GB.com_messages.ini', - '/administrator/language/en-GB/en-GB.com_messages.sys.ini', - '/administrator/language/en-GB/en-GB.com_modules.ini', - '/administrator/language/en-GB/en-GB.com_modules.sys.ini', - '/administrator/language/en-GB/en-GB.com_newsfeeds.ini', - '/administrator/language/en-GB/en-GB.com_newsfeeds.sys.ini', - '/administrator/language/en-GB/en-GB.com_plugins.ini', - '/administrator/language/en-GB/en-GB.com_plugins.sys.ini', - '/administrator/language/en-GB/en-GB.com_postinstall.ini', - '/administrator/language/en-GB/en-GB.com_postinstall.sys.ini', - '/administrator/language/en-GB/en-GB.com_privacy.ini', - '/administrator/language/en-GB/en-GB.com_privacy.sys.ini', - '/administrator/language/en-GB/en-GB.com_redirect.ini', - '/administrator/language/en-GB/en-GB.com_redirect.sys.ini', - '/administrator/language/en-GB/en-GB.com_tags.ini', - '/administrator/language/en-GB/en-GB.com_tags.sys.ini', - '/administrator/language/en-GB/en-GB.com_templates.ini', - '/administrator/language/en-GB/en-GB.com_templates.sys.ini', - '/administrator/language/en-GB/en-GB.com_users.ini', - '/administrator/language/en-GB/en-GB.com_users.sys.ini', - '/administrator/language/en-GB/en-GB.com_weblinks.ini', - '/administrator/language/en-GB/en-GB.com_weblinks.sys.ini', - '/administrator/language/en-GB/en-GB.com_wrapper.ini', - '/administrator/language/en-GB/en-GB.com_wrapper.sys.ini', - '/administrator/language/en-GB/en-GB.ini', - '/administrator/language/en-GB/en-GB.lib_joomla.ini', - '/administrator/language/en-GB/en-GB.localise.php', - '/administrator/language/en-GB/en-GB.mod_custom.ini', - '/administrator/language/en-GB/en-GB.mod_custom.sys.ini', - '/administrator/language/en-GB/en-GB.mod_feed.ini', - '/administrator/language/en-GB/en-GB.mod_feed.sys.ini', - '/administrator/language/en-GB/en-GB.mod_latest.ini', - '/administrator/language/en-GB/en-GB.mod_latest.sys.ini', - '/administrator/language/en-GB/en-GB.mod_latestactions.ini', - '/administrator/language/en-GB/en-GB.mod_latestactions.sys.ini', - '/administrator/language/en-GB/en-GB.mod_logged.ini', - '/administrator/language/en-GB/en-GB.mod_logged.sys.ini', - '/administrator/language/en-GB/en-GB.mod_login.ini', - '/administrator/language/en-GB/en-GB.mod_login.sys.ini', - '/administrator/language/en-GB/en-GB.mod_menu.ini', - '/administrator/language/en-GB/en-GB.mod_menu.sys.ini', - '/administrator/language/en-GB/en-GB.mod_multilangstatus.ini', - '/administrator/language/en-GB/en-GB.mod_multilangstatus.sys.ini', - '/administrator/language/en-GB/en-GB.mod_popular.ini', - '/administrator/language/en-GB/en-GB.mod_popular.sys.ini', - '/administrator/language/en-GB/en-GB.mod_privacy_dashboard.ini', - '/administrator/language/en-GB/en-GB.mod_privacy_dashboard.sys.ini', - '/administrator/language/en-GB/en-GB.mod_quickicon.ini', - '/administrator/language/en-GB/en-GB.mod_quickicon.sys.ini', - '/administrator/language/en-GB/en-GB.mod_sampledata.ini', - '/administrator/language/en-GB/en-GB.mod_sampledata.sys.ini', - '/administrator/language/en-GB/en-GB.mod_stats_admin.ini', - '/administrator/language/en-GB/en-GB.mod_stats_admin.sys.ini', - '/administrator/language/en-GB/en-GB.mod_status.ini', - '/administrator/language/en-GB/en-GB.mod_status.sys.ini', - '/administrator/language/en-GB/en-GB.mod_submenu.ini', - '/administrator/language/en-GB/en-GB.mod_submenu.sys.ini', - '/administrator/language/en-GB/en-GB.mod_title.ini', - '/administrator/language/en-GB/en-GB.mod_title.sys.ini', - '/administrator/language/en-GB/en-GB.mod_toolbar.ini', - '/administrator/language/en-GB/en-GB.mod_toolbar.sys.ini', - '/administrator/language/en-GB/en-GB.mod_version.ini', - '/administrator/language/en-GB/en-GB.mod_version.sys.ini', - '/administrator/language/en-GB/en-GB.plg_actionlog_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_actionlog_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_cookie.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_cookie.sys.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_gmail.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_gmail.sys.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_ldap.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_ldap.sys.ini', - '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.ini', - '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.sys.ini', - '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.ini', - '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_confirmconsent.ini', - '/administrator/language/en-GB/en-GB.plg_content_confirmconsent.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_contact.ini', - '/administrator/language/en-GB/en-GB.plg_content_contact.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_emailcloak.ini', - '/administrator/language/en-GB/en-GB.plg_content_emailcloak.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_fields.ini', - '/administrator/language/en-GB/en-GB.plg_content_fields.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_finder.ini', - '/administrator/language/en-GB/en-GB.plg_content_finder.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_content_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_loadmodule.ini', - '/administrator/language/en-GB/en-GB.plg_content_loadmodule.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_pagebreak.ini', - '/administrator/language/en-GB/en-GB.plg_content_pagebreak.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_pagenavigation.ini', - '/administrator/language/en-GB/en-GB.plg_content_pagenavigation.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_vote.ini', - '/administrator/language/en-GB/en-GB.plg_content_vote.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_article.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_article.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_image.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_image.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_module.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_module.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors_codemirror.ini', - '/administrator/language/en-GB/en-GB.plg_editors_codemirror.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors_none.ini', - '/administrator/language/en-GB/en-GB.plg_editors_none.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors_tinymce.ini', - '/administrator/language/en-GB/en-GB.plg_editors_tinymce.sys.ini', - '/administrator/language/en-GB/en-GB.plg_extension_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_extension_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_calendar.ini', - '/administrator/language/en-GB/en-GB.plg_fields_calendar.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_checkboxes.ini', - '/administrator/language/en-GB/en-GB.plg_fields_checkboxes.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_color.ini', - '/administrator/language/en-GB/en-GB.plg_fields_color.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_editor.ini', - '/administrator/language/en-GB/en-GB.plg_fields_editor.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_image.ini', - '/administrator/language/en-GB/en-GB.plg_fields_image.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_imagelist.ini', - '/administrator/language/en-GB/en-GB.plg_fields_imagelist.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_integer.ini', - '/administrator/language/en-GB/en-GB.plg_fields_integer.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_list.ini', - '/administrator/language/en-GB/en-GB.plg_fields_list.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_media.ini', - '/administrator/language/en-GB/en-GB.plg_fields_media.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_radio.ini', - '/administrator/language/en-GB/en-GB.plg_fields_radio.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_sql.ini', - '/administrator/language/en-GB/en-GB.plg_fields_sql.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_text.ini', - '/administrator/language/en-GB/en-GB.plg_fields_text.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_textarea.ini', - '/administrator/language/en-GB/en-GB.plg_fields_textarea.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_url.ini', - '/administrator/language/en-GB/en-GB.plg_fields_url.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_user.ini', - '/administrator/language/en-GB/en-GB.plg_fields_user.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.ini', - '/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_categories.ini', - '/administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_contacts.ini', - '/administrator/language/en-GB/en-GB.plg_finder_contacts.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_content.ini', - '/administrator/language/en-GB/en-GB.plg_finder_content.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.ini', - '/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_tags.ini', - '/administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_weblinks.ini', - '/administrator/language/en-GB/en-GB.plg_finder_weblinks.sys.ini', - '/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.ini', - '/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.sys.ini', - '/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.ini', - '/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.sys.ini', - '/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.ini', - '/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.sys.ini', - '/administrator/language/en-GB/en-GB.plg_installer_webinstaller.ini', - '/administrator/language/en-GB/en-GB.plg_installer_webinstaller.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_consents.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_consents.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_contact.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_contact.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_content.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_content.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_message.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_message.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_user.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_user.sys.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.sys.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.sys.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.sys.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.sys.ini', - '/administrator/language/en-GB/en-GB.plg_sampledata_blog.ini', - '/administrator/language/en-GB/en-GB.plg_sampledata_blog.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_actionlogs.ini', - '/administrator/language/en-GB/en-GB.plg_system_actionlogs.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_cache.ini', - '/administrator/language/en-GB/en-GB.plg_system_cache.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_debug.ini', - '/administrator/language/en-GB/en-GB.plg_system_debug.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_fields.ini', - '/administrator/language/en-GB/en-GB.plg_system_fields.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_highlight.ini', - '/administrator/language/en-GB/en-GB.plg_system_highlight.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_languagecode.ini', - '/administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_languagefilter.ini', - '/administrator/language/en-GB/en-GB.plg_system_languagefilter.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_log.ini', - '/administrator/language/en-GB/en-GB.plg_system_log.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_logout.ini', - '/administrator/language/en-GB/en-GB.plg_system_logout.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_logrotation.ini', - '/administrator/language/en-GB/en-GB.plg_system_logrotation.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_p3p.ini', - '/administrator/language/en-GB/en-GB.plg_system_p3p.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_privacyconsent.ini', - '/administrator/language/en-GB/en-GB.plg_system_privacyconsent.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_redirect.ini', - '/administrator/language/en-GB/en-GB.plg_system_redirect.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_remember.ini', - '/administrator/language/en-GB/en-GB.plg_system_remember.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_sef.ini', - '/administrator/language/en-GB/en-GB.plg_system_sef.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_sessiongc.ini', - '/administrator/language/en-GB/en-GB.plg_system_sessiongc.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_stats.ini', - '/administrator/language/en-GB/en-GB.plg_system_stats.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_updatenotification.ini', - '/administrator/language/en-GB/en-GB.plg_system_updatenotification.sys.ini', - '/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini', - '/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini', - '/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini', - '/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.sys.ini', - '/administrator/language/en-GB/en-GB.plg_user_contactcreator.ini', - '/administrator/language/en-GB/en-GB.plg_user_contactcreator.sys.ini', - '/administrator/language/en-GB/en-GB.plg_user_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_user_profile.ini', - '/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini', - '/administrator/language/en-GB/en-GB.plg_user_terms.ini', - '/administrator/language/en-GB/en-GB.plg_user_terms.sys.ini', - '/administrator/language/en-GB/en-GB.tpl_hathor.ini', - '/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini', - '/administrator/language/en-GB/en-GB.tpl_isis.ini', - '/administrator/language/en-GB/en-GB.tpl_isis.sys.ini', - '/administrator/language/en-GB/en-GB.xml', - '/administrator/manifests/libraries/fof.xml', - '/administrator/manifests/libraries/idna_convert.xml', - '/administrator/manifests/libraries/phputf8.xml', - '/administrator/modules/mod_feed/helper.php', - '/administrator/modules/mod_latest/helper.php', - '/administrator/modules/mod_latestactions/helper.php', - '/administrator/modules/mod_logged/helper.php', - '/administrator/modules/mod_login/helper.php', - '/administrator/modules/mod_menu/helper.php', - '/administrator/modules/mod_menu/menu.php', - '/administrator/modules/mod_multilangstatus/language/en-GB/en-GB.mod_multilangstatus.ini', - '/administrator/modules/mod_multilangstatus/language/en-GB/en-GB.mod_multilangstatus.sys.ini', - '/administrator/modules/mod_popular/helper.php', - '/administrator/modules/mod_privacy_dashboard/helper.php', - '/administrator/modules/mod_quickicon/helper.php', - '/administrator/modules/mod_quickicon/mod_quickicon.php', - '/administrator/modules/mod_sampledata/helper.php', - '/administrator/modules/mod_stats_admin/helper.php', - '/administrator/modules/mod_stats_admin/language/en-GB.mod_stats_admin.ini', - '/administrator/modules/mod_stats_admin/language/en-GB.mod_stats_admin.sys.ini', - '/administrator/modules/mod_status/mod_status.php', - '/administrator/modules/mod_status/mod_status.xml', - '/administrator/modules/mod_status/tmpl/default.php', - '/administrator/modules/mod_version/helper.php', - '/administrator/modules/mod_version/language/en-GB/en-GB.mod_version.ini', - '/administrator/modules/mod_version/language/en-GB/en-GB.mod_version.sys.ini', - '/administrator/templates/hathor/LICENSE.txt', - '/administrator/templates/hathor/component.php', - '/administrator/templates/hathor/cpanel.php', - '/administrator/templates/hathor/css/boldtext.css', - '/administrator/templates/hathor/css/colour_blue.css', - '/administrator/templates/hathor/css/colour_blue_rtl.css', - '/administrator/templates/hathor/css/colour_brown.css', - '/administrator/templates/hathor/css/colour_brown_rtl.css', - '/administrator/templates/hathor/css/colour_highcontrast.css', - '/administrator/templates/hathor/css/colour_highcontrast_rtl.css', - '/administrator/templates/hathor/css/colour_standard.css', - '/administrator/templates/hathor/css/colour_standard_rtl.css', - '/administrator/templates/hathor/css/error.css', - '/administrator/templates/hathor/css/ie7.css', - '/administrator/templates/hathor/css/ie8.css', - '/administrator/templates/hathor/css/template.css', - '/administrator/templates/hathor/css/template_rtl.css', - '/administrator/templates/hathor/css/theme.css', - '/administrator/templates/hathor/error.php', - '/administrator/templates/hathor/favicon.ico', - '/administrator/templates/hathor/html/com_admin/help/default.php', - '/administrator/templates/hathor/html/com_admin/profile/edit.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_config.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_directory.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_navigation.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_phpsettings.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_system.php', - '/administrator/templates/hathor/html/com_associations/associations/default.php', - '/administrator/templates/hathor/html/com_banners/banner/edit.php', - '/administrator/templates/hathor/html/com_banners/banners/default.php', - '/administrator/templates/hathor/html/com_banners/client/edit.php', - '/administrator/templates/hathor/html/com_banners/clients/default.php', - '/administrator/templates/hathor/html/com_banners/download/default.php', - '/administrator/templates/hathor/html/com_banners/tracks/default.php', - '/administrator/templates/hathor/html/com_cache/cache/default.php', - '/administrator/templates/hathor/html/com_cache/purge/default.php', - '/administrator/templates/hathor/html/com_categories/categories/default.php', - '/administrator/templates/hathor/html/com_categories/category/edit.php', - '/administrator/templates/hathor/html/com_categories/category/edit_options.php', - '/administrator/templates/hathor/html/com_checkin/checkin/default.php', - '/administrator/templates/hathor/html/com_config/application/default.php', - '/administrator/templates/hathor/html/com_config/application/default_cache.php', - '/administrator/templates/hathor/html/com_config/application/default_cookie.php', - '/administrator/templates/hathor/html/com_config/application/default_database.php', - '/administrator/templates/hathor/html/com_config/application/default_debug.php', - '/administrator/templates/hathor/html/com_config/application/default_filters.php', - '/administrator/templates/hathor/html/com_config/application/default_ftp.php', - '/administrator/templates/hathor/html/com_config/application/default_ftplogin.php', - '/administrator/templates/hathor/html/com_config/application/default_locale.php', - '/administrator/templates/hathor/html/com_config/application/default_mail.php', - '/administrator/templates/hathor/html/com_config/application/default_metadata.php', - '/administrator/templates/hathor/html/com_config/application/default_navigation.php', - '/administrator/templates/hathor/html/com_config/application/default_permissions.php', - '/administrator/templates/hathor/html/com_config/application/default_seo.php', - '/administrator/templates/hathor/html/com_config/application/default_server.php', - '/administrator/templates/hathor/html/com_config/application/default_session.php', - '/administrator/templates/hathor/html/com_config/application/default_site.php', - '/administrator/templates/hathor/html/com_config/application/default_system.php', - '/administrator/templates/hathor/html/com_config/component/default.php', - '/administrator/templates/hathor/html/com_contact/contact/edit.php', - '/administrator/templates/hathor/html/com_contact/contact/edit_params.php', - '/administrator/templates/hathor/html/com_contact/contacts/default.php', - '/administrator/templates/hathor/html/com_contact/contacts/modal.php', - '/administrator/templates/hathor/html/com_content/article/edit.php', - '/administrator/templates/hathor/html/com_content/articles/default.php', - '/administrator/templates/hathor/html/com_content/articles/modal.php', - '/administrator/templates/hathor/html/com_content/featured/default.php', - '/administrator/templates/hathor/html/com_contenthistory/history/modal.php', - '/administrator/templates/hathor/html/com_cpanel/cpanel/default.php', - '/administrator/templates/hathor/html/com_fields/field/edit.php', - '/administrator/templates/hathor/html/com_fields/fields/default.php', - '/administrator/templates/hathor/html/com_fields/group/edit.php', - '/administrator/templates/hathor/html/com_fields/groups/default.php', - '/administrator/templates/hathor/html/com_finder/filters/default.php', - '/administrator/templates/hathor/html/com_finder/index/default.php', - '/administrator/templates/hathor/html/com_finder/maps/default.php', - '/administrator/templates/hathor/html/com_installer/database/default.php', - '/administrator/templates/hathor/html/com_installer/default/default_ftp.php', - '/administrator/templates/hathor/html/com_installer/discover/default.php', - '/administrator/templates/hathor/html/com_installer/install/default.php', - '/administrator/templates/hathor/html/com_installer/install/default_form.php', - '/administrator/templates/hathor/html/com_installer/languages/default.php', - '/administrator/templates/hathor/html/com_installer/languages/default_filter.php', - '/administrator/templates/hathor/html/com_installer/manage/default.php', - '/administrator/templates/hathor/html/com_installer/manage/default_filter.php', - '/administrator/templates/hathor/html/com_installer/update/default.php', - '/administrator/templates/hathor/html/com_installer/warnings/default.php', - '/administrator/templates/hathor/html/com_joomlaupdate/default/default.php', - '/administrator/templates/hathor/html/com_languages/installed/default.php', - '/administrator/templates/hathor/html/com_languages/installed/default_ftp.php', - '/administrator/templates/hathor/html/com_languages/languages/default.php', - '/administrator/templates/hathor/html/com_languages/overrides/default.php', - '/administrator/templates/hathor/html/com_menus/item/edit.php', - '/administrator/templates/hathor/html/com_menus/item/edit_options.php', - '/administrator/templates/hathor/html/com_menus/items/default.php', - '/administrator/templates/hathor/html/com_menus/menu/edit.php', - '/administrator/templates/hathor/html/com_menus/menus/default.php', - '/administrator/templates/hathor/html/com_menus/menutypes/default.php', - '/administrator/templates/hathor/html/com_messages/message/edit.php', - '/administrator/templates/hathor/html/com_messages/messages/default.php', - '/administrator/templates/hathor/html/com_modules/module/edit.php', - '/administrator/templates/hathor/html/com_modules/module/edit_assignment.php', - '/administrator/templates/hathor/html/com_modules/module/edit_options.php', - '/administrator/templates/hathor/html/com_modules/modules/default.php', - '/administrator/templates/hathor/html/com_modules/positions/modal.php', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeed/edit.php', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeed/edit_params.php', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds/default.php', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds/modal.php', - '/administrator/templates/hathor/html/com_plugins/plugin/edit.php', - '/administrator/templates/hathor/html/com_plugins/plugin/edit_options.php', - '/administrator/templates/hathor/html/com_plugins/plugins/default.php', - '/administrator/templates/hathor/html/com_postinstall/messages/default.php', - '/administrator/templates/hathor/html/com_redirect/links/default.php', - '/administrator/templates/hathor/html/com_search/searches/default.php', - '/administrator/templates/hathor/html/com_tags/tag/edit.php', - '/administrator/templates/hathor/html/com_tags/tag/edit_metadata.php', - '/administrator/templates/hathor/html/com_tags/tag/edit_options.php', - '/administrator/templates/hathor/html/com_tags/tags/default.php', - '/administrator/templates/hathor/html/com_templates/style/edit.php', - '/administrator/templates/hathor/html/com_templates/style/edit_assignment.php', - '/administrator/templates/hathor/html/com_templates/style/edit_options.php', - '/administrator/templates/hathor/html/com_templates/styles/default.php', - '/administrator/templates/hathor/html/com_templates/template/default.php', - '/administrator/templates/hathor/html/com_templates/template/default_description.php', - '/administrator/templates/hathor/html/com_templates/template/default_folders.php', - '/administrator/templates/hathor/html/com_templates/template/default_tree.php', - '/administrator/templates/hathor/html/com_templates/templates/default.php', - '/administrator/templates/hathor/html/com_users/debuggroup/default.php', - '/administrator/templates/hathor/html/com_users/debuguser/default.php', - '/administrator/templates/hathor/html/com_users/groups/default.php', - '/administrator/templates/hathor/html/com_users/levels/default.php', - '/administrator/templates/hathor/html/com_users/note/edit.php', - '/administrator/templates/hathor/html/com_users/notes/default.php', - '/administrator/templates/hathor/html/com_users/user/edit.php', - '/administrator/templates/hathor/html/com_users/users/default.php', - '/administrator/templates/hathor/html/com_users/users/modal.php', - '/administrator/templates/hathor/html/com_weblinks/weblink/edit.php', - '/administrator/templates/hathor/html/com_weblinks/weblink/edit_params.php', - '/administrator/templates/hathor/html/com_weblinks/weblinks/default.php', - '/administrator/templates/hathor/html/layouts/com_media/toolbar/deletemedia.php', - '/administrator/templates/hathor/html/layouts/com_media/toolbar/newfolder.php', - '/administrator/templates/hathor/html/layouts/com_media/toolbar/uploadmedia.php', - '/administrator/templates/hathor/html/layouts/com_messages/toolbar/mysettings.php', - '/administrator/templates/hathor/html/layouts/com_modules/toolbar/cancelselect.php', - '/administrator/templates/hathor/html/layouts/com_modules/toolbar/newmodule.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/details.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/fieldset.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/global.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/metadata.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/params.php', - '/administrator/templates/hathor/html/layouts/joomla/quickicons/icon.php', - '/administrator/templates/hathor/html/layouts/joomla/sidebars/submenu.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/base.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/batch.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/confirm.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/containerclose.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/containeropen.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/help.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/iconclass.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/link.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/modal.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/popup.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/separator.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/slider.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/standard.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/title.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/versions.php', - '/administrator/templates/hathor/html/layouts/plugins/user/profile/fields/dob.php', - '/administrator/templates/hathor/html/mod_login/default.php', - '/administrator/templates/hathor/html/mod_quickicon/default.php', - '/administrator/templates/hathor/html/modules.php', - '/administrator/templates/hathor/html/pagination.php', - '/administrator/templates/hathor/images/admin/blank.png', - '/administrator/templates/hathor/images/admin/checked_out.png', - '/administrator/templates/hathor/images/admin/collapseall.png', - '/administrator/templates/hathor/images/admin/disabled.png', - '/administrator/templates/hathor/images/admin/downarrow-1.png', - '/administrator/templates/hathor/images/admin/downarrow.png', - '/administrator/templates/hathor/images/admin/downarrow0.png', - '/administrator/templates/hathor/images/admin/expandall.png', - '/administrator/templates/hathor/images/admin/featured.png', - '/administrator/templates/hathor/images/admin/filesave.png', - '/administrator/templates/hathor/images/admin/filter_16.png', - '/administrator/templates/hathor/images/admin/icon-16-allow.png', - '/administrator/templates/hathor/images/admin/icon-16-allowinactive.png', - '/administrator/templates/hathor/images/admin/icon-16-deny.png', - '/administrator/templates/hathor/images/admin/icon-16-denyinactive.png', - '/administrator/templates/hathor/images/admin/icon-16-links.png', - '/administrator/templates/hathor/images/admin/icon-16-notice-note.png', - '/administrator/templates/hathor/images/admin/icon-16-protected.png', - '/administrator/templates/hathor/images/admin/menu_divider.png', - '/administrator/templates/hathor/images/admin/note_add_16.png', - '/administrator/templates/hathor/images/admin/publish_g.png', - '/administrator/templates/hathor/images/admin/publish_r.png', - '/administrator/templates/hathor/images/admin/publish_x.png', - '/administrator/templates/hathor/images/admin/publish_y.png', - '/administrator/templates/hathor/images/admin/sort_asc.png', - '/administrator/templates/hathor/images/admin/sort_desc.png', - '/administrator/templates/hathor/images/admin/tick.png', - '/administrator/templates/hathor/images/admin/trash.png', - '/administrator/templates/hathor/images/admin/uparrow-1.png', - '/administrator/templates/hathor/images/admin/uparrow.png', - '/administrator/templates/hathor/images/admin/uparrow0.png', - '/administrator/templates/hathor/images/arrow.png', - '/administrator/templates/hathor/images/bg-menu.gif', - '/administrator/templates/hathor/images/calendar.png', - '/administrator/templates/hathor/images/header/icon-48-alert.png', - '/administrator/templates/hathor/images/header/icon-48-apply.png', - '/administrator/templates/hathor/images/header/icon-48-archive.png', - '/administrator/templates/hathor/images/header/icon-48-article-add.png', - '/administrator/templates/hathor/images/header/icon-48-article-edit.png', - '/administrator/templates/hathor/images/header/icon-48-article.png', - '/administrator/templates/hathor/images/header/icon-48-assoc.png', - '/administrator/templates/hathor/images/header/icon-48-banner-categories.png', - '/administrator/templates/hathor/images/header/icon-48-banner-client.png', - '/administrator/templates/hathor/images/header/icon-48-banner-tracks.png', - '/administrator/templates/hathor/images/header/icon-48-banner.png', - '/administrator/templates/hathor/images/header/icon-48-calendar.png', - '/administrator/templates/hathor/images/header/icon-48-category-add.png', - '/administrator/templates/hathor/images/header/icon-48-category.png', - '/administrator/templates/hathor/images/header/icon-48-checkin.png', - '/administrator/templates/hathor/images/header/icon-48-clear.png', - '/administrator/templates/hathor/images/header/icon-48-component.png', - '/administrator/templates/hathor/images/header/icon-48-config.png', - '/administrator/templates/hathor/images/header/icon-48-contacts-categories.png', - '/administrator/templates/hathor/images/header/icon-48-contacts.png', - '/administrator/templates/hathor/images/header/icon-48-content.png', - '/administrator/templates/hathor/images/header/icon-48-cpanel.png', - '/administrator/templates/hathor/images/header/icon-48-default.png', - '/administrator/templates/hathor/images/header/icon-48-deny.png', - '/administrator/templates/hathor/images/header/icon-48-download.png', - '/administrator/templates/hathor/images/header/icon-48-edit.png', - '/administrator/templates/hathor/images/header/icon-48-extension.png', - '/administrator/templates/hathor/images/header/icon-48-featured.png', - '/administrator/templates/hathor/images/header/icon-48-frontpage.png', - '/administrator/templates/hathor/images/header/icon-48-generic.png', - '/administrator/templates/hathor/images/header/icon-48-groups-add.png', - '/administrator/templates/hathor/images/header/icon-48-groups.png', - '/administrator/templates/hathor/images/header/icon-48-help-forum.png', - '/administrator/templates/hathor/images/header/icon-48-help-this.png', - '/administrator/templates/hathor/images/header/icon-48-help_header.png', - '/administrator/templates/hathor/images/header/icon-48-inbox.png', - '/administrator/templates/hathor/images/header/icon-48-info.png', - '/administrator/templates/hathor/images/header/icon-48-install.png', - '/administrator/templates/hathor/images/header/icon-48-jupdate-updatefound.png', - '/administrator/templates/hathor/images/header/icon-48-jupdate-uptodate.png', - '/administrator/templates/hathor/images/header/icon-48-language.png', - '/administrator/templates/hathor/images/header/icon-48-levels-add.png', - '/administrator/templates/hathor/images/header/icon-48-levels.png', - '/administrator/templates/hathor/images/header/icon-48-links-cat.png', - '/administrator/templates/hathor/images/header/icon-48-links.png', - '/administrator/templates/hathor/images/header/icon-48-massmail.png', - '/administrator/templates/hathor/images/header/icon-48-media.png', - '/administrator/templates/hathor/images/header/icon-48-menu-add.png', - '/administrator/templates/hathor/images/header/icon-48-menu.png', - '/administrator/templates/hathor/images/header/icon-48-menumgr.png', - '/administrator/templates/hathor/images/header/icon-48-module.png', - '/administrator/templates/hathor/images/header/icon-48-move.png', - '/administrator/templates/hathor/images/header/icon-48-new-privatemessage.png', - '/administrator/templates/hathor/images/header/icon-48-newcategory.png', - '/administrator/templates/hathor/images/header/icon-48-newsfeeds-cat.png', - '/administrator/templates/hathor/images/header/icon-48-newsfeeds.png', - '/administrator/templates/hathor/images/header/icon-48-notice.png', - '/administrator/templates/hathor/images/header/icon-48-plugin.png', - '/administrator/templates/hathor/images/header/icon-48-preview.png', - '/administrator/templates/hathor/images/header/icon-48-print.png', - '/administrator/templates/hathor/images/header/icon-48-purge.png', - '/administrator/templates/hathor/images/header/icon-48-puzzle.png', - '/administrator/templates/hathor/images/header/icon-48-read-privatemessage.png', - '/administrator/templates/hathor/images/header/icon-48-readmess.png', - '/administrator/templates/hathor/images/header/icon-48-redirect.png', - '/administrator/templates/hathor/images/header/icon-48-revert.png', - '/administrator/templates/hathor/images/header/icon-48-search.png', - '/administrator/templates/hathor/images/header/icon-48-section.png', - '/administrator/templates/hathor/images/header/icon-48-send.png', - '/administrator/templates/hathor/images/header/icon-48-static.png', - '/administrator/templates/hathor/images/header/icon-48-stats.png', - '/administrator/templates/hathor/images/header/icon-48-tags.png', - '/administrator/templates/hathor/images/header/icon-48-themes.png', - '/administrator/templates/hathor/images/header/icon-48-trash.png', - '/administrator/templates/hathor/images/header/icon-48-unarchive.png', - '/administrator/templates/hathor/images/header/icon-48-upload.png', - '/administrator/templates/hathor/images/header/icon-48-user-add.png', - '/administrator/templates/hathor/images/header/icon-48-user-edit.png', - '/administrator/templates/hathor/images/header/icon-48-user-profile.png', - '/administrator/templates/hathor/images/header/icon-48-user.png', - '/administrator/templates/hathor/images/header/icon-48-writemess.png', - '/administrator/templates/hathor/images/header/icon-messaging.png', - '/administrator/templates/hathor/images/j_arrow.png', - '/administrator/templates/hathor/images/j_arrow_down.png', - '/administrator/templates/hathor/images/j_arrow_left.png', - '/administrator/templates/hathor/images/j_arrow_right.png', - '/administrator/templates/hathor/images/j_login_lock.png', - '/administrator/templates/hathor/images/j_logo.png', - '/administrator/templates/hathor/images/logo.png', - '/administrator/templates/hathor/images/menu/icon-16-alert.png', - '/administrator/templates/hathor/images/menu/icon-16-apply.png', - '/administrator/templates/hathor/images/menu/icon-16-archive.png', - '/administrator/templates/hathor/images/menu/icon-16-article.png', - '/administrator/templates/hathor/images/menu/icon-16-assoc.png', - '/administrator/templates/hathor/images/menu/icon-16-back-user.png', - '/administrator/templates/hathor/images/menu/icon-16-banner-categories.png', - '/administrator/templates/hathor/images/menu/icon-16-banner-client.png', - '/administrator/templates/hathor/images/menu/icon-16-banner-tracks.png', - '/administrator/templates/hathor/images/menu/icon-16-banner.png', - '/administrator/templates/hathor/images/menu/icon-16-calendar.png', - '/administrator/templates/hathor/images/menu/icon-16-category.png', - '/administrator/templates/hathor/images/menu/icon-16-checkin.png', - '/administrator/templates/hathor/images/menu/icon-16-clear.png', - '/administrator/templates/hathor/images/menu/icon-16-component.png', - '/administrator/templates/hathor/images/menu/icon-16-config.png', - '/administrator/templates/hathor/images/menu/icon-16-contacts-categories.png', - '/administrator/templates/hathor/images/menu/icon-16-contacts.png', - '/administrator/templates/hathor/images/menu/icon-16-content.png', - '/administrator/templates/hathor/images/menu/icon-16-cpanel.png', - '/administrator/templates/hathor/images/menu/icon-16-default.png', - '/administrator/templates/hathor/images/menu/icon-16-delete.png', - '/administrator/templates/hathor/images/menu/icon-16-deny.png', - '/administrator/templates/hathor/images/menu/icon-16-download.png', - '/administrator/templates/hathor/images/menu/icon-16-edit.png', - '/administrator/templates/hathor/images/menu/icon-16-featured.png', - '/administrator/templates/hathor/images/menu/icon-16-frontpage.png', - '/administrator/templates/hathor/images/menu/icon-16-generic.png', - '/administrator/templates/hathor/images/menu/icon-16-groups.png', - '/administrator/templates/hathor/images/menu/icon-16-help-community.png', - '/administrator/templates/hathor/images/menu/icon-16-help-dev.png', - '/administrator/templates/hathor/images/menu/icon-16-help-docs.png', - '/administrator/templates/hathor/images/menu/icon-16-help-forum.png', - '/administrator/templates/hathor/images/menu/icon-16-help-jed.png', - '/administrator/templates/hathor/images/menu/icon-16-help-jrd.png', - '/administrator/templates/hathor/images/menu/icon-16-help-security.png', - '/administrator/templates/hathor/images/menu/icon-16-help-shop.png', - '/administrator/templates/hathor/images/menu/icon-16-help-this.png', - '/administrator/templates/hathor/images/menu/icon-16-help-trans.png', - '/administrator/templates/hathor/images/menu/icon-16-help.png', - '/administrator/templates/hathor/images/menu/icon-16-inbox.png', - '/administrator/templates/hathor/images/menu/icon-16-info.png', - '/administrator/templates/hathor/images/menu/icon-16-install.png', - '/administrator/templates/hathor/images/menu/icon-16-language.png', - '/administrator/templates/hathor/images/menu/icon-16-levels.png', - '/administrator/templates/hathor/images/menu/icon-16-links-cat.png', - '/administrator/templates/hathor/images/menu/icon-16-links.png', - '/administrator/templates/hathor/images/menu/icon-16-logout.png', - '/administrator/templates/hathor/images/menu/icon-16-maintenance.png', - '/administrator/templates/hathor/images/menu/icon-16-massmail.png', - '/administrator/templates/hathor/images/menu/icon-16-media.png', - '/administrator/templates/hathor/images/menu/icon-16-menu.png', - '/administrator/templates/hathor/images/menu/icon-16-menumgr.png', - '/administrator/templates/hathor/images/menu/icon-16-messages.png', - '/administrator/templates/hathor/images/menu/icon-16-messaging.png', - '/administrator/templates/hathor/images/menu/icon-16-module.png', - '/administrator/templates/hathor/images/menu/icon-16-move.png', - '/administrator/templates/hathor/images/menu/icon-16-new-privatemessage.png', - '/administrator/templates/hathor/images/menu/icon-16-new.png', - '/administrator/templates/hathor/images/menu/icon-16-newarticle.png', - '/administrator/templates/hathor/images/menu/icon-16-newcategory.png', - '/administrator/templates/hathor/images/menu/icon-16-newgroup.png', - '/administrator/templates/hathor/images/menu/icon-16-newlevel.png', - '/administrator/templates/hathor/images/menu/icon-16-newsfeeds-cat.png', - '/administrator/templates/hathor/images/menu/icon-16-newsfeeds.png', - '/administrator/templates/hathor/images/menu/icon-16-newuser.png', - '/administrator/templates/hathor/images/menu/icon-16-nopreview.png', - '/administrator/templates/hathor/images/menu/icon-16-notdefault.png', - '/administrator/templates/hathor/images/menu/icon-16-notice.png', - '/administrator/templates/hathor/images/menu/icon-16-plugin.png', - '/administrator/templates/hathor/images/menu/icon-16-preview.png', - '/administrator/templates/hathor/images/menu/icon-16-print.png', - '/administrator/templates/hathor/images/menu/icon-16-purge.png', - '/administrator/templates/hathor/images/menu/icon-16-puzzle.png', - '/administrator/templates/hathor/images/menu/icon-16-read-privatemessage.png', - '/administrator/templates/hathor/images/menu/icon-16-readmess.png', - '/administrator/templates/hathor/images/menu/icon-16-redirect.png', - '/administrator/templates/hathor/images/menu/icon-16-revert.png', - '/administrator/templates/hathor/images/menu/icon-16-search.png', - '/administrator/templates/hathor/images/menu/icon-16-send.png', - '/administrator/templates/hathor/images/menu/icon-16-stats.png', - '/administrator/templates/hathor/images/menu/icon-16-tags.png', - '/administrator/templates/hathor/images/menu/icon-16-themes.png', - '/administrator/templates/hathor/images/menu/icon-16-trash.png', - '/administrator/templates/hathor/images/menu/icon-16-unarticle.png', - '/administrator/templates/hathor/images/menu/icon-16-upload.png', - '/administrator/templates/hathor/images/menu/icon-16-user-dd.png', - '/administrator/templates/hathor/images/menu/icon-16-user-note.png', - '/administrator/templates/hathor/images/menu/icon-16-user.png', - '/administrator/templates/hathor/images/menu/icon-16-viewsite.png', - '/administrator/templates/hathor/images/menu/icon-16-writemess.png', - '/administrator/templates/hathor/images/mini_icon.png', - '/administrator/templates/hathor/images/notice-alert.png', - '/administrator/templates/hathor/images/notice-info.png', - '/administrator/templates/hathor/images/notice-note.png', - '/administrator/templates/hathor/images/required.png', - '/administrator/templates/hathor/images/selector-arrow-hc.png', - '/administrator/templates/hathor/images/selector-arrow-rtl.png', - '/administrator/templates/hathor/images/selector-arrow-std.png', - '/administrator/templates/hathor/images/selector-arrow.png', - '/administrator/templates/hathor/images/system/calendar.png', - '/administrator/templates/hathor/images/system/selector-arrow.png', - '/administrator/templates/hathor/images/toolbar/icon-32-adduser.png', - '/administrator/templates/hathor/images/toolbar/icon-32-alert.png', - '/administrator/templates/hathor/images/toolbar/icon-32-apply.png', - '/administrator/templates/hathor/images/toolbar/icon-32-archive.png', - '/administrator/templates/hathor/images/toolbar/icon-32-article-add.png', - '/administrator/templates/hathor/images/toolbar/icon-32-article.png', - '/administrator/templates/hathor/images/toolbar/icon-32-back.png', - '/administrator/templates/hathor/images/toolbar/icon-32-banner-categories.png', - '/administrator/templates/hathor/images/toolbar/icon-32-banner-client.png', - '/administrator/templates/hathor/images/toolbar/icon-32-banner-tracks.png', - '/administrator/templates/hathor/images/toolbar/icon-32-banner.png', - '/administrator/templates/hathor/images/toolbar/icon-32-batch.png', - '/administrator/templates/hathor/images/toolbar/icon-32-calendar.png', - '/administrator/templates/hathor/images/toolbar/icon-32-cancel.png', - '/administrator/templates/hathor/images/toolbar/icon-32-checkin.png', - '/administrator/templates/hathor/images/toolbar/icon-32-cog.png', - '/administrator/templates/hathor/images/toolbar/icon-32-component.png', - '/administrator/templates/hathor/images/toolbar/icon-32-config.png', - '/administrator/templates/hathor/images/toolbar/icon-32-contacts-categories.png', - '/administrator/templates/hathor/images/toolbar/icon-32-contacts.png', - '/administrator/templates/hathor/images/toolbar/icon-32-copy.png', - '/administrator/templates/hathor/images/toolbar/icon-32-css.png', - '/administrator/templates/hathor/images/toolbar/icon-32-default.png', - '/administrator/templates/hathor/images/toolbar/icon-32-delete-style.png', - '/administrator/templates/hathor/images/toolbar/icon-32-delete.png', - '/administrator/templates/hathor/images/toolbar/icon-32-deny.png', - '/administrator/templates/hathor/images/toolbar/icon-32-download.png', - '/administrator/templates/hathor/images/toolbar/icon-32-edit.png', - '/administrator/templates/hathor/images/toolbar/icon-32-error.png', - '/administrator/templates/hathor/images/toolbar/icon-32-export.png', - '/administrator/templates/hathor/images/toolbar/icon-32-extension.png', - '/administrator/templates/hathor/images/toolbar/icon-32-featured.png', - '/administrator/templates/hathor/images/toolbar/icon-32-forward.png', - '/administrator/templates/hathor/images/toolbar/icon-32-help.png', - '/administrator/templates/hathor/images/toolbar/icon-32-html.png', - '/administrator/templates/hathor/images/toolbar/icon-32-inbox.png', - '/administrator/templates/hathor/images/toolbar/icon-32-info.png', - '/administrator/templates/hathor/images/toolbar/icon-32-links.png', - '/administrator/templates/hathor/images/toolbar/icon-32-lock.png', - '/administrator/templates/hathor/images/toolbar/icon-32-menu.png', - '/administrator/templates/hathor/images/toolbar/icon-32-messaging.png', - '/administrator/templates/hathor/images/toolbar/icon-32-messanging.png', - '/administrator/templates/hathor/images/toolbar/icon-32-module.png', - '/administrator/templates/hathor/images/toolbar/icon-32-move.png', - '/administrator/templates/hathor/images/toolbar/icon-32-new-privatemessage.png', - '/administrator/templates/hathor/images/toolbar/icon-32-new-style.png', - '/administrator/templates/hathor/images/toolbar/icon-32-new.png', - '/administrator/templates/hathor/images/toolbar/icon-32-notice.png', - '/administrator/templates/hathor/images/toolbar/icon-32-preview.png', - '/administrator/templates/hathor/images/toolbar/icon-32-print.png', - '/administrator/templates/hathor/images/toolbar/icon-32-publish.png', - '/administrator/templates/hathor/images/toolbar/icon-32-purge.png', - '/administrator/templates/hathor/images/toolbar/icon-32-read-privatemessage.png', - '/administrator/templates/hathor/images/toolbar/icon-32-refresh.png', - '/administrator/templates/hathor/images/toolbar/icon-32-remove.png', - '/administrator/templates/hathor/images/toolbar/icon-32-revert.png', - '/administrator/templates/hathor/images/toolbar/icon-32-save-copy.png', - '/administrator/templates/hathor/images/toolbar/icon-32-save-new.png', - '/administrator/templates/hathor/images/toolbar/icon-32-save.png', - '/administrator/templates/hathor/images/toolbar/icon-32-search.png', - '/administrator/templates/hathor/images/toolbar/icon-32-send.png', - '/administrator/templates/hathor/images/toolbar/icon-32-stats.png', - '/administrator/templates/hathor/images/toolbar/icon-32-trash.png', - '/administrator/templates/hathor/images/toolbar/icon-32-unarchive.png', - '/administrator/templates/hathor/images/toolbar/icon-32-unblock.png', - '/administrator/templates/hathor/images/toolbar/icon-32-unpublish.png', - '/administrator/templates/hathor/images/toolbar/icon-32-upload.png', - '/administrator/templates/hathor/images/toolbar/icon-32-user-add.png', - '/administrator/templates/hathor/images/toolbar/icon-32-xml.png', - '/administrator/templates/hathor/index.php', - '/administrator/templates/hathor/js/template.js', - '/administrator/templates/hathor/language/en-GB/en-GB.tpl_hathor.ini', - '/administrator/templates/hathor/language/en-GB/en-GB.tpl_hathor.sys.ini', - '/administrator/templates/hathor/less/buttons.less', - '/administrator/templates/hathor/less/colour_baseline.less', - '/administrator/templates/hathor/less/colour_blue.less', - '/administrator/templates/hathor/less/colour_brown.less', - '/administrator/templates/hathor/less/colour_standard.less', - '/administrator/templates/hathor/less/forms.less', - '/administrator/templates/hathor/less/hathor_variables.less', - '/administrator/templates/hathor/less/icomoon.less', - '/administrator/templates/hathor/less/modals.less', - '/administrator/templates/hathor/less/template.less', - '/administrator/templates/hathor/less/variables.less', - '/administrator/templates/hathor/login.php', - '/administrator/templates/hathor/postinstall/hathormessage.php', - '/administrator/templates/hathor/templateDetails.xml', - '/administrator/templates/hathor/template_preview.png', - '/administrator/templates/hathor/template_thumbnail.png', - '/administrator/templates/isis/component.php', - '/administrator/templates/isis/cpanel.php', - '/administrator/templates/isis/css/template-rtl.css', - '/administrator/templates/isis/css/template.css', - '/administrator/templates/isis/error.php', - '/administrator/templates/isis/favicon.ico', - '/administrator/templates/isis/html/com_media/imageslist/default_folder.php', - '/administrator/templates/isis/html/com_media/imageslist/default_image.php', - '/administrator/templates/isis/html/com_media/medialist/thumbs_folders.php', - '/administrator/templates/isis/html/com_media/medialist/thumbs_imgs.php', - '/administrator/templates/isis/html/editor_content.css', - '/administrator/templates/isis/html/layouts/joomla/form/field/media.php', - '/administrator/templates/isis/html/layouts/joomla/form/field/user.php', - '/administrator/templates/isis/html/layouts/joomla/pagination/link.php', - '/administrator/templates/isis/html/layouts/joomla/pagination/links.php', - '/administrator/templates/isis/html/layouts/joomla/system/message.php', - '/administrator/templates/isis/html/layouts/joomla/toolbar/versions.php', - '/administrator/templates/isis/html/mod_version/default.php', - '/administrator/templates/isis/html/modules.php', - '/administrator/templates/isis/html/pagination.php', - '/administrator/templates/isis/images/admin/blank.png', - '/administrator/templates/isis/images/admin/checked_out.png', - '/administrator/templates/isis/images/admin/collapseall.png', - '/administrator/templates/isis/images/admin/disabled.png', - '/administrator/templates/isis/images/admin/downarrow-1.png', - '/administrator/templates/isis/images/admin/downarrow.png', - '/administrator/templates/isis/images/admin/downarrow0.png', - '/administrator/templates/isis/images/admin/expandall.png', - '/administrator/templates/isis/images/admin/featured.png', - '/administrator/templates/isis/images/admin/filesave.png', - '/administrator/templates/isis/images/admin/filter_16.png', - '/administrator/templates/isis/images/admin/icon-16-add.png', - '/administrator/templates/isis/images/admin/icon-16-allow.png', - '/administrator/templates/isis/images/admin/icon-16-allowinactive.png', - '/administrator/templates/isis/images/admin/icon-16-deny.png', - '/administrator/templates/isis/images/admin/icon-16-denyinactive.png', - '/administrator/templates/isis/images/admin/icon-16-links.png', - '/administrator/templates/isis/images/admin/icon-16-notice-note.png', - '/administrator/templates/isis/images/admin/icon-16-protected.png', - '/administrator/templates/isis/images/admin/menu_divider.png', - '/administrator/templates/isis/images/admin/note_add_16.png', - '/administrator/templates/isis/images/admin/publish_g.png', - '/administrator/templates/isis/images/admin/publish_r.png', - '/administrator/templates/isis/images/admin/publish_x.png', - '/administrator/templates/isis/images/admin/publish_y.png', - '/administrator/templates/isis/images/admin/sort_asc.png', - '/administrator/templates/isis/images/admin/sort_desc.png', - '/administrator/templates/isis/images/admin/tick.png', - '/administrator/templates/isis/images/admin/trash.png', - '/administrator/templates/isis/images/admin/uparrow-1.png', - '/administrator/templates/isis/images/admin/uparrow.png', - '/administrator/templates/isis/images/admin/uparrow0.png', - '/administrator/templates/isis/images/emailButton.png', - '/administrator/templates/isis/images/joomla.png', - '/administrator/templates/isis/images/login-joomla-inverse.png', - '/administrator/templates/isis/images/login-joomla.png', - '/administrator/templates/isis/images/logo-inverse.png', - '/administrator/templates/isis/images/logo.png', - '/administrator/templates/isis/images/pdf_button.png', - '/administrator/templates/isis/images/printButton.png', - '/administrator/templates/isis/images/system/sort_asc.png', - '/administrator/templates/isis/images/system/sort_desc.png', - '/administrator/templates/isis/img/glyphicons-halflings-white.png', - '/administrator/templates/isis/img/glyphicons-halflings.png', - '/administrator/templates/isis/index.php', - '/administrator/templates/isis/js/application.js', - '/administrator/templates/isis/js/classes.js', - '/administrator/templates/isis/js/template.js', - '/administrator/templates/isis/language/en-GB/en-GB.tpl_isis.ini', - '/administrator/templates/isis/language/en-GB/en-GB.tpl_isis.sys.ini', - '/administrator/templates/isis/less/blocks/_chzn-override.less', - '/administrator/templates/isis/less/blocks/_custom.less', - '/administrator/templates/isis/less/blocks/_editors.less', - '/administrator/templates/isis/less/blocks/_forms.less', - '/administrator/templates/isis/less/blocks/_global.less', - '/administrator/templates/isis/less/blocks/_header.less', - '/administrator/templates/isis/less/blocks/_login.less', - '/administrator/templates/isis/less/blocks/_media.less', - '/administrator/templates/isis/less/blocks/_modals.less', - '/administrator/templates/isis/less/blocks/_navbar.less', - '/administrator/templates/isis/less/blocks/_quickicons.less', - '/administrator/templates/isis/less/blocks/_sidebar.less', - '/administrator/templates/isis/less/blocks/_status.less', - '/administrator/templates/isis/less/blocks/_tables.less', - '/administrator/templates/isis/less/blocks/_toolbar.less', - '/administrator/templates/isis/less/blocks/_treeselect.less', - '/administrator/templates/isis/less/blocks/_utility-classes.less', - '/administrator/templates/isis/less/bootstrap/button-groups.less', - '/administrator/templates/isis/less/bootstrap/buttons.less', - '/administrator/templates/isis/less/bootstrap/mixins.less', - '/administrator/templates/isis/less/bootstrap/responsive-1200px-min.less', - '/administrator/templates/isis/less/bootstrap/responsive-768px-979px.less', - '/administrator/templates/isis/less/bootstrap/wells.less', - '/administrator/templates/isis/less/icomoon.less', - '/administrator/templates/isis/less/pages/_com_cpanel.less', - '/administrator/templates/isis/less/pages/_com_postinstall.less', - '/administrator/templates/isis/less/pages/_com_privacy.less', - '/administrator/templates/isis/less/pages/_com_templates.less', - '/administrator/templates/isis/less/template-rtl.less', - '/administrator/templates/isis/less/template.less', - '/administrator/templates/isis/less/variables.less', - '/administrator/templates/isis/login.php', - '/administrator/templates/isis/templateDetails.xml', - '/administrator/templates/isis/template_preview.png', - '/administrator/templates/isis/template_thumbnail.png', - '/administrator/templates/system/html/modules.php', - '/bin/index.html', - '/bin/keychain.php', - '/cli/deletefiles.php', - '/cli/finder_indexer.php', - '/cli/garbagecron.php', - '/cli/sessionGc.php', - '/cli/sessionMetadataGc.php', - '/cli/update_cron.php', - '/components/com_banners/banners.php', - '/components/com_banners/controller.php', - '/components/com_banners/helpers/banner.php', - '/components/com_banners/helpers/category.php', - '/components/com_banners/models/banner.php', - '/components/com_banners/models/banners.php', - '/components/com_banners/router.php', - '/components/com_config/config.php', - '/components/com_config/controller/cancel.php', - '/components/com_config/controller/canceladmin.php', - '/components/com_config/controller/cmsbase.php', - '/components/com_config/controller/config/display.php', - '/components/com_config/controller/config/save.php', - '/components/com_config/controller/display.php', - '/components/com_config/controller/helper.php', - '/components/com_config/controller/modules/cancel.php', - '/components/com_config/controller/modules/display.php', - '/components/com_config/controller/modules/save.php', - '/components/com_config/controller/templates/display.php', - '/components/com_config/controller/templates/save.php', - '/components/com_config/model/cms.php', - '/components/com_config/model/config.php', - '/components/com_config/model/form.php', - '/components/com_config/model/form/config.xml', - '/components/com_config/model/form/modules.xml', - '/components/com_config/model/form/modules_advanced.xml', - '/components/com_config/model/form/templates.xml', - '/components/com_config/model/modules.php', - '/components/com_config/model/templates.php', - '/components/com_config/view/cms/html.php', - '/components/com_config/view/cms/json.php', - '/components/com_config/view/config/html.php', - '/components/com_config/view/config/tmpl/default.php', - '/components/com_config/view/config/tmpl/default.xml', - '/components/com_config/view/config/tmpl/default_metadata.php', - '/components/com_config/view/config/tmpl/default_seo.php', - '/components/com_config/view/config/tmpl/default_site.php', - '/components/com_config/view/modules/html.php', - '/components/com_config/view/modules/tmpl/default.php', - '/components/com_config/view/modules/tmpl/default_options.php', - '/components/com_config/view/modules/tmpl/default_positions.php', - '/components/com_config/view/templates/html.php', - '/components/com_config/view/templates/tmpl/default.php', - '/components/com_config/view/templates/tmpl/default.xml', - '/components/com_config/view/templates/tmpl/default_options.php', - '/components/com_contact/contact.php', - '/components/com_contact/controller.php', - '/components/com_contact/controllers/contact.php', - '/components/com_contact/helpers/association.php', - '/components/com_contact/helpers/category.php', - '/components/com_contact/helpers/legacyrouter.php', - '/components/com_contact/layouts/joomla/form/renderfield.php', - '/components/com_contact/models/categories.php', - '/components/com_contact/models/category.php', - '/components/com_contact/models/contact.php', - '/components/com_contact/models/featured.php', - '/components/com_contact/models/forms/contact.xml', - '/components/com_contact/models/forms/filter_contacts.xml', - '/components/com_contact/models/forms/form.xml', - '/components/com_contact/models/rules/contactemail.php', - '/components/com_contact/models/rules/contactemailmessage.php', - '/components/com_contact/models/rules/contactemailsubject.php', - '/components/com_contact/router.php', - '/components/com_contact/views/categories/tmpl/default.php', - '/components/com_contact/views/categories/tmpl/default.xml', - '/components/com_contact/views/categories/tmpl/default_items.php', - '/components/com_contact/views/categories/view.html.php', - '/components/com_contact/views/category/tmpl/default.php', - '/components/com_contact/views/category/tmpl/default.xml', - '/components/com_contact/views/category/tmpl/default_children.php', - '/components/com_contact/views/category/tmpl/default_items.php', - '/components/com_contact/views/category/view.feed.php', - '/components/com_contact/views/category/view.html.php', - '/components/com_contact/views/contact/tmpl/default.php', - '/components/com_contact/views/contact/tmpl/default.xml', - '/components/com_contact/views/contact/tmpl/default_address.php', - '/components/com_contact/views/contact/tmpl/default_articles.php', - '/components/com_contact/views/contact/tmpl/default_form.php', - '/components/com_contact/views/contact/tmpl/default_links.php', - '/components/com_contact/views/contact/tmpl/default_profile.php', - '/components/com_contact/views/contact/tmpl/default_user_custom_fields.php', - '/components/com_contact/views/contact/view.html.php', - '/components/com_contact/views/contact/view.vcf.php', - '/components/com_contact/views/featured/tmpl/default.php', - '/components/com_contact/views/featured/tmpl/default.xml', - '/components/com_contact/views/featured/tmpl/default_items.php', - '/components/com_contact/views/featured/view.html.php', - '/components/com_content/content.php', - '/components/com_content/controller.php', - '/components/com_content/controllers/article.php', - '/components/com_content/helpers/association.php', - '/components/com_content/helpers/category.php', - '/components/com_content/helpers/legacyrouter.php', - '/components/com_content/helpers/query.php', - '/components/com_content/helpers/route.php', - '/components/com_content/models/archive.php', - '/components/com_content/models/article.php', - '/components/com_content/models/articles.php', - '/components/com_content/models/categories.php', - '/components/com_content/models/category.php', - '/components/com_content/models/featured.php', - '/components/com_content/models/form.php', - '/components/com_content/models/forms/article.xml', - '/components/com_content/models/forms/filter_articles.xml', - '/components/com_content/router.php', - '/components/com_content/views/archive/tmpl/default.php', - '/components/com_content/views/archive/tmpl/default.xml', - '/components/com_content/views/archive/tmpl/default_items.php', - '/components/com_content/views/archive/view.html.php', - '/components/com_content/views/article/tmpl/default.php', - '/components/com_content/views/article/tmpl/default.xml', - '/components/com_content/views/article/tmpl/default_links.php', - '/components/com_content/views/article/view.html.php', - '/components/com_content/views/categories/tmpl/default.php', - '/components/com_content/views/categories/tmpl/default.xml', - '/components/com_content/views/categories/tmpl/default_items.php', - '/components/com_content/views/categories/view.html.php', - '/components/com_content/views/category/tmpl/blog.php', - '/components/com_content/views/category/tmpl/blog.xml', - '/components/com_content/views/category/tmpl/blog_children.php', - '/components/com_content/views/category/tmpl/blog_item.php', - '/components/com_content/views/category/tmpl/blog_links.php', - '/components/com_content/views/category/tmpl/default.php', - '/components/com_content/views/category/tmpl/default.xml', - '/components/com_content/views/category/tmpl/default_articles.php', - '/components/com_content/views/category/tmpl/default_children.php', - '/components/com_content/views/category/view.feed.php', - '/components/com_content/views/category/view.html.php', - '/components/com_content/views/featured/tmpl/default.php', - '/components/com_content/views/featured/tmpl/default.xml', - '/components/com_content/views/featured/tmpl/default_item.php', - '/components/com_content/views/featured/tmpl/default_links.php', - '/components/com_content/views/featured/view.feed.php', - '/components/com_content/views/featured/view.html.php', - '/components/com_content/views/form/tmpl/edit.php', - '/components/com_content/views/form/tmpl/edit.xml', - '/components/com_content/views/form/view.html.php', - '/components/com_contenthistory/contenthistory.php', - '/components/com_fields/controller.php', - '/components/com_fields/fields.php', - '/components/com_fields/models/forms/filter_fields.xml', - '/components/com_finder/controller.php', - '/components/com_finder/controllers/suggestions.json.php', - '/components/com_finder/finder.php', - '/components/com_finder/helpers/html/filter.php', - '/components/com_finder/helpers/html/query.php', - '/components/com_finder/models/search.php', - '/components/com_finder/models/suggestions.php', - '/components/com_finder/router.php', - '/components/com_finder/views/search/tmpl/default.php', - '/components/com_finder/views/search/tmpl/default.xml', - '/components/com_finder/views/search/tmpl/default_form.php', - '/components/com_finder/views/search/tmpl/default_result.php', - '/components/com_finder/views/search/tmpl/default_results.php', - '/components/com_finder/views/search/view.feed.php', - '/components/com_finder/views/search/view.html.php', - '/components/com_finder/views/search/view.opensearch.php', - '/components/com_mailto/controller.php', - '/components/com_mailto/helpers/mailto.php', - '/components/com_mailto/mailto.php', - '/components/com_mailto/mailto.xml', - '/components/com_mailto/models/forms/mailto.xml', - '/components/com_mailto/models/mailto.php', - '/components/com_mailto/views/mailto/tmpl/default.php', - '/components/com_mailto/views/mailto/view.html.php', - '/components/com_mailto/views/sent/tmpl/default.php', - '/components/com_mailto/views/sent/view.html.php', - '/components/com_media/media.php', - '/components/com_menus/controller.php', - '/components/com_menus/menus.php', - '/components/com_menus/models/forms/filter_items.xml', - '/components/com_modules/controller.php', - '/components/com_modules/models/forms/filter_modules.xml', - '/components/com_modules/modules.php', - '/components/com_newsfeeds/controller.php', - '/components/com_newsfeeds/helpers/association.php', - '/components/com_newsfeeds/helpers/category.php', - '/components/com_newsfeeds/helpers/legacyrouter.php', - '/components/com_newsfeeds/models/categories.php', - '/components/com_newsfeeds/models/category.php', - '/components/com_newsfeeds/models/newsfeed.php', - '/components/com_newsfeeds/newsfeeds.php', - '/components/com_newsfeeds/router.php', - '/components/com_newsfeeds/views/categories/tmpl/default.php', - '/components/com_newsfeeds/views/categories/tmpl/default.xml', - '/components/com_newsfeeds/views/categories/tmpl/default_items.php', - '/components/com_newsfeeds/views/categories/view.html.php', - '/components/com_newsfeeds/views/category/tmpl/default.php', - '/components/com_newsfeeds/views/category/tmpl/default.xml', - '/components/com_newsfeeds/views/category/tmpl/default_children.php', - '/components/com_newsfeeds/views/category/tmpl/default_items.php', - '/components/com_newsfeeds/views/category/view.html.php', - '/components/com_newsfeeds/views/newsfeed/tmpl/default.php', - '/components/com_newsfeeds/views/newsfeed/tmpl/default.xml', - '/components/com_newsfeeds/views/newsfeed/view.html.php', - '/components/com_privacy/controller.php', - '/components/com_privacy/controllers/request.php', - '/components/com_privacy/models/confirm.php', - '/components/com_privacy/models/forms/confirm.xml', - '/components/com_privacy/models/forms/remind.xml', - '/components/com_privacy/models/forms/request.xml', - '/components/com_privacy/models/remind.php', - '/components/com_privacy/models/request.php', - '/components/com_privacy/privacy.php', - '/components/com_privacy/router.php', - '/components/com_privacy/views/confirm/tmpl/default.php', - '/components/com_privacy/views/confirm/tmpl/default.xml', - '/components/com_privacy/views/confirm/view.html.php', - '/components/com_privacy/views/remind/tmpl/default.php', - '/components/com_privacy/views/remind/tmpl/default.xml', - '/components/com_privacy/views/remind/view.html.php', - '/components/com_privacy/views/request/tmpl/default.php', - '/components/com_privacy/views/request/tmpl/default.xml', - '/components/com_privacy/views/request/view.html.php', - '/components/com_tags/controller.php', - '/components/com_tags/controllers/tags.php', - '/components/com_tags/models/tag.php', - '/components/com_tags/models/tags.php', - '/components/com_tags/router.php', - '/components/com_tags/tags.php', - '/components/com_tags/views/tag/tmpl/default.php', - '/components/com_tags/views/tag/tmpl/default.xml', - '/components/com_tags/views/tag/tmpl/default_items.php', - '/components/com_tags/views/tag/tmpl/list.php', - '/components/com_tags/views/tag/tmpl/list.xml', - '/components/com_tags/views/tag/tmpl/list_items.php', - '/components/com_tags/views/tag/view.feed.php', - '/components/com_tags/views/tag/view.html.php', - '/components/com_tags/views/tags/tmpl/default.php', - '/components/com_tags/views/tags/tmpl/default.xml', - '/components/com_tags/views/tags/tmpl/default_items.php', - '/components/com_tags/views/tags/view.feed.php', - '/components/com_tags/views/tags/view.html.php', - '/components/com_users/controller.php', - '/components/com_users/controllers/profile.php', - '/components/com_users/controllers/registration.php', - '/components/com_users/controllers/remind.php', - '/components/com_users/controllers/reset.php', - '/components/com_users/controllers/user.php', - '/components/com_users/helpers/html/users.php', - '/components/com_users/helpers/legacyrouter.php', - '/components/com_users/helpers/route.php', - '/components/com_users/layouts/joomla/form/renderfield.php', - '/components/com_users/models/forms/frontend.xml', - '/components/com_users/models/forms/frontend_admin.xml', - '/components/com_users/models/forms/login.xml', - '/components/com_users/models/forms/profile.xml', - '/components/com_users/models/forms/registration.xml', - '/components/com_users/models/forms/remind.xml', - '/components/com_users/models/forms/reset_complete.xml', - '/components/com_users/models/forms/reset_confirm.xml', - '/components/com_users/models/forms/reset_request.xml', - '/components/com_users/models/forms/sitelang.xml', - '/components/com_users/models/login.php', - '/components/com_users/models/profile.php', - '/components/com_users/models/registration.php', - '/components/com_users/models/remind.php', - '/components/com_users/models/reset.php', - '/components/com_users/models/rules/loginuniquefield.php', - '/components/com_users/models/rules/logoutuniquefield.php', - '/components/com_users/router.php', - '/components/com_users/users.php', - '/components/com_users/views/login/tmpl/default.php', - '/components/com_users/views/login/tmpl/default.xml', - '/components/com_users/views/login/tmpl/default_login.php', - '/components/com_users/views/login/tmpl/default_logout.php', - '/components/com_users/views/login/tmpl/logout.xml', - '/components/com_users/views/login/view.html.php', - '/components/com_users/views/profile/tmpl/default.php', - '/components/com_users/views/profile/tmpl/default.xml', - '/components/com_users/views/profile/tmpl/default_core.php', - '/components/com_users/views/profile/tmpl/default_custom.php', - '/components/com_users/views/profile/tmpl/default_params.php', - '/components/com_users/views/profile/tmpl/edit.php', - '/components/com_users/views/profile/tmpl/edit.xml', - '/components/com_users/views/profile/view.html.php', - '/components/com_users/views/registration/tmpl/complete.php', - '/components/com_users/views/registration/tmpl/default.php', - '/components/com_users/views/registration/tmpl/default.xml', - '/components/com_users/views/registration/view.html.php', - '/components/com_users/views/remind/tmpl/default.php', - '/components/com_users/views/remind/tmpl/default.xml', - '/components/com_users/views/remind/view.html.php', - '/components/com_users/views/reset/tmpl/complete.php', - '/components/com_users/views/reset/tmpl/confirm.php', - '/components/com_users/views/reset/tmpl/default.php', - '/components/com_users/views/reset/tmpl/default.xml', - '/components/com_users/views/reset/view.html.php', - '/components/com_wrapper/controller.php', - '/components/com_wrapper/router.php', - '/components/com_wrapper/views/wrapper/tmpl/default.php', - '/components/com_wrapper/views/wrapper/tmpl/default.xml', - '/components/com_wrapper/views/wrapper/view.html.php', - '/components/com_wrapper/wrapper.php', - '/components/com_wrapper/wrapper.xml', - '/language/en-GB/en-GB.com_ajax.ini', - '/language/en-GB/en-GB.com_config.ini', - '/language/en-GB/en-GB.com_contact.ini', - '/language/en-GB/en-GB.com_content.ini', - '/language/en-GB/en-GB.com_finder.ini', - '/language/en-GB/en-GB.com_mailto.ini', - '/language/en-GB/en-GB.com_media.ini', - '/language/en-GB/en-GB.com_messages.ini', - '/language/en-GB/en-GB.com_newsfeeds.ini', - '/language/en-GB/en-GB.com_privacy.ini', - '/language/en-GB/en-GB.com_tags.ini', - '/language/en-GB/en-GB.com_users.ini', - '/language/en-GB/en-GB.com_weblinks.ini', - '/language/en-GB/en-GB.com_wrapper.ini', - '/language/en-GB/en-GB.files_joomla.sys.ini', - '/language/en-GB/en-GB.finder_cli.ini', - '/language/en-GB/en-GB.ini', - '/language/en-GB/en-GB.lib_fof.ini', - '/language/en-GB/en-GB.lib_fof.sys.ini', - '/language/en-GB/en-GB.lib_idna_convert.sys.ini', - '/language/en-GB/en-GB.lib_joomla.ini', - '/language/en-GB/en-GB.lib_joomla.sys.ini', - '/language/en-GB/en-GB.lib_phpass.sys.ini', - '/language/en-GB/en-GB.lib_phputf8.sys.ini', - '/language/en-GB/en-GB.lib_simplepie.sys.ini', - '/language/en-GB/en-GB.localise.php', - '/language/en-GB/en-GB.mod_articles_archive.ini', - '/language/en-GB/en-GB.mod_articles_archive.sys.ini', - '/language/en-GB/en-GB.mod_articles_categories.ini', - '/language/en-GB/en-GB.mod_articles_categories.sys.ini', - '/language/en-GB/en-GB.mod_articles_category.ini', - '/language/en-GB/en-GB.mod_articles_category.sys.ini', - '/language/en-GB/en-GB.mod_articles_latest.ini', - '/language/en-GB/en-GB.mod_articles_latest.sys.ini', - '/language/en-GB/en-GB.mod_articles_news.ini', - '/language/en-GB/en-GB.mod_articles_news.sys.ini', - '/language/en-GB/en-GB.mod_articles_popular.ini', - '/language/en-GB/en-GB.mod_articles_popular.sys.ini', - '/language/en-GB/en-GB.mod_banners.ini', - '/language/en-GB/en-GB.mod_banners.sys.ini', - '/language/en-GB/en-GB.mod_breadcrumbs.ini', - '/language/en-GB/en-GB.mod_breadcrumbs.sys.ini', - '/language/en-GB/en-GB.mod_custom.ini', - '/language/en-GB/en-GB.mod_custom.sys.ini', - '/language/en-GB/en-GB.mod_feed.ini', - '/language/en-GB/en-GB.mod_feed.sys.ini', - '/language/en-GB/en-GB.mod_finder.ini', - '/language/en-GB/en-GB.mod_finder.sys.ini', - '/language/en-GB/en-GB.mod_footer.ini', - '/language/en-GB/en-GB.mod_footer.sys.ini', - '/language/en-GB/en-GB.mod_languages.ini', - '/language/en-GB/en-GB.mod_languages.sys.ini', - '/language/en-GB/en-GB.mod_login.ini', - '/language/en-GB/en-GB.mod_login.sys.ini', - '/language/en-GB/en-GB.mod_menu.ini', - '/language/en-GB/en-GB.mod_menu.sys.ini', - '/language/en-GB/en-GB.mod_random_image.ini', - '/language/en-GB/en-GB.mod_random_image.sys.ini', - '/language/en-GB/en-GB.mod_related_items.ini', - '/language/en-GB/en-GB.mod_related_items.sys.ini', - '/language/en-GB/en-GB.mod_stats.ini', - '/language/en-GB/en-GB.mod_stats.sys.ini', - '/language/en-GB/en-GB.mod_syndicate.ini', - '/language/en-GB/en-GB.mod_syndicate.sys.ini', - '/language/en-GB/en-GB.mod_tags_popular.ini', - '/language/en-GB/en-GB.mod_tags_popular.sys.ini', - '/language/en-GB/en-GB.mod_tags_similar.ini', - '/language/en-GB/en-GB.mod_tags_similar.sys.ini', - '/language/en-GB/en-GB.mod_users_latest.ini', - '/language/en-GB/en-GB.mod_users_latest.sys.ini', - '/language/en-GB/en-GB.mod_weblinks.ini', - '/language/en-GB/en-GB.mod_weblinks.sys.ini', - '/language/en-GB/en-GB.mod_whosonline.ini', - '/language/en-GB/en-GB.mod_whosonline.sys.ini', - '/language/en-GB/en-GB.mod_wrapper.ini', - '/language/en-GB/en-GB.mod_wrapper.sys.ini', - '/language/en-GB/en-GB.tpl_beez3.ini', - '/language/en-GB/en-GB.tpl_beez3.sys.ini', - '/language/en-GB/en-GB.tpl_protostar.ini', - '/language/en-GB/en-GB.tpl_protostar.sys.ini', - '/language/en-GB/en-GB.xml', - '/layouts/joomla/content/blog_style_default_links.php', - '/layouts/joomla/content/icons/email.php', - '/layouts/joomla/content/icons/print_popup.php', - '/layouts/joomla/content/icons/print_screen.php', - '/layouts/joomla/content/info_block/block.php', - '/layouts/joomla/edit/details.php', - '/layouts/joomla/edit/item_title.php', - '/layouts/joomla/form/field/radio.php', - '/layouts/joomla/html/formbehavior/ajaxchosen.php', - '/layouts/joomla/html/formbehavior/chosen.php', - '/layouts/joomla/html/sortablelist.php', - '/layouts/joomla/html/tag.php', - '/layouts/joomla/modal/body.php', - '/layouts/joomla/modal/footer.php', - '/layouts/joomla/modal/header.php', - '/layouts/joomla/modal/iframe.php', - '/layouts/joomla/modal/main.php', - '/layouts/joomla/sidebars/toggle.php', - '/layouts/joomla/tinymce/buttons.php', - '/layouts/joomla/tinymce/buttons/button.php', - '/layouts/joomla/toolbar/confirm.php', - '/layouts/joomla/toolbar/help.php', - '/layouts/joomla/toolbar/modal.php', - '/layouts/joomla/toolbar/slider.php', - '/layouts/libraries/cms/html/bootstrap/addtab.php', - '/layouts/libraries/cms/html/bootstrap/addtabscript.php', - '/layouts/libraries/cms/html/bootstrap/endtab.php', - '/layouts/libraries/cms/html/bootstrap/endtabset.php', - '/layouts/libraries/cms/html/bootstrap/starttabset.php', - '/layouts/libraries/cms/html/bootstrap/starttabsetscript.php', - '/libraries/cms/class/loader.php', - '/libraries/cms/html/access.php', - '/libraries/cms/html/actionsdropdown.php', - '/libraries/cms/html/adminlanguage.php', - '/libraries/cms/html/batch.php', - '/libraries/cms/html/behavior.php', - '/libraries/cms/html/bootstrap.php', - '/libraries/cms/html/category.php', - '/libraries/cms/html/content.php', - '/libraries/cms/html/contentlanguage.php', - '/libraries/cms/html/date.php', - '/libraries/cms/html/debug.php', - '/libraries/cms/html/dropdown.php', - '/libraries/cms/html/email.php', - '/libraries/cms/html/form.php', - '/libraries/cms/html/formbehavior.php', - '/libraries/cms/html/grid.php', - '/libraries/cms/html/icons.php', - '/libraries/cms/html/jgrid.php', - '/libraries/cms/html/jquery.php', - '/libraries/cms/html/language/en-GB/en-GB.jhtmldate.ini', - '/libraries/cms/html/links.php', - '/libraries/cms/html/list.php', - '/libraries/cms/html/menu.php', - '/libraries/cms/html/number.php', - '/libraries/cms/html/rules.php', - '/libraries/cms/html/searchtools.php', - '/libraries/cms/html/select.php', - '/libraries/cms/html/sidebar.php', - '/libraries/cms/html/sliders.php', - '/libraries/cms/html/sortablelist.php', - '/libraries/cms/html/string.php', - '/libraries/cms/html/tabs.php', - '/libraries/cms/html/tag.php', - '/libraries/cms/html/tel.php', - '/libraries/cms/html/user.php', - '/libraries/cms/less/formatter/joomla.php', - '/libraries/cms/less/less.php', - '/libraries/fof/LICENSE.txt', - '/libraries/fof/autoloader/component.php', - '/libraries/fof/autoloader/fof.php', - '/libraries/fof/config/domain/dispatcher.php', - '/libraries/fof/config/domain/interface.php', - '/libraries/fof/config/domain/tables.php', - '/libraries/fof/config/domain/views.php', - '/libraries/fof/config/provider.php', - '/libraries/fof/controller/controller.php', - '/libraries/fof/database/database.php', - '/libraries/fof/database/driver.php', - '/libraries/fof/database/driver/joomla.php', - '/libraries/fof/database/driver/mysql.php', - '/libraries/fof/database/driver/mysqli.php', - '/libraries/fof/database/driver/oracle.php', - '/libraries/fof/database/driver/pdo.php', - '/libraries/fof/database/driver/pdomysql.php', - '/libraries/fof/database/driver/postgresql.php', - '/libraries/fof/database/driver/sqlazure.php', - '/libraries/fof/database/driver/sqlite.php', - '/libraries/fof/database/driver/sqlsrv.php', - '/libraries/fof/database/factory.php', - '/libraries/fof/database/installer.php', - '/libraries/fof/database/interface.php', - '/libraries/fof/database/iterator.php', - '/libraries/fof/database/iterator/azure.php', - '/libraries/fof/database/iterator/mysql.php', - '/libraries/fof/database/iterator/mysqli.php', - '/libraries/fof/database/iterator/oracle.php', - '/libraries/fof/database/iterator/pdo.php', - '/libraries/fof/database/iterator/pdomysql.php', - '/libraries/fof/database/iterator/postgresql.php', - '/libraries/fof/database/iterator/sqlite.php', - '/libraries/fof/database/iterator/sqlsrv.php', - '/libraries/fof/database/query.php', - '/libraries/fof/database/query/element.php', - '/libraries/fof/database/query/limitable.php', - '/libraries/fof/database/query/mysql.php', - '/libraries/fof/database/query/mysqli.php', - '/libraries/fof/database/query/oracle.php', - '/libraries/fof/database/query/pdo.php', - '/libraries/fof/database/query/pdomysql.php', - '/libraries/fof/database/query/postgresql.php', - '/libraries/fof/database/query/preparable.php', - '/libraries/fof/database/query/sqlazure.php', - '/libraries/fof/database/query/sqlite.php', - '/libraries/fof/database/query/sqlsrv.php', - '/libraries/fof/dispatcher/dispatcher.php', - '/libraries/fof/download/adapter/abstract.php', - '/libraries/fof/download/adapter/cacert.pem', - '/libraries/fof/download/adapter/curl.php', - '/libraries/fof/download/adapter/fopen.php', - '/libraries/fof/download/download.php', - '/libraries/fof/download/interface.php', - '/libraries/fof/encrypt/aes.php', - '/libraries/fof/encrypt/aes/abstract.php', - '/libraries/fof/encrypt/aes/interface.php', - '/libraries/fof/encrypt/aes/mcrypt.php', - '/libraries/fof/encrypt/aes/openssl.php', - '/libraries/fof/encrypt/base32.php', - '/libraries/fof/encrypt/randval.php', - '/libraries/fof/encrypt/randvalinterface.php', - '/libraries/fof/encrypt/totp.php', - '/libraries/fof/form/field.php', - '/libraries/fof/form/field/accesslevel.php', - '/libraries/fof/form/field/actions.php', - '/libraries/fof/form/field/button.php', - '/libraries/fof/form/field/cachehandler.php', - '/libraries/fof/form/field/calendar.php', - '/libraries/fof/form/field/captcha.php', - '/libraries/fof/form/field/checkbox.php', - '/libraries/fof/form/field/checkboxes.php', - '/libraries/fof/form/field/components.php', - '/libraries/fof/form/field/editor.php', - '/libraries/fof/form/field/email.php', - '/libraries/fof/form/field/groupedbutton.php', - '/libraries/fof/form/field/groupedlist.php', - '/libraries/fof/form/field/hidden.php', - '/libraries/fof/form/field/image.php', - '/libraries/fof/form/field/imagelist.php', - '/libraries/fof/form/field/integer.php', - '/libraries/fof/form/field/language.php', - '/libraries/fof/form/field/list.php', - '/libraries/fof/form/field/media.php', - '/libraries/fof/form/field/model.php', - '/libraries/fof/form/field/ordering.php', - '/libraries/fof/form/field/password.php', - '/libraries/fof/form/field/plugins.php', - '/libraries/fof/form/field/published.php', - '/libraries/fof/form/field/radio.php', - '/libraries/fof/form/field/relation.php', - '/libraries/fof/form/field/rules.php', - '/libraries/fof/form/field/selectrow.php', - '/libraries/fof/form/field/sessionhandler.php', - '/libraries/fof/form/field/spacer.php', - '/libraries/fof/form/field/sql.php', - '/libraries/fof/form/field/tag.php', - '/libraries/fof/form/field/tel.php', - '/libraries/fof/form/field/text.php', - '/libraries/fof/form/field/textarea.php', - '/libraries/fof/form/field/timezone.php', - '/libraries/fof/form/field/title.php', - '/libraries/fof/form/field/url.php', - '/libraries/fof/form/field/user.php', - '/libraries/fof/form/field/usergroup.php', - '/libraries/fof/form/form.php', - '/libraries/fof/form/header.php', - '/libraries/fof/form/header/accesslevel.php', - '/libraries/fof/form/header/field.php', - '/libraries/fof/form/header/fielddate.php', - '/libraries/fof/form/header/fieldfilterable.php', - '/libraries/fof/form/header/fieldsearchable.php', - '/libraries/fof/form/header/fieldselectable.php', - '/libraries/fof/form/header/fieldsql.php', - '/libraries/fof/form/header/filterdate.php', - '/libraries/fof/form/header/filterfilterable.php', - '/libraries/fof/form/header/filtersearchable.php', - '/libraries/fof/form/header/filterselectable.php', - '/libraries/fof/form/header/filtersql.php', - '/libraries/fof/form/header/language.php', - '/libraries/fof/form/header/model.php', - '/libraries/fof/form/header/ordering.php', - '/libraries/fof/form/header/published.php', - '/libraries/fof/form/header/rowselect.php', - '/libraries/fof/form/helper.php', - '/libraries/fof/hal/document.php', - '/libraries/fof/hal/link.php', - '/libraries/fof/hal/links.php', - '/libraries/fof/hal/render/interface.php', - '/libraries/fof/hal/render/json.php', - '/libraries/fof/include.php', - '/libraries/fof/inflector/inflector.php', - '/libraries/fof/input/input.php', - '/libraries/fof/input/jinput/cli.php', - '/libraries/fof/input/jinput/cookie.php', - '/libraries/fof/input/jinput/files.php', - '/libraries/fof/input/jinput/input.php', - '/libraries/fof/input/jinput/json.php', - '/libraries/fof/integration/joomla/filesystem/filesystem.php', - '/libraries/fof/integration/joomla/platform.php', - '/libraries/fof/layout/file.php', - '/libraries/fof/layout/helper.php', - '/libraries/fof/less/formatter/classic.php', - '/libraries/fof/less/formatter/compressed.php', - '/libraries/fof/less/formatter/joomla.php', - '/libraries/fof/less/formatter/lessjs.php', - '/libraries/fof/less/less.php', - '/libraries/fof/less/parser/parser.php', - '/libraries/fof/model/behavior.php', - '/libraries/fof/model/behavior/access.php', - '/libraries/fof/model/behavior/emptynonzero.php', - '/libraries/fof/model/behavior/enabled.php', - '/libraries/fof/model/behavior/filters.php', - '/libraries/fof/model/behavior/language.php', - '/libraries/fof/model/behavior/private.php', - '/libraries/fof/model/dispatcher/behavior.php', - '/libraries/fof/model/field.php', - '/libraries/fof/model/field/boolean.php', - '/libraries/fof/model/field/date.php', - '/libraries/fof/model/field/number.php', - '/libraries/fof/model/field/text.php', - '/libraries/fof/model/model.php', - '/libraries/fof/platform/filesystem/filesystem.php', - '/libraries/fof/platform/filesystem/interface.php', - '/libraries/fof/platform/interface.php', - '/libraries/fof/platform/platform.php', - '/libraries/fof/query/abstract.php', - '/libraries/fof/render/abstract.php', - '/libraries/fof/render/joomla.php', - '/libraries/fof/render/joomla3.php', - '/libraries/fof/render/strapper.php', - '/libraries/fof/string/utils.php', - '/libraries/fof/table/behavior.php', - '/libraries/fof/table/behavior/assets.php', - '/libraries/fof/table/behavior/contenthistory.php', - '/libraries/fof/table/behavior/tags.php', - '/libraries/fof/table/dispatcher/behavior.php', - '/libraries/fof/table/nested.php', - '/libraries/fof/table/relations.php', - '/libraries/fof/table/table.php', - '/libraries/fof/template/utils.php', - '/libraries/fof/toolbar/toolbar.php', - '/libraries/fof/utils/array/array.php', - '/libraries/fof/utils/cache/cleaner.php', - '/libraries/fof/utils/config/helper.php', - '/libraries/fof/utils/filescheck/filescheck.php', - '/libraries/fof/utils/ini/parser.php', - '/libraries/fof/utils/installscript/installscript.php', - '/libraries/fof/utils/ip/ip.php', - '/libraries/fof/utils/object/object.php', - '/libraries/fof/utils/observable/dispatcher.php', - '/libraries/fof/utils/observable/event.php', - '/libraries/fof/utils/phpfunc/phpfunc.php', - '/libraries/fof/utils/timer/timer.php', - '/libraries/fof/utils/update/collection.php', - '/libraries/fof/utils/update/extension.php', - '/libraries/fof/utils/update/joomla.php', - '/libraries/fof/utils/update/update.php', - '/libraries/fof/version.txt', - '/libraries/fof/view/csv.php', - '/libraries/fof/view/form.php', - '/libraries/fof/view/html.php', - '/libraries/fof/view/json.php', - '/libraries/fof/view/raw.php', - '/libraries/fof/view/view.php', - '/libraries/idna_convert/LICENCE', - '/libraries/idna_convert/ReadMe.txt', - '/libraries/idna_convert/idna_convert.class.php', - '/libraries/idna_convert/transcode_wrapper.php', - '/libraries/idna_convert/uctc.php', - '/libraries/joomla/application/web/router.php', - '/libraries/joomla/application/web/router/base.php', - '/libraries/joomla/application/web/router/rest.php', - '/libraries/joomla/archive/archive.php', - '/libraries/joomla/archive/bzip2.php', - '/libraries/joomla/archive/extractable.php', - '/libraries/joomla/archive/gzip.php', - '/libraries/joomla/archive/tar.php', - '/libraries/joomla/archive/wrapper/archive.php', - '/libraries/joomla/archive/zip.php', - '/libraries/joomla/controller/base.php', - '/libraries/joomla/controller/controller.php', - '/libraries/joomla/database/database.php', - '/libraries/joomla/database/driver.php', - '/libraries/joomla/database/driver/mysql.php', - '/libraries/joomla/database/driver/mysqli.php', - '/libraries/joomla/database/driver/oracle.php', - '/libraries/joomla/database/driver/pdo.php', - '/libraries/joomla/database/driver/pdomysql.php', - '/libraries/joomla/database/driver/pgsql.php', - '/libraries/joomla/database/driver/postgresql.php', - '/libraries/joomla/database/driver/sqlazure.php', - '/libraries/joomla/database/driver/sqlite.php', - '/libraries/joomla/database/driver/sqlsrv.php', - '/libraries/joomla/database/exception/connecting.php', - '/libraries/joomla/database/exception/executing.php', - '/libraries/joomla/database/exception/unsupported.php', - '/libraries/joomla/database/exporter.php', - '/libraries/joomla/database/exporter/mysql.php', - '/libraries/joomla/database/exporter/mysqli.php', - '/libraries/joomla/database/exporter/pdomysql.php', - '/libraries/joomla/database/exporter/pgsql.php', - '/libraries/joomla/database/exporter/postgresql.php', - '/libraries/joomla/database/factory.php', - '/libraries/joomla/database/importer.php', - '/libraries/joomla/database/importer/mysql.php', - '/libraries/joomla/database/importer/mysqli.php', - '/libraries/joomla/database/importer/pdomysql.php', - '/libraries/joomla/database/importer/pgsql.php', - '/libraries/joomla/database/importer/postgresql.php', - '/libraries/joomla/database/interface.php', - '/libraries/joomla/database/iterator.php', - '/libraries/joomla/database/iterator/mysql.php', - '/libraries/joomla/database/iterator/mysqli.php', - '/libraries/joomla/database/iterator/oracle.php', - '/libraries/joomla/database/iterator/pdo.php', - '/libraries/joomla/database/iterator/pdomysql.php', - '/libraries/joomla/database/iterator/pgsql.php', - '/libraries/joomla/database/iterator/postgresql.php', - '/libraries/joomla/database/iterator/sqlazure.php', - '/libraries/joomla/database/iterator/sqlite.php', - '/libraries/joomla/database/iterator/sqlsrv.php', - '/libraries/joomla/database/query.php', - '/libraries/joomla/database/query/element.php', - '/libraries/joomla/database/query/limitable.php', - '/libraries/joomla/database/query/mysql.php', - '/libraries/joomla/database/query/mysqli.php', - '/libraries/joomla/database/query/oracle.php', - '/libraries/joomla/database/query/pdo.php', - '/libraries/joomla/database/query/pdomysql.php', - '/libraries/joomla/database/query/pgsql.php', - '/libraries/joomla/database/query/postgresql.php', - '/libraries/joomla/database/query/preparable.php', - '/libraries/joomla/database/query/sqlazure.php', - '/libraries/joomla/database/query/sqlite.php', - '/libraries/joomla/database/query/sqlsrv.php', - '/libraries/joomla/event/dispatcher.php', - '/libraries/joomla/event/event.php', - '/libraries/joomla/facebook/album.php', - '/libraries/joomla/facebook/checkin.php', - '/libraries/joomla/facebook/comment.php', - '/libraries/joomla/facebook/event.php', - '/libraries/joomla/facebook/facebook.php', - '/libraries/joomla/facebook/group.php', - '/libraries/joomla/facebook/link.php', - '/libraries/joomla/facebook/note.php', - '/libraries/joomla/facebook/oauth.php', - '/libraries/joomla/facebook/object.php', - '/libraries/joomla/facebook/photo.php', - '/libraries/joomla/facebook/post.php', - '/libraries/joomla/facebook/status.php', - '/libraries/joomla/facebook/user.php', - '/libraries/joomla/facebook/video.php', - '/libraries/joomla/form/fields/accesslevel.php', - '/libraries/joomla/form/fields/aliastag.php', - '/libraries/joomla/form/fields/cachehandler.php', - '/libraries/joomla/form/fields/calendar.php', - '/libraries/joomla/form/fields/checkbox.php', - '/libraries/joomla/form/fields/checkboxes.php', - '/libraries/joomla/form/fields/color.php', - '/libraries/joomla/form/fields/combo.php', - '/libraries/joomla/form/fields/components.php', - '/libraries/joomla/form/fields/databaseconnection.php', - '/libraries/joomla/form/fields/email.php', - '/libraries/joomla/form/fields/file.php', - '/libraries/joomla/form/fields/filelist.php', - '/libraries/joomla/form/fields/folderlist.php', - '/libraries/joomla/form/fields/groupedlist.php', - '/libraries/joomla/form/fields/hidden.php', - '/libraries/joomla/form/fields/imagelist.php', - '/libraries/joomla/form/fields/integer.php', - '/libraries/joomla/form/fields/language.php', - '/libraries/joomla/form/fields/list.php', - '/libraries/joomla/form/fields/meter.php', - '/libraries/joomla/form/fields/note.php', - '/libraries/joomla/form/fields/number.php', - '/libraries/joomla/form/fields/password.php', - '/libraries/joomla/form/fields/plugins.php', - '/libraries/joomla/form/fields/predefinedlist.php', - '/libraries/joomla/form/fields/radio.php', - '/libraries/joomla/form/fields/range.php', - '/libraries/joomla/form/fields/repeatable.php', - '/libraries/joomla/form/fields/rules.php', - '/libraries/joomla/form/fields/sessionhandler.php', - '/libraries/joomla/form/fields/spacer.php', - '/libraries/joomla/form/fields/sql.php', - '/libraries/joomla/form/fields/subform.php', - '/libraries/joomla/form/fields/tel.php', - '/libraries/joomla/form/fields/text.php', - '/libraries/joomla/form/fields/textarea.php', - '/libraries/joomla/form/fields/timezone.php', - '/libraries/joomla/form/fields/url.php', - '/libraries/joomla/form/fields/usergroup.php', - '/libraries/joomla/github/account.php', - '/libraries/joomla/github/commits.php', - '/libraries/joomla/github/forks.php', - '/libraries/joomla/github/github.php', - '/libraries/joomla/github/hooks.php', - '/libraries/joomla/github/http.php', - '/libraries/joomla/github/meta.php', - '/libraries/joomla/github/milestones.php', - '/libraries/joomla/github/object.php', - '/libraries/joomla/github/package.php', - '/libraries/joomla/github/package/activity.php', - '/libraries/joomla/github/package/activity/events.php', - '/libraries/joomla/github/package/activity/notifications.php', - '/libraries/joomla/github/package/activity/starring.php', - '/libraries/joomla/github/package/activity/watching.php', - '/libraries/joomla/github/package/authorization.php', - '/libraries/joomla/github/package/data.php', - '/libraries/joomla/github/package/data/blobs.php', - '/libraries/joomla/github/package/data/commits.php', - '/libraries/joomla/github/package/data/refs.php', - '/libraries/joomla/github/package/data/tags.php', - '/libraries/joomla/github/package/data/trees.php', - '/libraries/joomla/github/package/gists.php', - '/libraries/joomla/github/package/gists/comments.php', - '/libraries/joomla/github/package/gitignore.php', - '/libraries/joomla/github/package/issues.php', - '/libraries/joomla/github/package/issues/assignees.php', - '/libraries/joomla/github/package/issues/comments.php', - '/libraries/joomla/github/package/issues/events.php', - '/libraries/joomla/github/package/issues/labels.php', - '/libraries/joomla/github/package/issues/milestones.php', - '/libraries/joomla/github/package/markdown.php', - '/libraries/joomla/github/package/orgs.php', - '/libraries/joomla/github/package/orgs/members.php', - '/libraries/joomla/github/package/orgs/teams.php', - '/libraries/joomla/github/package/pulls.php', - '/libraries/joomla/github/package/pulls/comments.php', - '/libraries/joomla/github/package/repositories.php', - '/libraries/joomla/github/package/repositories/collaborators.php', - '/libraries/joomla/github/package/repositories/comments.php', - '/libraries/joomla/github/package/repositories/commits.php', - '/libraries/joomla/github/package/repositories/contents.php', - '/libraries/joomla/github/package/repositories/downloads.php', - '/libraries/joomla/github/package/repositories/forks.php', - '/libraries/joomla/github/package/repositories/hooks.php', - '/libraries/joomla/github/package/repositories/keys.php', - '/libraries/joomla/github/package/repositories/merging.php', - '/libraries/joomla/github/package/repositories/statistics.php', - '/libraries/joomla/github/package/repositories/statuses.php', - '/libraries/joomla/github/package/search.php', - '/libraries/joomla/github/package/users.php', - '/libraries/joomla/github/package/users/emails.php', - '/libraries/joomla/github/package/users/followers.php', - '/libraries/joomla/github/package/users/keys.php', - '/libraries/joomla/github/refs.php', - '/libraries/joomla/github/statuses.php', - '/libraries/joomla/google/auth.php', - '/libraries/joomla/google/auth/oauth2.php', - '/libraries/joomla/google/data.php', - '/libraries/joomla/google/data/adsense.php', - '/libraries/joomla/google/data/calendar.php', - '/libraries/joomla/google/data/picasa.php', - '/libraries/joomla/google/data/picasa/album.php', - '/libraries/joomla/google/data/picasa/photo.php', - '/libraries/joomla/google/data/plus.php', - '/libraries/joomla/google/data/plus/activities.php', - '/libraries/joomla/google/data/plus/comments.php', - '/libraries/joomla/google/data/plus/people.php', - '/libraries/joomla/google/embed.php', - '/libraries/joomla/google/embed/analytics.php', - '/libraries/joomla/google/embed/maps.php', - '/libraries/joomla/google/google.php', - '/libraries/joomla/grid/grid.php', - '/libraries/joomla/keychain/keychain.php', - '/libraries/joomla/linkedin/communications.php', - '/libraries/joomla/linkedin/companies.php', - '/libraries/joomla/linkedin/groups.php', - '/libraries/joomla/linkedin/jobs.php', - '/libraries/joomla/linkedin/linkedin.php', - '/libraries/joomla/linkedin/oauth.php', - '/libraries/joomla/linkedin/object.php', - '/libraries/joomla/linkedin/people.php', - '/libraries/joomla/linkedin/stream.php', - '/libraries/joomla/mediawiki/categories.php', - '/libraries/joomla/mediawiki/http.php', - '/libraries/joomla/mediawiki/images.php', - '/libraries/joomla/mediawiki/links.php', - '/libraries/joomla/mediawiki/mediawiki.php', - '/libraries/joomla/mediawiki/object.php', - '/libraries/joomla/mediawiki/pages.php', - '/libraries/joomla/mediawiki/search.php', - '/libraries/joomla/mediawiki/sites.php', - '/libraries/joomla/mediawiki/users.php', - '/libraries/joomla/model/base.php', - '/libraries/joomla/model/database.php', - '/libraries/joomla/model/model.php', - '/libraries/joomla/oauth1/client.php', - '/libraries/joomla/oauth2/client.php', - '/libraries/joomla/observable/interface.php', - '/libraries/joomla/observer/interface.php', - '/libraries/joomla/observer/mapper.php', - '/libraries/joomla/observer/updater.php', - '/libraries/joomla/observer/updater/interface.php', - '/libraries/joomla/observer/wrapper/mapper.php', - '/libraries/joomla/openstreetmap/changesets.php', - '/libraries/joomla/openstreetmap/elements.php', - '/libraries/joomla/openstreetmap/gps.php', - '/libraries/joomla/openstreetmap/info.php', - '/libraries/joomla/openstreetmap/oauth.php', - '/libraries/joomla/openstreetmap/object.php', - '/libraries/joomla/openstreetmap/openstreetmap.php', - '/libraries/joomla/openstreetmap/user.php', - '/libraries/joomla/platform.php', - '/libraries/joomla/route/wrapper/route.php', - '/libraries/joomla/session/handler/interface.php', - '/libraries/joomla/session/handler/joomla.php', - '/libraries/joomla/session/handler/native.php', - '/libraries/joomla/session/storage.php', - '/libraries/joomla/session/storage/apc.php', - '/libraries/joomla/session/storage/apcu.php', - '/libraries/joomla/session/storage/database.php', - '/libraries/joomla/session/storage/memcache.php', - '/libraries/joomla/session/storage/memcached.php', - '/libraries/joomla/session/storage/none.php', - '/libraries/joomla/session/storage/redis.php', - '/libraries/joomla/session/storage/wincache.php', - '/libraries/joomla/session/storage/xcache.php', - '/libraries/joomla/string/string.php', - '/libraries/joomla/string/wrapper/normalise.php', - '/libraries/joomla/string/wrapper/punycode.php', - '/libraries/joomla/twitter/block.php', - '/libraries/joomla/twitter/directmessages.php', - '/libraries/joomla/twitter/favorites.php', - '/libraries/joomla/twitter/friends.php', - '/libraries/joomla/twitter/help.php', - '/libraries/joomla/twitter/lists.php', - '/libraries/joomla/twitter/oauth.php', - '/libraries/joomla/twitter/object.php', - '/libraries/joomla/twitter/places.php', - '/libraries/joomla/twitter/profile.php', - '/libraries/joomla/twitter/search.php', - '/libraries/joomla/twitter/statuses.php', - '/libraries/joomla/twitter/trends.php', - '/libraries/joomla/twitter/twitter.php', - '/libraries/joomla/twitter/users.php', - '/libraries/joomla/utilities/arrayhelper.php', - '/libraries/joomla/view/base.php', - '/libraries/joomla/view/html.php', - '/libraries/joomla/view/view.php', - '/libraries/legacy/application/application.php', - '/libraries/legacy/base/node.php', - '/libraries/legacy/base/observable.php', - '/libraries/legacy/base/observer.php', - '/libraries/legacy/base/tree.php', - '/libraries/legacy/database/exception.php', - '/libraries/legacy/database/mysql.php', - '/libraries/legacy/database/mysqli.php', - '/libraries/legacy/database/sqlazure.php', - '/libraries/legacy/database/sqlsrv.php', - '/libraries/legacy/dispatcher/dispatcher.php', - '/libraries/legacy/error/error.php', - '/libraries/legacy/exception/exception.php', - '/libraries/legacy/form/field/category.php', - '/libraries/legacy/form/field/componentlayout.php', - '/libraries/legacy/form/field/modulelayout.php', - '/libraries/legacy/log/logexception.php', - '/libraries/legacy/request/request.php', - '/libraries/legacy/response/response.php', - '/libraries/legacy/simplecrypt/simplecrypt.php', - '/libraries/legacy/simplepie/factory.php', - '/libraries/legacy/table/session.php', - '/libraries/legacy/utilities/xmlelement.php', - '/libraries/phputf8/LICENSE', - '/libraries/phputf8/README', - '/libraries/phputf8/mbstring/core.php', - '/libraries/phputf8/native/core.php', - '/libraries/phputf8/ord.php', - '/libraries/phputf8/str_ireplace.php', - '/libraries/phputf8/str_pad.php', - '/libraries/phputf8/str_split.php', - '/libraries/phputf8/strcasecmp.php', - '/libraries/phputf8/strcspn.php', - '/libraries/phputf8/stristr.php', - '/libraries/phputf8/strrev.php', - '/libraries/phputf8/strspn.php', - '/libraries/phputf8/substr_replace.php', - '/libraries/phputf8/trim.php', - '/libraries/phputf8/ucfirst.php', - '/libraries/phputf8/ucwords.php', - '/libraries/phputf8/utf8.php', - '/libraries/phputf8/utils/ascii.php', - '/libraries/phputf8/utils/bad.php', - '/libraries/phputf8/utils/patterns.php', - '/libraries/phputf8/utils/position.php', - '/libraries/phputf8/utils/specials.php', - '/libraries/phputf8/utils/unicode.php', - '/libraries/phputf8/utils/validation.php', - '/libraries/src/Access/Wrapper/Access.php', - '/libraries/src/Cache/Storage/ApcStorage.php', - '/libraries/src/Cache/Storage/CacheliteStorage.php', - '/libraries/src/Cache/Storage/MemcacheStorage.php', - '/libraries/src/Cache/Storage/XcacheStorage.php', - '/libraries/src/Client/ClientWrapper.php', - '/libraries/src/Crypt/Cipher/BlowfishCipher.php', - '/libraries/src/Crypt/Cipher/McryptCipher.php', - '/libraries/src/Crypt/Cipher/Rijndael256Cipher.php', - '/libraries/src/Crypt/Cipher/SimpleCipher.php', - '/libraries/src/Crypt/Cipher/TripleDesCipher.php', - '/libraries/src/Crypt/CipherInterface.php', - '/libraries/src/Crypt/CryptPassword.php', - '/libraries/src/Crypt/Key.php', - '/libraries/src/Crypt/Password/SimpleCryptPassword.php', - '/libraries/src/Crypt/README.md', - '/libraries/src/Filesystem/Wrapper/FileWrapper.php', - '/libraries/src/Filesystem/Wrapper/FolderWrapper.php', - '/libraries/src/Filesystem/Wrapper/PathWrapper.php', - '/libraries/src/Filter/Wrapper/OutputFilterWrapper.php', - '/libraries/src/Form/Field/HelpsiteField.php', - '/libraries/src/Form/FormWrapper.php', - '/libraries/src/Helper/ContentHistoryHelper.php', - '/libraries/src/Helper/SearchHelper.php', - '/libraries/src/Http/Transport/cacert.pem', - '/libraries/src/Http/Wrapper/FactoryWrapper.php', - '/libraries/src/Language/LanguageStemmer.php', - '/libraries/src/Language/Stemmer/Porteren.php', - '/libraries/src/Language/Wrapper/JTextWrapper.php', - '/libraries/src/Language/Wrapper/LanguageHelperWrapper.php', - '/libraries/src/Language/Wrapper/TransliterateWrapper.php', - '/libraries/src/Mail/MailWrapper.php', - '/libraries/src/Menu/MenuHelper.php', - '/libraries/src/Menu/Node.php', - '/libraries/src/Menu/Node/Component.php', - '/libraries/src/Menu/Node/Container.php', - '/libraries/src/Menu/Node/Heading.php', - '/libraries/src/Menu/Node/Separator.php', - '/libraries/src/Menu/Node/Url.php', - '/libraries/src/Menu/Tree.php', - '/libraries/src/Table/Observer/AbstractObserver.php', - '/libraries/src/Table/Observer/ContentHistory.php', - '/libraries/src/Table/Observer/Tags.php', - '/libraries/src/Toolbar/Button/SliderButton.php', - '/libraries/src/User/UserWrapper.php', - '/libraries/vendor/.htaccess', - '/libraries/vendor/brumann/polyfill-unserialize/LICENSE', - '/libraries/vendor/brumann/polyfill-unserialize/composer.json', - '/libraries/vendor/brumann/polyfill-unserialize/src/DisallowedClassesSubstitutor.php', - '/libraries/vendor/brumann/polyfill-unserialize/src/Unserialize.php', - '/libraries/vendor/ircmaxell/password-compat/LICENSE.md', - '/libraries/vendor/ircmaxell/password-compat/lib/password.php', - '/libraries/vendor/joomla/application/src/AbstractCliApplication.php', - '/libraries/vendor/joomla/application/src/AbstractDaemonApplication.php', - '/libraries/vendor/joomla/application/src/Cli/CliInput.php', - '/libraries/vendor/joomla/application/src/Cli/CliOutput.php', - '/libraries/vendor/joomla/application/src/Cli/ColorProcessor.php', - '/libraries/vendor/joomla/application/src/Cli/ColorStyle.php', - '/libraries/vendor/joomla/application/src/Cli/Output/Processor/ColorProcessor.php', - '/libraries/vendor/joomla/application/src/Cli/Output/Processor/ProcessorInterface.php', - '/libraries/vendor/joomla/application/src/Cli/Output/Stdout.php', - '/libraries/vendor/joomla/application/src/Cli/Output/Xml.php', - '/libraries/vendor/joomla/compat/LICENSE', - '/libraries/vendor/joomla/compat/src/CallbackFilterIterator.php', - '/libraries/vendor/joomla/compat/src/JsonSerializable.php', - '/libraries/vendor/joomla/event/src/DelegatingDispatcher.php', - '/libraries/vendor/joomla/filesystem/src/Stream/String.php', - '/libraries/vendor/joomla/image/LICENSE', - '/libraries/vendor/joomla/image/src/Filter/Backgroundfill.php', - '/libraries/vendor/joomla/image/src/Filter/Brightness.php', - '/libraries/vendor/joomla/image/src/Filter/Contrast.php', - '/libraries/vendor/joomla/image/src/Filter/Edgedetect.php', - '/libraries/vendor/joomla/image/src/Filter/Emboss.php', - '/libraries/vendor/joomla/image/src/Filter/Grayscale.php', - '/libraries/vendor/joomla/image/src/Filter/Negate.php', - '/libraries/vendor/joomla/image/src/Filter/Sketchy.php', - '/libraries/vendor/joomla/image/src/Filter/Smooth.php', - '/libraries/vendor/joomla/image/src/Image.php', - '/libraries/vendor/joomla/image/src/ImageFilter.php', - '/libraries/vendor/joomla/input/src/Cli.php', - '/libraries/vendor/joomla/registry/src/AbstractRegistryFormat.php', - '/libraries/vendor/joomla/session/Joomla/Session/LICENSE', - '/libraries/vendor/joomla/session/Joomla/Session/Session.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Apc.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Apcu.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Database.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Memcache.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Memcached.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/None.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Wincache.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Xcache.php', - '/libraries/vendor/joomla/string/src/String.php', - '/libraries/vendor/leafo/lessphp/LICENSE', - '/libraries/vendor/leafo/lessphp/lessc.inc.php', - '/libraries/vendor/leafo/lessphp/lessify', - '/libraries/vendor/leafo/lessphp/lessify.inc.php', - '/libraries/vendor/leafo/lessphp/plessc', - '/libraries/vendor/paragonie/random_compat/LICENSE', - '/libraries/vendor/paragonie/random_compat/lib/byte_safe_strings.php', - '/libraries/vendor/paragonie/random_compat/lib/cast_to_int.php', - '/libraries/vendor/paragonie/random_compat/lib/error_polyfill.php', - '/libraries/vendor/paragonie/random_compat/lib/random.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_com_dotnet.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_dev_urandom.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_libsodium.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_libsodium_legacy.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_mcrypt.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_openssl.php', - '/libraries/vendor/paragonie/random_compat/lib/random_int.php', - '/libraries/vendor/paragonie/sodium_compat/src/Core32/Curve25519/README.md', - '/libraries/vendor/phpmailer/phpmailer/PHPMailerAutoload.php', - '/libraries/vendor/phpmailer/phpmailer/class.phpmailer.php', - '/libraries/vendor/phpmailer/phpmailer/class.phpmaileroauth.php', - '/libraries/vendor/phpmailer/phpmailer/class.phpmaileroauthgoogle.php', - '/libraries/vendor/phpmailer/phpmailer/class.pop3.php', - '/libraries/vendor/phpmailer/phpmailer/class.smtp.php', - '/libraries/vendor/phpmailer/phpmailer/extras/EasyPeasyICS.php', - '/libraries/vendor/phpmailer/phpmailer/extras/htmlfilter.php', - '/libraries/vendor/phpmailer/phpmailer/extras/ntlm_sasl_client.php', - '/libraries/vendor/simplepie/simplepie/LICENSE.txt', - '/libraries/vendor/simplepie/simplepie/autoloader.php', - '/libraries/vendor/simplepie/simplepie/db.sql', - '/libraries/vendor/simplepie/simplepie/idn/LICENCE', - '/libraries/vendor/simplepie/simplepie/idn/idna_convert.class.php', - '/libraries/vendor/simplepie/simplepie/idn/npdata.ser', - '/libraries/vendor/simplepie/simplepie/library/SimplePie.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Author.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/Base.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/DB.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/File.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/Memcache.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/MySQL.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Caption.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Category.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content/Type/Sniffer.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Copyright.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Core.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Credit.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML/Entities.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Enclosure.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Exception.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/File.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/HTTP/Parser.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/IRI.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Item.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Locator.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Misc.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Net/IPv6.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parse/Date.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parser.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Rating.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Registry.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Restriction.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Sanitize.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Source.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration/Parser.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/gzdecode.php', - '/libraries/vendor/symfony/polyfill-php55/LICENSE', - '/libraries/vendor/symfony/polyfill-php55/Php55.php', - '/libraries/vendor/symfony/polyfill-php55/Php55ArrayColumn.php', - '/libraries/vendor/symfony/polyfill-php55/bootstrap.php', - '/libraries/vendor/symfony/polyfill-php56/LICENSE', - '/libraries/vendor/symfony/polyfill-php56/Php56.php', - '/libraries/vendor/symfony/polyfill-php56/bootstrap.php', - '/libraries/vendor/symfony/polyfill-php71/LICENSE', - '/libraries/vendor/symfony/polyfill-php71/Php71.php', - '/libraries/vendor/symfony/polyfill-php71/bootstrap.php', - '/libraries/vendor/symfony/polyfill-util/Binary.php', - '/libraries/vendor/symfony/polyfill-util/BinaryNoFuncOverload.php', - '/libraries/vendor/symfony/polyfill-util/BinaryOnFuncOverload.php', - '/libraries/vendor/symfony/polyfill-util/LICENSE', - '/libraries/vendor/typo3/phar-stream-wrapper/composer.json', - '/libraries/vendor/web.config', - '/media/cms/css/debug.css', - '/media/com_associations/js/sidebyside-uncompressed.js', - '/media/com_contenthistory/css/jquery.pretty-text-diff.css', - '/media/com_contenthistory/js/diff_match_patch.js', - '/media/com_contenthistory/js/jquery.pretty-text-diff.js', - '/media/com_contenthistory/js/jquery.pretty-text-diff.min.js', - '/media/com_finder/js/autocompleter.js', - '/media/com_joomlaupdate/js/encryption.js', - '/media/com_joomlaupdate/js/encryption.min.js', - '/media/com_joomlaupdate/js/json2.js', - '/media/com_joomlaupdate/js/json2.min.js', - '/media/com_joomlaupdate/js/update.js', - '/media/com_joomlaupdate/js/update.min.js', - '/media/contacts/images/con_address.png', - '/media/contacts/images/con_fax.png', - '/media/contacts/images/con_info.png', - '/media/contacts/images/con_mobile.png', - '/media/contacts/images/con_tel.png', - '/media/contacts/images/emailButton.png', - '/media/editors/codemirror/LICENSE', - '/media/editors/codemirror/addon/comment/comment.js', - '/media/editors/codemirror/addon/comment/comment.min.js', - '/media/editors/codemirror/addon/comment/continuecomment.js', - '/media/editors/codemirror/addon/comment/continuecomment.min.js', - '/media/editors/codemirror/addon/dialog/dialog.css', - '/media/editors/codemirror/addon/dialog/dialog.js', - '/media/editors/codemirror/addon/dialog/dialog.min.css', - '/media/editors/codemirror/addon/dialog/dialog.min.js', - '/media/editors/codemirror/addon/display/autorefresh.js', - '/media/editors/codemirror/addon/display/autorefresh.min.js', - '/media/editors/codemirror/addon/display/fullscreen.css', - '/media/editors/codemirror/addon/display/fullscreen.js', - '/media/editors/codemirror/addon/display/fullscreen.min.css', - '/media/editors/codemirror/addon/display/fullscreen.min.js', - '/media/editors/codemirror/addon/display/panel.js', - '/media/editors/codemirror/addon/display/panel.min.js', - '/media/editors/codemirror/addon/display/placeholder.js', - '/media/editors/codemirror/addon/display/placeholder.min.js', - '/media/editors/codemirror/addon/display/rulers.js', - '/media/editors/codemirror/addon/display/rulers.min.js', - '/media/editors/codemirror/addon/edit/closebrackets.js', - '/media/editors/codemirror/addon/edit/closebrackets.min.js', - '/media/editors/codemirror/addon/edit/closetag.js', - '/media/editors/codemirror/addon/edit/closetag.min.js', - '/media/editors/codemirror/addon/edit/continuelist.js', - '/media/editors/codemirror/addon/edit/continuelist.min.js', - '/media/editors/codemirror/addon/edit/matchbrackets.js', - '/media/editors/codemirror/addon/edit/matchbrackets.min.js', - '/media/editors/codemirror/addon/edit/matchtags.js', - '/media/editors/codemirror/addon/edit/matchtags.min.js', - '/media/editors/codemirror/addon/edit/trailingspace.js', - '/media/editors/codemirror/addon/edit/trailingspace.min.js', - '/media/editors/codemirror/addon/fold/brace-fold.js', - '/media/editors/codemirror/addon/fold/brace-fold.min.js', - '/media/editors/codemirror/addon/fold/comment-fold.js', - '/media/editors/codemirror/addon/fold/comment-fold.min.js', - '/media/editors/codemirror/addon/fold/foldcode.js', - '/media/editors/codemirror/addon/fold/foldcode.min.js', - '/media/editors/codemirror/addon/fold/foldgutter.css', - '/media/editors/codemirror/addon/fold/foldgutter.js', - '/media/editors/codemirror/addon/fold/foldgutter.min.css', - '/media/editors/codemirror/addon/fold/foldgutter.min.js', - '/media/editors/codemirror/addon/fold/indent-fold.js', - '/media/editors/codemirror/addon/fold/indent-fold.min.js', - '/media/editors/codemirror/addon/fold/markdown-fold.js', - '/media/editors/codemirror/addon/fold/markdown-fold.min.js', - '/media/editors/codemirror/addon/fold/xml-fold.js', - '/media/editors/codemirror/addon/fold/xml-fold.min.js', - '/media/editors/codemirror/addon/hint/anyword-hint.js', - '/media/editors/codemirror/addon/hint/anyword-hint.min.js', - '/media/editors/codemirror/addon/hint/css-hint.js', - '/media/editors/codemirror/addon/hint/css-hint.min.js', - '/media/editors/codemirror/addon/hint/html-hint.js', - '/media/editors/codemirror/addon/hint/html-hint.min.js', - '/media/editors/codemirror/addon/hint/javascript-hint.js', - '/media/editors/codemirror/addon/hint/javascript-hint.min.js', - '/media/editors/codemirror/addon/hint/show-hint.css', - '/media/editors/codemirror/addon/hint/show-hint.js', - '/media/editors/codemirror/addon/hint/show-hint.min.css', - '/media/editors/codemirror/addon/hint/show-hint.min.js', - '/media/editors/codemirror/addon/hint/sql-hint.js', - '/media/editors/codemirror/addon/hint/sql-hint.min.js', - '/media/editors/codemirror/addon/hint/xml-hint.js', - '/media/editors/codemirror/addon/hint/xml-hint.min.js', - '/media/editors/codemirror/addon/lint/coffeescript-lint.js', - '/media/editors/codemirror/addon/lint/coffeescript-lint.min.js', - '/media/editors/codemirror/addon/lint/css-lint.js', - '/media/editors/codemirror/addon/lint/css-lint.min.js', - '/media/editors/codemirror/addon/lint/html-lint.js', - '/media/editors/codemirror/addon/lint/html-lint.min.js', - '/media/editors/codemirror/addon/lint/javascript-lint.js', - '/media/editors/codemirror/addon/lint/javascript-lint.min.js', - '/media/editors/codemirror/addon/lint/json-lint.js', - '/media/editors/codemirror/addon/lint/json-lint.min.js', - '/media/editors/codemirror/addon/lint/lint.css', - '/media/editors/codemirror/addon/lint/lint.js', - '/media/editors/codemirror/addon/lint/lint.min.css', - '/media/editors/codemirror/addon/lint/lint.min.js', - '/media/editors/codemirror/addon/lint/yaml-lint.js', - '/media/editors/codemirror/addon/lint/yaml-lint.min.js', - '/media/editors/codemirror/addon/merge/merge.css', - '/media/editors/codemirror/addon/merge/merge.js', - '/media/editors/codemirror/addon/merge/merge.min.css', - '/media/editors/codemirror/addon/merge/merge.min.js', - '/media/editors/codemirror/addon/mode/loadmode.js', - '/media/editors/codemirror/addon/mode/loadmode.min.js', - '/media/editors/codemirror/addon/mode/multiplex.js', - '/media/editors/codemirror/addon/mode/multiplex.min.js', - '/media/editors/codemirror/addon/mode/multiplex_test.js', - '/media/editors/codemirror/addon/mode/multiplex_test.min.js', - '/media/editors/codemirror/addon/mode/overlay.js', - '/media/editors/codemirror/addon/mode/overlay.min.js', - '/media/editors/codemirror/addon/mode/simple.js', - '/media/editors/codemirror/addon/mode/simple.min.js', - '/media/editors/codemirror/addon/runmode/colorize.js', - '/media/editors/codemirror/addon/runmode/colorize.min.js', - '/media/editors/codemirror/addon/runmode/runmode-standalone.js', - '/media/editors/codemirror/addon/runmode/runmode-standalone.min.js', - '/media/editors/codemirror/addon/runmode/runmode.js', - '/media/editors/codemirror/addon/runmode/runmode.min.js', - '/media/editors/codemirror/addon/runmode/runmode.node.js', - '/media/editors/codemirror/addon/scroll/annotatescrollbar.js', - '/media/editors/codemirror/addon/scroll/annotatescrollbar.min.js', - '/media/editors/codemirror/addon/scroll/scrollpastend.js', - '/media/editors/codemirror/addon/scroll/scrollpastend.min.js', - '/media/editors/codemirror/addon/scroll/simplescrollbars.css', - '/media/editors/codemirror/addon/scroll/simplescrollbars.js', - '/media/editors/codemirror/addon/scroll/simplescrollbars.min.css', - '/media/editors/codemirror/addon/scroll/simplescrollbars.min.js', - '/media/editors/codemirror/addon/search/jump-to-line.js', - '/media/editors/codemirror/addon/search/jump-to-line.min.js', - '/media/editors/codemirror/addon/search/match-highlighter.js', - '/media/editors/codemirror/addon/search/match-highlighter.min.js', - '/media/editors/codemirror/addon/search/matchesonscrollbar.css', - '/media/editors/codemirror/addon/search/matchesonscrollbar.js', - '/media/editors/codemirror/addon/search/matchesonscrollbar.min.css', - '/media/editors/codemirror/addon/search/matchesonscrollbar.min.js', - '/media/editors/codemirror/addon/search/search.js', - '/media/editors/codemirror/addon/search/search.min.js', - '/media/editors/codemirror/addon/search/searchcursor.js', - '/media/editors/codemirror/addon/search/searchcursor.min.js', - '/media/editors/codemirror/addon/selection/active-line.js', - '/media/editors/codemirror/addon/selection/active-line.min.js', - '/media/editors/codemirror/addon/selection/mark-selection.js', - '/media/editors/codemirror/addon/selection/mark-selection.min.js', - '/media/editors/codemirror/addon/selection/selection-pointer.js', - '/media/editors/codemirror/addon/selection/selection-pointer.min.js', - '/media/editors/codemirror/addon/tern/tern.css', - '/media/editors/codemirror/addon/tern/tern.js', - '/media/editors/codemirror/addon/tern/tern.min.css', - '/media/editors/codemirror/addon/tern/tern.min.js', - '/media/editors/codemirror/addon/tern/worker.js', - '/media/editors/codemirror/addon/tern/worker.min.js', - '/media/editors/codemirror/addon/wrap/hardwrap.js', - '/media/editors/codemirror/addon/wrap/hardwrap.min.js', - '/media/editors/codemirror/keymap/emacs.js', - '/media/editors/codemirror/keymap/emacs.min.js', - '/media/editors/codemirror/keymap/sublime.js', - '/media/editors/codemirror/keymap/sublime.min.js', - '/media/editors/codemirror/keymap/vim.js', - '/media/editors/codemirror/keymap/vim.min.js', - '/media/editors/codemirror/lib/addons.css', - '/media/editors/codemirror/lib/addons.js', - '/media/editors/codemirror/lib/addons.min.css', - '/media/editors/codemirror/lib/addons.min.js', - '/media/editors/codemirror/lib/codemirror.css', - '/media/editors/codemirror/lib/codemirror.js', - '/media/editors/codemirror/lib/codemirror.min.css', - '/media/editors/codemirror/lib/codemirror.min.js', - '/media/editors/codemirror/mode/apl/apl.js', - '/media/editors/codemirror/mode/apl/apl.min.js', - '/media/editors/codemirror/mode/asciiarmor/asciiarmor.js', - '/media/editors/codemirror/mode/asciiarmor/asciiarmor.min.js', - '/media/editors/codemirror/mode/asn.1/asn.1.js', - '/media/editors/codemirror/mode/asn.1/asn.min.js', - '/media/editors/codemirror/mode/asterisk/asterisk.js', - '/media/editors/codemirror/mode/asterisk/asterisk.min.js', - '/media/editors/codemirror/mode/brainfuck/brainfuck.js', - '/media/editors/codemirror/mode/brainfuck/brainfuck.min.js', - '/media/editors/codemirror/mode/clike/clike.js', - '/media/editors/codemirror/mode/clike/clike.min.js', - '/media/editors/codemirror/mode/clojure/clojure.js', - '/media/editors/codemirror/mode/clojure/clojure.min.js', - '/media/editors/codemirror/mode/cmake/cmake.js', - '/media/editors/codemirror/mode/cmake/cmake.min.js', - '/media/editors/codemirror/mode/cobol/cobol.js', - '/media/editors/codemirror/mode/cobol/cobol.min.js', - '/media/editors/codemirror/mode/coffeescript/coffeescript.js', - '/media/editors/codemirror/mode/coffeescript/coffeescript.min.js', - '/media/editors/codemirror/mode/commonlisp/commonlisp.js', - '/media/editors/codemirror/mode/commonlisp/commonlisp.min.js', - '/media/editors/codemirror/mode/crystal/crystal.js', - '/media/editors/codemirror/mode/crystal/crystal.min.js', - '/media/editors/codemirror/mode/css/css.js', - '/media/editors/codemirror/mode/css/css.min.js', - '/media/editors/codemirror/mode/cypher/cypher.js', - '/media/editors/codemirror/mode/cypher/cypher.min.js', - '/media/editors/codemirror/mode/d/d.js', - '/media/editors/codemirror/mode/d/d.min.js', - '/media/editors/codemirror/mode/dart/dart.js', - '/media/editors/codemirror/mode/dart/dart.min.js', - '/media/editors/codemirror/mode/diff/diff.js', - '/media/editors/codemirror/mode/diff/diff.min.js', - '/media/editors/codemirror/mode/django/django.js', - '/media/editors/codemirror/mode/django/django.min.js', - '/media/editors/codemirror/mode/dockerfile/dockerfile.js', - '/media/editors/codemirror/mode/dockerfile/dockerfile.min.js', - '/media/editors/codemirror/mode/dtd/dtd.js', - '/media/editors/codemirror/mode/dtd/dtd.min.js', - '/media/editors/codemirror/mode/dylan/dylan.js', - '/media/editors/codemirror/mode/dylan/dylan.min.js', - '/media/editors/codemirror/mode/ebnf/ebnf.js', - '/media/editors/codemirror/mode/ebnf/ebnf.min.js', - '/media/editors/codemirror/mode/ecl/ecl.js', - '/media/editors/codemirror/mode/ecl/ecl.min.js', - '/media/editors/codemirror/mode/eiffel/eiffel.js', - '/media/editors/codemirror/mode/eiffel/eiffel.min.js', - '/media/editors/codemirror/mode/elm/elm.js', - '/media/editors/codemirror/mode/elm/elm.min.js', - '/media/editors/codemirror/mode/erlang/erlang.js', - '/media/editors/codemirror/mode/erlang/erlang.min.js', - '/media/editors/codemirror/mode/factor/factor.js', - '/media/editors/codemirror/mode/factor/factor.min.js', - '/media/editors/codemirror/mode/fcl/fcl.js', - '/media/editors/codemirror/mode/fcl/fcl.min.js', - '/media/editors/codemirror/mode/forth/forth.js', - '/media/editors/codemirror/mode/forth/forth.min.js', - '/media/editors/codemirror/mode/fortran/fortran.js', - '/media/editors/codemirror/mode/fortran/fortran.min.js', - '/media/editors/codemirror/mode/gas/gas.js', - '/media/editors/codemirror/mode/gas/gas.min.js', - '/media/editors/codemirror/mode/gfm/gfm.js', - '/media/editors/codemirror/mode/gfm/gfm.min.js', - '/media/editors/codemirror/mode/gherkin/gherkin.js', - '/media/editors/codemirror/mode/gherkin/gherkin.min.js', - '/media/editors/codemirror/mode/go/go.js', - '/media/editors/codemirror/mode/go/go.min.js', - '/media/editors/codemirror/mode/groovy/groovy.js', - '/media/editors/codemirror/mode/groovy/groovy.min.js', - '/media/editors/codemirror/mode/haml/haml.js', - '/media/editors/codemirror/mode/haml/haml.min.js', - '/media/editors/codemirror/mode/handlebars/handlebars.js', - '/media/editors/codemirror/mode/handlebars/handlebars.min.js', - '/media/editors/codemirror/mode/haskell-literate/haskell-literate.js', - '/media/editors/codemirror/mode/haskell-literate/haskell-literate.min.js', - '/media/editors/codemirror/mode/haskell/haskell.js', - '/media/editors/codemirror/mode/haskell/haskell.min.js', - '/media/editors/codemirror/mode/haxe/haxe.js', - '/media/editors/codemirror/mode/haxe/haxe.min.js', - '/media/editors/codemirror/mode/htmlembedded/htmlembedded.js', - '/media/editors/codemirror/mode/htmlembedded/htmlembedded.min.js', - '/media/editors/codemirror/mode/htmlmixed/htmlmixed.js', - '/media/editors/codemirror/mode/htmlmixed/htmlmixed.min.js', - '/media/editors/codemirror/mode/http/http.js', - '/media/editors/codemirror/mode/http/http.min.js', - '/media/editors/codemirror/mode/idl/idl.js', - '/media/editors/codemirror/mode/idl/idl.min.js', - '/media/editors/codemirror/mode/javascript/javascript.js', - '/media/editors/codemirror/mode/javascript/javascript.min.js', - '/media/editors/codemirror/mode/jinja2/jinja2.js', - '/media/editors/codemirror/mode/jinja2/jinja2.min.js', - '/media/editors/codemirror/mode/jsx/jsx.js', - '/media/editors/codemirror/mode/jsx/jsx.min.js', - '/media/editors/codemirror/mode/julia/julia.js', - '/media/editors/codemirror/mode/julia/julia.min.js', - '/media/editors/codemirror/mode/livescript/livescript.js', - '/media/editors/codemirror/mode/livescript/livescript.min.js', - '/media/editors/codemirror/mode/lua/lua.js', - '/media/editors/codemirror/mode/lua/lua.min.js', - '/media/editors/codemirror/mode/markdown/markdown.js', - '/media/editors/codemirror/mode/markdown/markdown.min.js', - '/media/editors/codemirror/mode/mathematica/mathematica.js', - '/media/editors/codemirror/mode/mathematica/mathematica.min.js', - '/media/editors/codemirror/mode/mbox/mbox.js', - '/media/editors/codemirror/mode/mbox/mbox.min.js', - '/media/editors/codemirror/mode/meta.js', - '/media/editors/codemirror/mode/meta.min.js', - '/media/editors/codemirror/mode/mirc/mirc.js', - '/media/editors/codemirror/mode/mirc/mirc.min.js', - '/media/editors/codemirror/mode/mllike/mllike.js', - '/media/editors/codemirror/mode/mllike/mllike.min.js', - '/media/editors/codemirror/mode/modelica/modelica.js', - '/media/editors/codemirror/mode/modelica/modelica.min.js', - '/media/editors/codemirror/mode/mscgen/mscgen.js', - '/media/editors/codemirror/mode/mscgen/mscgen.min.js', - '/media/editors/codemirror/mode/mumps/mumps.js', - '/media/editors/codemirror/mode/mumps/mumps.min.js', - '/media/editors/codemirror/mode/nginx/nginx.js', - '/media/editors/codemirror/mode/nginx/nginx.min.js', - '/media/editors/codemirror/mode/nsis/nsis.js', - '/media/editors/codemirror/mode/nsis/nsis.min.js', - '/media/editors/codemirror/mode/ntriples/ntriples.js', - '/media/editors/codemirror/mode/ntriples/ntriples.min.js', - '/media/editors/codemirror/mode/octave/octave.js', - '/media/editors/codemirror/mode/octave/octave.min.js', - '/media/editors/codemirror/mode/oz/oz.js', - '/media/editors/codemirror/mode/oz/oz.min.js', - '/media/editors/codemirror/mode/pascal/pascal.js', - '/media/editors/codemirror/mode/pascal/pascal.min.js', - '/media/editors/codemirror/mode/pegjs/pegjs.js', - '/media/editors/codemirror/mode/pegjs/pegjs.min.js', - '/media/editors/codemirror/mode/perl/perl.js', - '/media/editors/codemirror/mode/perl/perl.min.js', - '/media/editors/codemirror/mode/php/php.js', - '/media/editors/codemirror/mode/php/php.min.js', - '/media/editors/codemirror/mode/pig/pig.js', - '/media/editors/codemirror/mode/pig/pig.min.js', - '/media/editors/codemirror/mode/powershell/powershell.js', - '/media/editors/codemirror/mode/powershell/powershell.min.js', - '/media/editors/codemirror/mode/properties/properties.js', - '/media/editors/codemirror/mode/properties/properties.min.js', - '/media/editors/codemirror/mode/protobuf/protobuf.js', - '/media/editors/codemirror/mode/protobuf/protobuf.min.js', - '/media/editors/codemirror/mode/pug/pug.js', - '/media/editors/codemirror/mode/pug/pug.min.js', - '/media/editors/codemirror/mode/puppet/puppet.js', - '/media/editors/codemirror/mode/puppet/puppet.min.js', - '/media/editors/codemirror/mode/python/python.js', - '/media/editors/codemirror/mode/python/python.min.js', - '/media/editors/codemirror/mode/q/q.js', - '/media/editors/codemirror/mode/q/q.min.js', - '/media/editors/codemirror/mode/r/r.js', - '/media/editors/codemirror/mode/r/r.min.js', - '/media/editors/codemirror/mode/rpm/changes/index.html', - '/media/editors/codemirror/mode/rpm/rpm.js', - '/media/editors/codemirror/mode/rpm/rpm.min.js', - '/media/editors/codemirror/mode/rst/rst.js', - '/media/editors/codemirror/mode/rst/rst.min.js', - '/media/editors/codemirror/mode/ruby/ruby.js', - '/media/editors/codemirror/mode/ruby/ruby.min.js', - '/media/editors/codemirror/mode/rust/rust.js', - '/media/editors/codemirror/mode/rust/rust.min.js', - '/media/editors/codemirror/mode/sas/sas.js', - '/media/editors/codemirror/mode/sas/sas.min.js', - '/media/editors/codemirror/mode/sass/sass.js', - '/media/editors/codemirror/mode/sass/sass.min.js', - '/media/editors/codemirror/mode/scheme/scheme.js', - '/media/editors/codemirror/mode/scheme/scheme.min.js', - '/media/editors/codemirror/mode/shell/shell.js', - '/media/editors/codemirror/mode/shell/shell.min.js', - '/media/editors/codemirror/mode/sieve/sieve.js', - '/media/editors/codemirror/mode/sieve/sieve.min.js', - '/media/editors/codemirror/mode/slim/slim.js', - '/media/editors/codemirror/mode/slim/slim.min.js', - '/media/editors/codemirror/mode/smalltalk/smalltalk.js', - '/media/editors/codemirror/mode/smalltalk/smalltalk.min.js', - '/media/editors/codemirror/mode/smarty/smarty.js', - '/media/editors/codemirror/mode/smarty/smarty.min.js', - '/media/editors/codemirror/mode/solr/solr.js', - '/media/editors/codemirror/mode/solr/solr.min.js', - '/media/editors/codemirror/mode/soy/soy.js', - '/media/editors/codemirror/mode/soy/soy.min.js', - '/media/editors/codemirror/mode/sparql/sparql.js', - '/media/editors/codemirror/mode/sparql/sparql.min.js', - '/media/editors/codemirror/mode/spreadsheet/spreadsheet.js', - '/media/editors/codemirror/mode/spreadsheet/spreadsheet.min.js', - '/media/editors/codemirror/mode/sql/sql.js', - '/media/editors/codemirror/mode/sql/sql.min.js', - '/media/editors/codemirror/mode/stex/stex.js', - '/media/editors/codemirror/mode/stex/stex.min.js', - '/media/editors/codemirror/mode/stylus/stylus.js', - '/media/editors/codemirror/mode/stylus/stylus.min.js', - '/media/editors/codemirror/mode/swift/swift.js', - '/media/editors/codemirror/mode/swift/swift.min.js', - '/media/editors/codemirror/mode/tcl/tcl.js', - '/media/editors/codemirror/mode/tcl/tcl.min.js', - '/media/editors/codemirror/mode/textile/textile.js', - '/media/editors/codemirror/mode/textile/textile.min.js', - '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.css', - '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.js', - '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.min.css', - '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.min.js', - '/media/editors/codemirror/mode/tiki/tiki.css', - '/media/editors/codemirror/mode/tiki/tiki.js', - '/media/editors/codemirror/mode/tiki/tiki.min.css', - '/media/editors/codemirror/mode/tiki/tiki.min.js', - '/media/editors/codemirror/mode/toml/toml.js', - '/media/editors/codemirror/mode/toml/toml.min.js', - '/media/editors/codemirror/mode/tornado/tornado.js', - '/media/editors/codemirror/mode/tornado/tornado.min.js', - '/media/editors/codemirror/mode/troff/troff.js', - '/media/editors/codemirror/mode/troff/troff.min.js', - '/media/editors/codemirror/mode/ttcn-cfg/ttcn-cfg.js', - '/media/editors/codemirror/mode/ttcn-cfg/ttcn-cfg.min.js', - '/media/editors/codemirror/mode/ttcn/ttcn.js', - '/media/editors/codemirror/mode/ttcn/ttcn.min.js', - '/media/editors/codemirror/mode/turtle/turtle.js', - '/media/editors/codemirror/mode/turtle/turtle.min.js', - '/media/editors/codemirror/mode/twig/twig.js', - '/media/editors/codemirror/mode/twig/twig.min.js', - '/media/editors/codemirror/mode/vb/vb.js', - '/media/editors/codemirror/mode/vb/vb.min.js', - '/media/editors/codemirror/mode/vbscript/vbscript.js', - '/media/editors/codemirror/mode/vbscript/vbscript.min.js', - '/media/editors/codemirror/mode/velocity/velocity.js', - '/media/editors/codemirror/mode/velocity/velocity.min.js', - '/media/editors/codemirror/mode/verilog/verilog.js', - '/media/editors/codemirror/mode/verilog/verilog.min.js', - '/media/editors/codemirror/mode/vhdl/vhdl.js', - '/media/editors/codemirror/mode/vhdl/vhdl.min.js', - '/media/editors/codemirror/mode/vue/vue.js', - '/media/editors/codemirror/mode/vue/vue.min.js', - '/media/editors/codemirror/mode/wast/wast.js', - '/media/editors/codemirror/mode/wast/wast.min.js', - '/media/editors/codemirror/mode/webidl/webidl.js', - '/media/editors/codemirror/mode/webidl/webidl.min.js', - '/media/editors/codemirror/mode/xml/xml.js', - '/media/editors/codemirror/mode/xml/xml.min.js', - '/media/editors/codemirror/mode/xquery/xquery.js', - '/media/editors/codemirror/mode/xquery/xquery.min.js', - '/media/editors/codemirror/mode/yacas/yacas.js', - '/media/editors/codemirror/mode/yacas/yacas.min.js', - '/media/editors/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js', - '/media/editors/codemirror/mode/yaml-frontmatter/yaml-frontmatter.min.js', - '/media/editors/codemirror/mode/yaml/yaml.js', - '/media/editors/codemirror/mode/yaml/yaml.min.js', - '/media/editors/codemirror/mode/z80/z80.js', - '/media/editors/codemirror/mode/z80/z80.min.js', - '/media/editors/codemirror/theme/3024-day.css', - '/media/editors/codemirror/theme/3024-night.css', - '/media/editors/codemirror/theme/abcdef.css', - '/media/editors/codemirror/theme/ambiance-mobile.css', - '/media/editors/codemirror/theme/ambiance.css', - '/media/editors/codemirror/theme/ayu-dark.css', - '/media/editors/codemirror/theme/ayu-mirage.css', - '/media/editors/codemirror/theme/base16-dark.css', - '/media/editors/codemirror/theme/base16-light.css', - '/media/editors/codemirror/theme/bespin.css', - '/media/editors/codemirror/theme/blackboard.css', - '/media/editors/codemirror/theme/cobalt.css', - '/media/editors/codemirror/theme/colorforth.css', - '/media/editors/codemirror/theme/darcula.css', - '/media/editors/codemirror/theme/dracula.css', - '/media/editors/codemirror/theme/duotone-dark.css', - '/media/editors/codemirror/theme/duotone-light.css', - '/media/editors/codemirror/theme/eclipse.css', - '/media/editors/codemirror/theme/elegant.css', - '/media/editors/codemirror/theme/erlang-dark.css', - '/media/editors/codemirror/theme/gruvbox-dark.css', - '/media/editors/codemirror/theme/hopscotch.css', - '/media/editors/codemirror/theme/icecoder.css', - '/media/editors/codemirror/theme/idea.css', - '/media/editors/codemirror/theme/isotope.css', - '/media/editors/codemirror/theme/lesser-dark.css', - '/media/editors/codemirror/theme/liquibyte.css', - '/media/editors/codemirror/theme/lucario.css', - '/media/editors/codemirror/theme/material-darker.css', - '/media/editors/codemirror/theme/material-ocean.css', - '/media/editors/codemirror/theme/material-palenight.css', - '/media/editors/codemirror/theme/material.css', - '/media/editors/codemirror/theme/mbo.css', - '/media/editors/codemirror/theme/mdn-like.css', - '/media/editors/codemirror/theme/midnight.css', - '/media/editors/codemirror/theme/monokai.css', - '/media/editors/codemirror/theme/moxer.css', - '/media/editors/codemirror/theme/neat.css', - '/media/editors/codemirror/theme/neo.css', - '/media/editors/codemirror/theme/night.css', - '/media/editors/codemirror/theme/nord.css', - '/media/editors/codemirror/theme/oceanic-next.css', - '/media/editors/codemirror/theme/panda-syntax.css', - '/media/editors/codemirror/theme/paraiso-dark.css', - '/media/editors/codemirror/theme/paraiso-light.css', - '/media/editors/codemirror/theme/pastel-on-dark.css', - '/media/editors/codemirror/theme/railscasts.css', - '/media/editors/codemirror/theme/rubyblue.css', - '/media/editors/codemirror/theme/seti.css', - '/media/editors/codemirror/theme/shadowfox.css', - '/media/editors/codemirror/theme/solarized.css', - '/media/editors/codemirror/theme/ssms.css', - '/media/editors/codemirror/theme/the-matrix.css', - '/media/editors/codemirror/theme/tomorrow-night-bright.css', - '/media/editors/codemirror/theme/tomorrow-night-eighties.css', - '/media/editors/codemirror/theme/ttcn.css', - '/media/editors/codemirror/theme/twilight.css', - '/media/editors/codemirror/theme/vibrant-ink.css', - '/media/editors/codemirror/theme/xq-dark.css', - '/media/editors/codemirror/theme/xq-light.css', - '/media/editors/codemirror/theme/yeti.css', - '/media/editors/codemirror/theme/yonce.css', - '/media/editors/codemirror/theme/zenburn.css', - '/media/editors/none/js/none.js', - '/media/editors/none/js/none.min.js', - '/media/editors/tinymce/changelog.txt', - '/media/editors/tinymce/js/plugins/dragdrop/plugin.js', - '/media/editors/tinymce/js/plugins/dragdrop/plugin.min.js', - '/media/editors/tinymce/js/tiny-close.js', - '/media/editors/tinymce/js/tiny-close.min.js', - '/media/editors/tinymce/js/tinymce-builder.js', - '/media/editors/tinymce/js/tinymce.js', - '/media/editors/tinymce/js/tinymce.min.js', - '/media/editors/tinymce/langs/af.js', - '/media/editors/tinymce/langs/ar.js', - '/media/editors/tinymce/langs/be.js', - '/media/editors/tinymce/langs/bg.js', - '/media/editors/tinymce/langs/bs.js', - '/media/editors/tinymce/langs/ca.js', - '/media/editors/tinymce/langs/cs.js', - '/media/editors/tinymce/langs/cy.js', - '/media/editors/tinymce/langs/da.js', - '/media/editors/tinymce/langs/de.js', - '/media/editors/tinymce/langs/el.js', - '/media/editors/tinymce/langs/es.js', - '/media/editors/tinymce/langs/et.js', - '/media/editors/tinymce/langs/eu.js', - '/media/editors/tinymce/langs/fa.js', - '/media/editors/tinymce/langs/fi.js', - '/media/editors/tinymce/langs/fo.js', - '/media/editors/tinymce/langs/fr.js', - '/media/editors/tinymce/langs/ga.js', - '/media/editors/tinymce/langs/gl.js', - '/media/editors/tinymce/langs/he.js', - '/media/editors/tinymce/langs/hr.js', - '/media/editors/tinymce/langs/hu.js', - '/media/editors/tinymce/langs/id.js', - '/media/editors/tinymce/langs/it.js', - '/media/editors/tinymce/langs/ja.js', - '/media/editors/tinymce/langs/ka.js', - '/media/editors/tinymce/langs/kk.js', - '/media/editors/tinymce/langs/km.js', - '/media/editors/tinymce/langs/ko.js', - '/media/editors/tinymce/langs/lb.js', - '/media/editors/tinymce/langs/lt.js', - '/media/editors/tinymce/langs/lv.js', - '/media/editors/tinymce/langs/mk.js', - '/media/editors/tinymce/langs/ms.js', - '/media/editors/tinymce/langs/nb.js', - '/media/editors/tinymce/langs/nl.js', - '/media/editors/tinymce/langs/pl.js', - '/media/editors/tinymce/langs/pt-BR.js', - '/media/editors/tinymce/langs/pt-PT.js', - '/media/editors/tinymce/langs/readme.md', - '/media/editors/tinymce/langs/ro.js', - '/media/editors/tinymce/langs/ru.js', - '/media/editors/tinymce/langs/si-LK.js', - '/media/editors/tinymce/langs/sk.js', - '/media/editors/tinymce/langs/sl.js', - '/media/editors/tinymce/langs/sr.js', - '/media/editors/tinymce/langs/sv.js', - '/media/editors/tinymce/langs/sw.js', - '/media/editors/tinymce/langs/sy.js', - '/media/editors/tinymce/langs/ta.js', - '/media/editors/tinymce/langs/th.js', - '/media/editors/tinymce/langs/tr.js', - '/media/editors/tinymce/langs/ug.js', - '/media/editors/tinymce/langs/uk.js', - '/media/editors/tinymce/langs/vi.js', - '/media/editors/tinymce/langs/zh-CN.js', - '/media/editors/tinymce/langs/zh-TW.js', - '/media/editors/tinymce/license.txt', - '/media/editors/tinymce/plugins/advlist/plugin.min.js', - '/media/editors/tinymce/plugins/anchor/plugin.min.js', - '/media/editors/tinymce/plugins/autolink/plugin.min.js', - '/media/editors/tinymce/plugins/autoresize/plugin.min.js', - '/media/editors/tinymce/plugins/autosave/plugin.min.js', - '/media/editors/tinymce/plugins/bbcode/plugin.min.js', - '/media/editors/tinymce/plugins/charmap/plugin.min.js', - '/media/editors/tinymce/plugins/code/plugin.min.js', - '/media/editors/tinymce/plugins/codesample/css/prism.css', - '/media/editors/tinymce/plugins/codesample/plugin.min.js', - '/media/editors/tinymce/plugins/colorpicker/plugin.min.js', - '/media/editors/tinymce/plugins/contextmenu/plugin.min.js', - '/media/editors/tinymce/plugins/directionality/plugin.min.js', - '/media/editors/tinymce/plugins/emoticons/img/smiley-cool.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-cry.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-embarassed.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-foot-in-mouth.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-frown.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-innocent.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-kiss.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-laughing.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-money-mouth.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-sealed.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-smile.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-surprised.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-tongue-out.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-undecided.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-wink.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-yell.gif', - '/media/editors/tinymce/plugins/emoticons/plugin.min.js', - '/media/editors/tinymce/plugins/example/dialog.html', - '/media/editors/tinymce/plugins/example/plugin.min.js', - '/media/editors/tinymce/plugins/example_dependency/plugin.min.js', - '/media/editors/tinymce/plugins/fullpage/plugin.min.js', - '/media/editors/tinymce/plugins/fullscreen/plugin.min.js', - '/media/editors/tinymce/plugins/hr/plugin.min.js', - '/media/editors/tinymce/plugins/image/plugin.min.js', - '/media/editors/tinymce/plugins/imagetools/plugin.min.js', - '/media/editors/tinymce/plugins/importcss/plugin.min.js', - '/media/editors/tinymce/plugins/insertdatetime/plugin.min.js', - '/media/editors/tinymce/plugins/layer/plugin.min.js', - '/media/editors/tinymce/plugins/legacyoutput/plugin.min.js', - '/media/editors/tinymce/plugins/link/plugin.min.js', - '/media/editors/tinymce/plugins/lists/plugin.min.js', - '/media/editors/tinymce/plugins/media/plugin.min.js', - '/media/editors/tinymce/plugins/nonbreaking/plugin.min.js', - '/media/editors/tinymce/plugins/noneditable/plugin.min.js', - '/media/editors/tinymce/plugins/pagebreak/plugin.min.js', - '/media/editors/tinymce/plugins/paste/plugin.min.js', - '/media/editors/tinymce/plugins/preview/plugin.min.js', - '/media/editors/tinymce/plugins/print/plugin.min.js', - '/media/editors/tinymce/plugins/save/plugin.min.js', - '/media/editors/tinymce/plugins/searchreplace/plugin.min.js', - '/media/editors/tinymce/plugins/spellchecker/plugin.min.js', - '/media/editors/tinymce/plugins/tabfocus/plugin.min.js', - '/media/editors/tinymce/plugins/table/plugin.min.js', - '/media/editors/tinymce/plugins/template/plugin.min.js', - '/media/editors/tinymce/plugins/textcolor/plugin.min.js', - '/media/editors/tinymce/plugins/textpattern/plugin.min.js', - '/media/editors/tinymce/plugins/toc/plugin.min.js', - '/media/editors/tinymce/plugins/visualblocks/css/visualblocks.css', - '/media/editors/tinymce/plugins/visualblocks/plugin.min.js', - '/media/editors/tinymce/plugins/visualchars/plugin.min.js', - '/media/editors/tinymce/plugins/wordcount/plugin.min.js', - '/media/editors/tinymce/skins/lightgray/content.inline.min.css', - '/media/editors/tinymce/skins/lightgray/content.min.css', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.eot', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.svg', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.ttf', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.woff', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce.eot', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce.svg', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce.ttf', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce.woff', - '/media/editors/tinymce/skins/lightgray/img/anchor.gif', - '/media/editors/tinymce/skins/lightgray/img/loader.gif', - '/media/editors/tinymce/skins/lightgray/img/object.gif', - '/media/editors/tinymce/skins/lightgray/img/trans.gif', - '/media/editors/tinymce/skins/lightgray/skin.ie7.min.css', - '/media/editors/tinymce/skins/lightgray/skin.min.css', - '/media/editors/tinymce/templates/layout1.html', - '/media/editors/tinymce/templates/snippet1.html', - '/media/editors/tinymce/themes/modern/theme.min.js', - '/media/editors/tinymce/tinymce.min.js', - '/media/jui/css/bootstrap-extended.css', - '/media/jui/css/bootstrap-responsive.css', - '/media/jui/css/bootstrap-responsive.min.css', - '/media/jui/css/bootstrap-rtl.css', - '/media/jui/css/bootstrap-tooltip-extended.css', - '/media/jui/css/bootstrap.css', - '/media/jui/css/bootstrap.min.css', - '/media/jui/css/chosen-sprite.png', - '/media/jui/css/chosen-sprite@2x.png', - '/media/jui/css/chosen.css', - '/media/jui/css/icomoon.css', - '/media/jui/css/jquery.minicolors.css', - '/media/jui/css/jquery.searchtools.css', - '/media/jui/css/jquery.simplecolors.css', - '/media/jui/css/sortablelist.css', - '/media/jui/fonts/IcoMoon.dev.commented.svg', - '/media/jui/fonts/IcoMoon.dev.svg', - '/media/jui/fonts/IcoMoon.eot', - '/media/jui/fonts/IcoMoon.svg', - '/media/jui/fonts/IcoMoon.ttf', - '/media/jui/fonts/IcoMoon.woff', - '/media/jui/fonts/icomoon-license.txt', - '/media/jui/images/ajax-loader.gif', - '/media/jui/img/ajax-loader.gif', - '/media/jui/img/alpha.png', - '/media/jui/img/bg-overlay.png', - '/media/jui/img/glyphicons-halflings-white.png', - '/media/jui/img/glyphicons-halflings.png', - '/media/jui/img/hue.png', - '/media/jui/img/joomla.png', - '/media/jui/img/jquery.minicolors.png', - '/media/jui/img/saturation.png', - '/media/jui/js/ajax-chosen.js', - '/media/jui/js/ajax-chosen.min.js', - '/media/jui/js/bootstrap-tooltip-extended.js', - '/media/jui/js/bootstrap-tooltip-extended.min.js', - '/media/jui/js/bootstrap.js', - '/media/jui/js/bootstrap.min.js', - '/media/jui/js/chosen.jquery.js', - '/media/jui/js/chosen.jquery.min.js', - '/media/jui/js/cms-uncompressed.js', - '/media/jui/js/cms.js', - '/media/jui/js/fielduser.js', - '/media/jui/js/fielduser.min.js', - '/media/jui/js/html5-uncompressed.js', - '/media/jui/js/html5.js', - '/media/jui/js/icomoon-lte-ie7.js', - '/media/jui/js/jquery-migrate.js', - '/media/jui/js/jquery-migrate.min.js', - '/media/jui/js/jquery-noconflict.js', - '/media/jui/js/jquery.autocomplete.js', - '/media/jui/js/jquery.autocomplete.min.js', - '/media/jui/js/jquery.js', - '/media/jui/js/jquery.min.js', - '/media/jui/js/jquery.minicolors.js', - '/media/jui/js/jquery.minicolors.min.js', - '/media/jui/js/jquery.searchtools.js', - '/media/jui/js/jquery.searchtools.min.js', - '/media/jui/js/jquery.simplecolors.js', - '/media/jui/js/jquery.simplecolors.min.js', - '/media/jui/js/jquery.ui.core.js', - '/media/jui/js/jquery.ui.core.min.js', - '/media/jui/js/jquery.ui.sortable.js', - '/media/jui/js/jquery.ui.sortable.min.js', - '/media/jui/js/sortablelist.js', - '/media/jui/js/treeselectmenu.jquery.js', - '/media/jui/js/treeselectmenu.jquery.min.js', - '/media/jui/less/accordion.less', - '/media/jui/less/alerts.less', - '/media/jui/less/bootstrap-extended.less', - '/media/jui/less/bootstrap-rtl.less', - '/media/jui/less/bootstrap.less', - '/media/jui/less/breadcrumbs.less', - '/media/jui/less/button-groups.less', - '/media/jui/less/buttons.less', - '/media/jui/less/carousel.less', - '/media/jui/less/close.less', - '/media/jui/less/code.less', - '/media/jui/less/component-animations.less', - '/media/jui/less/dropdowns.less', - '/media/jui/less/forms.less', - '/media/jui/less/grid.less', - '/media/jui/less/hero-unit.less', - '/media/jui/less/icomoon.less', - '/media/jui/less/labels-badges.less', - '/media/jui/less/layouts.less', - '/media/jui/less/media.less', - '/media/jui/less/mixins.less', - '/media/jui/less/modals.joomla.less', - '/media/jui/less/modals.less', - '/media/jui/less/navbar.less', - '/media/jui/less/navs.less', - '/media/jui/less/pager.less', - '/media/jui/less/pagination.less', - '/media/jui/less/popovers.less', - '/media/jui/less/progress-bars.less', - '/media/jui/less/reset.less', - '/media/jui/less/responsive-1200px-min.less', - '/media/jui/less/responsive-767px-max.joomla.less', - '/media/jui/less/responsive-767px-max.less', - '/media/jui/less/responsive-768px-979px.less', - '/media/jui/less/responsive-navbar.less', - '/media/jui/less/responsive-utilities.less', - '/media/jui/less/responsive.less', - '/media/jui/less/scaffolding.less', - '/media/jui/less/sprites.less', - '/media/jui/less/tables.less', - '/media/jui/less/thumbnails.less', - '/media/jui/less/tooltip.less', - '/media/jui/less/type.less', - '/media/jui/less/utilities.less', - '/media/jui/less/variables.less', - '/media/jui/less/wells.less', - '/media/media/css/background.png', - '/media/media/css/bigplay.fw.png', - '/media/media/css/bigplay.png', - '/media/media/css/bigplay.svg', - '/media/media/css/controls-ted.png', - '/media/media/css/controls-wmp-bg.png', - '/media/media/css/controls-wmp.png', - '/media/media/css/controls.fw.png', - '/media/media/css/controls.png', - '/media/media/css/controls.svg', - '/media/media/css/jumpforward.png', - '/media/media/css/loading.gif', - '/media/media/css/mediaelementplayer.css', - '/media/media/css/mediaelementplayer.min.css', - '/media/media/css/medialist-details.css', - '/media/media/css/medialist-details_rtl.css', - '/media/media/css/medialist-thumbs.css', - '/media/media/css/medialist-thumbs_rtl.css', - '/media/media/css/mediamanager.css', - '/media/media/css/mediamanager_rtl.css', - '/media/media/css/mejs-skins.css', - '/media/media/css/popup-imagelist.css', - '/media/media/css/popup-imagelist_rtl.css', - '/media/media/css/popup-imagemanager.css', - '/media/media/css/popup-imagemanager_rtl.css', - '/media/media/css/skipback.png', - '/media/media/images/bar.gif', - '/media/media/images/con_info.png', - '/media/media/images/delete.png', - '/media/media/images/dots.gif', - '/media/media/images/failed.png', - '/media/media/images/folder.gif', - '/media/media/images/folder.png', - '/media/media/images/folder_sm.png', - '/media/media/images/folderup_16.png', - '/media/media/images/folderup_32.png', - '/media/media/images/mime-icon-16/avi.png', - '/media/media/images/mime-icon-16/doc.png', - '/media/media/images/mime-icon-16/mov.png', - '/media/media/images/mime-icon-16/mp3.png', - '/media/media/images/mime-icon-16/mp4.png', - '/media/media/images/mime-icon-16/odc.png', - '/media/media/images/mime-icon-16/odd.png', - '/media/media/images/mime-icon-16/odt.png', - '/media/media/images/mime-icon-16/ogg.png', - '/media/media/images/mime-icon-16/pdf.png', - '/media/media/images/mime-icon-16/ppt.png', - '/media/media/images/mime-icon-16/rar.png', - '/media/media/images/mime-icon-16/rtf.png', - '/media/media/images/mime-icon-16/svg.png', - '/media/media/images/mime-icon-16/sxd.png', - '/media/media/images/mime-icon-16/tar.png', - '/media/media/images/mime-icon-16/tgz.png', - '/media/media/images/mime-icon-16/wma.png', - '/media/media/images/mime-icon-16/wmv.png', - '/media/media/images/mime-icon-16/xls.png', - '/media/media/images/mime-icon-16/zip.png', - '/media/media/images/mime-icon-32/avi.png', - '/media/media/images/mime-icon-32/doc.png', - '/media/media/images/mime-icon-32/mov.png', - '/media/media/images/mime-icon-32/mp3.png', - '/media/media/images/mime-icon-32/mp4.png', - '/media/media/images/mime-icon-32/odc.png', - '/media/media/images/mime-icon-32/odd.png', - '/media/media/images/mime-icon-32/odt.png', - '/media/media/images/mime-icon-32/ogg.png', - '/media/media/images/mime-icon-32/pdf.png', - '/media/media/images/mime-icon-32/ppt.png', - '/media/media/images/mime-icon-32/rar.png', - '/media/media/images/mime-icon-32/rtf.png', - '/media/media/images/mime-icon-32/svg.png', - '/media/media/images/mime-icon-32/sxd.png', - '/media/media/images/mime-icon-32/tar.png', - '/media/media/images/mime-icon-32/tgz.png', - '/media/media/images/mime-icon-32/wma.png', - '/media/media/images/mime-icon-32/wmv.png', - '/media/media/images/mime-icon-32/xls.png', - '/media/media/images/mime-icon-32/zip.png', - '/media/media/images/progress.gif', - '/media/media/images/remove.png', - '/media/media/images/success.png', - '/media/media/images/upload.png', - '/media/media/images/uploading.png', - '/media/media/js/flashmediaelement-cdn.swf', - '/media/media/js/flashmediaelement.swf', - '/media/media/js/mediaelement-and-player.js', - '/media/media/js/mediaelement-and-player.min.js', - '/media/media/js/mediafield-mootools.js', - '/media/media/js/mediafield-mootools.min.js', - '/media/media/js/mediafield.js', - '/media/media/js/mediafield.min.js', - '/media/media/js/mediamanager.js', - '/media/media/js/mediamanager.min.js', - '/media/media/js/popup-imagemanager.js', - '/media/media/js/popup-imagemanager.min.js', - '/media/media/js/silverlightmediaelement.xap', - '/media/overrider/css/overrider.css', - '/media/overrider/js/overrider.js', - '/media/overrider/js/overrider.min.js', - '/media/plg_system_highlight/highlight.css', - '/media/plg_twofactorauth_totp/js/qrcode.js', - '/media/plg_twofactorauth_totp/js/qrcode.min.js', - '/media/plg_twofactorauth_totp/js/qrcode_SJIS.js', - '/media/plg_twofactorauth_totp/js/qrcode_UTF8.js', - '/media/system/css/adminlist.css', - '/media/system/css/jquery.Jcrop.min.css', - '/media/system/css/modal.css', - '/media/system/css/system.css', - '/media/system/js/associations-edit-uncompressed.js', - '/media/system/js/associations-edit.js', - '/media/system/js/calendar-setup-uncompressed.js', - '/media/system/js/calendar-setup.js', - '/media/system/js/calendar-uncompressed.js', - '/media/system/js/calendar.js', - '/media/system/js/caption-uncompressed.js', - '/media/system/js/caption.js', - '/media/system/js/color-field-adv-init.js', - '/media/system/js/color-field-adv-init.min.js', - '/media/system/js/color-field-init.js', - '/media/system/js/color-field-init.min.js', - '/media/system/js/combobox-uncompressed.js', - '/media/system/js/combobox.js', - '/media/system/js/core-uncompressed.js', - '/media/system/js/fields/calendar-locales/af.js', - '/media/system/js/fields/calendar-locales/ar.js', - '/media/system/js/fields/calendar-locales/bg.js', - '/media/system/js/fields/calendar-locales/bn.js', - '/media/system/js/fields/calendar-locales/bs.js', - '/media/system/js/fields/calendar-locales/ca.js', - '/media/system/js/fields/calendar-locales/cs.js', - '/media/system/js/fields/calendar-locales/cy.js', - '/media/system/js/fields/calendar-locales/da.js', - '/media/system/js/fields/calendar-locales/de.js', - '/media/system/js/fields/calendar-locales/el.js', - '/media/system/js/fields/calendar-locales/en.js', - '/media/system/js/fields/calendar-locales/es.js', - '/media/system/js/fields/calendar-locales/eu.js', - '/media/system/js/fields/calendar-locales/fa-ir.js', - '/media/system/js/fields/calendar-locales/fi.js', - '/media/system/js/fields/calendar-locales/fr.js', - '/media/system/js/fields/calendar-locales/ga.js', - '/media/system/js/fields/calendar-locales/hr.js', - '/media/system/js/fields/calendar-locales/hu.js', - '/media/system/js/fields/calendar-locales/it.js', - '/media/system/js/fields/calendar-locales/ja.js', - '/media/system/js/fields/calendar-locales/ka.js', - '/media/system/js/fields/calendar-locales/kk.js', - '/media/system/js/fields/calendar-locales/ko.js', - '/media/system/js/fields/calendar-locales/lt.js', - '/media/system/js/fields/calendar-locales/mk.js', - '/media/system/js/fields/calendar-locales/nb.js', - '/media/system/js/fields/calendar-locales/nl.js', - '/media/system/js/fields/calendar-locales/pl.js', - '/media/system/js/fields/calendar-locales/prs-af.js', - '/media/system/js/fields/calendar-locales/pt.js', - '/media/system/js/fields/calendar-locales/ru.js', - '/media/system/js/fields/calendar-locales/sk.js', - '/media/system/js/fields/calendar-locales/sl.js', - '/media/system/js/fields/calendar-locales/sr-rs.js', - '/media/system/js/fields/calendar-locales/sr-yu.js', - '/media/system/js/fields/calendar-locales/sv.js', - '/media/system/js/fields/calendar-locales/sw.js', - '/media/system/js/fields/calendar-locales/ta.js', - '/media/system/js/fields/calendar-locales/th.js', - '/media/system/js/fields/calendar-locales/uk.js', - '/media/system/js/fields/calendar-locales/zh-CN.js', - '/media/system/js/fields/calendar-locales/zh-TW.js', - '/media/system/js/frontediting-uncompressed.js', - '/media/system/js/frontediting.js', - '/media/system/js/helpsite.js', - '/media/system/js/highlighter-uncompressed.js', - '/media/system/js/highlighter.js', - '/media/system/js/html5fallback-uncompressed.js', - '/media/system/js/html5fallback.js', - '/media/system/js/jquery.Jcrop.js', - '/media/system/js/jquery.Jcrop.min.js', - '/media/system/js/keepalive-uncompressed.js', - '/media/system/js/modal-fields-uncompressed.js', - '/media/system/js/modal-fields.js', - '/media/system/js/modal-uncompressed.js', - '/media/system/js/modal.js', - '/media/system/js/moduleorder.js', - '/media/system/js/mootools-core-uncompressed.js', - '/media/system/js/mootools-core.js', - '/media/system/js/mootools-more-uncompressed.js', - '/media/system/js/mootools-more.js', - '/media/system/js/mootree-uncompressed.js', - '/media/system/js/mootree.js', - '/media/system/js/multiselect-uncompressed.js', - '/media/system/js/passwordstrength.js', - '/media/system/js/permissions-uncompressed.js', - '/media/system/js/permissions.js', - '/media/system/js/polyfill.classlist-uncompressed.js', - '/media/system/js/polyfill.classlist.js', - '/media/system/js/polyfill.event-uncompressed.js', - '/media/system/js/polyfill.event.js', - '/media/system/js/polyfill.filter-uncompressed.js', - '/media/system/js/polyfill.filter.js', - '/media/system/js/polyfill.map-uncompressed.js', - '/media/system/js/polyfill.map.js', - '/media/system/js/polyfill.xpath-uncompressed.js', - '/media/system/js/polyfill.xpath.js', - '/media/system/js/progressbar-uncompressed.js', - '/media/system/js/progressbar.js', - '/media/system/js/punycode-uncompressed.js', - '/media/system/js/punycode.js', - '/media/system/js/repeatable-uncompressed.js', - '/media/system/js/repeatable.js', - '/media/system/js/sendtestmail-uncompressed.js', - '/media/system/js/sendtestmail.js', - '/media/system/js/subform-repeatable-uncompressed.js', - '/media/system/js/subform-repeatable.js', - '/media/system/js/switcher-uncompressed.js', - '/media/system/js/switcher.js', - '/media/system/js/tabs-state-uncompressed.js', - '/media/system/js/tabs-state.js', - '/media/system/js/tabs.js', - '/media/system/js/validate-uncompressed.js', - '/media/system/js/validate.js', - '/modules/mod_articles_archive/helper.php', - '/modules/mod_articles_categories/helper.php', - '/modules/mod_articles_category/helper.php', - '/modules/mod_articles_latest/helper.php', - '/modules/mod_articles_news/helper.php', - '/modules/mod_articles_popular/helper.php', - '/modules/mod_banners/helper.php', - '/modules/mod_breadcrumbs/helper.php', - '/modules/mod_feed/helper.php', - '/modules/mod_finder/helper.php', - '/modules/mod_languages/helper.php', - '/modules/mod_login/helper.php', - '/modules/mod_menu/helper.php', - '/modules/mod_random_image/helper.php', - '/modules/mod_related_items/helper.php', - '/modules/mod_stats/helper.php', - '/modules/mod_syndicate/helper.php', - '/modules/mod_tags_popular/helper.php', - '/modules/mod_tags_similar/helper.php', - '/modules/mod_users_latest/helper.php', - '/modules/mod_whosonline/helper.php', - '/modules/mod_wrapper/helper.php', - '/plugins/authentication/gmail/gmail.php', - '/plugins/authentication/gmail/gmail.xml', - '/plugins/captcha/recaptcha/postinstall/actions.php', - '/plugins/content/confirmconsent/fields/consentbox.php', - '/plugins/editors/codemirror/fonts.php', - '/plugins/editors/codemirror/layouts/editors/codemirror/init.php', - '/plugins/editors/tinymce/field/skins.php', - '/plugins/editors/tinymce/field/tinymcebuilder.php', - '/plugins/editors/tinymce/field/uploaddirs.php', - '/plugins/editors/tinymce/form/setoptions.xml', - '/plugins/quickicon/joomlaupdate/joomlaupdate.php', - '/plugins/system/languagecode/language/en-GB/en-GB.plg_system_languagecode.ini', - '/plugins/system/languagecode/language/en-GB/en-GB.plg_system_languagecode.sys.ini', - '/plugins/system/p3p/p3p.php', - '/plugins/system/p3p/p3p.xml', - '/plugins/system/privacyconsent/field/privacy.php', - '/plugins/system/privacyconsent/privacyconsent/privacyconsent.xml', - '/plugins/system/stats/field/base.php', - '/plugins/system/stats/field/data.php', - '/plugins/system/stats/field/uniqueid.php', - '/plugins/user/profile/field/dob.php', - '/plugins/user/profile/field/tos.php', - '/plugins/user/profile/profiles/profile.xml', - '/plugins/user/terms/field/terms.php', - '/plugins/user/terms/terms/terms.xml', - '/templates/beez3/component.php', - '/templates/beez3/css/general.css', - '/templates/beez3/css/ie7only.css', - '/templates/beez3/css/ieonly.css', - '/templates/beez3/css/layout.css', - '/templates/beez3/css/nature.css', - '/templates/beez3/css/nature_rtl.css', - '/templates/beez3/css/personal.css', - '/templates/beez3/css/personal_rtl.css', - '/templates/beez3/css/position.css', - '/templates/beez3/css/print.css', - '/templates/beez3/css/red.css', - '/templates/beez3/css/template.css', - '/templates/beez3/css/template_rtl.css', - '/templates/beez3/css/turq.css', - '/templates/beez3/css/turq.less', - '/templates/beez3/error.php', - '/templates/beez3/favicon.ico', - '/templates/beez3/html/com_contact/categories/default.php', - '/templates/beez3/html/com_contact/categories/default_items.php', - '/templates/beez3/html/com_contact/category/default.php', - '/templates/beez3/html/com_contact/category/default_children.php', - '/templates/beez3/html/com_contact/category/default_items.php', - '/templates/beez3/html/com_contact/contact/default.php', - '/templates/beez3/html/com_contact/contact/default_address.php', - '/templates/beez3/html/com_contact/contact/default_articles.php', - '/templates/beez3/html/com_contact/contact/default_form.php', - '/templates/beez3/html/com_contact/contact/default_links.php', - '/templates/beez3/html/com_contact/contact/default_profile.php', - '/templates/beez3/html/com_contact/contact/default_user_custom_fields.php', - '/templates/beez3/html/com_contact/contact/encyclopedia.php', - '/templates/beez3/html/com_content/archive/default.php', - '/templates/beez3/html/com_content/archive/default_items.php', - '/templates/beez3/html/com_content/article/default.php', - '/templates/beez3/html/com_content/article/default_links.php', - '/templates/beez3/html/com_content/categories/default.php', - '/templates/beez3/html/com_content/categories/default_items.php', - '/templates/beez3/html/com_content/category/blog.php', - '/templates/beez3/html/com_content/category/blog_children.php', - '/templates/beez3/html/com_content/category/blog_item.php', - '/templates/beez3/html/com_content/category/blog_links.php', - '/templates/beez3/html/com_content/category/default.php', - '/templates/beez3/html/com_content/category/default_articles.php', - '/templates/beez3/html/com_content/category/default_children.php', - '/templates/beez3/html/com_content/featured/default.php', - '/templates/beez3/html/com_content/featured/default_item.php', - '/templates/beez3/html/com_content/featured/default_links.php', - '/templates/beez3/html/com_content/form/edit.php', - '/templates/beez3/html/com_newsfeeds/categories/default.php', - '/templates/beez3/html/com_newsfeeds/categories/default_items.php', - '/templates/beez3/html/com_newsfeeds/category/default.php', - '/templates/beez3/html/com_newsfeeds/category/default_children.php', - '/templates/beez3/html/com_newsfeeds/category/default_items.php', - '/templates/beez3/html/com_weblinks/categories/default.php', - '/templates/beez3/html/com_weblinks/categories/default_items.php', - '/templates/beez3/html/com_weblinks/category/default.php', - '/templates/beez3/html/com_weblinks/category/default_children.php', - '/templates/beez3/html/com_weblinks/category/default_items.php', - '/templates/beez3/html/com_weblinks/form/edit.php', - '/templates/beez3/html/layouts/joomla/system/message.php', - '/templates/beez3/html/mod_breadcrumbs/default.php', - '/templates/beez3/html/mod_languages/default.php', - '/templates/beez3/html/mod_login/default.php', - '/templates/beez3/html/mod_login/default_logout.php', - '/templates/beez3/html/modules.php', - '/templates/beez3/images/all_bg.gif', - '/templates/beez3/images/arrow.png', - '/templates/beez3/images/arrow2_grey.png', - '/templates/beez3/images/arrow_white_grey.png', - '/templates/beez3/images/blog_more.gif', - '/templates/beez3/images/blog_more_hover.gif', - '/templates/beez3/images/close.png', - '/templates/beez3/images/content_bg.gif', - '/templates/beez3/images/footer_bg.gif', - '/templates/beez3/images/footer_bg.png', - '/templates/beez3/images/header-bg.gif', - '/templates/beez3/images/minus.png', - '/templates/beez3/images/nature/arrow1.gif', - '/templates/beez3/images/nature/arrow1_rtl.gif', - '/templates/beez3/images/nature/arrow2.gif', - '/templates/beez3/images/nature/arrow2_grey.png', - '/templates/beez3/images/nature/arrow2_rtl.gif', - '/templates/beez3/images/nature/arrow_nav.gif', - '/templates/beez3/images/nature/arrow_small.png', - '/templates/beez3/images/nature/arrow_small_rtl.png', - '/templates/beez3/images/nature/blog_more.gif', - '/templates/beez3/images/nature/box.png', - '/templates/beez3/images/nature/box1.png', - '/templates/beez3/images/nature/grey_bg.png', - '/templates/beez3/images/nature/headingback.png', - '/templates/beez3/images/nature/karo.gif', - '/templates/beez3/images/nature/level4.png', - '/templates/beez3/images/nature/nav_level1_a.gif', - '/templates/beez3/images/nature/nav_level_1.gif', - '/templates/beez3/images/nature/pfeil.gif', - '/templates/beez3/images/nature/readmore_arrow.png', - '/templates/beez3/images/nature/searchbutton.png', - '/templates/beez3/images/nature/tabs.gif', - '/templates/beez3/images/nav_level_1.gif', - '/templates/beez3/images/news.gif', - '/templates/beez3/images/personal/arrow2_grey.jpg', - '/templates/beez3/images/personal/arrow2_grey.png', - '/templates/beez3/images/personal/bg2.png', - '/templates/beez3/images/personal/button.png', - '/templates/beez3/images/personal/dot.png', - '/templates/beez3/images/personal/ecke.gif', - '/templates/beez3/images/personal/footer.jpg', - '/templates/beez3/images/personal/grey_bg.png', - '/templates/beez3/images/personal/navi_active.png', - '/templates/beez3/images/personal/personal2.png', - '/templates/beez3/images/personal/readmore_arrow.png', - '/templates/beez3/images/personal/readmore_arrow_hover.png', - '/templates/beez3/images/personal/tabs_back.png', - '/templates/beez3/images/plus.png', - '/templates/beez3/images/req.png', - '/templates/beez3/images/slider_minus.png', - '/templates/beez3/images/slider_minus_rtl.png', - '/templates/beez3/images/slider_plus.png', - '/templates/beez3/images/slider_plus_rtl.png', - '/templates/beez3/images/system/arrow.png', - '/templates/beez3/images/system/arrow_rtl.png', - '/templates/beez3/images/system/calendar.png', - '/templates/beez3/images/system/j_button2_blank.png', - '/templates/beez3/images/system/j_button2_image.png', - '/templates/beez3/images/system/j_button2_left.png', - '/templates/beez3/images/system/j_button2_pagebreak.png', - '/templates/beez3/images/system/j_button2_readmore.png', - '/templates/beez3/images/system/notice-alert.png', - '/templates/beez3/images/system/notice-alert_rtl.png', - '/templates/beez3/images/system/notice-info.png', - '/templates/beez3/images/system/notice-info_rtl.png', - '/templates/beez3/images/system/notice-note.png', - '/templates/beez3/images/system/notice-note_rtl.png', - '/templates/beez3/images/system/selector-arrow.png', - '/templates/beez3/images/table_footer.gif', - '/templates/beez3/images/trans.gif', - '/templates/beez3/index.php', - '/templates/beez3/javascript/hide.js', - '/templates/beez3/javascript/md_stylechanger.js', - '/templates/beez3/javascript/respond.js', - '/templates/beez3/javascript/respond.src.js', - '/templates/beez3/javascript/template.js', - '/templates/beez3/jsstrings.php', - '/templates/beez3/language/en-GB/en-GB.tpl_beez3.ini', - '/templates/beez3/language/en-GB/en-GB.tpl_beez3.sys.ini', - '/templates/beez3/templateDetails.xml', - '/templates/beez3/template_preview.png', - '/templates/beez3/template_thumbnail.png', - '/templates/protostar/component.php', - '/templates/protostar/css/offline.css', - '/templates/protostar/css/template.css', - '/templates/protostar/error.php', - '/templates/protostar/favicon.ico', - '/templates/protostar/html/com_media/imageslist/default_folder.php', - '/templates/protostar/html/com_media/imageslist/default_image.php', - '/templates/protostar/html/layouts/joomla/form/field/contenthistory.php', - '/templates/protostar/html/layouts/joomla/form/field/media.php', - '/templates/protostar/html/layouts/joomla/form/field/user.php', - '/templates/protostar/html/layouts/joomla/system/message.php', - '/templates/protostar/html/modules.php', - '/templates/protostar/html/pagination.php', - '/templates/protostar/images/logo.png', - '/templates/protostar/images/system/rating_star.png', - '/templates/protostar/images/system/rating_star_blank.png', - '/templates/protostar/images/system/sort_asc.png', - '/templates/protostar/images/system/sort_desc.png', - '/templates/protostar/img/glyphicons-halflings-white.png', - '/templates/protostar/img/glyphicons-halflings.png', - '/templates/protostar/index.php', - '/templates/protostar/js/application.js', - '/templates/protostar/js/classes.js', - '/templates/protostar/js/template.js', - '/templates/protostar/language/en-GB/en-GB.tpl_protostar.ini', - '/templates/protostar/language/en-GB/en-GB.tpl_protostar.sys.ini', - '/templates/protostar/less/icomoon.less', - '/templates/protostar/less/template.less', - '/templates/protostar/less/template_rtl.less', - '/templates/protostar/less/variables.less', - '/templates/protostar/offline.php', - '/templates/protostar/templateDetails.xml', - '/templates/protostar/template_preview.png', - '/templates/protostar/template_thumbnail.png', - '/templates/system/css/system.css', - '/templates/system/css/toolbar.css', - '/templates/system/html/modules.php', - '/templates/system/images/calendar.png', - '/templates/system/images/j_button2_blank.png', - '/templates/system/images/j_button2_image.png', - '/templates/system/images/j_button2_left.png', - '/templates/system/images/j_button2_pagebreak.png', - '/templates/system/images/j_button2_readmore.png', - '/templates/system/images/j_button2_right.png', - '/templates/system/images/selector-arrow.png', - // 4.0 from Beta 1 to Beta 2 - '/administrator/components/com_finder/src/Indexer/Driver/Mysql.php', - '/administrator/components/com_finder/src/Indexer/Driver/Postgresql.php', - '/administrator/components/com_workflow/access.xml', - '/api/components/com_installer/src/Controller/LanguagesController.php', - '/api/components/com_installer/src/View/Languages/JsonapiView.php', - '/libraries/vendor/joomla/controller/LICENSE', - '/libraries/vendor/joomla/controller/src/AbstractController.php', - '/libraries/vendor/joomla/controller/src/ControllerInterface.php', - '/media/com_users/js/admin-users-user.es6.js', - '/media/com_users/js/admin-users-user.es6.min.js', - '/media/com_users/js/admin-users-user.es6.min.js.gz', - '/media/com_users/js/admin-users-user.js', - '/media/com_users/js/admin-users-user.min.js', - '/media/com_users/js/admin-users-user.min.js.gz', - // 4.0 from Beta 2 to Beta 3 - '/administrator/templates/atum/images/logo-blue.svg', - '/administrator/templates/atum/images/logo-joomla-blue.svg', - '/administrator/templates/atum/images/logo-joomla-white.svg', - '/administrator/templates/atum/images/logo.svg', - // 4.0 from Beta 3 to Beta 4 - '/components/com_config/src/Model/CmsModel.php', - // 4.0 from Beta 4 to Beta 5 - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-18.sql', - '/administrator/components/com_config/tmpl/application/default_system.php', - '/administrator/language/en-GB/plg_content_imagelazyload.sys.ini', - '/administrator/language/en-GB/plg_fields_image.ini', - '/administrator/language/en-GB/plg_fields_image.sys.ini', - '/administrator/templates/atum/scss/vendor/bootstrap/_nav.scss', - '/libraries/vendor/spomky-labs/base64url/phpstan.neon', - '/media/plg_system_webauthn/images/webauthn-black.png', - '/media/plg_system_webauthn/images/webauthn-color.png', - '/media/plg_system_webauthn/images/webauthn-white.png', - '/media/system/css/system.min.css', - '/media/system/css/system.min.css.gz', - '/plugins/content/imagelazyload/imagelazyload.php', - '/plugins/content/imagelazyload/imagelazyload.xml', - '/templates/cassiopeia/html/layouts/chromes/cardGrey.php', - '/templates/cassiopeia/html/layouts/chromes/default.php', - '/templates/cassiopeia/scss/vendor/bootstrap/_card.scss', - // 4.0 from Beta 5 to Beta 6 - '/administrator/modules/mod_multilangstatus/src/Helper/MultilangstatusAdminHelper.php', - '/administrator/templates/atum/favicon.ico', - '/libraries/vendor/nyholm/psr7/phpstan.baseline.dist', - '/libraries/vendor/spomky-labs/base64url/.php_cs.dist', - '/libraries/vendor/spomky-labs/base64url/infection.json.dist', - '/media/layouts/js/joomla/html/batch/batch-language.es6.js', - '/media/layouts/js/joomla/html/batch/batch-language.es6.min.js', - '/media/layouts/js/joomla/html/batch/batch-language.es6.min.js.gz', - '/media/layouts/js/joomla/html/batch/batch-language.js', - '/media/layouts/js/joomla/html/batch/batch-language.min.js', - '/media/layouts/js/joomla/html/batch/batch-language.min.js.gz', - '/media/plg_system_webauthn/images/webauthn-black.svg', - '/media/plg_system_webauthn/images/webauthn-white.svg', - '/media/system/js/core.es6/ajax.es6', - '/media/system/js/core.es6/customevent.es6', - '/media/system/js/core.es6/event.es6', - '/media/system/js/core.es6/form.es6', - '/media/system/js/core.es6/message.es6', - '/media/system/js/core.es6/options.es6', - '/media/system/js/core.es6/text.es6', - '/media/system/js/core.es6/token.es6', - '/media/system/js/core.es6/webcomponent.es6', - '/templates/cassiopeia/favicon.ico', - '/templates/cassiopeia/scss/_mixin.scss', - '/templates/cassiopeia/scss/_variables.scss', - '/templates/cassiopeia/scss/blocks/_demo-styling.scss', - // 4.0 from Beta 6 to Beta 7 - '/media/legacy/js/bootstrap-init.js', - '/media/legacy/js/bootstrap-init.min.js', - '/media/legacy/js/bootstrap-init.min.js.gz', - '/media/legacy/js/frontediting.js', - '/media/legacy/js/frontediting.min.js', - '/media/legacy/js/frontediting.min.js.gz', - '/media/vendor/bootstrap/js/bootstrap.bundle.js', - '/media/vendor/bootstrap/js/bootstrap.bundle.min.js', - '/media/vendor/bootstrap/js/bootstrap.bundle.min.js.gz', - '/media/vendor/bootstrap/js/bootstrap.bundle.min.js.map', - '/media/vendor/bootstrap/js/bootstrap.js', - '/media/vendor/bootstrap/js/bootstrap.min.js', - '/media/vendor/bootstrap/js/bootstrap.min.js.gz', - '/media/vendor/bootstrap/scss/_code.scss', - '/media/vendor/bootstrap/scss/_custom-forms.scss', - '/media/vendor/bootstrap/scss/_input-group.scss', - '/media/vendor/bootstrap/scss/_jumbotron.scss', - '/media/vendor/bootstrap/scss/_media.scss', - '/media/vendor/bootstrap/scss/_print.scss', - '/media/vendor/bootstrap/scss/mixins/_background-variant.scss', - '/media/vendor/bootstrap/scss/mixins/_badge.scss', - '/media/vendor/bootstrap/scss/mixins/_float.scss', - '/media/vendor/bootstrap/scss/mixins/_grid-framework.scss', - '/media/vendor/bootstrap/scss/mixins/_hover.scss', - '/media/vendor/bootstrap/scss/mixins/_nav-divider.scss', - '/media/vendor/bootstrap/scss/mixins/_screen-reader.scss', - '/media/vendor/bootstrap/scss/mixins/_size.scss', - '/media/vendor/bootstrap/scss/mixins/_table-row.scss', - '/media/vendor/bootstrap/scss/mixins/_text-emphasis.scss', - '/media/vendor/bootstrap/scss/mixins/_text-hide.scss', - '/media/vendor/bootstrap/scss/mixins/_visibility.scss', - '/media/vendor/bootstrap/scss/utilities/_align.scss', - '/media/vendor/bootstrap/scss/utilities/_background.scss', - '/media/vendor/bootstrap/scss/utilities/_borders.scss', - '/media/vendor/bootstrap/scss/utilities/_clearfix.scss', - '/media/vendor/bootstrap/scss/utilities/_display.scss', - '/media/vendor/bootstrap/scss/utilities/_embed.scss', - '/media/vendor/bootstrap/scss/utilities/_flex.scss', - '/media/vendor/bootstrap/scss/utilities/_float.scss', - '/media/vendor/bootstrap/scss/utilities/_interactions.scss', - '/media/vendor/bootstrap/scss/utilities/_overflow.scss', - '/media/vendor/bootstrap/scss/utilities/_position.scss', - '/media/vendor/bootstrap/scss/utilities/_screenreaders.scss', - '/media/vendor/bootstrap/scss/utilities/_shadows.scss', - '/media/vendor/bootstrap/scss/utilities/_sizing.scss', - '/media/vendor/bootstrap/scss/utilities/_spacing.scss', - '/media/vendor/bootstrap/scss/utilities/_stretched-link.scss', - '/media/vendor/bootstrap/scss/utilities/_text.scss', - '/media/vendor/bootstrap/scss/utilities/_visibility.scss', - '/media/vendor/skipto/css/SkipTo.css', - '/media/vendor/skipto/js/dropMenu.js', - // 4.0 from Beta 7 to RC 1 - '/administrator/components/com_admin/forms/profile.xml', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-07-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-09-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-09-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-10-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-03-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-04-25.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-05-31.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-06-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-10-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-02-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-07-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-08-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-09-12.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-10-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-02-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-31.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-05-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-06-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-23.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-25.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-27.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-10-13.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-10-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-11-07.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-11-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-05-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-09-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-09-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-02-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-07.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-07-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-09-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-09-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-10-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-03-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-04-25.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-05-31.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-06-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-10-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-02-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-08-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-09-12.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-10-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-02-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-31.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-05-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-06-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-23.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-25.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-27.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-10-13.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-10-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-11-07.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-11-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-05-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-09-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-09-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-02-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-07.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-21.sql', - '/administrator/components/com_admin/src/Controller/ProfileController.php', - '/administrator/components/com_admin/src/Model/ProfileModel.php', - '/administrator/components/com_admin/src/View/Profile/HtmlView.php', - '/administrator/components/com_admin/tmpl/profile/edit.php', - '/administrator/components/com_config/tmpl/application/default_ftp.php', - '/administrator/components/com_config/tmpl/application/default_ftplogin.php', - '/administrator/components/com_csp/access.xml', - '/administrator/components/com_csp/config.xml', - '/administrator/components/com_csp/csp.xml', - '/administrator/components/com_csp/forms/filter_reports.xml', - '/administrator/components/com_csp/services/provider.php', - '/administrator/components/com_csp/src/Controller/DisplayController.php', - '/administrator/components/com_csp/src/Controller/ReportsController.php', - '/administrator/components/com_csp/src/Helper/ReporterHelper.php', - '/administrator/components/com_csp/src/Model/ReportModel.php', - '/administrator/components/com_csp/src/Model/ReportsModel.php', - '/administrator/components/com_csp/src/Table/ReportTable.php', - '/administrator/components/com_csp/src/View/Reports/HtmlView.php', - '/administrator/components/com_csp/tmpl/reports/default.php', - '/administrator/components/com_csp/tmpl/reports/default.xml', - '/administrator/components/com_fields/src/Field/SubfieldstypeField.php', - '/administrator/components/com_installer/tmpl/installer/default_ftp.php', - '/administrator/components/com_joomlaupdate/src/Helper/Select.php', - '/administrator/language/en-GB/com_csp.ini', - '/administrator/language/en-GB/com_csp.sys.ini', - '/administrator/language/en-GB/plg_fields_subfields.ini', - '/administrator/language/en-GB/plg_fields_subfields.sys.ini', - '/administrator/templates/atum/Service/HTML/Atum.php', - '/components/com_csp/src/Controller/ReportController.php', - '/components/com_menus/src/Controller/DisplayController.php', - '/libraries/vendor/algo26-matthias/idna-convert/CODE_OF_CONDUCT.md', - '/libraries/vendor/algo26-matthias/idna-convert/UPGRADING.md', - '/libraries/vendor/algo26-matthias/idna-convert/docker-compose.yml', - '/libraries/vendor/beberlei/assert/phpstan-code.neon', - '/libraries/vendor/beberlei/assert/phpstan-tests.neon', - '/libraries/vendor/bin/generate-defuse-key', - '/libraries/vendor/bin/var-dump-server', - '/libraries/vendor/bin/yaml-lint', - '/libraries/vendor/brick/math/psalm-baseline.xml', - '/libraries/vendor/doctrine/inflector/phpstan.neon.dist', - '/libraries/vendor/jakeasmith/http_build_url/readme.md', - '/libraries/vendor/nyholm/psr7/src/LowercaseTrait.php', - '/libraries/vendor/ozdemirburak/iris/LICENSE.md', - '/libraries/vendor/ozdemirburak/iris/src/BaseColor.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Factory.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Hex.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Hsl.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Hsla.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Hsv.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Rgb.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Rgba.php', - '/libraries/vendor/ozdemirburak/iris/src/Exceptions/AmbiguousColorString.php', - '/libraries/vendor/ozdemirburak/iris/src/Exceptions/InvalidColorException.php', - '/libraries/vendor/ozdemirburak/iris/src/Helpers/DefinedColor.php', - '/libraries/vendor/ozdemirburak/iris/src/Traits/AlphaTrait.php', - '/libraries/vendor/ozdemirburak/iris/src/Traits/HsTrait.php', - '/libraries/vendor/ozdemirburak/iris/src/Traits/HslTrait.php', - '/libraries/vendor/ozdemirburak/iris/src/Traits/RgbTrait.php', - '/libraries/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey', - '/libraries/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc', - '/libraries/vendor/psr/http-factory/.pullapprove.yml', - '/libraries/vendor/spomky-labs/cbor-php/.php_cs.dist', - '/libraries/vendor/spomky-labs/cbor-php/CODE_OF_CONDUCT.md', - '/libraries/vendor/spomky-labs/cbor-php/infection.json.dist', - '/libraries/vendor/spomky-labs/cbor-php/phpstan.neon', - '/libraries/vendor/typo3/phar-stream-wrapper/_config.yml', - '/libraries/vendor/voku/portable-utf8/SUMMARY.md', - '/libraries/vendor/willdurand/negotiation/src/Negotiation/Match.php', - '/media/com_actionlogs/js/admin-actionlogs-default.es6.js', - '/media/com_actionlogs/js/admin-actionlogs-default.es6.min.js', - '/media/com_actionlogs/js/admin-actionlogs-default.es6.min.js.gz', - '/media/com_associations/js/admin-associations-default.es6.js', - '/media/com_associations/js/admin-associations-default.es6.min.js', - '/media/com_associations/js/admin-associations-default.es6.min.js.gz', - '/media/com_associations/js/admin-associations-modal.es6.js', - '/media/com_associations/js/admin-associations-modal.es6.min.js', - '/media/com_associations/js/admin-associations-modal.es6.min.js.gz', - '/media/com_associations/js/associations-edit.es6.js', - '/media/com_associations/js/associations-edit.es6.min.js', - '/media/com_associations/js/associations-edit.es6.min.js.gz', - '/media/com_banners/js/admin-banner-edit.es6.js', - '/media/com_banners/js/admin-banner-edit.es6.min.js', - '/media/com_banners/js/admin-banner-edit.es6.min.js.gz', - '/media/com_cache/js/admin-cache-default.es6.js', - '/media/com_cache/js/admin-cache-default.es6.min.js', - '/media/com_cache/js/admin-cache-default.es6.min.js.gz', - '/media/com_categories/js/shared-categories-accordion.es6.js', - '/media/com_categories/js/shared-categories-accordion.es6.min.js', - '/media/com_categories/js/shared-categories-accordion.es6.min.js.gz', - '/media/com_config/js/config-default.es6.js', - '/media/com_config/js/config-default.es6.min.js', - '/media/com_config/js/config-default.es6.min.js.gz', - '/media/com_config/js/modules-default.es6.js', - '/media/com_config/js/modules-default.es6.min.js', - '/media/com_config/js/modules-default.es6.min.js.gz', - '/media/com_config/js/templates-default.es6.js', - '/media/com_config/js/templates-default.es6.min.js', - '/media/com_config/js/templates-default.es6.min.js.gz', - '/media/com_contact/js/admin-contacts-modal.es6.js', - '/media/com_contact/js/admin-contacts-modal.es6.min.js', - '/media/com_contact/js/admin-contacts-modal.es6.min.js.gz', - '/media/com_contact/js/contacts-list.es6.js', - '/media/com_contact/js/contacts-list.es6.min.js', - '/media/com_contact/js/contacts-list.es6.min.js.gz', - '/media/com_content/js/admin-article-pagebreak.es6.js', - '/media/com_content/js/admin-article-pagebreak.es6.min.js', - '/media/com_content/js/admin-article-pagebreak.es6.min.js.gz', - '/media/com_content/js/admin-article-readmore.es6.js', - '/media/com_content/js/admin-article-readmore.es6.min.js', - '/media/com_content/js/admin-article-readmore.es6.min.js.gz', - '/media/com_content/js/admin-articles-default-batch-footer.es6.js', - '/media/com_content/js/admin-articles-default-batch-footer.es6.min.js', - '/media/com_content/js/admin-articles-default-batch-footer.es6.min.js.gz', - '/media/com_content/js/admin-articles-default-stage-footer.es6.js', - '/media/com_content/js/admin-articles-default-stage-footer.es6.min.js', - '/media/com_content/js/admin-articles-default-stage-footer.es6.min.js.gz', - '/media/com_content/js/admin-articles-modal.es6.js', - '/media/com_content/js/admin-articles-modal.es6.min.js', - '/media/com_content/js/admin-articles-modal.es6.min.js.gz', - '/media/com_content/js/articles-list.es6.js', - '/media/com_content/js/articles-list.es6.min.js', - '/media/com_content/js/articles-list.es6.min.js.gz', - '/media/com_content/js/form-edit.es6.js', - '/media/com_content/js/form-edit.es6.min.js', - '/media/com_content/js/form-edit.es6.min.js.gz', - '/media/com_contenthistory/js/admin-compare-compare.es6.js', - '/media/com_contenthistory/js/admin-compare-compare.es6.min.js', - '/media/com_contenthistory/js/admin-compare-compare.es6.min.js.gz', - '/media/com_contenthistory/js/admin-history-modal.es6.js', - '/media/com_contenthistory/js/admin-history-modal.es6.min.js', - '/media/com_contenthistory/js/admin-history-modal.es6.min.js.gz', - '/media/com_contenthistory/js/admin-history-versions.es6.js', - '/media/com_contenthistory/js/admin-history-versions.es6.min.js', - '/media/com_contenthistory/js/admin-history-versions.es6.min.js.gz', - '/media/com_cpanel/js/admin-add_module.es6.js', - '/media/com_cpanel/js/admin-add_module.es6.min.js', - '/media/com_cpanel/js/admin-add_module.es6.min.js.gz', - '/media/com_cpanel/js/admin-cpanel-default.es6.js', - '/media/com_cpanel/js/admin-cpanel-default.es6.min.js', - '/media/com_cpanel/js/admin-cpanel-default.es6.min.js.gz', - '/media/com_cpanel/js/admin-system-loader.es6.js', - '/media/com_cpanel/js/admin-system-loader.es6.min.js', - '/media/com_cpanel/js/admin-system-loader.es6.min.js.gz', - '/media/com_fields/js/admin-field-changecontext.es6.js', - '/media/com_fields/js/admin-field-changecontext.es6.min.js', - '/media/com_fields/js/admin-field-changecontext.es6.min.js.gz', - '/media/com_fields/js/admin-field-edit-modal.es6.js', - '/media/com_fields/js/admin-field-edit-modal.es6.min.js', - '/media/com_fields/js/admin-field-edit-modal.es6.min.js.gz', - '/media/com_fields/js/admin-field-edit.es6.js', - '/media/com_fields/js/admin-field-edit.es6.min.js', - '/media/com_fields/js/admin-field-edit.es6.min.js.gz', - '/media/com_fields/js/admin-field-typehaschanged.es6.js', - '/media/com_fields/js/admin-field-typehaschanged.es6.min.js', - '/media/com_fields/js/admin-field-typehaschanged.es6.min.js.gz', - '/media/com_fields/js/admin-fields-default-batch.es6.js', - '/media/com_fields/js/admin-fields-default-batch.es6.min.js', - '/media/com_fields/js/admin-fields-default-batch.es6.min.js.gz', - '/media/com_fields/js/admin-fields-modal.es6.js', - '/media/com_fields/js/admin-fields-modal.es6.min.js', - '/media/com_fields/js/admin-fields-modal.es6.min.js.gz', - '/media/com_finder/js/filters.es6.js', - '/media/com_finder/js/filters.es6.min.js', - '/media/com_finder/js/filters.es6.min.js.gz', - '/media/com_finder/js/finder-edit.es6.js', - '/media/com_finder/js/finder-edit.es6.min.js', - '/media/com_finder/js/finder-edit.es6.min.js.gz', - '/media/com_finder/js/finder.es6.js', - '/media/com_finder/js/finder.es6.min.js', - '/media/com_finder/js/finder.es6.min.js.gz', - '/media/com_finder/js/index.es6.js', - '/media/com_finder/js/index.es6.min.js', - '/media/com_finder/js/index.es6.min.js.gz', - '/media/com_finder/js/indexer.es6.js', - '/media/com_finder/js/indexer.es6.min.js', - '/media/com_finder/js/indexer.es6.min.js.gz', - '/media/com_finder/js/maps.es6.js', - '/media/com_finder/js/maps.es6.min.js', - '/media/com_finder/js/maps.es6.min.js.gz', - '/media/com_installer/js/changelog.es6.js', - '/media/com_installer/js/changelog.es6.min.js', - '/media/com_installer/js/changelog.es6.min.js.gz', - '/media/com_installer/js/installer.es6.js', - '/media/com_installer/js/installer.es6.min.js', - '/media/com_installer/js/installer.es6.min.js.gz', - '/media/com_joomlaupdate/js/admin-update-default.es6.js', - '/media/com_joomlaupdate/js/admin-update-default.es6.min.js', - '/media/com_joomlaupdate/js/admin-update-default.es6.min.js.gz', - '/media/com_languages/js/admin-language-edit-change-flag.es6.js', - '/media/com_languages/js/admin-language-edit-change-flag.es6.min.js', - '/media/com_languages/js/admin-language-edit-change-flag.es6.min.js.gz', - '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.js', - '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.min.js', - '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.min.js.gz', - '/media/com_languages/js/overrider.es6.js', - '/media/com_languages/js/overrider.es6.min.js', - '/media/com_languages/js/overrider.es6.min.js.gz', - '/media/com_mails/js/admin-email-template-edit.es6.js', - '/media/com_mails/js/admin-email-template-edit.es6.min.js', - '/media/com_mails/js/admin-email-template-edit.es6.min.js.gz', - '/media/com_media/css/mediamanager.min.css', - '/media/com_media/css/mediamanager.min.css.gz', - '/media/com_media/css/mediamanager.min.css.map', - '/media/com_media/js/edit-images.es6.js', - '/media/com_media/js/edit-images.es6.min.js', - '/media/com_media/js/mediamanager.min.js', - '/media/com_media/js/mediamanager.min.js.gz', - '/media/com_media/js/mediamanager.min.js.map', - '/media/com_menus/js/admin-item-edit.es6.js', - '/media/com_menus/js/admin-item-edit.es6.min.js', - '/media/com_menus/js/admin-item-edit.es6.min.js.gz', - '/media/com_menus/js/admin-item-edit_container.es6.js', - '/media/com_menus/js/admin-item-edit_container.es6.min.js', - '/media/com_menus/js/admin-item-edit_container.es6.min.js.gz', - '/media/com_menus/js/admin-item-edit_modules.es6.js', - '/media/com_menus/js/admin-item-edit_modules.es6.min.js', - '/media/com_menus/js/admin-item-edit_modules.es6.min.js.gz', - '/media/com_menus/js/admin-item-modal.es6.js', - '/media/com_menus/js/admin-item-modal.es6.min.js', - '/media/com_menus/js/admin-item-modal.es6.min.js.gz', - '/media/com_menus/js/admin-items-modal.es6.js', - '/media/com_menus/js/admin-items-modal.es6.min.js', - '/media/com_menus/js/admin-items-modal.es6.min.js.gz', - '/media/com_menus/js/admin-menus-default.es6.js', - '/media/com_menus/js/admin-menus-default.es6.min.js', - '/media/com_menus/js/admin-menus-default.es6.min.js.gz', - '/media/com_menus/js/default-batch-body.es6.js', - '/media/com_menus/js/default-batch-body.es6.min.js', - '/media/com_menus/js/default-batch-body.es6.min.js.gz', - '/media/com_modules/js/admin-module-edit.es6.js', - '/media/com_modules/js/admin-module-edit.es6.min.js', - '/media/com_modules/js/admin-module-edit.es6.min.js.gz', - '/media/com_modules/js/admin-module-edit_assignment.es6.js', - '/media/com_modules/js/admin-module-edit_assignment.es6.min.js', - '/media/com_modules/js/admin-module-edit_assignment.es6.min.js.gz', - '/media/com_modules/js/admin-module-search.es6.js', - '/media/com_modules/js/admin-module-search.es6.min.js', - '/media/com_modules/js/admin-module-search.es6.min.js.gz', - '/media/com_modules/js/admin-modules-modal.es6.js', - '/media/com_modules/js/admin-modules-modal.es6.min.js', - '/media/com_modules/js/admin-modules-modal.es6.min.js.gz', - '/media/com_modules/js/admin-select-modal.es6.js', - '/media/com_modules/js/admin-select-modal.es6.min.js', - '/media/com_modules/js/admin-select-modal.es6.min.js.gz', - '/media/com_tags/js/tag-default.es6.js', - '/media/com_tags/js/tag-default.es6.min.js', - '/media/com_tags/js/tag-default.es6.min.js.gz', - '/media/com_tags/js/tag-list.es6.js', - '/media/com_tags/js/tag-list.es6.min.js', - '/media/com_tags/js/tag-list.es6.min.js.gz', - '/media/com_tags/js/tags-default.es6.js', - '/media/com_tags/js/tags-default.es6.min.js', - '/media/com_tags/js/tags-default.es6.min.js.gz', - '/media/com_templates/js/admin-template-compare.es6.js', - '/media/com_templates/js/admin-template-compare.es6.min.js', - '/media/com_templates/js/admin-template-compare.es6.min.js.gz', - '/media/com_templates/js/admin-template-toggle-assignment.es6.js', - '/media/com_templates/js/admin-template-toggle-assignment.es6.min.js', - '/media/com_templates/js/admin-template-toggle-assignment.es6.min.js.gz', - '/media/com_templates/js/admin-template-toggle-switch.es6.js', - '/media/com_templates/js/admin-template-toggle-switch.es6.min.js', - '/media/com_templates/js/admin-template-toggle-switch.es6.min.js.gz', - '/media/com_templates/js/admin-templates-default.es6.js', - '/media/com_templates/js/admin-templates-default.es6.min.js', - '/media/com_templates/js/admin-templates-default.es6.min.js.gz', - '/media/com_users/js/admin-users-groups.es6.js', - '/media/com_users/js/admin-users-groups.es6.min.js', - '/media/com_users/js/admin-users-groups.es6.min.js.gz', - '/media/com_users/js/admin-users-mail.es6.js', - '/media/com_users/js/admin-users-mail.es6.min.js', - '/media/com_users/js/admin-users-mail.es6.min.js.gz', - '/media/com_users/js/two-factor-switcher.es6.js', - '/media/com_users/js/two-factor-switcher.es6.min.js', - '/media/com_users/js/two-factor-switcher.es6.min.js.gz', - '/media/com_workflow/js/admin-items-workflow-buttons.es6.js', - '/media/com_workflow/js/admin-items-workflow-buttons.es6.min.js', - '/media/com_workflow/js/admin-items-workflow-buttons.es6.min.js.gz', - '/media/com_wrapper/js/iframe-height.es6.js', - '/media/com_wrapper/js/iframe-height.es6.min.js', - '/media/com_wrapper/js/iframe-height.es6.min.js.gz', - '/media/layouts/js/joomla/form/field/category-change.es6.js', - '/media/layouts/js/joomla/form/field/category-change.es6.min.js', - '/media/layouts/js/joomla/form/field/category-change.es6.min.js.gz', - '/media/layouts/js/joomla/html/batch/batch-copymove.es6.js', - '/media/layouts/js/joomla/html/batch/batch-copymove.es6.min.js', - '/media/layouts/js/joomla/html/batch/batch-copymove.es6.min.js.gz', - '/media/legacy/js/highlighter.js', - '/media/legacy/js/highlighter.min.js', - '/media/legacy/js/highlighter.min.js.gz', - '/media/mod_login/js/admin-login.es6.js', - '/media/mod_login/js/admin-login.es6.min.js', - '/media/mod_login/js/admin-login.es6.min.js.gz', - '/media/mod_menu/js/admin-menu.es6.js', - '/media/mod_menu/js/admin-menu.es6.min.js', - '/media/mod_menu/js/admin-menu.es6.min.js.gz', - '/media/mod_menu/js/menu.es6.js', - '/media/mod_menu/js/menu.es6.min.js', - '/media/mod_menu/js/menu.es6.min.js.gz', - '/media/mod_multilangstatus/js/admin-multilangstatus.es6.js', - '/media/mod_multilangstatus/js/admin-multilangstatus.es6.min.js', - '/media/mod_multilangstatus/js/admin-multilangstatus.es6.min.js.gz', - '/media/mod_quickicon/js/quickicon.es6.js', - '/media/mod_quickicon/js/quickicon.es6.min.js', - '/media/mod_quickicon/js/quickicon.es6.min.js.gz', - '/media/mod_sampledata/js/sampledata-process.es6.js', - '/media/mod_sampledata/js/sampledata-process.es6.min.js', - '/media/mod_sampledata/js/sampledata-process.es6.min.js.gz', - '/media/plg_captcha_recaptcha/js/recaptcha.es6.js', - '/media/plg_captcha_recaptcha/js/recaptcha.es6.min.js', - '/media/plg_captcha_recaptcha/js/recaptcha.es6.min.js.gz', - '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.js', - '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.min.js', - '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.min.js.gz', - '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.js', - '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.min.js', - '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.min.js.gz', - '/media/plg_editors_tinymce/js/tinymce-builder.es6.js', - '/media/plg_editors_tinymce/js/tinymce-builder.es6.min.js', - '/media/plg_editors_tinymce/js/tinymce-builder.es6.min.js.gz', - '/media/plg_editors_tinymce/js/tinymce.es6.js', - '/media/plg_editors_tinymce/js/tinymce.es6.min.js', - '/media/plg_editors_tinymce/js/tinymce.es6.min.js.gz', - '/media/plg_installer_folderinstaller/js/folderinstaller.es6.js', - '/media/plg_installer_folderinstaller/js/folderinstaller.es6.min.js', - '/media/plg_installer_folderinstaller/js/folderinstaller.es6.min.js.gz', - '/media/plg_installer_packageinstaller/js/packageinstaller.es6.js', - '/media/plg_installer_packageinstaller/js/packageinstaller.es6.min.js', - '/media/plg_installer_packageinstaller/js/packageinstaller.es6.min.js.gz', - '/media/plg_installer_urlinstaller/js/urlinstaller.es6.js', - '/media/plg_installer_urlinstaller/js/urlinstaller.es6.min.js', - '/media/plg_installer_urlinstaller/js/urlinstaller.es6.min.js.gz', - '/media/plg_installer_webinstaller/js/client.es6.js', - '/media/plg_installer_webinstaller/js/client.es6.min.js', - '/media/plg_installer_webinstaller/js/client.es6.min.js.gz', - '/media/plg_media-action_crop/js/crop.es6.js', - '/media/plg_media-action_crop/js/crop.es6.min.js', - '/media/plg_media-action_crop/js/crop.es6.min.js.gz', - '/media/plg_media-action_resize/js/resize.es6.js', - '/media/plg_media-action_resize/js/resize.es6.min.js', - '/media/plg_media-action_resize/js/resize.es6.min.js.gz', - '/media/plg_media-action_rotate/js/rotate.es6.js', - '/media/plg_media-action_rotate/js/rotate.es6.min.js', - '/media/plg_media-action_rotate/js/rotate.es6.min.js.gz', - '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.js', - '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.min.js', - '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.min.js.gz', - '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.js', - '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.min.js', - '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.min.js.gz', - '/media/plg_quickicon_overridecheck/js/overridecheck.es6.js', - '/media/plg_quickicon_overridecheck/js/overridecheck.es6.min.js', - '/media/plg_quickicon_overridecheck/js/overridecheck.es6.min.js.gz', - '/media/plg_quickicon_privacycheck/js/privacycheck.es6.js', - '/media/plg_quickicon_privacycheck/js/privacycheck.es6.min.js', - '/media/plg_quickicon_privacycheck/js/privacycheck.es6.min.js.gz', - '/media/plg_system_debug/js/debug.es6.js', - '/media/plg_system_debug/js/debug.es6.min.js', - '/media/plg_system_debug/js/debug.es6.min.js.gz', - '/media/plg_system_highlight/highlight.min.css', - '/media/plg_system_highlight/highlight.min.css.gz', - '/media/plg_system_stats/js/stats-message.es6.js', - '/media/plg_system_stats/js/stats-message.es6.min.js', - '/media/plg_system_stats/js/stats-message.es6.min.js.gz', - '/media/plg_system_stats/js/stats.es6.js', - '/media/plg_system_stats/js/stats.es6.min.js', - '/media/plg_system_stats/js/stats.es6.min.js.gz', - '/media/plg_system_webauthn/js/login.es6.js', - '/media/plg_system_webauthn/js/login.es6.min.js', - '/media/plg_system_webauthn/js/login.es6.min.js.gz', - '/media/plg_system_webauthn/js/management.es6.js', - '/media/plg_system_webauthn/js/management.es6.min.js', - '/media/plg_system_webauthn/js/management.es6.min.js.gz', - '/media/plg_user_token/js/token.es6.js', - '/media/plg_user_token/js/token.es6.min.js', - '/media/plg_user_token/js/token.es6.min.js.gz', - '/media/system/js/core.es6.js', - '/media/system/js/core.es6.min.js', - '/media/system/js/core.es6.min.js.gz', - '/media/system/js/draggable.es6.js', - '/media/system/js/draggable.es6.min.js', - '/media/system/js/draggable.es6.min.js.gz', - '/media/system/js/fields/joomla-field-color-slider.es6.js', - '/media/system/js/fields/joomla-field-color-slider.es6.min.js', - '/media/system/js/fields/joomla-field-color-slider.es6.min.js.gz', - '/media/system/js/fields/passwordstrength.es6.js', - '/media/system/js/fields/passwordstrength.es6.min.js', - '/media/system/js/fields/passwordstrength.es6.min.js.gz', - '/media/system/js/fields/passwordview.es6.js', - '/media/system/js/fields/passwordview.es6.min.js', - '/media/system/js/fields/passwordview.es6.min.js.gz', - '/media/system/js/fields/select-colour.es6.js', - '/media/system/js/fields/select-colour.es6.min.js', - '/media/system/js/fields/select-colour.es6.min.js.gz', - '/media/system/js/fields/validate.es6.js', - '/media/system/js/fields/validate.es6.min.js', - '/media/system/js/fields/validate.es6.min.js.gz', - '/media/system/js/keepalive.es6.js', - '/media/system/js/keepalive.es6.min.js', - '/media/system/js/keepalive.es6.min.js.gz', - '/media/system/js/multiselect.es6.js', - '/media/system/js/multiselect.es6.min.js', - '/media/system/js/multiselect.es6.min.js.gz', - '/media/system/js/searchtools.es6.js', - '/media/system/js/searchtools.es6.min.js', - '/media/system/js/searchtools.es6.min.js.gz', - '/media/system/js/showon.es6.js', - '/media/system/js/showon.es6.min.js', - '/media/system/js/showon.es6.min.js.gz', - '/media/templates/atum/js/template.es6.js', - '/media/templates/atum/js/template.es6.min.js', - '/media/templates/atum/js/template.es6.min.js.gz', - '/media/templates/atum/js/template.js', - '/media/templates/atum/js/template.min.js', - '/media/templates/atum/js/template.min.js.gz', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.min.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.min.js.gz', - '/media/vendor/bootstrap/js/alert.es6.js', - '/media/vendor/bootstrap/js/alert.es6.min.js', - '/media/vendor/bootstrap/js/alert.es6.min.js.gz', - '/media/vendor/bootstrap/js/bootstrap.es5.js', - '/media/vendor/bootstrap/js/bootstrap.es5.min.js', - '/media/vendor/bootstrap/js/bootstrap.es5.min.js.gz', - '/media/vendor/bootstrap/js/button.es6.js', - '/media/vendor/bootstrap/js/button.es6.min.js', - '/media/vendor/bootstrap/js/button.es6.min.js.gz', - '/media/vendor/bootstrap/js/carousel.es6.js', - '/media/vendor/bootstrap/js/carousel.es6.min.js', - '/media/vendor/bootstrap/js/carousel.es6.min.js.gz', - '/media/vendor/bootstrap/js/collapse.es6.js', - '/media/vendor/bootstrap/js/collapse.es6.min.js', - '/media/vendor/bootstrap/js/collapse.es6.min.js.gz', - '/media/vendor/bootstrap/js/dom-8eef6b5f.js', - '/media/vendor/bootstrap/js/dropdown.es6.js', - '/media/vendor/bootstrap/js/dropdown.es6.min.js', - '/media/vendor/bootstrap/js/dropdown.es6.min.js.gz', - '/media/vendor/bootstrap/js/modal.es6.js', - '/media/vendor/bootstrap/js/modal.es6.min.js', - '/media/vendor/bootstrap/js/modal.es6.min.js.gz', - '/media/vendor/bootstrap/js/popover.es6.js', - '/media/vendor/bootstrap/js/popover.es6.min.js', - '/media/vendor/bootstrap/js/popover.es6.min.js.gz', - '/media/vendor/bootstrap/js/popper-5304749a.js', - '/media/vendor/bootstrap/js/scrollspy.es6.js', - '/media/vendor/bootstrap/js/scrollspy.es6.min.js', - '/media/vendor/bootstrap/js/scrollspy.es6.min.js.gz', - '/media/vendor/bootstrap/js/tab.es6.js', - '/media/vendor/bootstrap/js/tab.es6.min.js', - '/media/vendor/bootstrap/js/tab.es6.min.js.gz', - '/media/vendor/bootstrap/js/toast.es6.js', - '/media/vendor/bootstrap/js/toast.es6.min.js', - '/media/vendor/bootstrap/js/toast.es6.min.js.gz', - '/media/vendor/codemirror/lib/codemirror-ce.js', - '/media/vendor/codemirror/lib/codemirror-ce.min.js', - '/media/vendor/codemirror/lib/codemirror-ce.min.js.gz', - '/media/vendor/punycode/js/punycode.js', - '/media/vendor/punycode/js/punycode.min.js', - '/media/vendor/punycode/js/punycode.min.js.gz', - '/media/vendor/tinymce/changelog.txt', - '/media/vendor/webcomponentsjs/js/webcomponents-ce.js', - '/media/vendor/webcomponentsjs/js/webcomponents-ce.min.js', - '/media/vendor/webcomponentsjs/js/webcomponents-ce.min.js.gz', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.min.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.min.js.gz', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.min.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.min.js.gz', - '/media/vendor/webcomponentsjs/js/webcomponents-sd.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd.min.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd.min.js.gz', - '/plugins/fields/subfields/params/subfields.xml', - '/plugins/fields/subfields/subfields.php', - '/plugins/fields/subfields/subfields.xml', - '/plugins/fields/subfields/tmpl/subfields.php', - '/templates/cassiopeia/images/system/rating_star.png', - '/templates/cassiopeia/images/system/rating_star_blank.png', - '/templates/cassiopeia/scss/tools/mixins/_margin.scss', - '/templates/cassiopeia/scss/tools/mixins/_visually-hidden.scss', - '/templates/system/js/error-locales.js', - // 4.0 from RC 1 to RC 2 - '/administrator/components/com_fields/tmpl/field/modal.php', - '/administrator/templates/atum/scss/pages/_com_admin.scss', - '/administrator/templates/atum/scss/pages/_com_finder.scss', - '/libraries/src/Error/JsonApi/InstallLanguageExceptionHandler.php', - '/libraries/src/MVC/Controller/Exception/InstallLanguage.php', - '/media/com_fields/js/admin-field-edit-modal-es5.js', - '/media/com_fields/js/admin-field-edit-modal-es5.min.js', - '/media/com_fields/js/admin-field-edit-modal-es5.min.js.gz', - '/media/com_fields/js/admin-field-edit-modal.js', - '/media/com_fields/js/admin-field-edit-modal.min.js', - '/media/com_fields/js/admin-field-edit-modal.min.js.gz', - // 4.0 from RC 3 to RC 4 - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_nodownload.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_noupdate.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_preupdatecheck.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_reinstall.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_update.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_updatemefirst.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_upload.php', - '/language/en-GB/com_messages.ini', - '/media/system/css/fields/joomla-image-select.css', - '/media/system/css/fields/joomla-image-select.min.css', - '/media/system/css/fields/joomla-image-select.min.css.gz', - '/media/system/js/fields/joomla-image-select-es5.js', - '/media/system/js/fields/joomla-image-select-es5.min.js', - '/media/system/js/fields/joomla-image-select-es5.min.js.gz', - '/media/system/js/fields/joomla-image-select.js', - '/media/system/js/fields/joomla-image-select.min.js', - '/media/system/js/fields/joomla-image-select.min.js.gz', - // 4.0 from RC 4 to RC 5 - '/media/system/js/fields/calendar-locales/af.min.js', - '/media/system/js/fields/calendar-locales/af.min.js.gz', - '/media/system/js/fields/calendar-locales/ar.min.js', - '/media/system/js/fields/calendar-locales/ar.min.js.gz', - '/media/system/js/fields/calendar-locales/bg.min.js', - '/media/system/js/fields/calendar-locales/bg.min.js.gz', - '/media/system/js/fields/calendar-locales/bn.min.js', - '/media/system/js/fields/calendar-locales/bn.min.js.gz', - '/media/system/js/fields/calendar-locales/bs.min.js', - '/media/system/js/fields/calendar-locales/bs.min.js.gz', - '/media/system/js/fields/calendar-locales/ca.min.js', - '/media/system/js/fields/calendar-locales/ca.min.js.gz', - '/media/system/js/fields/calendar-locales/cs.min.js', - '/media/system/js/fields/calendar-locales/cs.min.js.gz', - '/media/system/js/fields/calendar-locales/cy.min.js', - '/media/system/js/fields/calendar-locales/cy.min.js.gz', - '/media/system/js/fields/calendar-locales/da.min.js', - '/media/system/js/fields/calendar-locales/da.min.js.gz', - '/media/system/js/fields/calendar-locales/de.min.js', - '/media/system/js/fields/calendar-locales/de.min.js.gz', - '/media/system/js/fields/calendar-locales/el.min.js', - '/media/system/js/fields/calendar-locales/el.min.js.gz', - '/media/system/js/fields/calendar-locales/en.min.js', - '/media/system/js/fields/calendar-locales/en.min.js.gz', - '/media/system/js/fields/calendar-locales/es.min.js', - '/media/system/js/fields/calendar-locales/es.min.js.gz', - '/media/system/js/fields/calendar-locales/eu.min.js', - '/media/system/js/fields/calendar-locales/eu.min.js.gz', - '/media/system/js/fields/calendar-locales/fa-ir.min.js', - '/media/system/js/fields/calendar-locales/fa-ir.min.js.gz', - '/media/system/js/fields/calendar-locales/fi.min.js', - '/media/system/js/fields/calendar-locales/fi.min.js.gz', - '/media/system/js/fields/calendar-locales/fr.min.js', - '/media/system/js/fields/calendar-locales/fr.min.js.gz', - '/media/system/js/fields/calendar-locales/ga.min.js', - '/media/system/js/fields/calendar-locales/ga.min.js.gz', - '/media/system/js/fields/calendar-locales/hr.min.js', - '/media/system/js/fields/calendar-locales/hr.min.js.gz', - '/media/system/js/fields/calendar-locales/hu.min.js', - '/media/system/js/fields/calendar-locales/hu.min.js.gz', - '/media/system/js/fields/calendar-locales/it.min.js', - '/media/system/js/fields/calendar-locales/it.min.js.gz', - '/media/system/js/fields/calendar-locales/ja.min.js', - '/media/system/js/fields/calendar-locales/ja.min.js.gz', - '/media/system/js/fields/calendar-locales/ka.min.js', - '/media/system/js/fields/calendar-locales/ka.min.js.gz', - '/media/system/js/fields/calendar-locales/kk.min.js', - '/media/system/js/fields/calendar-locales/kk.min.js.gz', - '/media/system/js/fields/calendar-locales/ko.min.js', - '/media/system/js/fields/calendar-locales/ko.min.js.gz', - '/media/system/js/fields/calendar-locales/lt.min.js', - '/media/system/js/fields/calendar-locales/lt.min.js.gz', - '/media/system/js/fields/calendar-locales/mk.min.js', - '/media/system/js/fields/calendar-locales/mk.min.js.gz', - '/media/system/js/fields/calendar-locales/nb.min.js', - '/media/system/js/fields/calendar-locales/nb.min.js.gz', - '/media/system/js/fields/calendar-locales/nl.min.js', - '/media/system/js/fields/calendar-locales/nl.min.js.gz', - '/media/system/js/fields/calendar-locales/pl.min.js', - '/media/system/js/fields/calendar-locales/pl.min.js.gz', - '/media/system/js/fields/calendar-locales/prs-af.min.js', - '/media/system/js/fields/calendar-locales/prs-af.min.js.gz', - '/media/system/js/fields/calendar-locales/pt.min.js', - '/media/system/js/fields/calendar-locales/pt.min.js.gz', - '/media/system/js/fields/calendar-locales/ru.min.js', - '/media/system/js/fields/calendar-locales/ru.min.js.gz', - '/media/system/js/fields/calendar-locales/sk.min.js', - '/media/system/js/fields/calendar-locales/sk.min.js.gz', - '/media/system/js/fields/calendar-locales/sl.min.js', - '/media/system/js/fields/calendar-locales/sl.min.js.gz', - '/media/system/js/fields/calendar-locales/sr-rs.min.js', - '/media/system/js/fields/calendar-locales/sr-rs.min.js.gz', - '/media/system/js/fields/calendar-locales/sr-yu.min.js', - '/media/system/js/fields/calendar-locales/sr-yu.min.js.gz', - '/media/system/js/fields/calendar-locales/sv.min.js', - '/media/system/js/fields/calendar-locales/sv.min.js.gz', - '/media/system/js/fields/calendar-locales/sw.min.js', - '/media/system/js/fields/calendar-locales/sw.min.js.gz', - '/media/system/js/fields/calendar-locales/ta.min.js', - '/media/system/js/fields/calendar-locales/ta.min.js.gz', - '/media/system/js/fields/calendar-locales/th.min.js', - '/media/system/js/fields/calendar-locales/th.min.js.gz', - '/media/system/js/fields/calendar-locales/uk.min.js', - '/media/system/js/fields/calendar-locales/uk.min.js.gz', - '/media/system/js/fields/calendar-locales/zh-CN.min.js', - '/media/system/js/fields/calendar-locales/zh-CN.min.js.gz', - '/media/system/js/fields/calendar-locales/zh-TW.min.js', - '/media/system/js/fields/calendar-locales/zh-TW.min.js.gz', - // 4.0 from RC 5 to RC 6 - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.min.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.min.js.gz', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.min.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.min.js.gz', - '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.css', - '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.min.css', - '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.min.css.gz', - '/templates/cassiopeia/scss/vendor/fontawesome-free/fontawesome.scss', - // 4.0 from RC 6 to 4.0.0 (stable) - '/libraries/vendor/algo26-matthias/idna-convert/tests/integration/ToIdnTest.php', - '/libraries/vendor/algo26-matthias/idna-convert/tests/integration/ToUnicodeTest.php', - '/libraries/vendor/algo26-matthias/idna-convert/tests/unit/.gitkeep', - '/libraries/vendor/algo26-matthias/idna-convert/tests/unit/namePrepTest.php', - '/libraries/vendor/doctrine/inflector/docs/en/index.rst', - '/libraries/vendor/jakeasmith/http_build_url/tests/HttpBuildUrlTest.php', - '/libraries/vendor/jakeasmith/http_build_url/tests/bootstrap.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/AcceptLanguageTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/AcceptTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/BaseAcceptTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/CharsetNegotiatorTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/EncodingNegotiatorTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/LanguageNegotiatorTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/MatchTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/NegotiatorTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/TestCase.php', - '/libraries/vendor/willdurand/negotiation/tests/bootstrap.php', - // From 4.0.2 to 4.0.3 - '/templates/cassiopeia/css/global/fonts-web_fira-sans.css', - '/templates/cassiopeia/css/global/fonts-web_fira-sans.min.css', - '/templates/cassiopeia/css/global/fonts-web_fira-sans.min.css.gz', - '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.css', - '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.min.css', - '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.min.css.gz', - '/templates/cassiopeia/scss/global/fonts-web_fira-sans.scss', - '/templates/cassiopeia/scss/global/fonts-web_roboto+noto-sans.scss', - // From 4.0.3 to 4.0.4 - '/administrator/templates/atum/scss/_mixin.scss', - '/media/com_joomlaupdate/js/encryption.min.js.gz', - '/media/com_joomlaupdate/js/update.min.js.gz', - '/templates/cassiopeia/images/system/sort_asc.png', - '/templates/cassiopeia/images/system/sort_desc.png', - // From 4.0.4 to 4.0.5 - '/media/vendor/codemirror/lib/#codemirror.js#', - // From 4.0.5 to 4.0.6 - '/media/vendor/mediaelement/css/mejs-controls.png', - // From 4.0.x to 4.1.0-beta1 - '/administrator/templates/atum/css/system/searchtools/searchtools.css', - '/administrator/templates/atum/css/system/searchtools/searchtools.min.css', - '/administrator/templates/atum/css/system/searchtools/searchtools.min.css.gz', - '/administrator/templates/atum/css/template-rtl.css', - '/administrator/templates/atum/css/template-rtl.min.css', - '/administrator/templates/atum/css/template-rtl.min.css.gz', - '/administrator/templates/atum/css/template.css', - '/administrator/templates/atum/css/template.min.css', - '/administrator/templates/atum/css/template.min.css.gz', - '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.css', - '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.min.css', - '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.min.css.gz', - '/administrator/templates/atum/css/vendor/choicesjs/choices.css', - '/administrator/templates/atum/css/vendor/choicesjs/choices.min.css', - '/administrator/templates/atum/css/vendor/choicesjs/choices.min.css.gz', - '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.css', - '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.min.css', - '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.min.css.gz', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.css', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.min.css', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.min.css.gz', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.css', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.min.css', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.min.css.gz', - '/administrator/templates/atum/css/vendor/minicolors/minicolors.css', - '/administrator/templates/atum/css/vendor/minicolors/minicolors.min.css', - '/administrator/templates/atum/css/vendor/minicolors/minicolors.min.css.gz', - '/administrator/templates/atum/images/joomla-pattern.svg', - '/administrator/templates/atum/images/logos/brand-large.svg', - '/administrator/templates/atum/images/logos/brand-small.svg', - '/administrator/templates/atum/images/logos/login.svg', - '/administrator/templates/atum/images/select-bg-active-rtl.svg', - '/administrator/templates/atum/images/select-bg-active.svg', - '/administrator/templates/atum/images/select-bg-rtl.svg', - '/administrator/templates/atum/images/select-bg.svg', - '/administrator/templates/atum/scss/_root.scss', - '/administrator/templates/atum/scss/_variables.scss', - '/administrator/templates/atum/scss/blocks/_alerts.scss', - '/administrator/templates/atum/scss/blocks/_edit.scss', - '/administrator/templates/atum/scss/blocks/_form.scss', - '/administrator/templates/atum/scss/blocks/_global.scss', - '/administrator/templates/atum/scss/blocks/_header.scss', - '/administrator/templates/atum/scss/blocks/_icons.scss', - '/administrator/templates/atum/scss/blocks/_iframe.scss', - '/administrator/templates/atum/scss/blocks/_layout.scss', - '/administrator/templates/atum/scss/blocks/_lists.scss', - '/administrator/templates/atum/scss/blocks/_login.scss', - '/administrator/templates/atum/scss/blocks/_modals.scss', - '/administrator/templates/atum/scss/blocks/_quickicons.scss', - '/administrator/templates/atum/scss/blocks/_sidebar-nav.scss', - '/administrator/templates/atum/scss/blocks/_sidebar.scss', - '/administrator/templates/atum/scss/blocks/_switcher.scss', - '/administrator/templates/atum/scss/blocks/_toolbar.scss', - '/administrator/templates/atum/scss/blocks/_treeselect.scss', - '/administrator/templates/atum/scss/blocks/_utilities.scss', - '/administrator/templates/atum/scss/pages/_com_config.scss', - '/administrator/templates/atum/scss/pages/_com_content.scss', - '/administrator/templates/atum/scss/pages/_com_cpanel.scss', - '/administrator/templates/atum/scss/pages/_com_joomlaupdate.scss', - '/administrator/templates/atum/scss/pages/_com_modules.scss', - '/administrator/templates/atum/scss/pages/_com_privacy.scss', - '/administrator/templates/atum/scss/pages/_com_tags.scss', - '/administrator/templates/atum/scss/pages/_com_templates.scss', - '/administrator/templates/atum/scss/pages/_com_users.scss', - '/administrator/templates/atum/scss/system/searchtools/searchtools.scss', - '/administrator/templates/atum/scss/template-rtl.scss', - '/administrator/templates/atum/scss/template.scss', - '/administrator/templates/atum/scss/vendor/_bootstrap.scss', - '/administrator/templates/atum/scss/vendor/_codemirror.scss', - '/administrator/templates/atum/scss/vendor/_dragula.scss', - '/administrator/templates/atum/scss/vendor/_tinymce.scss', - '/administrator/templates/atum/scss/vendor/awesomplete/awesomplete.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_badge.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_bootstrap-rtl.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_buttons.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_card.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_collapse.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_custom-forms.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_dropdown.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_form.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_lists.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_modal.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_pagination.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_reboot.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_table.scss', - '/administrator/templates/atum/scss/vendor/choicesjs/choices.scss', - '/administrator/templates/atum/scss/vendor/fontawesome-free/fontawesome.scss', - '/administrator/templates/atum/scss/vendor/joomla-custom-elements/joomla-alert.scss', - '/administrator/templates/atum/scss/vendor/joomla-custom-elements/joomla-tab.scss', - '/administrator/templates/atum/scss/vendor/minicolors/minicolors.scss', - '/administrator/templates/atum/template_preview.png', - '/administrator/templates/atum/template_thumbnail.png', - '/administrator/templates/system/css/error.css', - '/administrator/templates/system/css/error.min.css', - '/administrator/templates/system/css/error.min.css.gz', - '/administrator/templates/system/css/system.css', - '/administrator/templates/system/css/system.min.css', - '/administrator/templates/system/css/system.min.css.gz', - '/administrator/templates/system/images/calendar.png', - '/administrator/templates/system/scss/error.scss', - '/administrator/templates/system/scss/system.scss', - '/templates/cassiopeia/css/editor.css', - '/templates/cassiopeia/css/editor.min.css', - '/templates/cassiopeia/css/editor.min.css.gz', - '/templates/cassiopeia/css/global/colors_alternative.css', - '/templates/cassiopeia/css/global/colors_alternative.min.css', - '/templates/cassiopeia/css/global/colors_alternative.min.css.gz', - '/templates/cassiopeia/css/global/colors_standard.css', - '/templates/cassiopeia/css/global/colors_standard.min.css', - '/templates/cassiopeia/css/global/colors_standard.min.css.gz', - '/templates/cassiopeia/css/global/fonts-local_roboto.css', - '/templates/cassiopeia/css/global/fonts-local_roboto.min.css', - '/templates/cassiopeia/css/global/fonts-local_roboto.min.css.gz', - '/templates/cassiopeia/css/offline.css', - '/templates/cassiopeia/css/offline.min.css', - '/templates/cassiopeia/css/offline.min.css.gz', - '/templates/cassiopeia/css/system/searchtools/searchtools.css', - '/templates/cassiopeia/css/system/searchtools/searchtools.min.css', - '/templates/cassiopeia/css/system/searchtools/searchtools.min.css.gz', - '/templates/cassiopeia/css/template-rtl.css', - '/templates/cassiopeia/css/template-rtl.min.css', - '/templates/cassiopeia/css/template-rtl.min.css.gz', - '/templates/cassiopeia/css/template.css', - '/templates/cassiopeia/css/template.min.css', - '/templates/cassiopeia/css/template.min.css.gz', - '/templates/cassiopeia/css/vendor/choicesjs/choices.css', - '/templates/cassiopeia/css/vendor/choicesjs/choices.min.css', - '/templates/cassiopeia/css/vendor/choicesjs/choices.min.css.gz', - '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.css', - '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.min.css', - '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.min.css.gz', - '/templates/cassiopeia/images/logo.svg', - '/templates/cassiopeia/images/select-bg-active-rtl.svg', - '/templates/cassiopeia/images/select-bg-active.svg', - '/templates/cassiopeia/images/select-bg-rtl.svg', - '/templates/cassiopeia/images/select-bg.svg', - '/templates/cassiopeia/js/template.es5.js', - '/templates/cassiopeia/js/template.js', - '/templates/cassiopeia/js/template.min.js', - '/templates/cassiopeia/js/template.min.js.gz', - '/templates/cassiopeia/scss/blocks/_alerts.scss', - '/templates/cassiopeia/scss/blocks/_back-to-top.scss', - '/templates/cassiopeia/scss/blocks/_banner.scss', - '/templates/cassiopeia/scss/blocks/_css-grid.scss', - '/templates/cassiopeia/scss/blocks/_footer.scss', - '/templates/cassiopeia/scss/blocks/_form.scss', - '/templates/cassiopeia/scss/blocks/_frontend-edit.scss', - '/templates/cassiopeia/scss/blocks/_global.scss', - '/templates/cassiopeia/scss/blocks/_header.scss', - '/templates/cassiopeia/scss/blocks/_icons.scss', - '/templates/cassiopeia/scss/blocks/_iframe.scss', - '/templates/cassiopeia/scss/blocks/_layout.scss', - '/templates/cassiopeia/scss/blocks/_legacy.scss', - '/templates/cassiopeia/scss/blocks/_modals.scss', - '/templates/cassiopeia/scss/blocks/_modifiers.scss', - '/templates/cassiopeia/scss/blocks/_tags.scss', - '/templates/cassiopeia/scss/blocks/_toolbar.scss', - '/templates/cassiopeia/scss/blocks/_utilities.scss', - '/templates/cassiopeia/scss/editor.scss', - '/templates/cassiopeia/scss/global/colors_alternative.scss', - '/templates/cassiopeia/scss/global/colors_standard.scss', - '/templates/cassiopeia/scss/global/fonts-local_roboto.scss', - '/templates/cassiopeia/scss/offline.scss', - '/templates/cassiopeia/scss/system/searchtools/searchtools.scss', - '/templates/cassiopeia/scss/template-rtl.scss', - '/templates/cassiopeia/scss/template.scss', - '/templates/cassiopeia/scss/tools/_tools.scss', - '/templates/cassiopeia/scss/tools/functions/_max-width.scss', - '/templates/cassiopeia/scss/tools/variables/_variables.scss', - '/templates/cassiopeia/scss/vendor/_awesomplete.scss', - '/templates/cassiopeia/scss/vendor/_chosen.scss', - '/templates/cassiopeia/scss/vendor/_dragula.scss', - '/templates/cassiopeia/scss/vendor/_minicolors.scss', - '/templates/cassiopeia/scss/vendor/_tinymce.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_bootstrap-rtl.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_buttons.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_collapse.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_custom-forms.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_dropdown.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_forms.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_lists.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_modal.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_nav.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_pagination.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_table.scss', - '/templates/cassiopeia/scss/vendor/choicesjs/choices.scss', - '/templates/cassiopeia/scss/vendor/joomla-custom-elements/joomla-alert.scss', - '/templates/cassiopeia/scss/vendor/metismenu/_metismenu.scss', - '/templates/cassiopeia/template_preview.png', - '/templates/cassiopeia/template_thumbnail.png', - '/templates/system/css/editor.css', - '/templates/system/css/editor.min.css', - '/templates/system/css/editor.min.css.gz', - '/templates/system/css/error.css', - '/templates/system/css/error.min.css', - '/templates/system/css/error.min.css.gz', - '/templates/system/css/error_rtl.css', - '/templates/system/css/error_rtl.min.css', - '/templates/system/css/error_rtl.min.css.gz', - '/templates/system/css/general.css', - '/templates/system/css/general.min.css', - '/templates/system/css/general.min.css.gz', - '/templates/system/css/offline.css', - '/templates/system/css/offline.min.css', - '/templates/system/css/offline.min.css.gz', - '/templates/system/css/offline_rtl.css', - '/templates/system/css/offline_rtl.min.css', - '/templates/system/css/offline_rtl.min.css.gz', - '/templates/system/scss/editor.scss', - '/templates/system/scss/error.scss', - '/templates/system/scss/error_rtl.scss', - '/templates/system/scss/general.scss', - '/templates/system/scss/offline.scss', - '/templates/system/scss/offline_rtl.scss', - // From 4.1.0-beta3 to 4.1.0-rc1 - '/api/components/com_media/src/Helper/AdapterTrait.php', - // From 4.1.0 to 4.1.1 - '/libraries/vendor/tobscure/json-api/.git/HEAD', - '/libraries/vendor/tobscure/json-api/.git/ORIG_HEAD', - '/libraries/vendor/tobscure/json-api/.git/config', - '/libraries/vendor/tobscure/json-api/.git/description', - '/libraries/vendor/tobscure/json-api/.git/hooks/applypatch-msg.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/commit-msg.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/fsmonitor-watchman.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/post-update.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-applypatch.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-commit.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-merge-commit.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-push.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-rebase.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-receive.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/prepare-commit-msg.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/push-to-checkout.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/update.sample', - '/libraries/vendor/tobscure/json-api/.git/index', - '/libraries/vendor/tobscure/json-api/.git/info/exclude', - '/libraries/vendor/tobscure/json-api/.git/info/refs', - '/libraries/vendor/tobscure/json-api/.git/logs/HEAD', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/heads/joomla-backports', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes/origin/HEAD', - '/libraries/vendor/tobscure/json-api/.git/objects/info/packs', - '/libraries/vendor/tobscure/json-api/.git/objects/pack/pack-51530cba04703b17f3c11b9e8458a171092cf5e3.idx', - '/libraries/vendor/tobscure/json-api/.git/objects/pack/pack-51530cba04703b17f3c11b9e8458a171092cf5e3.pack', - '/libraries/vendor/tobscure/json-api/.git/packed-refs', - '/libraries/vendor/tobscure/json-api/.git/refs/heads/joomla-backports', - '/libraries/vendor/tobscure/json-api/.git/refs/remotes/origin/HEAD', - '/libraries/vendor/tobscure/json-api/.php_cs', - '/libraries/vendor/tobscure/json-api/tests/AbstractSerializerTest.php', - '/libraries/vendor/tobscure/json-api/tests/AbstractTestCase.php', - '/libraries/vendor/tobscure/json-api/tests/CollectionTest.php', - '/libraries/vendor/tobscure/json-api/tests/DocumentTest.php', - '/libraries/vendor/tobscure/json-api/tests/ErrorHandlerTest.php', - '/libraries/vendor/tobscure/json-api/tests/Exception/Handler/FallbackExceptionHandlerTest.php', - '/libraries/vendor/tobscure/json-api/tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php', - '/libraries/vendor/tobscure/json-api/tests/LinksTraitTest.php', - '/libraries/vendor/tobscure/json-api/tests/ParametersTest.php', - '/libraries/vendor/tobscure/json-api/tests/ResourceTest.php', - '/libraries/vendor/tobscure/json-api/tests/UtilTest.php', - // From 4.1.1 to 4.1.2 - '/administrator/components/com_users/src/Field/PrimaryauthprovidersField.php', - // From 4.1.2 to 4.1.3 - '/libraries/vendor/webmozart/assert/.php_cs', - // From 4.1.3 to 4.1.4 - '/libraries/vendor/maximebf/debugbar/.bowerrc', - '/libraries/vendor/maximebf/debugbar/bower.json', - '/libraries/vendor/maximebf/debugbar/build/namespaceFontAwesome.php', - '/libraries/vendor/maximebf/debugbar/demo/ajax.php', - '/libraries/vendor/maximebf/debugbar/demo/ajax_exception.php', - '/libraries/vendor/maximebf/debugbar/demo/bootstrap.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/cachecache/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/bootstrap.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/build.sh', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/cli-config.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src/Demo/Product.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/monolog/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/build.properties', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/build.sh', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/runtime-conf.xml', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/schema.xml', - '/libraries/vendor/maximebf/debugbar/demo/bridge/slim/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/swiftmailer/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/foobar.html', - '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/hello.html', - '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/index.php', - '/libraries/vendor/maximebf/debugbar/demo/dump_assets.php', - '/libraries/vendor/maximebf/debugbar/demo/index.php', - '/libraries/vendor/maximebf/debugbar/demo/open.php', - '/libraries/vendor/maximebf/debugbar/demo/pdo.php', - '/libraries/vendor/maximebf/debugbar/demo/stack.php', - '/libraries/vendor/maximebf/debugbar/docs/ajax_and_stack.md', - '/libraries/vendor/maximebf/debugbar/docs/base_collectors.md', - '/libraries/vendor/maximebf/debugbar/docs/bridge_collectors.md', - '/libraries/vendor/maximebf/debugbar/docs/data_collectors.md', - '/libraries/vendor/maximebf/debugbar/docs/data_formatter.md', - '/libraries/vendor/maximebf/debugbar/docs/http_drivers.md', - '/libraries/vendor/maximebf/debugbar/docs/javascript_bar.md', - '/libraries/vendor/maximebf/debugbar/docs/manifest.json', - '/libraries/vendor/maximebf/debugbar/docs/openhandler.md', - '/libraries/vendor/maximebf/debugbar/docs/rendering.md', - '/libraries/vendor/maximebf/debugbar/docs/screenshot.png', - '/libraries/vendor/maximebf/debugbar/docs/storage.md', - '/libraries/vendor/maximebf/debugbar/docs/style.css', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/AggregatedCollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/ConfigCollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/MessagesCollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/MockCollector.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/Propel2CollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/TimeDataCollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter/DataFormatterTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter/DebugBarVarDumperTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DebugBarTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DebugBarTestCase.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/JavascriptRendererTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/MockHttpDriver.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/OpenHandlerTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage/FileStorageTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage/MockStorage.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/TracedStatementTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/full_init.html', - '/libraries/vendor/maximebf/debugbar/tests/bootstrap.php', - ); - - $folders = array( - // From 3.10 to 4.1 - '/templates/system/images', - '/templates/system/html', - '/templates/protostar/less', - '/templates/protostar/language/en-GB', - '/templates/protostar/language', - '/templates/protostar/js', - '/templates/protostar/img', - '/templates/protostar/images/system', - '/templates/protostar/images', - '/templates/protostar/html/layouts/joomla/system', - '/templates/protostar/html/layouts/joomla/form/field', - '/templates/protostar/html/layouts/joomla/form', - '/templates/protostar/html/layouts/joomla', - '/templates/protostar/html/layouts', - '/templates/protostar/html/com_media/imageslist', - '/templates/protostar/html/com_media', - '/templates/protostar/html', - '/templates/protostar/css', - '/templates/protostar', - '/templates/beez3/language/en-GB', - '/templates/beez3/language', - '/templates/beez3/javascript', - '/templates/beez3/images/system', - '/templates/beez3/images/personal', - '/templates/beez3/images/nature', - '/templates/beez3/images', - '/templates/beez3/html/mod_login', - '/templates/beez3/html/mod_languages', - '/templates/beez3/html/mod_breadcrumbs', - '/templates/beez3/html/layouts/joomla/system', - '/templates/beez3/html/layouts/joomla', - '/templates/beez3/html/layouts', - '/templates/beez3/html/com_weblinks/form', - '/templates/beez3/html/com_weblinks/category', - '/templates/beez3/html/com_weblinks/categories', - '/templates/beez3/html/com_weblinks', - '/templates/beez3/html/com_newsfeeds/category', - '/templates/beez3/html/com_newsfeeds/categories', - '/templates/beez3/html/com_newsfeeds', - '/templates/beez3/html/com_content/form', - '/templates/beez3/html/com_content/featured', - '/templates/beez3/html/com_content/category', - '/templates/beez3/html/com_content/categories', - '/templates/beez3/html/com_content/article', - '/templates/beez3/html/com_content/archive', - '/templates/beez3/html/com_content', - '/templates/beez3/html/com_contact/contact', - '/templates/beez3/html/com_contact/category', - '/templates/beez3/html/com_contact/categories', - '/templates/beez3/html/com_contact', - '/templates/beez3/html', - '/templates/beez3/css', - '/templates/beez3', - '/plugins/user/terms/terms', - '/plugins/user/terms/field', - '/plugins/user/profile/profiles', - '/plugins/user/profile/field', - '/plugins/system/stats/field', - '/plugins/system/privacyconsent/privacyconsent', - '/plugins/system/privacyconsent/field', - '/plugins/system/p3p', - '/plugins/system/languagecode/language/en-GB', - '/plugins/system/languagecode/language', - '/plugins/editors/tinymce/form', - '/plugins/editors/tinymce/field', - '/plugins/content/confirmconsent/fields', - '/plugins/captcha/recaptcha/postinstall', - '/plugins/authentication/gmail', - '/media/plg_twofactorauth_totp/js', - '/media/plg_twofactorauth_totp', - '/media/plg_system_highlight', - '/media/overrider/js', - '/media/overrider/css', - '/media/overrider', - '/media/media/js', - '/media/media/images/mime-icon-32', - '/media/media/images/mime-icon-16', - '/media/media/images', - '/media/media/css', - '/media/media', - '/media/jui/less', - '/media/jui/js', - '/media/jui/img', - '/media/jui/images', - '/media/jui/fonts', - '/media/jui/css', - '/media/jui', - '/media/editors/tinymce/themes/modern', - '/media/editors/tinymce/themes', - '/media/editors/tinymce/templates', - '/media/editors/tinymce/skins/lightgray/img', - '/media/editors/tinymce/skins/lightgray/fonts', - '/media/editors/tinymce/skins/lightgray', - '/media/editors/tinymce/skins', - '/media/editors/tinymce/plugins/wordcount', - '/media/editors/tinymce/plugins/visualchars', - '/media/editors/tinymce/plugins/visualblocks/css', - '/media/editors/tinymce/plugins/visualblocks', - '/media/editors/tinymce/plugins/toc', - '/media/editors/tinymce/plugins/textpattern', - '/media/editors/tinymce/plugins/textcolor', - '/media/editors/tinymce/plugins/template', - '/media/editors/tinymce/plugins/table', - '/media/editors/tinymce/plugins/tabfocus', - '/media/editors/tinymce/plugins/spellchecker', - '/media/editors/tinymce/plugins/searchreplace', - '/media/editors/tinymce/plugins/save', - '/media/editors/tinymce/plugins/print', - '/media/editors/tinymce/plugins/preview', - '/media/editors/tinymce/plugins/paste', - '/media/editors/tinymce/plugins/pagebreak', - '/media/editors/tinymce/plugins/noneditable', - '/media/editors/tinymce/plugins/nonbreaking', - '/media/editors/tinymce/plugins/media', - '/media/editors/tinymce/plugins/lists', - '/media/editors/tinymce/plugins/link', - '/media/editors/tinymce/plugins/legacyoutput', - '/media/editors/tinymce/plugins/layer', - '/media/editors/tinymce/plugins/insertdatetime', - '/media/editors/tinymce/plugins/importcss', - '/media/editors/tinymce/plugins/imagetools', - '/media/editors/tinymce/plugins/image', - '/media/editors/tinymce/plugins/hr', - '/media/editors/tinymce/plugins/fullscreen', - '/media/editors/tinymce/plugins/fullpage', - '/media/editors/tinymce/plugins/example_dependency', - '/media/editors/tinymce/plugins/example', - '/media/editors/tinymce/plugins/emoticons/img', - '/media/editors/tinymce/plugins/emoticons', - '/media/editors/tinymce/plugins/directionality', - '/media/editors/tinymce/plugins/contextmenu', - '/media/editors/tinymce/plugins/colorpicker', - '/media/editors/tinymce/plugins/codesample/css', - '/media/editors/tinymce/plugins/codesample', - '/media/editors/tinymce/plugins/code', - '/media/editors/tinymce/plugins/charmap', - '/media/editors/tinymce/plugins/bbcode', - '/media/editors/tinymce/plugins/autosave', - '/media/editors/tinymce/plugins/autoresize', - '/media/editors/tinymce/plugins/autolink', - '/media/editors/tinymce/plugins/anchor', - '/media/editors/tinymce/plugins/advlist', - '/media/editors/tinymce/plugins', - '/media/editors/tinymce/langs', - '/media/editors/tinymce/js/plugins/dragdrop', - '/media/editors/tinymce/js/plugins', - '/media/editors/tinymce/js', - '/media/editors/tinymce', - '/media/editors/none/js', - '/media/editors/none', - '/media/editors/codemirror/theme', - '/media/editors/codemirror/mode/z80', - '/media/editors/codemirror/mode/yaml-frontmatter', - '/media/editors/codemirror/mode/yaml', - '/media/editors/codemirror/mode/yacas', - '/media/editors/codemirror/mode/xquery', - '/media/editors/codemirror/mode/xml', - '/media/editors/codemirror/mode/webidl', - '/media/editors/codemirror/mode/wast', - '/media/editors/codemirror/mode/vue', - '/media/editors/codemirror/mode/vhdl', - '/media/editors/codemirror/mode/verilog', - '/media/editors/codemirror/mode/velocity', - '/media/editors/codemirror/mode/vbscript', - '/media/editors/codemirror/mode/vb', - '/media/editors/codemirror/mode/twig', - '/media/editors/codemirror/mode/turtle', - '/media/editors/codemirror/mode/ttcn-cfg', - '/media/editors/codemirror/mode/ttcn', - '/media/editors/codemirror/mode/troff', - '/media/editors/codemirror/mode/tornado', - '/media/editors/codemirror/mode/toml', - '/media/editors/codemirror/mode/tiki', - '/media/editors/codemirror/mode/tiddlywiki', - '/media/editors/codemirror/mode/textile', - '/media/editors/codemirror/mode/tcl', - '/media/editors/codemirror/mode/swift', - '/media/editors/codemirror/mode/stylus', - '/media/editors/codemirror/mode/stex', - '/media/editors/codemirror/mode/sql', - '/media/editors/codemirror/mode/spreadsheet', - '/media/editors/codemirror/mode/sparql', - '/media/editors/codemirror/mode/soy', - '/media/editors/codemirror/mode/solr', - '/media/editors/codemirror/mode/smarty', - '/media/editors/codemirror/mode/smalltalk', - '/media/editors/codemirror/mode/slim', - '/media/editors/codemirror/mode/sieve', - '/media/editors/codemirror/mode/shell', - '/media/editors/codemirror/mode/scheme', - '/media/editors/codemirror/mode/sass', - '/media/editors/codemirror/mode/sas', - '/media/editors/codemirror/mode/rust', - '/media/editors/codemirror/mode/ruby', - '/media/editors/codemirror/mode/rst', - '/media/editors/codemirror/mode/rpm/changes', - '/media/editors/codemirror/mode/rpm', - '/media/editors/codemirror/mode/r', - '/media/editors/codemirror/mode/q', - '/media/editors/codemirror/mode/python', - '/media/editors/codemirror/mode/puppet', - '/media/editors/codemirror/mode/pug', - '/media/editors/codemirror/mode/protobuf', - '/media/editors/codemirror/mode/properties', - '/media/editors/codemirror/mode/powershell', - '/media/editors/codemirror/mode/pig', - '/media/editors/codemirror/mode/php', - '/media/editors/codemirror/mode/perl', - '/media/editors/codemirror/mode/pegjs', - '/media/editors/codemirror/mode/pascal', - '/media/editors/codemirror/mode/oz', - '/media/editors/codemirror/mode/octave', - '/media/editors/codemirror/mode/ntriples', - '/media/editors/codemirror/mode/nsis', - '/media/editors/codemirror/mode/nginx', - '/media/editors/codemirror/mode/mumps', - '/media/editors/codemirror/mode/mscgen', - '/media/editors/codemirror/mode/modelica', - '/media/editors/codemirror/mode/mllike', - '/media/editors/codemirror/mode/mirc', - '/media/editors/codemirror/mode/mbox', - '/media/editors/codemirror/mode/mathematica', - '/media/editors/codemirror/mode/markdown', - '/media/editors/codemirror/mode/lua', - '/media/editors/codemirror/mode/livescript', - '/media/editors/codemirror/mode/julia', - '/media/editors/codemirror/mode/jsx', - '/media/editors/codemirror/mode/jinja2', - '/media/editors/codemirror/mode/javascript', - '/media/editors/codemirror/mode/idl', - '/media/editors/codemirror/mode/http', - '/media/editors/codemirror/mode/htmlmixed', - '/media/editors/codemirror/mode/htmlembedded', - '/media/editors/codemirror/mode/haxe', - '/media/editors/codemirror/mode/haskell-literate', - '/media/editors/codemirror/mode/haskell', - '/media/editors/codemirror/mode/handlebars', - '/media/editors/codemirror/mode/haml', - '/media/editors/codemirror/mode/groovy', - '/media/editors/codemirror/mode/go', - '/media/editors/codemirror/mode/gherkin', - '/media/editors/codemirror/mode/gfm', - '/media/editors/codemirror/mode/gas', - '/media/editors/codemirror/mode/fortran', - '/media/editors/codemirror/mode/forth', - '/media/editors/codemirror/mode/fcl', - '/media/editors/codemirror/mode/factor', - '/media/editors/codemirror/mode/erlang', - '/media/editors/codemirror/mode/elm', - '/media/editors/codemirror/mode/eiffel', - '/media/editors/codemirror/mode/ecl', - '/media/editors/codemirror/mode/ebnf', - '/media/editors/codemirror/mode/dylan', - '/media/editors/codemirror/mode/dtd', - '/media/editors/codemirror/mode/dockerfile', - '/media/editors/codemirror/mode/django', - '/media/editors/codemirror/mode/diff', - '/media/editors/codemirror/mode/dart', - '/media/editors/codemirror/mode/d', - '/media/editors/codemirror/mode/cypher', - '/media/editors/codemirror/mode/css', - '/media/editors/codemirror/mode/crystal', - '/media/editors/codemirror/mode/commonlisp', - '/media/editors/codemirror/mode/coffeescript', - '/media/editors/codemirror/mode/cobol', - '/media/editors/codemirror/mode/cmake', - '/media/editors/codemirror/mode/clojure', - '/media/editors/codemirror/mode/clike', - '/media/editors/codemirror/mode/brainfuck', - '/media/editors/codemirror/mode/asterisk', - '/media/editors/codemirror/mode/asn.1', - '/media/editors/codemirror/mode/asciiarmor', - '/media/editors/codemirror/mode/apl', - '/media/editors/codemirror/mode', - '/media/editors/codemirror/lib', - '/media/editors/codemirror/keymap', - '/media/editors/codemirror/addon/wrap', - '/media/editors/codemirror/addon/tern', - '/media/editors/codemirror/addon/selection', - '/media/editors/codemirror/addon/search', - '/media/editors/codemirror/addon/scroll', - '/media/editors/codemirror/addon/runmode', - '/media/editors/codemirror/addon/mode', - '/media/editors/codemirror/addon/merge', - '/media/editors/codemirror/addon/lint', - '/media/editors/codemirror/addon/hint', - '/media/editors/codemirror/addon/fold', - '/media/editors/codemirror/addon/edit', - '/media/editors/codemirror/addon/display', - '/media/editors/codemirror/addon/dialog', - '/media/editors/codemirror/addon/comment', - '/media/editors/codemirror/addon', - '/media/editors/codemirror', - '/media/editors', - '/media/contacts/images', - '/media/contacts', - '/media/com_contenthistory/css', - '/media/cms/css', - '/media/cms', - '/libraries/vendor/symfony/polyfill-util', - '/libraries/vendor/symfony/polyfill-php71', - '/libraries/vendor/symfony/polyfill-php56', - '/libraries/vendor/symfony/polyfill-php55', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parse', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Net', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/HTTP', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content/Type', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache', - '/libraries/vendor/simplepie/simplepie/library/SimplePie', - '/libraries/vendor/simplepie/simplepie/library', - '/libraries/vendor/simplepie/simplepie/idn', - '/libraries/vendor/simplepie/simplepie', - '/libraries/vendor/simplepie', - '/libraries/vendor/phpmailer/phpmailer/extras', - '/libraries/vendor/paragonie/random_compat/lib', - '/libraries/vendor/leafo/lessphp', - '/libraries/vendor/leafo', - '/libraries/vendor/joomla/session/Joomla/Session/Storage', - '/libraries/vendor/joomla/session/Joomla/Session', - '/libraries/vendor/joomla/session/Joomla', - '/libraries/vendor/joomla/image/src/Filter', - '/libraries/vendor/joomla/image/src', - '/libraries/vendor/joomla/image', - '/libraries/vendor/joomla/compat/src', - '/libraries/vendor/joomla/compat', - '/libraries/vendor/joomla/application/src/Cli/Output/Processor', - '/libraries/vendor/joomla/application/src/Cli/Output', - '/libraries/vendor/joomla/application/src/Cli', - '/libraries/vendor/ircmaxell/password-compat/lib', - '/libraries/vendor/ircmaxell/password-compat', - '/libraries/vendor/ircmaxell', - '/libraries/vendor/brumann/polyfill-unserialize/src', - '/libraries/vendor/brumann/polyfill-unserialize', - '/libraries/vendor/brumann', - '/libraries/src/Table/Observer', - '/libraries/src/Menu/Node', - '/libraries/src/Language/Wrapper', - '/libraries/src/Language/Stemmer', - '/libraries/src/Http/Wrapper', - '/libraries/src/Filter/Wrapper', - '/libraries/src/Filesystem/Wrapper', - '/libraries/src/Crypt/Password', - '/libraries/src/Access/Wrapper', - '/libraries/phputf8/utils', - '/libraries/phputf8/native', - '/libraries/phputf8/mbstring', - '/libraries/phputf8', - '/libraries/legacy/utilities', - '/libraries/legacy/table', - '/libraries/legacy/simplepie', - '/libraries/legacy/simplecrypt', - '/libraries/legacy/response', - '/libraries/legacy/request', - '/libraries/legacy/log', - '/libraries/legacy/form/field', - '/libraries/legacy/form', - '/libraries/legacy/exception', - '/libraries/legacy/error', - '/libraries/legacy/dispatcher', - '/libraries/legacy/database', - '/libraries/legacy/base', - '/libraries/legacy/application', - '/libraries/legacy', - '/libraries/joomla/view', - '/libraries/joomla/utilities', - '/libraries/joomla/twitter', - '/libraries/joomla/string/wrapper', - '/libraries/joomla/string', - '/libraries/joomla/session/storage', - '/libraries/joomla/session/handler', - '/libraries/joomla/session', - '/libraries/joomla/route/wrapper', - '/libraries/joomla/route', - '/libraries/joomla/openstreetmap', - '/libraries/joomla/observer/wrapper', - '/libraries/joomla/observer/updater', - '/libraries/joomla/observer', - '/libraries/joomla/observable', - '/libraries/joomla/oauth2', - '/libraries/joomla/oauth1', - '/libraries/joomla/model', - '/libraries/joomla/mediawiki', - '/libraries/joomla/linkedin', - '/libraries/joomla/keychain', - '/libraries/joomla/grid', - '/libraries/joomla/google/embed', - '/libraries/joomla/google/data/plus', - '/libraries/joomla/google/data/picasa', - '/libraries/joomla/google/data', - '/libraries/joomla/google/auth', - '/libraries/joomla/google', - '/libraries/joomla/github/package/users', - '/libraries/joomla/github/package/repositories', - '/libraries/joomla/github/package/pulls', - '/libraries/joomla/github/package/orgs', - '/libraries/joomla/github/package/issues', - '/libraries/joomla/github/package/gists', - '/libraries/joomla/github/package/data', - '/libraries/joomla/github/package/activity', - '/libraries/joomla/github/package', - '/libraries/joomla/github', - '/libraries/joomla/form/fields', - '/libraries/joomla/form', - '/libraries/joomla/facebook', - '/libraries/joomla/event', - '/libraries/joomla/database/query', - '/libraries/joomla/database/iterator', - '/libraries/joomla/database/importer', - '/libraries/joomla/database/exporter', - '/libraries/joomla/database/exception', - '/libraries/joomla/database/driver', - '/libraries/joomla/database', - '/libraries/joomla/controller', - '/libraries/joomla/archive/wrapper', - '/libraries/joomla/archive', - '/libraries/joomla/application/web/router', - '/libraries/joomla/application/web', - '/libraries/joomla/application', - '/libraries/joomla', - '/libraries/idna_convert', - '/libraries/fof/view', - '/libraries/fof/utils/update', - '/libraries/fof/utils/timer', - '/libraries/fof/utils/phpfunc', - '/libraries/fof/utils/observable', - '/libraries/fof/utils/object', - '/libraries/fof/utils/ip', - '/libraries/fof/utils/installscript', - '/libraries/fof/utils/ini', - '/libraries/fof/utils/filescheck', - '/libraries/fof/utils/config', - '/libraries/fof/utils/cache', - '/libraries/fof/utils/array', - '/libraries/fof/utils', - '/libraries/fof/toolbar', - '/libraries/fof/template', - '/libraries/fof/table/dispatcher', - '/libraries/fof/table/behavior', - '/libraries/fof/table', - '/libraries/fof/string', - '/libraries/fof/render', - '/libraries/fof/query', - '/libraries/fof/platform/filesystem', - '/libraries/fof/platform', - '/libraries/fof/model/field', - '/libraries/fof/model/dispatcher', - '/libraries/fof/model/behavior', - '/libraries/fof/model', - '/libraries/fof/less/parser', - '/libraries/fof/less/formatter', - '/libraries/fof/less', - '/libraries/fof/layout', - '/libraries/fof/integration/joomla/filesystem', - '/libraries/fof/integration/joomla', - '/libraries/fof/integration', - '/libraries/fof/input/jinput', - '/libraries/fof/input', - '/libraries/fof/inflector', - '/libraries/fof/hal/render', - '/libraries/fof/hal', - '/libraries/fof/form/header', - '/libraries/fof/form/field', - '/libraries/fof/form', - '/libraries/fof/encrypt/aes', - '/libraries/fof/encrypt', - '/libraries/fof/download/adapter', - '/libraries/fof/download', - '/libraries/fof/dispatcher', - '/libraries/fof/database/query', - '/libraries/fof/database/iterator', - '/libraries/fof/database/driver', - '/libraries/fof/database', - '/libraries/fof/controller', - '/libraries/fof/config/domain', - '/libraries/fof/config', - '/libraries/fof/autoloader', - '/libraries/fof', - '/libraries/cms/less/formatter', - '/libraries/cms/less', - '/libraries/cms/html/language/en-GB', - '/libraries/cms/html/language', - '/libraries/cms/html', - '/libraries/cms/class', - '/libraries/cms', - '/layouts/libraries/cms/html/bootstrap', - '/layouts/libraries/cms/html', - '/layouts/libraries/cms', - '/layouts/joomla/tinymce/buttons', - '/layouts/joomla/modal', - '/layouts/joomla/html/formbehavior', - '/components/com_wrapper/views/wrapper/tmpl', - '/components/com_wrapper/views/wrapper', - '/components/com_wrapper/views', - '/components/com_users/views/reset/tmpl', - '/components/com_users/views/reset', - '/components/com_users/views/remind/tmpl', - '/components/com_users/views/remind', - '/components/com_users/views/registration/tmpl', - '/components/com_users/views/registration', - '/components/com_users/views/profile/tmpl', - '/components/com_users/views/profile', - '/components/com_users/views/login/tmpl', - '/components/com_users/views/login', - '/components/com_users/views', - '/components/com_users/models/rules', - '/components/com_users/models/forms', - '/components/com_users/models', - '/components/com_users/layouts/joomla/form', - '/components/com_users/layouts/joomla', - '/components/com_users/layouts', - '/components/com_users/helpers/html', - '/components/com_users/helpers', - '/components/com_users/controllers', - '/components/com_tags/views/tags/tmpl', - '/components/com_tags/views/tags', - '/components/com_tags/views/tag/tmpl', - '/components/com_tags/views/tag', - '/components/com_tags/views', - '/components/com_tags/models', - '/components/com_tags/controllers', - '/components/com_privacy/views/request/tmpl', - '/components/com_privacy/views/request', - '/components/com_privacy/views/remind/tmpl', - '/components/com_privacy/views/remind', - '/components/com_privacy/views/confirm/tmpl', - '/components/com_privacy/views/confirm', - '/components/com_privacy/views', - '/components/com_privacy/models/forms', - '/components/com_privacy/models', - '/components/com_privacy/controllers', - '/components/com_newsfeeds/views/newsfeed/tmpl', - '/components/com_newsfeeds/views/newsfeed', - '/components/com_newsfeeds/views/category/tmpl', - '/components/com_newsfeeds/views/category', - '/components/com_newsfeeds/views/categories/tmpl', - '/components/com_newsfeeds/views/categories', - '/components/com_newsfeeds/views', - '/components/com_newsfeeds/models', - '/components/com_modules/models/forms', - '/components/com_modules/models', - '/components/com_menus/models/forms', - '/components/com_menus/models', - '/components/com_mailto/views/sent/tmpl', - '/components/com_mailto/views/sent', - '/components/com_mailto/views/mailto/tmpl', - '/components/com_mailto/views/mailto', - '/components/com_mailto/views', - '/components/com_mailto/models/forms', - '/components/com_mailto/models', - '/components/com_mailto/helpers', - '/components/com_mailto', - '/components/com_finder/views/search/tmpl', - '/components/com_finder/views/search', - '/components/com_finder/views', - '/components/com_finder/models', - '/components/com_finder/helpers/html', - '/components/com_finder/controllers', - '/components/com_fields/models/forms', - '/components/com_fields/models', - '/components/com_content/views/form/tmpl', - '/components/com_content/views/form', - '/components/com_content/views/featured/tmpl', - '/components/com_content/views/featured', - '/components/com_content/views/category/tmpl', - '/components/com_content/views/category', - '/components/com_content/views/categories/tmpl', - '/components/com_content/views/categories', - '/components/com_content/views/article/tmpl', - '/components/com_content/views/article', - '/components/com_content/views/archive/tmpl', - '/components/com_content/views/archive', - '/components/com_content/views', - '/components/com_content/models/forms', - '/components/com_content/models', - '/components/com_content/controllers', - '/components/com_contact/views/featured/tmpl', - '/components/com_contact/views/featured', - '/components/com_contact/views/contact/tmpl', - '/components/com_contact/views/contact', - '/components/com_contact/views/category/tmpl', - '/components/com_contact/views/category', - '/components/com_contact/views/categories/tmpl', - '/components/com_contact/views/categories', - '/components/com_contact/views', - '/components/com_contact/models/rules', - '/components/com_contact/models/forms', - '/components/com_contact/models', - '/components/com_contact/layouts/joomla/form', - '/components/com_contact/layouts/joomla', - '/components/com_contact/controllers', - '/components/com_config/view/templates/tmpl', - '/components/com_config/view/templates', - '/components/com_config/view/modules/tmpl', - '/components/com_config/view/modules', - '/components/com_config/view/config/tmpl', - '/components/com_config/view/config', - '/components/com_config/view/cms', - '/components/com_config/view', - '/components/com_config/model/form', - '/components/com_config/model', - '/components/com_config/controller/templates', - '/components/com_config/controller/modules', - '/components/com_config/controller/config', - '/components/com_config/controller', - '/components/com_banners/models', - '/components/com_banners/helpers', - '/administrator/templates/system/html', - '/administrator/templates/isis/less/pages', - '/administrator/templates/isis/less/bootstrap', - '/administrator/templates/isis/less/blocks', - '/administrator/templates/isis/less', - '/administrator/templates/isis/language/en-GB', - '/administrator/templates/isis/language', - '/administrator/templates/isis/js', - '/administrator/templates/isis/img', - '/administrator/templates/isis/images/system', - '/administrator/templates/isis/images/admin', - '/administrator/templates/isis/images', - '/administrator/templates/isis/html/mod_version', - '/administrator/templates/isis/html/layouts/joomla/toolbar', - '/administrator/templates/isis/html/layouts/joomla/system', - '/administrator/templates/isis/html/layouts/joomla/pagination', - '/administrator/templates/isis/html/layouts/joomla/form/field', - '/administrator/templates/isis/html/layouts/joomla/form', - '/administrator/templates/isis/html/layouts/joomla', - '/administrator/templates/isis/html/layouts', - '/administrator/templates/isis/html/com_media/medialist', - '/administrator/templates/isis/html/com_media/imageslist', - '/administrator/templates/isis/html/com_media', - '/administrator/templates/isis/html', - '/administrator/templates/isis/css', - '/administrator/templates/isis', - '/administrator/templates/hathor/postinstall', - '/administrator/templates/hathor/less', - '/administrator/templates/hathor/language/en-GB', - '/administrator/templates/hathor/language', - '/administrator/templates/hathor/js', - '/administrator/templates/hathor/images/toolbar', - '/administrator/templates/hathor/images/system', - '/administrator/templates/hathor/images/menu', - '/administrator/templates/hathor/images/header', - '/administrator/templates/hathor/images/admin', - '/administrator/templates/hathor/images', - '/administrator/templates/hathor/html/mod_quickicon', - '/administrator/templates/hathor/html/mod_login', - '/administrator/templates/hathor/html/layouts/plugins/user/profile/fields', - '/administrator/templates/hathor/html/layouts/plugins/user/profile', - '/administrator/templates/hathor/html/layouts/plugins/user', - '/administrator/templates/hathor/html/layouts/plugins', - '/administrator/templates/hathor/html/layouts/joomla/toolbar', - '/administrator/templates/hathor/html/layouts/joomla/sidebars', - '/administrator/templates/hathor/html/layouts/joomla/quickicons', - '/administrator/templates/hathor/html/layouts/joomla/edit', - '/administrator/templates/hathor/html/layouts/joomla', - '/administrator/templates/hathor/html/layouts/com_modules/toolbar', - '/administrator/templates/hathor/html/layouts/com_modules', - '/administrator/templates/hathor/html/layouts/com_messages/toolbar', - '/administrator/templates/hathor/html/layouts/com_messages', - '/administrator/templates/hathor/html/layouts/com_media/toolbar', - '/administrator/templates/hathor/html/layouts/com_media', - '/administrator/templates/hathor/html/layouts', - '/administrator/templates/hathor/html/com_weblinks/weblinks', - '/administrator/templates/hathor/html/com_weblinks/weblink', - '/administrator/templates/hathor/html/com_weblinks', - '/administrator/templates/hathor/html/com_users/users', - '/administrator/templates/hathor/html/com_users/user', - '/administrator/templates/hathor/html/com_users/notes', - '/administrator/templates/hathor/html/com_users/note', - '/administrator/templates/hathor/html/com_users/levels', - '/administrator/templates/hathor/html/com_users/groups', - '/administrator/templates/hathor/html/com_users/debuguser', - '/administrator/templates/hathor/html/com_users/debuggroup', - '/administrator/templates/hathor/html/com_users', - '/administrator/templates/hathor/html/com_templates/templates', - '/administrator/templates/hathor/html/com_templates/template', - '/administrator/templates/hathor/html/com_templates/styles', - '/administrator/templates/hathor/html/com_templates/style', - '/administrator/templates/hathor/html/com_templates', - '/administrator/templates/hathor/html/com_tags/tags', - '/administrator/templates/hathor/html/com_tags/tag', - '/administrator/templates/hathor/html/com_tags', - '/administrator/templates/hathor/html/com_search/searches', - '/administrator/templates/hathor/html/com_search', - '/administrator/templates/hathor/html/com_redirect/links', - '/administrator/templates/hathor/html/com_redirect', - '/administrator/templates/hathor/html/com_postinstall/messages', - '/administrator/templates/hathor/html/com_postinstall', - '/administrator/templates/hathor/html/com_plugins/plugins', - '/administrator/templates/hathor/html/com_plugins/plugin', - '/administrator/templates/hathor/html/com_plugins', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeed', - '/administrator/templates/hathor/html/com_newsfeeds', - '/administrator/templates/hathor/html/com_modules/positions', - '/administrator/templates/hathor/html/com_modules/modules', - '/administrator/templates/hathor/html/com_modules/module', - '/administrator/templates/hathor/html/com_modules', - '/administrator/templates/hathor/html/com_messages/messages', - '/administrator/templates/hathor/html/com_messages/message', - '/administrator/templates/hathor/html/com_messages', - '/administrator/templates/hathor/html/com_menus/menutypes', - '/administrator/templates/hathor/html/com_menus/menus', - '/administrator/templates/hathor/html/com_menus/menu', - '/administrator/templates/hathor/html/com_menus/items', - '/administrator/templates/hathor/html/com_menus/item', - '/administrator/templates/hathor/html/com_menus', - '/administrator/templates/hathor/html/com_languages/overrides', - '/administrator/templates/hathor/html/com_languages/languages', - '/administrator/templates/hathor/html/com_languages/installed', - '/administrator/templates/hathor/html/com_languages', - '/administrator/templates/hathor/html/com_joomlaupdate/default', - '/administrator/templates/hathor/html/com_joomlaupdate', - '/administrator/templates/hathor/html/com_installer/warnings', - '/administrator/templates/hathor/html/com_installer/update', - '/administrator/templates/hathor/html/com_installer/manage', - '/administrator/templates/hathor/html/com_installer/languages', - '/administrator/templates/hathor/html/com_installer/install', - '/administrator/templates/hathor/html/com_installer/discover', - '/administrator/templates/hathor/html/com_installer/default', - '/administrator/templates/hathor/html/com_installer/database', - '/administrator/templates/hathor/html/com_installer', - '/administrator/templates/hathor/html/com_finder/maps', - '/administrator/templates/hathor/html/com_finder/index', - '/administrator/templates/hathor/html/com_finder/filters', - '/administrator/templates/hathor/html/com_finder', - '/administrator/templates/hathor/html/com_fields/groups', - '/administrator/templates/hathor/html/com_fields/group', - '/administrator/templates/hathor/html/com_fields/fields', - '/administrator/templates/hathor/html/com_fields/field', - '/administrator/templates/hathor/html/com_fields', - '/administrator/templates/hathor/html/com_cpanel/cpanel', - '/administrator/templates/hathor/html/com_cpanel', - '/administrator/templates/hathor/html/com_contenthistory/history', - '/administrator/templates/hathor/html/com_contenthistory', - '/administrator/templates/hathor/html/com_content/featured', - '/administrator/templates/hathor/html/com_content/articles', - '/administrator/templates/hathor/html/com_content/article', - '/administrator/templates/hathor/html/com_content', - '/administrator/templates/hathor/html/com_contact/contacts', - '/administrator/templates/hathor/html/com_contact/contact', - '/administrator/templates/hathor/html/com_contact', - '/administrator/templates/hathor/html/com_config/component', - '/administrator/templates/hathor/html/com_config/application', - '/administrator/templates/hathor/html/com_config', - '/administrator/templates/hathor/html/com_checkin/checkin', - '/administrator/templates/hathor/html/com_checkin', - '/administrator/templates/hathor/html/com_categories/category', - '/administrator/templates/hathor/html/com_categories/categories', - '/administrator/templates/hathor/html/com_categories', - '/administrator/templates/hathor/html/com_cache/purge', - '/administrator/templates/hathor/html/com_cache/cache', - '/administrator/templates/hathor/html/com_cache', - '/administrator/templates/hathor/html/com_banners/tracks', - '/administrator/templates/hathor/html/com_banners/download', - '/administrator/templates/hathor/html/com_banners/clients', - '/administrator/templates/hathor/html/com_banners/client', - '/administrator/templates/hathor/html/com_banners/banners', - '/administrator/templates/hathor/html/com_banners/banner', - '/administrator/templates/hathor/html/com_banners', - '/administrator/templates/hathor/html/com_associations/associations', - '/administrator/templates/hathor/html/com_associations', - '/administrator/templates/hathor/html/com_admin/sysinfo', - '/administrator/templates/hathor/html/com_admin/profile', - '/administrator/templates/hathor/html/com_admin/help', - '/administrator/templates/hathor/html/com_admin', - '/administrator/templates/hathor/html', - '/administrator/templates/hathor/css', - '/administrator/templates/hathor', - '/administrator/modules/mod_version/language/en-GB', - '/administrator/modules/mod_version/language', - '/administrator/modules/mod_status/tmpl', - '/administrator/modules/mod_status', - '/administrator/modules/mod_stats_admin/language', - '/administrator/modules/mod_multilangstatus/language/en-GB', - '/administrator/modules/mod_multilangstatus/language', - '/administrator/components/com_users/views/users/tmpl', - '/administrator/components/com_users/views/users', - '/administrator/components/com_users/views/user/tmpl', - '/administrator/components/com_users/views/user', - '/administrator/components/com_users/views/notes/tmpl', - '/administrator/components/com_users/views/notes', - '/administrator/components/com_users/views/note/tmpl', - '/administrator/components/com_users/views/note', - '/administrator/components/com_users/views/mail/tmpl', - '/administrator/components/com_users/views/mail', - '/administrator/components/com_users/views/levels/tmpl', - '/administrator/components/com_users/views/levels', - '/administrator/components/com_users/views/level/tmpl', - '/administrator/components/com_users/views/level', - '/administrator/components/com_users/views/groups/tmpl', - '/administrator/components/com_users/views/groups', - '/administrator/components/com_users/views/group/tmpl', - '/administrator/components/com_users/views/group', - '/administrator/components/com_users/views/debuguser/tmpl', - '/administrator/components/com_users/views/debuguser', - '/administrator/components/com_users/views/debuggroup/tmpl', - '/administrator/components/com_users/views/debuggroup', - '/administrator/components/com_users/views', - '/administrator/components/com_users/tables', - '/administrator/components/com_users/models/forms/fields', - '/administrator/components/com_users/models/forms', - '/administrator/components/com_users/models/fields', - '/administrator/components/com_users/models', - '/administrator/components/com_users/helpers/html', - '/administrator/components/com_users/controllers', - '/administrator/components/com_templates/views/templates/tmpl', - '/administrator/components/com_templates/views/templates', - '/administrator/components/com_templates/views/template/tmpl', - '/administrator/components/com_templates/views/template', - '/administrator/components/com_templates/views/styles/tmpl', - '/administrator/components/com_templates/views/styles', - '/administrator/components/com_templates/views/style/tmpl', - '/administrator/components/com_templates/views/style', - '/administrator/components/com_templates/views', - '/administrator/components/com_templates/tables', - '/administrator/components/com_templates/models/forms', - '/administrator/components/com_templates/models/fields', - '/administrator/components/com_templates/models', - '/administrator/components/com_templates/helpers/html', - '/administrator/components/com_templates/controllers', - '/administrator/components/com_tags/views/tags/tmpl', - '/administrator/components/com_tags/views/tags', - '/administrator/components/com_tags/views/tag/tmpl', - '/administrator/components/com_tags/views/tag', - '/administrator/components/com_tags/views', - '/administrator/components/com_tags/tables', - '/administrator/components/com_tags/models/forms', - '/administrator/components/com_tags/models', - '/administrator/components/com_tags/helpers', - '/administrator/components/com_tags/controllers', - '/administrator/components/com_redirect/views/links/tmpl', - '/administrator/components/com_redirect/views/links', - '/administrator/components/com_redirect/views/link/tmpl', - '/administrator/components/com_redirect/views/link', - '/administrator/components/com_redirect/views', - '/administrator/components/com_redirect/tables', - '/administrator/components/com_redirect/models/forms', - '/administrator/components/com_redirect/models/fields', - '/administrator/components/com_redirect/models', - '/administrator/components/com_redirect/helpers/html', - '/administrator/components/com_redirect/controllers', - '/administrator/components/com_privacy/views/requests/tmpl', - '/administrator/components/com_privacy/views/requests', - '/administrator/components/com_privacy/views/request/tmpl', - '/administrator/components/com_privacy/views/request', - '/administrator/components/com_privacy/views/export', - '/administrator/components/com_privacy/views/dashboard/tmpl', - '/administrator/components/com_privacy/views/dashboard', - '/administrator/components/com_privacy/views/consents/tmpl', - '/administrator/components/com_privacy/views/consents', - '/administrator/components/com_privacy/views/capabilities/tmpl', - '/administrator/components/com_privacy/views/capabilities', - '/administrator/components/com_privacy/views', - '/administrator/components/com_privacy/tables', - '/administrator/components/com_privacy/models/forms', - '/administrator/components/com_privacy/models/fields', - '/administrator/components/com_privacy/models', - '/administrator/components/com_privacy/helpers/removal', - '/administrator/components/com_privacy/helpers/html', - '/administrator/components/com_privacy/helpers/export', - '/administrator/components/com_privacy/helpers', - '/administrator/components/com_privacy/controllers', - '/administrator/components/com_postinstall/views/messages/tmpl', - '/administrator/components/com_postinstall/views/messages', - '/administrator/components/com_postinstall/views', - '/administrator/components/com_postinstall/models', - '/administrator/components/com_postinstall/controllers', - '/administrator/components/com_plugins/views/plugins/tmpl', - '/administrator/components/com_plugins/views/plugins', - '/administrator/components/com_plugins/views/plugin/tmpl', - '/administrator/components/com_plugins/views/plugin', - '/administrator/components/com_plugins/views', - '/administrator/components/com_plugins/models/forms', - '/administrator/components/com_plugins/models/fields', - '/administrator/components/com_plugins/models', - '/administrator/components/com_plugins/controllers', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl', - '/administrator/components/com_newsfeeds/views/newsfeeds', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl', - '/administrator/components/com_newsfeeds/views/newsfeed', - '/administrator/components/com_newsfeeds/views', - '/administrator/components/com_newsfeeds/tables', - '/administrator/components/com_newsfeeds/models/forms', - '/administrator/components/com_newsfeeds/models/fields/modal', - '/administrator/components/com_newsfeeds/models/fields', - '/administrator/components/com_newsfeeds/models', - '/administrator/components/com_newsfeeds/helpers/html', - '/administrator/components/com_newsfeeds/controllers', - '/administrator/components/com_modules/views/select/tmpl', - '/administrator/components/com_modules/views/select', - '/administrator/components/com_modules/views/preview/tmpl', - '/administrator/components/com_modules/views/preview', - '/administrator/components/com_modules/views/positions/tmpl', - '/administrator/components/com_modules/views/positions', - '/administrator/components/com_modules/views/modules/tmpl', - '/administrator/components/com_modules/views/modules', - '/administrator/components/com_modules/views/module/tmpl', - '/administrator/components/com_modules/views/module', - '/administrator/components/com_modules/views', - '/administrator/components/com_modules/models/forms', - '/administrator/components/com_modules/models/fields', - '/administrator/components/com_modules/models', - '/administrator/components/com_modules/helpers/html', - '/administrator/components/com_modules/controllers', - '/administrator/components/com_messages/views/messages/tmpl', - '/administrator/components/com_messages/views/messages', - '/administrator/components/com_messages/views/message/tmpl', - '/administrator/components/com_messages/views/message', - '/administrator/components/com_messages/views/config/tmpl', - '/administrator/components/com_messages/views/config', - '/administrator/components/com_messages/views', - '/administrator/components/com_messages/tables', - '/administrator/components/com_messages/models/forms', - '/administrator/components/com_messages/models/fields', - '/administrator/components/com_messages/models', - '/administrator/components/com_messages/helpers/html', - '/administrator/components/com_messages/helpers', - '/administrator/components/com_messages/controllers', - '/administrator/components/com_menus/views/menutypes/tmpl', - '/administrator/components/com_menus/views/menutypes', - '/administrator/components/com_menus/views/menus/tmpl', - '/administrator/components/com_menus/views/menus', - '/administrator/components/com_menus/views/menu/tmpl', - '/administrator/components/com_menus/views/menu', - '/administrator/components/com_menus/views/items/tmpl', - '/administrator/components/com_menus/views/items', - '/administrator/components/com_menus/views/item/tmpl', - '/administrator/components/com_menus/views/item', - '/administrator/components/com_menus/views', - '/administrator/components/com_menus/tables', - '/administrator/components/com_menus/models/forms', - '/administrator/components/com_menus/models/fields/modal', - '/administrator/components/com_menus/models/fields', - '/administrator/components/com_menus/models', - '/administrator/components/com_menus/layouts/joomla/searchtools/default', - '/administrator/components/com_menus/helpers/html', - '/administrator/components/com_menus/controllers', - '/administrator/components/com_media/views/medialist/tmpl', - '/administrator/components/com_media/views/medialist', - '/administrator/components/com_media/views/media/tmpl', - '/administrator/components/com_media/views/media', - '/administrator/components/com_media/views/imageslist/tmpl', - '/administrator/components/com_media/views/imageslist', - '/administrator/components/com_media/views/images/tmpl', - '/administrator/components/com_media/views/images', - '/administrator/components/com_media/views', - '/administrator/components/com_media/models', - '/administrator/components/com_media/controllers', - '/administrator/components/com_login/views/login/tmpl', - '/administrator/components/com_login/views/login', - '/administrator/components/com_login/views', - '/administrator/components/com_login/models', - '/administrator/components/com_languages/views/overrides/tmpl', - '/administrator/components/com_languages/views/overrides', - '/administrator/components/com_languages/views/override/tmpl', - '/administrator/components/com_languages/views/override', - '/administrator/components/com_languages/views/multilangstatus/tmpl', - '/administrator/components/com_languages/views/multilangstatus', - '/administrator/components/com_languages/views/languages/tmpl', - '/administrator/components/com_languages/views/languages', - '/administrator/components/com_languages/views/language/tmpl', - '/administrator/components/com_languages/views/language', - '/administrator/components/com_languages/views/installed/tmpl', - '/administrator/components/com_languages/views/installed', - '/administrator/components/com_languages/views', - '/administrator/components/com_languages/models/forms', - '/administrator/components/com_languages/models/fields', - '/administrator/components/com_languages/models', - '/administrator/components/com_languages/layouts/joomla/searchtools/default', - '/administrator/components/com_languages/layouts/joomla/searchtools', - '/administrator/components/com_languages/layouts/joomla', - '/administrator/components/com_languages/layouts', - '/administrator/components/com_languages/helpers/html', - '/administrator/components/com_languages/helpers', - '/administrator/components/com_languages/controllers', - '/administrator/components/com_joomlaupdate/views/upload/tmpl', - '/administrator/components/com_joomlaupdate/views/upload', - '/administrator/components/com_joomlaupdate/views/update/tmpl', - '/administrator/components/com_joomlaupdate/views/update', - '/administrator/components/com_joomlaupdate/views/default/tmpl', - '/administrator/components/com_joomlaupdate/views/default', - '/administrator/components/com_joomlaupdate/views', - '/administrator/components/com_joomlaupdate/models', - '/administrator/components/com_joomlaupdate/helpers', - '/administrator/components/com_joomlaupdate/controllers', - '/administrator/components/com_installer/views/warnings/tmpl', - '/administrator/components/com_installer/views/warnings', - '/administrator/components/com_installer/views/updatesites/tmpl', - '/administrator/components/com_installer/views/updatesites', - '/administrator/components/com_installer/views/update/tmpl', - '/administrator/components/com_installer/views/update', - '/administrator/components/com_installer/views/manage/tmpl', - '/administrator/components/com_installer/views/manage', - '/administrator/components/com_installer/views/languages/tmpl', - '/administrator/components/com_installer/views/languages', - '/administrator/components/com_installer/views/install/tmpl', - '/administrator/components/com_installer/views/install', - '/administrator/components/com_installer/views/discover/tmpl', - '/administrator/components/com_installer/views/discover', - '/administrator/components/com_installer/views/default/tmpl', - '/administrator/components/com_installer/views/default', - '/administrator/components/com_installer/views/database/tmpl', - '/administrator/components/com_installer/views/database', - '/administrator/components/com_installer/views', - '/administrator/components/com_installer/models/forms', - '/administrator/components/com_installer/models/fields', - '/administrator/components/com_installer/models', - '/administrator/components/com_installer/helpers/html', - '/administrator/components/com_installer/controllers', - '/administrator/components/com_finder/views/statistics/tmpl', - '/administrator/components/com_finder/views/statistics', - '/administrator/components/com_finder/views/maps/tmpl', - '/administrator/components/com_finder/views/maps', - '/administrator/components/com_finder/views/indexer/tmpl', - '/administrator/components/com_finder/views/indexer', - '/administrator/components/com_finder/views/index/tmpl', - '/administrator/components/com_finder/views/index', - '/administrator/components/com_finder/views/filters/tmpl', - '/administrator/components/com_finder/views/filters', - '/administrator/components/com_finder/views/filter/tmpl', - '/administrator/components/com_finder/views/filter', - '/administrator/components/com_finder/views', - '/administrator/components/com_finder/tables', - '/administrator/components/com_finder/models/forms', - '/administrator/components/com_finder/models/fields', - '/administrator/components/com_finder/models', - '/administrator/components/com_finder/helpers/indexer/stemmer', - '/administrator/components/com_finder/helpers/indexer/parser', - '/administrator/components/com_finder/helpers/indexer/driver', - '/administrator/components/com_finder/helpers/html', - '/administrator/components/com_finder/controllers', - '/administrator/components/com_fields/views/groups/tmpl', - '/administrator/components/com_fields/views/groups', - '/administrator/components/com_fields/views/group/tmpl', - '/administrator/components/com_fields/views/group', - '/administrator/components/com_fields/views/fields/tmpl', - '/administrator/components/com_fields/views/fields', - '/administrator/components/com_fields/views/field/tmpl', - '/administrator/components/com_fields/views/field', - '/administrator/components/com_fields/views', - '/administrator/components/com_fields/tables', - '/administrator/components/com_fields/models/forms', - '/administrator/components/com_fields/models/fields', - '/administrator/components/com_fields/models', - '/administrator/components/com_fields/libraries', - '/administrator/components/com_fields/controllers', - '/administrator/components/com_cpanel/views/cpanel/tmpl', - '/administrator/components/com_cpanel/views/cpanel', - '/administrator/components/com_cpanel/views', - '/administrator/components/com_contenthistory/views/preview/tmpl', - '/administrator/components/com_contenthistory/views/preview', - '/administrator/components/com_contenthistory/views/history/tmpl', - '/administrator/components/com_contenthistory/views/history', - '/administrator/components/com_contenthistory/views/compare/tmpl', - '/administrator/components/com_contenthistory/views/compare', - '/administrator/components/com_contenthistory/views', - '/administrator/components/com_contenthistory/models', - '/administrator/components/com_contenthistory/helpers/html', - '/administrator/components/com_contenthistory/controllers', - '/administrator/components/com_content/views/featured/tmpl', - '/administrator/components/com_content/views/featured', - '/administrator/components/com_content/views/articles/tmpl', - '/administrator/components/com_content/views/articles', - '/administrator/components/com_content/views/article/tmpl', - '/administrator/components/com_content/views/article', - '/administrator/components/com_content/views', - '/administrator/components/com_content/tables', - '/administrator/components/com_content/models/forms', - '/administrator/components/com_content/models/fields/modal', - '/administrator/components/com_content/models/fields', - '/administrator/components/com_content/models', - '/administrator/components/com_content/helpers/html', - '/administrator/components/com_content/controllers', - '/administrator/components/com_contact/views/contacts/tmpl', - '/administrator/components/com_contact/views/contacts', - '/administrator/components/com_contact/views/contact/tmpl', - '/administrator/components/com_contact/views/contact', - '/administrator/components/com_contact/views', - '/administrator/components/com_contact/tables', - '/administrator/components/com_contact/models/forms/fields', - '/administrator/components/com_contact/models/forms', - '/administrator/components/com_contact/models/fields/modal', - '/administrator/components/com_contact/models/fields', - '/administrator/components/com_contact/models', - '/administrator/components/com_contact/helpers/html', - '/administrator/components/com_contact/controllers', - '/administrator/components/com_config/view/component/tmpl', - '/administrator/components/com_config/view/component', - '/administrator/components/com_config/view/application/tmpl', - '/administrator/components/com_config/view/application', - '/administrator/components/com_config/view', - '/administrator/components/com_config/models', - '/administrator/components/com_config/model/form', - '/administrator/components/com_config/model/field', - '/administrator/components/com_config/model', - '/administrator/components/com_config/helper', - '/administrator/components/com_config/controllers', - '/administrator/components/com_config/controller/component', - '/administrator/components/com_config/controller/application', - '/administrator/components/com_config/controller', - '/administrator/components/com_checkin/views/checkin/tmpl', - '/administrator/components/com_checkin/views/checkin', - '/administrator/components/com_checkin/views', - '/administrator/components/com_checkin/models/forms', - '/administrator/components/com_checkin/models', - '/administrator/components/com_categories/views/category/tmpl', - '/administrator/components/com_categories/views/category', - '/administrator/components/com_categories/views/categories/tmpl', - '/administrator/components/com_categories/views/categories', - '/administrator/components/com_categories/views', - '/administrator/components/com_categories/tables', - '/administrator/components/com_categories/models/forms', - '/administrator/components/com_categories/models/fields/modal', - '/administrator/components/com_categories/models/fields', - '/administrator/components/com_categories/models', - '/administrator/components/com_categories/helpers/html', - '/administrator/components/com_categories/controllers', - '/administrator/components/com_cache/views/purge/tmpl', - '/administrator/components/com_cache/views/purge', - '/administrator/components/com_cache/views/cache/tmpl', - '/administrator/components/com_cache/views/cache', - '/administrator/components/com_cache/views', - '/administrator/components/com_cache/models/forms', - '/administrator/components/com_cache/models', - '/administrator/components/com_cache/helpers', - '/administrator/components/com_banners/views/tracks/tmpl', - '/administrator/components/com_banners/views/tracks', - '/administrator/components/com_banners/views/download/tmpl', - '/administrator/components/com_banners/views/download', - '/administrator/components/com_banners/views/clients/tmpl', - '/administrator/components/com_banners/views/clients', - '/administrator/components/com_banners/views/client/tmpl', - '/administrator/components/com_banners/views/client', - '/administrator/components/com_banners/views/banners/tmpl', - '/administrator/components/com_banners/views/banners', - '/administrator/components/com_banners/views/banner/tmpl', - '/administrator/components/com_banners/views/banner', - '/administrator/components/com_banners/views', - '/administrator/components/com_banners/tables', - '/administrator/components/com_banners/models/forms', - '/administrator/components/com_banners/models/fields', - '/administrator/components/com_banners/models', - '/administrator/components/com_banners/helpers/html', - '/administrator/components/com_banners/controllers', - '/administrator/components/com_associations/views/associations/tmpl', - '/administrator/components/com_associations/views/associations', - '/administrator/components/com_associations/views/association/tmpl', - '/administrator/components/com_associations/views/association', - '/administrator/components/com_associations/views', - '/administrator/components/com_associations/models/forms', - '/administrator/components/com_associations/models/fields', - '/administrator/components/com_associations/models', - '/administrator/components/com_associations/layouts/joomla/searchtools/default', - '/administrator/components/com_associations/helpers', - '/administrator/components/com_associations/controllers', - '/administrator/components/com_admin/views/sysinfo/tmpl', - '/administrator/components/com_admin/views/sysinfo', - '/administrator/components/com_admin/views/profile/tmpl', - '/administrator/components/com_admin/views/profile', - '/administrator/components/com_admin/views/help/tmpl', - '/administrator/components/com_admin/views/help', - '/administrator/components/com_admin/views', - '/administrator/components/com_admin/sql/updates/sqlazure', - '/administrator/components/com_admin/models/forms', - '/administrator/components/com_admin/models', - '/administrator/components/com_admin/helpers/html', - '/administrator/components/com_admin/helpers', - '/administrator/components/com_admin/controllers', - '/administrator/components/com_actionlogs/views/actionlogs/tmpl', - '/administrator/components/com_actionlogs/views/actionlogs', - '/administrator/components/com_actionlogs/views', - '/administrator/components/com_actionlogs/models/forms', - '/administrator/components/com_actionlogs/models/fields', - '/administrator/components/com_actionlogs/models', - '/administrator/components/com_actionlogs/libraries', - '/administrator/components/com_actionlogs/layouts', - '/administrator/components/com_actionlogs/helpers', - '/administrator/components/com_actionlogs/controllers', - // 4.0 from Beta 1 to Beta 2 - '/libraries/vendor/joomla/controller/src', - '/libraries/vendor/joomla/controller', - '/api/components/com_installer/src/View/Languages', - '/administrator/components/com_finder/src/Indexer/Driver', - // 4.0 from Beta 4 to Beta 5 - '/plugins/content/imagelazyload', - // 4.0 from Beta 5 to Beta 6 - '/media/system/js/core.es6', - '/administrator/modules/mod_multilangstatus/src/Helper', - '/administrator/modules/mod_multilangstatus/src', - // 4.0 from Beta 6 to Beta 7 - '/media/vendor/skipto/css', - // 4.0 from Beta 7 to RC 1 - '/templates/system/js', - '/templates/cassiopeia/scss/tools/mixins', - '/plugins/fields/subfields/tmpl', - '/plugins/fields/subfields/params', - '/plugins/fields/subfields', - '/media/vendor/punycode/js', - '/media/templates/atum/js', - '/media/templates/atum', - '/libraries/vendor/paragonie/random_compat/dist', - '/libraries/vendor/paragonie/random_compat', - '/libraries/vendor/ozdemirburak/iris/src/Traits', - '/libraries/vendor/ozdemirburak/iris/src/Helpers', - '/libraries/vendor/ozdemirburak/iris/src/Exceptions', - '/libraries/vendor/ozdemirburak/iris/src/Color', - '/libraries/vendor/ozdemirburak/iris/src', - '/libraries/vendor/ozdemirburak/iris', - '/libraries/vendor/ozdemirburak', - '/libraries/vendor/bin', - '/components/com_menus/src/Controller', - '/components/com_csp/src/Controller', - '/components/com_csp/src', - '/components/com_csp', - '/administrator/templates/atum/Service/HTML', - '/administrator/templates/atum/Service', - '/administrator/components/com_joomlaupdate/src/Helper', - '/administrator/components/com_csp/tmpl/reports', - '/administrator/components/com_csp/tmpl', - '/administrator/components/com_csp/src/View/Reports', - '/administrator/components/com_csp/src/View', - '/administrator/components/com_csp/src/Table', - '/administrator/components/com_csp/src/Model', - '/administrator/components/com_csp/src/Helper', - '/administrator/components/com_csp/src/Controller', - '/administrator/components/com_csp/src', - '/administrator/components/com_csp/services', - '/administrator/components/com_csp/forms', - '/administrator/components/com_csp', - '/administrator/components/com_admin/tmpl/profile', - '/administrator/components/com_admin/src/View/Profile', - '/administrator/components/com_admin/forms', - // 4.0 from RC 5 to RC 6 - '/templates/cassiopeia/scss/vendor/fontawesome-free', - '/templates/cassiopeia/css/vendor/fontawesome-free', - '/media/templates/cassiopeia/js/mod_menu', - '/media/templates/cassiopeia/js', - '/media/templates/cassiopeia', - // 4.0 from RC 6 to 4.0.0 (stable) - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation', - '/libraries/vendor/willdurand/negotiation/tests', - '/libraries/vendor/jakeasmith/http_build_url/tests', - '/libraries/vendor/doctrine/inflector/docs/en', - '/libraries/vendor/doctrine/inflector/docs', - '/libraries/vendor/algo26-matthias/idna-convert/tests/unit', - '/libraries/vendor/algo26-matthias/idna-convert/tests/integration', - '/libraries/vendor/algo26-matthias/idna-convert/tests', - // From 4.0.3 to 4.0.4 - '/templates/cassiopeia/images/system', - // From 4.0.x to 4.1.0-beta1 - '/templates/system/scss', - '/templates/system/css', - '/templates/cassiopeia/scss/vendor/metismenu', - '/templates/cassiopeia/scss/vendor/joomla-custom-elements', - '/templates/cassiopeia/scss/vendor/choicesjs', - '/templates/cassiopeia/scss/vendor/bootstrap', - '/templates/cassiopeia/scss/vendor', - '/templates/cassiopeia/scss/tools/variables', - '/templates/cassiopeia/scss/tools/functions', - '/templates/cassiopeia/scss/tools', - '/templates/cassiopeia/scss/system/searchtools', - '/templates/cassiopeia/scss/system', - '/templates/cassiopeia/scss/global', - '/templates/cassiopeia/scss/blocks', - '/templates/cassiopeia/scss', - '/templates/cassiopeia/js', - '/templates/cassiopeia/images', - '/templates/cassiopeia/css/vendor/joomla-custom-elements', - '/templates/cassiopeia/css/vendor/choicesjs', - '/templates/cassiopeia/css/vendor', - '/templates/cassiopeia/css/system/searchtools', - '/templates/cassiopeia/css/system', - '/templates/cassiopeia/css/global', - '/templates/cassiopeia/css', - '/administrator/templates/system/scss', - '/administrator/templates/system/images', - '/administrator/templates/system/css', - '/administrator/templates/atum/scss/vendor/minicolors', - '/administrator/templates/atum/scss/vendor/joomla-custom-elements', - '/administrator/templates/atum/scss/vendor/fontawesome-free', - '/administrator/templates/atum/scss/vendor/choicesjs', - '/administrator/templates/atum/scss/vendor/bootstrap', - '/administrator/templates/atum/scss/vendor/awesomplete', - '/administrator/templates/atum/scss/vendor', - '/administrator/templates/atum/scss/system/searchtools', - '/administrator/templates/atum/scss/system', - '/administrator/templates/atum/scss/pages', - '/administrator/templates/atum/scss/blocks', - '/administrator/templates/atum/scss', - '/administrator/templates/atum/images/logos', - '/administrator/templates/atum/images', - '/administrator/templates/atum/css/vendor/minicolors', - '/administrator/templates/atum/css/vendor/joomla-custom-elements', - '/administrator/templates/atum/css/vendor/fontawesome-free', - '/administrator/templates/atum/css/vendor/choicesjs', - '/administrator/templates/atum/css/vendor/awesomplete', - '/administrator/templates/atum/css/vendor', - '/administrator/templates/atum/css/system/searchtools', - '/administrator/templates/atum/css/system', - '/administrator/templates/atum/css', - // From 4.1.0-beta3 to 4.1.0-rc1 - '/api/components/com_media/src/Helper', - // From 4.1.0 to 4.1.1 - '/libraries/vendor/tobscure/json-api/tests/Exception/Handler', - '/libraries/vendor/tobscure/json-api/tests/Exception', - '/libraries/vendor/tobscure/json-api/tests', - '/libraries/vendor/tobscure/json-api/.git/refs/tags', - '/libraries/vendor/tobscure/json-api/.git/refs/remotes/origin', - '/libraries/vendor/tobscure/json-api/.git/refs/remotes', - '/libraries/vendor/tobscure/json-api/.git/refs/heads', - '/libraries/vendor/tobscure/json-api/.git/refs', - '/libraries/vendor/tobscure/json-api/.git/objects/pack', - '/libraries/vendor/tobscure/json-api/.git/objects/info', - '/libraries/vendor/tobscure/json-api/.git/objects', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes/origin', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/heads', - '/libraries/vendor/tobscure/json-api/.git/logs/refs', - '/libraries/vendor/tobscure/json-api/.git/logs', - '/libraries/vendor/tobscure/json-api/.git/info', - '/libraries/vendor/tobscure/json-api/.git/hooks', - '/libraries/vendor/tobscure/json-api/.git/branches', - '/libraries/vendor/tobscure/json-api/.git', - // From 4.1.3 to 4.1.4 - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar', - '/libraries/vendor/maximebf/debugbar/tests', - '/libraries/vendor/maximebf/debugbar/docs', - '/libraries/vendor/maximebf/debugbar/demo/bridge/twig', - '/libraries/vendor/maximebf/debugbar/demo/bridge/swiftmailer', - '/libraries/vendor/maximebf/debugbar/demo/bridge/slim', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel', - '/libraries/vendor/maximebf/debugbar/demo/bridge/monolog', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src/Demo', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine', - '/libraries/vendor/maximebf/debugbar/demo/bridge/cachecache', - '/libraries/vendor/maximebf/debugbar/demo/bridge', - '/libraries/vendor/maximebf/debugbar/demo', - '/libraries/vendor/maximebf/debugbar/build', - ); - - $status['files_checked'] = $files; - $status['folders_checked'] = $folders; - - foreach ($files as $file) - { - if ($fileExists = File::exists(JPATH_ROOT . $file)) - { - $status['files_exist'][] = $file; - - if ($dryRun === false) - { - if (File::delete(JPATH_ROOT . $file)) - { - $status['files_deleted'][] = $file; - } - else - { - $status['files_errors'][] = Text::sprintf('FILES_JOOMLA_ERROR_FILE_FOLDER', $file); - } - } - } - } - - $this->moveRemainingTemplateFiles(); - - foreach ($folders as $folder) - { - if ($folderExists = Folder::exists(JPATH_ROOT . $folder)) - { - $status['folders_exist'][] = $folder; - - if ($dryRun === false) - { - if (Folder::delete(JPATH_ROOT . $folder)) - { - $status['folders_deleted'][] = $folder; - } - else - { - $status['folders_errors'][] = Text::sprintf('FILES_JOOMLA_ERROR_FILE_FOLDER', $folder); - } - } - } - } - - $this->fixFilenameCasing(); - - /* - * Needed for updates from 3.10 - * If com_search doesn't exist then assume we can delete the search package manifest (included in the update packages) - * We deliberately check for the presence of the files in case people have previously uninstalled their search extension - * but an update has put the files back. In that case it exists even if they don't believe in it! - */ - if (!File::exists(JPATH_ROOT . '/administrator/components/com_search/search.php') - && File::exists(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml')) - { - File::delete(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml'); - } - - if ($suppressOutput === false && count($status['folders_errors'])) - { - echo implode('
', $status['folders_errors']); - } - - if ($suppressOutput === false && count($status['files_errors'])) - { - echo implode('
', $status['files_errors']); - } - - return $status; - } - - /** - * Method to create assets for newly installed components - * - * @param Installer $installer The class calling this method - * - * @return boolean - * - * @since 3.2 - */ - public function updateAssets($installer) - { - // List all components added since 4.0 - $newComponents = array( - // Components to be added here - ); - - foreach ($newComponents as $component) - { - /** @var \Joomla\CMS\Table\Asset $asset */ - $asset = Table::getInstance('Asset'); - - if ($asset->loadByName($component)) - { - continue; - } - - $asset->name = $component; - $asset->parent_id = 1; - $asset->rules = '{}'; - $asset->title = $component; - $asset->setLocation(1, 'last-child'); - - if (!$asset->store()) - { - // Install failed, roll back changes - $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_COMP_INSTALL_ROLLBACK', $asset->getError(true))); - - return false; - } - } - - return true; - } - - /** - * Converts the site's database tables to support UTF-8 Multibyte. - * - * @param boolean $doDbFixMsg Flag if message to be shown to check db fix - * - * @return void - * - * @since 3.5 - */ - public function convertTablesToUtf8mb4($doDbFixMsg = false) - { - $db = Factory::getDbo(); - - if ($db->getServerType() !== 'mysql') - { - return; - } - - // Check if the #__utf8_conversion table exists - $db->setQuery('SHOW TABLES LIKE ' . $db->quote($db->getPrefix() . 'utf8_conversion')); - - try - { - $rows = $db->loadRowList(0); - } - catch (Exception $e) - { - // Render the error message from the Exception object - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - if ($doDbFixMsg) - { - // Show an error message telling to check database problems - Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); - } - - return; - } - - // Nothing to do if the table doesn't exist because the CMS has never been updated from a pre-4.0 version - if (count($rows) === 0) - { - return; - } - - // Set required conversion status - $converted = 5; - - // Check conversion status in database - $db->setQuery( - 'SELECT ' . $db->quoteName('converted') - . ' FROM ' . $db->quoteName('#__utf8_conversion') - ); - - try - { - $convertedDB = $db->loadResult(); - } - catch (Exception $e) - { - // Render the error message from the Exception object - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - if ($doDbFixMsg) - { - // Show an error message telling to check database problems - Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); - } - - return; - } - - // If conversion status from DB is equal to required final status, try to drop the #__utf8_conversion table - if ($convertedDB === $converted) - { - $this->dropUtf8ConversionTable(); - - return; - } - - // Perform the required conversions of core tables if not done already in a previous step - if ($convertedDB !== 99) - { - $fileName1 = JPATH_ROOT . '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion.sql'; - - if (is_file($fileName1)) - { - $fileContents1 = @file_get_contents($fileName1); - $queries1 = $db->splitSql($fileContents1); - - if (!empty($queries1)) - { - foreach ($queries1 as $query1) - { - try - { - $db->setQuery($query1)->execute(); - } - catch (Exception $e) - { - $converted = $convertedDB; - - // Still render the error message from the Exception object - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - } - } - } - - // If no error before, perform the optional conversions of tables which might or might not exist - if ($converted === 5) - { - $fileName2 = JPATH_ROOT . '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion_optional.sql'; - - if (is_file($fileName2)) - { - $fileContents2 = @file_get_contents($fileName2); - $queries2 = $db->splitSql($fileContents2); - - if (!empty($queries2)) - { - foreach ($queries2 as $query2) - { - // Get table name from query - if (preg_match('/^ALTER\s+TABLE\s+([^\s]+)\s+/i', $query2, $matches) === 1) - { - $tableName = str_replace('`', '', $matches[1]); - $tableName = str_replace('#__', $db->getPrefix(), $tableName); - - // Check if the table exists and if yes, run the query - try - { - $db->setQuery('SHOW TABLES LIKE ' . $db->quote($tableName)); - - $rows = $db->loadRowList(0); - - if (count($rows) > 0) - { - $db->setQuery($query2)->execute(); - } - } - catch (Exception $e) - { - $converted = 99; - - // Still render the error message from the Exception object - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - } - } - } - } - - if ($doDbFixMsg && $converted !== 5) - { - // Show an error message telling to check database problems - Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); - } - - // If the conversion was successful try to drop the #__utf8_conversion table - if ($converted === 5 && $this->dropUtf8ConversionTable()) - { - // Table successfully dropped - return; - } - - // Set flag in database if the conversion status has changed. - if ($converted !== $convertedDB) - { - $db->setQuery('UPDATE ' . $db->quoteName('#__utf8_conversion') - . ' SET ' . $db->quoteName('converted') . ' = ' . $converted . ';' - )->execute(); - } - } - - /** - * This method clean the Joomla Cache using the method `clean` from the com_cache model - * - * @return void - * - * @since 3.5.1 - */ - private function cleanJoomlaCache() - { - /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ - $model = Factory::getApplication()->bootComponent('com_cache')->getMVCFactory() - ->createModel('Cache', 'Administrator', ['ignore_request' => true]); - - // Clean frontend cache - $model->clean(); - - // Clean admin cache - $model->setState('client_id', 1); - $model->clean(); - } - - /** - * This method drops the #__utf8_conversion table - * - * @return boolean True on success - * - * @since 4.0.0 - */ - private function dropUtf8ConversionTable() - { - $db = Factory::getDbo(); - - try - { - $db->setQuery('DROP TABLE ' . $db->quoteName('#__utf8_conversion') . ';' - )->execute(); - } - catch (Exception $e) - { - return false; - } - - return true; - } - - /** - * Called after any type of action - * - * @param string $action Which action is happening (install|uninstall|discover_install|update) - * @param Installer $installer The class calling this method - * - * @return boolean True on success - * - * @since 4.0.0 - */ - public function postflight($action, $installer) - { - if ($action !== 'update') - { - return true; - } - - if (empty($this->fromVersion) || version_compare($this->fromVersion, '4.0.0', 'ge')) - { - return true; - } - - // Update UCM content types. - $this->updateContentTypes(); - - $db = Factory::getDbo(); - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/Table/'); - - $tableItem = new \Joomla\Component\Menus\Administrator\Table\MenuTable($db); - - $contactItems = $this->contactItems($tableItem); - $finderItems = $this->finderItems($tableItem); - - $menuItems = array_merge($contactItems, $finderItems); - - foreach ($menuItems as $menuItem) - { - // Check an existing record - $keys = [ - 'menutype' => $menuItem['menutype'], - 'type' => $menuItem['type'], - 'title' => $menuItem['title'], - 'parent_id' => $menuItem['parent_id'], - 'client_id' => $menuItem['client_id'], - ]; - - if ($tableItem->load($keys)) - { - continue; - } - - $newTableItem = new \Joomla\Component\Menus\Administrator\Table\MenuTable($db); - - // Bind the data. - if (!$newTableItem->bind($menuItem)) - { - return false; - } - - $newTableItem->setLocation($menuItem['parent_id'], 'last-child'); - - // Check the data. - if (!$newTableItem->check()) - { - return false; - } - - // Store the data. - if (!$newTableItem->store()) - { - return false; - } - - // Rebuild the tree path. - if (!$newTableItem->rebuildPath($newTableItem->id)) - { - return false; - } - } - - return true; - } - - /** - * Prepare the contact menu items - * - * @return array Menu items - * - * @since 4.0.0 - */ - private function contactItems(Table $tableItem): array - { - // Check for the Contact parent Id Menu Item - $keys = [ - 'menutype' => 'main', - 'type' => 'component', - 'title' => 'com_contact', - 'parent_id' => 1, - 'client_id' => 1, - ]; - - $contactMenuitem = $tableItem->load($keys); - - if (!$contactMenuitem) - { - return []; - } - - $parentId = $tableItem->id; - $componentId = ExtensionHelper::getExtensionRecord('com_fields', 'component')->extension_id; - - // Add Contact Fields Menu Items. - $menuItems = [ - [ - 'menutype' => 'main', - 'title' => '-', - 'alias' => microtime(true), - 'note' => '', - 'path' => '', - 'link' => '#', - 'type' => 'separator', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'mod_menu_fields', - 'alias' => 'Contact Custom Fields', - 'note' => '', - 'path' => 'contact/Custom Fields', - 'link' => 'index.php?option=com_fields&context=com_contact.contact', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'mod_menu_fields_group', - 'alias' => 'Contact Custom Fields Group', - 'note' => '', - 'path' => 'contact/Custom Fields Group', - 'link' => 'index.php?option=com_fields&view=groups&context=com_contact.contact', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ] - ]; - - return $menuItems; - } - - /** - * Prepare the finder menu items - * - * @return array Menu items - * - * @since 4.0.0 - */ - private function finderItems(Table $tableItem): array - { - // Check for the Finder parent Id Menu Item - $keys = [ - 'menutype' => 'main', - 'type' => 'component', - 'title' => 'com_finder', - 'parent_id' => 1, - 'client_id' => 1, - ]; - - $finderMenuitem = $tableItem->load($keys); - - if (!$finderMenuitem) - { - return []; - } - - $parentId = $tableItem->id; - $componentId = ExtensionHelper::getExtensionRecord('com_finder', 'component')->extension_id; - - // Add Finder Fields Menu Items. - $menuItems = [ - [ - 'menutype' => 'main', - 'title' => '-', - 'alias' => microtime(true), - 'note' => '', - 'path' => '', - 'link' => '#', - 'type' => 'separator', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'com_finder_index', - 'alias' => 'Smart-Search-Index', - 'note' => '', - 'path' => 'Smart Search/Index', - 'link' => 'index.php?option=com_finder&view=index', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'com_finder_maps', - 'alias' => 'Smart-Search-Maps', - 'note' => '', - 'path' => 'Smart Search/Maps', - 'link' => 'index.php?option=com_finder&view=maps', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'com_finder_filters', - 'alias' => 'Smart-Search-Filters', - 'note' => '', - 'path' => 'Smart Search/Filters', - 'link' => 'index.php?option=com_finder&view=filters', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'com_finder_searches', - 'alias' => 'Smart-Search-Searches', - 'note' => '', - 'path' => 'Smart Search/Searches', - 'link' => 'index.php?option=com_finder&view=searches', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ] - ]; - - return $menuItems; - } - - /** - * Updates content type table classes. - * - * @return void - * - * @since 4.0.0 - */ - private function updateContentTypes(): void - { - // Content types to update. - $contentTypes = [ - 'com_content.article', - 'com_contact.contact', - 'com_newsfeeds.newsfeed', - 'com_tags.tag', - 'com_banners.banner', - 'com_banners.client', - 'com_users.note', - 'com_content.category', - 'com_contact.category', - 'com_newsfeeds.category', - 'com_banners.category', - 'com_users.category', - 'com_users.user', - ]; - - // Get table definitions. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('type_alias'), - $db->quoteName('table'), - ] - ) - ->from($db->quoteName('#__content_types')) - ->whereIn($db->quoteName('type_alias'), $contentTypes, ParameterType::STRING); - - $db->setQuery($query); - $contentTypes = $db->loadObjectList(); - - // Prepare the update query. - $query = $db->getQuery(true) - ->update($db->quoteName('#__content_types')) - ->set($db->quoteName('table') . ' = :table') - ->where($db->quoteName('type_alias') . ' = :typeAlias') - ->bind(':table', $table) - ->bind(':typeAlias', $typeAlias); - - $db->setQuery($query); - - foreach ($contentTypes as $contentType) - { - list($component, $tableType) = explode('.', $contentType->type_alias); - - // Special case for core table classes. - if ($contentType->type_alias === 'com_users.users' || $tableType === 'category') - { - $tablePrefix = 'Joomla\\CMS\Table\\'; - $tableType = ucfirst($tableType); - } - else - { - $tablePrefix = 'Joomla\\Component\\' . ucfirst(substr($component, 4)) . '\\Administrator\\Table\\'; - $tableType = ucfirst($tableType) . 'Table'; - } - - // Bind type alias. - $typeAlias = $contentType->type_alias; - - $table = json_decode($contentType->table); - - // Update table definitions. - $table->special->type = $tableType; - $table->special->prefix = $tablePrefix; - - // Some content types don't have this property. - if (!empty($table->common->prefix)) - { - $table->common->prefix = 'Joomla\\CMS\\Table\\'; - } - - $table = json_encode($table); - - // Execute the query. - $db->execute(); - } - } - - /** - * Renames or removes incorrectly cased files. - * - * @return void - * - * @since 3.9.25 - */ - protected function fixFilenameCasing() - { - $files = array( - // 3.10 changes - '/libraries/src/Filesystem/Support/Stringcontroller.php' => '/libraries/src/Filesystem/Support/StringController.php', - '/libraries/src/Form/Rule/SubFormRule.php' => '/libraries/src/Form/Rule/SubformRule.php', - // 4.0.0 - '/media/vendor/skipto/js/skipTo.js' => '/media/vendor/skipto/js/skipto.js', - ); - - foreach ($files as $old => $expected) - { - $oldRealpath = realpath(JPATH_ROOT . $old); - - // On Unix without incorrectly cased file. - if ($oldRealpath === false) - { - continue; - } - - $oldBasename = basename($oldRealpath); - $newRealpath = realpath(JPATH_ROOT . $expected); - $newBasename = basename($newRealpath); - $expectedBasename = basename($expected); - - // On Windows or Unix with only the incorrectly cased file. - if ($newBasename !== $expectedBasename) - { - // Rename the file. - File::move(JPATH_ROOT . $old, JPATH_ROOT . $old . '.tmp'); - File::move(JPATH_ROOT . $old . '.tmp', JPATH_ROOT . $expected); - - continue; - } - - // There might still be an incorrectly cased file on other OS than Windows. - if ($oldBasename === basename($old)) - { - // Check if case-insensitive file system, eg on OSX. - if (fileinode($oldRealpath) === fileinode($newRealpath)) - { - // Check deeper because even realpath or glob might not return the actual case. - if (!in_array($expectedBasename, scandir(dirname($newRealpath)))) - { - // Rename the file. - File::move(JPATH_ROOT . $old, JPATH_ROOT . $old . '.tmp'); - File::move(JPATH_ROOT . $old . '.tmp', JPATH_ROOT . $expected); - } - } - else - { - // On Unix with both files: Delete the incorrectly cased file. - File::delete(JPATH_ROOT . $old); - } - } - } - } - - /** - * Move core template (s)css or js or image files which are left after deleting - * obsolete core files to the right place in media folder. - * - * @return void - * - * @since 4.1.0 - */ - protected function moveRemainingTemplateFiles() - { - $folders = [ - '/administrator/templates/atum/css' => '/media/templates/administrator/atum/css', - '/administrator/templates/atum/images' => '/media/templates/administrator/atum/images', - '/administrator/templates/atum/js' => '/media/templates/administrator/atum/js', - '/administrator/templates/atum/scss' => '/media/templates/administrator/atum/scss', - '/templates/cassiopeia/css' => '/media/templates/site/cassiopeia/css', - '/templates/cassiopeia/images' => '/media/templates/site/cassiopeia/images', - '/templates/cassiopeia/js' => '/media/templates/site/cassiopeia/js', - '/templates/cassiopeia/scss' => '/media/templates/site/cassiopeia/scss', - ]; - - foreach ($folders as $oldFolder => $newFolder) - { - if (Folder::exists(JPATH_ROOT . $oldFolder)) - { - $oldPath = realpath(JPATH_ROOT . $oldFolder); - $newPath = realpath(JPATH_ROOT . $newFolder); - $directory = new \RecursiveDirectoryIterator($oldPath); - $directory->setFlags(RecursiveDirectoryIterator::SKIP_DOTS); - $iterator = new \RecursiveIteratorIterator($directory); - - // Handle all files in this folder and all sub-folders - foreach ($iterator as $oldFile) - { - if ($oldFile->isDir()) - { - continue; - } - - $newFile = $newPath . substr($oldFile, strlen($oldPath)); - - // Create target folder and parent folders if they don't exist yet - if (is_dir(dirname($newFile)) || @mkdir(dirname($newFile), 0755, true)) - { - File::move($oldFile, $newFile); - } - } - } - } - } - - /** - * Ensure the core templates are correctly moved to the new mode. - * - * @return void - * - * @since 4.1.0 - */ - protected function fixTemplateMode(): void - { - $db = Factory::getContainer()->get('DatabaseDriver'); - - array_map( - function ($template) use ($db) - { - $clientId = $template === 'atum' ? 1 : 0; - $query = $db->getQuery(true) - ->update($db->quoteName('#__template_styles')) - ->set($db->quoteName('inheritable') . ' = 1') - ->where($db->quoteName('template') . ' = ' . $db->quote($template)) - ->where($db->quoteName('client_id') . ' = ' . $clientId); - - try - { - $db->setQuery($query)->execute(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - }, - ['atum', 'cassiopeia'] - ); - } - - /** - * Add the user Auth Provider Column as it could be present from 3.10 already - * - * @return void - * - * @since 4.1.1 - */ - protected function addUserAuthProviderColumn(): void - { - $db = Factory::getContainer()->get('DatabaseDriver'); - - // Check if the column already exists - $fields = $db->getTableColumns('#__users'); - - // Column exists, skip - if (isset($fields['authProvider'])) - { - return; - } - - $query = 'ALTER TABLE ' . $db->quoteName('#__users') - . ' ADD COLUMN ' . $db->quoteName('authProvider') . ' varchar(100) DEFAULT ' . $db->quote('') . ' NOT NULL'; - - // Add column - try - { - $db->setQuery($query)->execute(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - } + /** + * The Joomla Version we are updating from + * + * @var string + * @since 3.7 + */ + protected $fromVersion = null; + + /** + * Function to act prior to installation process begins + * + * @param string $action Which action is happening (install|uninstall|discover_install|update) + * @param Installer $installer The class calling this method + * + * @return boolean True on success + * + * @since 3.7.0 + */ + public function preflight($action, $installer) + { + if ($action === 'update') { + // Get the version we are updating from + if (!empty($installer->extension->manifest_cache)) { + $manifestValues = json_decode($installer->extension->manifest_cache, true); + + if (array_key_exists('version', $manifestValues)) { + $this->fromVersion = $manifestValues['version']; + + // Ensure templates are moved to the correct mode + $this->fixTemplateMode(); + + return true; + } + } + + return false; + } + + return true; + } + + /** + * Method to update Joomla! + * + * @param Installer $installer The class calling this method + * + * @return void + */ + public function update($installer) + { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + + try { + Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_DELETE_FILES'), Log::INFO, 'Update'); + } catch (RuntimeException $exception) { + // Informational log only + } + + // Uninstall plugins before removing their files and folders + $this->uninstallRepeatableFieldsPlugin(); + $this->uninstallEosPlugin(); + + // This needs to stay for 2.5 update compatibility + $this->deleteUnexistingFiles(); + $this->updateManifestCaches(); + $this->updateDatabase(); + $this->updateAssets($installer); + $this->clearStatsCache(); + $this->convertTablesToUtf8mb4(true); + $this->addUserAuthProviderColumn(); + $this->cleanJoomlaCache(); + } + + /** + * Method to clear our stats plugin cache to ensure we get fresh data on Joomla Update + * + * @return void + * + * @since 3.5 + */ + protected function clearStatsCache() + { + $db = Factory::getDbo(); + + try { + // Get the params for the stats plugin + $params = $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('stats')) + )->loadResult(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + + $params = json_decode($params, true); + + // Reset the last run parameter + if (isset($params['lastrun'])) { + $params['lastrun'] = ''; + } + + $params = json_encode($params); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote($params)) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('stats')); + + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + } + + /** + * Method to update Database + * + * @return void + */ + protected function updateDatabase() + { + if (Factory::getDbo()->getServerType() === 'mysql') { + $this->updateDatabaseMysql(); + } + } + + /** + * Method to update MySQL Database + * + * @return void + */ + protected function updateDatabaseMysql() + { + $db = Factory::getDbo(); + + $db->setQuery('SHOW ENGINES'); + + try { + $results = $db->loadObjectList(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + + foreach ($results as $result) { + if ($result->Support != 'DEFAULT') { + continue; + } + + $db->setQuery('ALTER TABLE #__update_sites_extensions ENGINE = ' . $result->Engine); + + try { + $db->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + + break; + } + } + + /** + * Uninstalls the plg_fields_repeatable plugin and transforms its custom field instances + * to instances of the plg_fields_subfields plugin. + * + * @return void + * + * @since 4.0.0 + */ + protected function uninstallRepeatableFieldsPlugin() + { + $app = Factory::getApplication(); + $db = Factory::getDbo(); + + // Check if the plg_fields_repeatable plugin is present + $extensionId = $db->setQuery( + $db->getQuery(true) + ->select('extension_id') + ->from('#__extensions') + ->where('name = ' . $db->quote('plg_fields_repeatable')) + )->loadResult(); + + // Skip uninstalling when it doesn't exist + if (!$extensionId) { + return; + } + + // Ensure the FieldsHelper class is loaded for the Repeatable fields plugin we're about to remove + \JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php'); + + try { + $db->transactionStart(); + + // Get the FieldsModelField, we need it in a sec + $fieldModel = $app->bootComponent('com_fields')->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); + /** @var FieldModel $fieldModel */ + + // Now get a list of all `repeatable` custom field instances + $db->setQuery( + $db->getQuery(true) + ->select('*') + ->from('#__fields') + ->where($db->quoteName('type') . ' = ' . $db->quote('repeatable')) + ); + + // Execute the query and iterate over the `repeatable` instances + foreach ($db->loadObjectList() as $row) { + // Skip broken rows - just a security measure, should not happen + if (!isset($row->fieldparams) || !($oldFieldparams = json_decode($row->fieldparams)) || !is_object($oldFieldparams)) { + continue; + } + + /** + * We basically want to transform this `repeatable` type into a `subfields` type. While $oldFieldparams + * holds the `fieldparams` of the `repeatable` type, $newFieldparams shall hold the `fieldparams` + * of the `subfields` type. + */ + $newFieldparams = [ + 'repeat' => '1', + 'options' => [], + ]; + + /** + * This array is used to store the mapping between the name of form fields from Repeatable field + * with ID of the child-fields. It will then be used to migrate data later + */ + $mapping = []; + + /** + * Store name of media fields which we need to convert data from old format (string) to new + * format (json) during the migration + */ + $mediaFields = []; + + // If this repeatable fields actually had child-fields (normally this is always the case) + if (isset($oldFieldparams->fields) && is_object($oldFieldparams->fields)) { + // Small counter for the child-fields (aka sub fields) + $newFieldCount = 0; + + // Iterate over the sub fields + foreach (get_object_vars($oldFieldparams->fields) as $oldField) { + // Used for field name collision prevention + $fieldname_prefix = ''; + $fieldname_suffix = 0; + + // Try to save the new sub field in a loop because of field name collisions + while (true) { + /** + * We basically want to create a completely new custom fields instance for every sub field + * of the `repeatable` instance. This is what we use $data for, we create a new custom field + * for each of the sub fields of the `repeatable` instance. + */ + $data = [ + 'context' => $row->context, + 'group_id' => $row->group_id, + 'title' => $oldField->fieldname, + 'name' => ( + $fieldname_prefix + . $oldField->fieldname + . ($fieldname_suffix > 0 ? ('_' . $fieldname_suffix) : '') + ), + 'label' => $oldField->fieldname, + 'default_value' => $row->default_value, + 'type' => $oldField->fieldtype, + 'description' => $row->description, + 'state' => '1', + 'params' => $row->params, + 'language' => '*', + 'assigned_cat_ids' => [-1], + 'only_use_in_subform' => 1, + ]; + + // `number` is not a valid custom field type, so use `text` instead. + if ($data['type'] == 'number') { + $data['type'] = 'text'; + } + + if ($data['type'] == 'media') { + $mediaFields[] = $oldField->fieldname; + } + + // Reset the state because else \Joomla\CMS\MVC\Model\AdminModel will take an already + // existing value (e.g. from previous save) and do an UPDATE instead of INSERT. + $fieldModel->setState('field.id', 0); + + // If an error occurred when trying to save this. + if (!$fieldModel->save($data)) { + // If the error is, that the name collided, increase the collision prevention + $error = $fieldModel->getError(); + + if ($error == 'COM_FIELDS_ERROR_UNIQUE_NAME') { + // If this is the first time this error occurs, set only the prefix + if ($fieldname_prefix == '') { + $fieldname_prefix = ($row->name . '_'); + } else { + // Else increase the suffix + $fieldname_suffix++; + } + + // And start again with the while loop. + continue 1; + } + + // Else bail out with the error. Something is totally wrong. + throw new \Exception($error); + } + + // Break out of the while loop, saving was successful. + break 1; + } + + // Get the newly created id + $subfield_id = $fieldModel->getState('field.id'); + + // Really check that it is valid + if (!is_numeric($subfield_id) || $subfield_id < 1) { + throw new \Exception('Something went wrong.'); + } + + // And tell our new `subfields` field about his child + $newFieldparams['options'][('option' . $newFieldCount)] = [ + 'customfield' => $subfield_id, + 'render_values' => '1', + ]; + + $newFieldCount++; + + $mapping[$oldField->fieldname] = 'field' . $subfield_id; + } + } + + // Write back the changed stuff to the database + $db->setQuery( + $db->getQuery(true) + ->update('#__fields') + ->set($db->quoteName('type') . ' = ' . $db->quote('subform')) + ->set($db->quoteName('fieldparams') . ' = ' . $db->quote(json_encode($newFieldparams))) + ->where($db->quoteName('id') . ' = ' . $db->quote($row->id)) + )->execute(); + + // Migrate data for this field + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__fields_values')) + ->where($db->quoteName('field_id') . ' = ' . $row->id); + $db->setQuery($query); + + foreach ($db->loadObjectList() as $rowFieldValue) { + // Do not do the version if no data is entered for the custom field this item + if (!$rowFieldValue->value) { + continue; + } + + /** + * Here we will have to update the stored value of the field to new format + * The key for each row changes from repeatable to row, for example repeatable0 to row0, and so on + * The key for each sub-field change from name of field to field + ID of the new sub-field + * Example data format stored in J3: {"repeatable0":{"id":"1","username":"admin"}} + * Example data format stored in J4: {"row0":{"field1":"1","field2":"admin"}} + */ + $newFieldValue = []; + + // Convert to array to change key + $fieldValue = json_decode($rowFieldValue->value, true); + + // If data could not be decoded for some reason, ignore + if (!$fieldValue) { + continue; + } + + $rowIndex = 0; + + foreach ($fieldValue as $rowKey => $rowValue) { + $rowKey = 'row' . ($rowIndex++); + $newFieldValue[$rowKey] = []; + + foreach ($rowValue as $subFieldName => $subFieldValue) { + // This is a media field, so we need to convert data to new format required in Joomla! 4 + if (in_array($subFieldName, $mediaFields)) { + $subFieldValue = ['imagefile' => $subFieldValue, 'alt_text' => '']; + } + + if (isset($mapping[$subFieldName])) { + $newFieldValue[$rowKey][$mapping[$subFieldName]] = $subFieldValue; + } else { + // Not found, use the old key to avoid data lost + $newFieldValue[$subFieldName] = $subFieldValue; + } + } + } + + $query->clear() + ->update($db->quoteName('#__fields_values')) + ->set($db->quoteName('value') . ' = ' . $db->quote(json_encode($newFieldValue))) + ->where($db->quoteName('field_id') . ' = ' . $rowFieldValue->field_id) + ->where($db->quoteName('item_id') . ' =' . $rowFieldValue->item_id); + $db->setQuery($query) + ->execute(); + } + } + + // Now, unprotect the plugin so we can uninstall it + $db->setQuery( + $db->getQuery(true) + ->update('#__extensions') + ->set('protected = 0') + ->where($db->quoteName('extension_id') . ' = ' . $extensionId) + )->execute(); + + // And now uninstall the plugin + $installer = new Installer(); + $installer->setDatabase($db); + $installer->uninstall('plugin', $extensionId); + + $db->transactionCommit(); + } catch (\Exception $e) { + $db->transactionRollback(); + throw $e; + } + } + + /** + * Uninstall the 3.10 EOS plugin + * + * @return void + * + * @since 4.0.0 + */ + protected function uninstallEosPlugin() + { + $db = Factory::getDbo(); + + // Check if the plg_quickicon_eos310 plugin is present + $extensionId = $db->setQuery( + $db->getQuery(true) + ->select('extension_id') + ->from('#__extensions') + ->where('name = ' . $db->quote('plg_quickicon_eos310')) + )->loadResult(); + + // Skip uninstalling if it doesn't exist + if (!$extensionId) { + return; + } + + try { + $db->transactionStart(); + + // Unprotect the plugin so we can uninstall it + $db->setQuery( + $db->getQuery(true) + ->update('#__extensions') + ->set('protected = 0') + ->where($db->quoteName('extension_id') . ' = ' . $extensionId) + )->execute(); + + // Uninstall the plugin + $installer = new Installer(); + $installer->setDatabase($db); + $installer->uninstall('plugin', $extensionId); + + $db->transactionCommit(); + } catch (\Exception $e) { + $db->transactionRollback(); + throw $e; + } + } + + /** + * Update the manifest caches + * + * @return void + */ + protected function updateManifestCaches() + { + $extensions = ExtensionHelper::getCoreExtensions(); + + // If we have the search package around, it may not have a manifest cache entry after upgrades from 3.x, so add it to the list + if (File::exists(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml')) { + $extensions[] = array('package', 'pkg_search', '', 0); + } + + // Attempt to refresh manifest caches + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->from('#__extensions'); + + foreach ($extensions as $extension) { + $query->where( + 'type=' . $db->quote($extension[0]) + . ' AND element=' . $db->quote($extension[1]) + . ' AND folder=' . $db->quote($extension[2]) + . ' AND client_id=' . $extension[3], + 'OR' + ); + } + + $db->setQuery($query); + + try { + $extensions = $db->loadObjectList(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + + $installer = new Installer(); + $installer->setDatabase($db); + + foreach ($extensions as $extension) { + if (!$installer->refreshManifestCache($extension->extension_id)) { + echo Text::sprintf('FILES_JOOMLA_ERROR_MANIFEST', $extension->type, $extension->element, $extension->name, $extension->client_id) . '
'; + } + } + } + + /** + * Delete files that should not exist + * + * @param bool $dryRun If set to true, will not actually delete files, but just report their status for use in CLI + * @param bool $suppressOutput Set to true to suppress echoing any errors, and just return the $status array + * + * @return array + */ + public function deleteUnexistingFiles($dryRun = false, $suppressOutput = false) + { + $status = [ + 'files_exist' => [], + 'folders_exist' => [], + 'files_deleted' => [], + 'folders_deleted' => [], + 'files_errors' => [], + 'folders_errors' => [], + 'folders_checked' => [], + 'files_checked' => [], + ]; + + $files = array( + // From 3.10 to 4.1 + '/administrator/components/com_actionlogs/actionlogs.php', + '/administrator/components/com_actionlogs/controller.php', + '/administrator/components/com_actionlogs/controllers/actionlogs.php', + '/administrator/components/com_actionlogs/helpers/actionlogs.php', + '/administrator/components/com_actionlogs/helpers/actionlogsphp55.php', + '/administrator/components/com_actionlogs/layouts/logstable.php', + '/administrator/components/com_actionlogs/libraries/actionlogplugin.php', + '/administrator/components/com_actionlogs/models/actionlog.php', + '/administrator/components/com_actionlogs/models/actionlogs.php', + '/administrator/components/com_actionlogs/models/fields/extension.php', + '/administrator/components/com_actionlogs/models/fields/logcreator.php', + '/administrator/components/com_actionlogs/models/fields/logsdaterange.php', + '/administrator/components/com_actionlogs/models/fields/logtype.php', + '/administrator/components/com_actionlogs/models/fields/plugininfo.php', + '/administrator/components/com_actionlogs/models/forms/filter_actionlogs.xml', + '/administrator/components/com_actionlogs/views/actionlogs/tmpl/default.php', + '/administrator/components/com_actionlogs/views/actionlogs/tmpl/default.xml', + '/administrator/components/com_actionlogs/views/actionlogs/view.html.php', + '/administrator/components/com_admin/admin.php', + '/administrator/components/com_admin/controller.php', + '/administrator/components/com_admin/controllers/profile.php', + '/administrator/components/com_admin/helpers/html/directory.php', + '/administrator/components/com_admin/helpers/html/phpsetting.php', + '/administrator/components/com_admin/helpers/html/system.php', + '/administrator/components/com_admin/models/forms/profile.xml', + '/administrator/components/com_admin/models/help.php', + '/administrator/components/com_admin/models/profile.php', + '/administrator/components/com_admin/models/sysinfo.php', + '/administrator/components/com_admin/postinstall/eaccelerator.php', + '/administrator/components/com_admin/postinstall/htaccess.php', + '/administrator/components/com_admin/postinstall/joomla40checks.php', + '/administrator/components/com_admin/postinstall/updatedefaultsettings.php', + '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-01.sql', + '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-02.sql', + '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-06.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-21-1.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-21-2.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-23.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2012-01-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2012-01-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.1-2012-01-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.2-2012-03-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.3-2012-03-13.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.4-2012-03-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.4-2012-03-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.5.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.6.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.7.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.0.0.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.0.1.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.0.2.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.0.3.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.0.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.1.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.2.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.3.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.4.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.5.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.10.0-2020-08-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.10.0-2021-05-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.10.7-2022-02-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.10.7-2022-03-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.0.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.1.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2013-12-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2013-12-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-23.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.3-2014-02-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.3.0-2014-02-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.3.0-2014-04-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.3.4-2014-08-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.3.6-2014-09-30.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-08-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-09-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-09-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-10-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-12-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2015-01-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2015-02-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-07-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-13.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-30.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-11-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-11-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2016-02-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2016-03-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.1-2016-03-25.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.1-2016-03-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-06.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-05-06.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-06-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-06-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.3-2016-08-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.3-2016-08-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-06.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-09-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-10-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-27.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-17.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-31.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-17.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-04-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-04-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.3-2017-06-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.4-2017-07-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.0-2017-07-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.0-2017-07-31.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.2-2017-10-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.4-2018-01-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.6-2018-02-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.8-2018-05-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.9-2018-06-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-27.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-12.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-13.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-17.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-12.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-09-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.10-2019-07-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.16-2020-02-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.16-2020-03-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.19-2020-05-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.19-2020-06-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.21-2020-08-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.22-2020-09-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.26-2021-04-07.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.27-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.3-2019-01-12.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.3-2019-02-07.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-04-23.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-04-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-05-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.8-2019-06-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.8-2019-06-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.0.0.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.0.1.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.0.2.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.0.3.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.0.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.1.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.2.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.3.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.4.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.5.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.0-2020-08-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.0-2021-05-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-02-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-02-20.sql.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-03-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.0.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.1.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2013-12-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2013-12-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-23.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.3-2014-02-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2013-12-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2014-02-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2014-04-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.4-2014-08-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.6-2014-09-30.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-08-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-09-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-09-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-10-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-12-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2015-01-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2015-02-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.4-2015-07-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-13.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-30.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-11-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-11-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2016-03-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-05-06.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-06-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-06-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-08-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-08-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-10-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-06.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-09-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-10-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-17.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-31.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-17.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-03-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-03-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-04-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-04-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.4-2017-07-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.0-2017-07-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.0-2017-07-31.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.2-2017-10-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.4-2018-01-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.6-2018-02-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.8-2018-05-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.9-2018-06-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-27.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-12.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-13.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-17.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-12.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-09-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.10-2019-07-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.15-2020-01-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.16-2020-02-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.16-2020-03-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.19-2020-06-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.21-2020-08-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.22-2020-09-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.26-2021-04-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.27-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.3-2019-01-12.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.3-2019-02-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-04-23.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-04-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-05-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.8-2019-06-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.8-2019-06-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.2-2012-03-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.3-2012-03-13.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.4-2012-03-18.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.4-2012-03-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.5.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.6.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.7.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.0.0.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.0.1.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.0.2.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.0.3.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.0.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.1.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.2.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.3.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.4.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.5.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.0-2021-05-28.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.1-2021-08-17.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-02-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-02-20.sql.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-03-18.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.0.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.1.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2013-12-22.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2013-12-28.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-08.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-18.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-23.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.3-2014-02-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.3.0-2014-02-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.3.0-2014-04-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.3.4-2014-08-03.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.3.6-2014-09-30.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-08-24.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-09-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-09-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-10-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-12-03.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2015-01-21.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2015-02-26.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.4-2015-07-11.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-13.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-26.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-30.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-11-04.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-11-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2016-03-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-06.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-08.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-05-06.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-06-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-06-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.3-2016-08-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.3-2016-08-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-06.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-22.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-29.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-09-29.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-10-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-04.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-24.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-08.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-17.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-31.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-17.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-03-03.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-03-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-04-10.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-04-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.4-2017-07-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.0-2017-07-28.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.0-2017-07-31.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.2-2017-10-14.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.4-2018-01-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.6-2018-02-14.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.8-2018-05-18.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.9-2018-06-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-03.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-24.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-27.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-12.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-13.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-14.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-17.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-10.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-11.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-12.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-28.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-29.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-09-04.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-21.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.10-2019-07-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.16-2020-03-04.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.19-2020-06-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.21-2020-08-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.22-2020-09-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.26-2021-04-07.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.27-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.3-2019-01-12.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.3-2019-02-07.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.4-2019-03-06.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-04-23.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-04-26.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-05-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.8-2019-06-11.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.8-2019-06-15.sql', + '/administrator/components/com_admin/views/help/tmpl/default.php', + '/administrator/components/com_admin/views/help/tmpl/default.xml', + '/administrator/components/com_admin/views/help/tmpl/langforum.php', + '/administrator/components/com_admin/views/help/view.html.php', + '/administrator/components/com_admin/views/profile/tmpl/edit.php', + '/administrator/components/com_admin/views/profile/view.html.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default.xml', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_config.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_directory.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_phpinfo.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_phpsettings.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_system.php', + '/administrator/components/com_admin/views/sysinfo/view.html.php', + '/administrator/components/com_admin/views/sysinfo/view.json.php', + '/administrator/components/com_admin/views/sysinfo/view.text.php', + '/administrator/components/com_associations/associations.php', + '/administrator/components/com_associations/controller.php', + '/administrator/components/com_associations/controllers/association.php', + '/administrator/components/com_associations/controllers/associations.php', + '/administrator/components/com_associations/helpers/associations.php', + '/administrator/components/com_associations/layouts/joomla/searchtools/default/bar.php', + '/administrator/components/com_associations/models/association.php', + '/administrator/components/com_associations/models/associations.php', + '/administrator/components/com_associations/models/fields/itemlanguage.php', + '/administrator/components/com_associations/models/fields/itemtype.php', + '/administrator/components/com_associations/models/fields/modalassociation.php', + '/administrator/components/com_associations/models/forms/association.xml', + '/administrator/components/com_associations/models/forms/filter_associations.xml', + '/administrator/components/com_associations/views/association/tmpl/edit.php', + '/administrator/components/com_associations/views/association/view.html.php', + '/administrator/components/com_associations/views/associations/tmpl/default.php', + '/administrator/components/com_associations/views/associations/tmpl/default.xml', + '/administrator/components/com_associations/views/associations/tmpl/modal.php', + '/administrator/components/com_associations/views/associations/view.html.php', + '/administrator/components/com_banners/banners.php', + '/administrator/components/com_banners/controller.php', + '/administrator/components/com_banners/controllers/banner.php', + '/administrator/components/com_banners/controllers/banners.php', + '/administrator/components/com_banners/controllers/client.php', + '/administrator/components/com_banners/controllers/clients.php', + '/administrator/components/com_banners/controllers/tracks.php', + '/administrator/components/com_banners/controllers/tracks.raw.php', + '/administrator/components/com_banners/helpers/html/banner.php', + '/administrator/components/com_banners/models/banner.php', + '/administrator/components/com_banners/models/banners.php', + '/administrator/components/com_banners/models/client.php', + '/administrator/components/com_banners/models/clients.php', + '/administrator/components/com_banners/models/download.php', + '/administrator/components/com_banners/models/fields/bannerclient.php', + '/administrator/components/com_banners/models/fields/clicks.php', + '/administrator/components/com_banners/models/fields/impmade.php', + '/administrator/components/com_banners/models/fields/imptotal.php', + '/administrator/components/com_banners/models/forms/banner.xml', + '/administrator/components/com_banners/models/forms/client.xml', + '/administrator/components/com_banners/models/forms/download.xml', + '/administrator/components/com_banners/models/forms/filter_banners.xml', + '/administrator/components/com_banners/models/forms/filter_clients.xml', + '/administrator/components/com_banners/models/forms/filter_tracks.xml', + '/administrator/components/com_banners/models/tracks.php', + '/administrator/components/com_banners/tables/banner.php', + '/administrator/components/com_banners/tables/client.php', + '/administrator/components/com_banners/views/banner/tmpl/edit.php', + '/administrator/components/com_banners/views/banner/view.html.php', + '/administrator/components/com_banners/views/banners/tmpl/default.php', + '/administrator/components/com_banners/views/banners/tmpl/default_batch_body.php', + '/administrator/components/com_banners/views/banners/tmpl/default_batch_footer.php', + '/administrator/components/com_banners/views/banners/view.html.php', + '/administrator/components/com_banners/views/client/tmpl/edit.php', + '/administrator/components/com_banners/views/client/view.html.php', + '/administrator/components/com_banners/views/clients/tmpl/default.php', + '/administrator/components/com_banners/views/clients/view.html.php', + '/administrator/components/com_banners/views/download/tmpl/default.php', + '/administrator/components/com_banners/views/download/view.html.php', + '/administrator/components/com_banners/views/tracks/tmpl/default.php', + '/administrator/components/com_banners/views/tracks/view.html.php', + '/administrator/components/com_banners/views/tracks/view.raw.php', + '/administrator/components/com_cache/cache.php', + '/administrator/components/com_cache/controller.php', + '/administrator/components/com_cache/helpers/cache.php', + '/administrator/components/com_cache/models/cache.php', + '/administrator/components/com_cache/models/forms/filter_cache.xml', + '/administrator/components/com_cache/views/cache/tmpl/default.php', + '/administrator/components/com_cache/views/cache/tmpl/default.xml', + '/administrator/components/com_cache/views/cache/view.html.php', + '/administrator/components/com_cache/views/purge/tmpl/default.php', + '/administrator/components/com_cache/views/purge/tmpl/default.xml', + '/administrator/components/com_cache/views/purge/view.html.php', + '/administrator/components/com_categories/categories.php', + '/administrator/components/com_categories/controller.php', + '/administrator/components/com_categories/controllers/ajax.json.php', + '/administrator/components/com_categories/controllers/categories.php', + '/administrator/components/com_categories/controllers/category.php', + '/administrator/components/com_categories/helpers/association.php', + '/administrator/components/com_categories/helpers/html/categoriesadministrator.php', + '/administrator/components/com_categories/models/categories.php', + '/administrator/components/com_categories/models/category.php', + '/administrator/components/com_categories/models/fields/categoryedit.php', + '/administrator/components/com_categories/models/fields/categoryparent.php', + '/administrator/components/com_categories/models/fields/modal/category.php', + '/administrator/components/com_categories/models/forms/category.xml', + '/administrator/components/com_categories/models/forms/filter_categories.xml', + '/administrator/components/com_categories/tables/category.php', + '/administrator/components/com_categories/views/categories/tmpl/default.php', + '/administrator/components/com_categories/views/categories/tmpl/default.xml', + '/administrator/components/com_categories/views/categories/tmpl/default_batch_body.php', + '/administrator/components/com_categories/views/categories/tmpl/default_batch_footer.php', + '/administrator/components/com_categories/views/categories/tmpl/modal.php', + '/administrator/components/com_categories/views/categories/view.html.php', + '/administrator/components/com_categories/views/category/tmpl/edit.php', + '/administrator/components/com_categories/views/category/tmpl/edit.xml', + '/administrator/components/com_categories/views/category/tmpl/edit_associations.php', + '/administrator/components/com_categories/views/category/tmpl/edit_metadata.php', + '/administrator/components/com_categories/views/category/tmpl/modal.php', + '/administrator/components/com_categories/views/category/tmpl/modal_associations.php', + '/administrator/components/com_categories/views/category/tmpl/modal_extrafields.php', + '/administrator/components/com_categories/views/category/tmpl/modal_metadata.php', + '/administrator/components/com_categories/views/category/tmpl/modal_options.php', + '/administrator/components/com_categories/views/category/view.html.php', + '/administrator/components/com_checkin/checkin.php', + '/administrator/components/com_checkin/controller.php', + '/administrator/components/com_checkin/models/checkin.php', + '/administrator/components/com_checkin/models/forms/filter_checkin.xml', + '/administrator/components/com_checkin/views/checkin/tmpl/default.php', + '/administrator/components/com_checkin/views/checkin/tmpl/default.xml', + '/administrator/components/com_checkin/views/checkin/view.html.php', + '/administrator/components/com_config/config.php', + '/administrator/components/com_config/controller.php', + '/administrator/components/com_config/controller/application/cancel.php', + '/administrator/components/com_config/controller/application/display.php', + '/administrator/components/com_config/controller/application/removeroot.php', + '/administrator/components/com_config/controller/application/save.php', + '/administrator/components/com_config/controller/application/sendtestmail.php', + '/administrator/components/com_config/controller/application/store.php', + '/administrator/components/com_config/controller/component/cancel.php', + '/administrator/components/com_config/controller/component/display.php', + '/administrator/components/com_config/controller/component/save.php', + '/administrator/components/com_config/controllers/application.php', + '/administrator/components/com_config/controllers/component.php', + '/administrator/components/com_config/helper/config.php', + '/administrator/components/com_config/model/application.php', + '/administrator/components/com_config/model/component.php', + '/administrator/components/com_config/model/field/configcomponents.php', + '/administrator/components/com_config/model/field/filters.php', + '/administrator/components/com_config/model/form/application.xml', + '/administrator/components/com_config/models/application.php', + '/administrator/components/com_config/models/component.php', + '/administrator/components/com_config/view/application/html.php', + '/administrator/components/com_config/view/application/json.php', + '/administrator/components/com_config/view/application/tmpl/default.php', + '/administrator/components/com_config/view/application/tmpl/default.xml', + '/administrator/components/com_config/view/application/tmpl/default_cache.php', + '/administrator/components/com_config/view/application/tmpl/default_cookie.php', + '/administrator/components/com_config/view/application/tmpl/default_database.php', + '/administrator/components/com_config/view/application/tmpl/default_debug.php', + '/administrator/components/com_config/view/application/tmpl/default_filters.php', + '/administrator/components/com_config/view/application/tmpl/default_ftp.php', + '/administrator/components/com_config/view/application/tmpl/default_ftplogin.php', + '/administrator/components/com_config/view/application/tmpl/default_locale.php', + '/administrator/components/com_config/view/application/tmpl/default_mail.php', + '/administrator/components/com_config/view/application/tmpl/default_metadata.php', + '/administrator/components/com_config/view/application/tmpl/default_navigation.php', + '/administrator/components/com_config/view/application/tmpl/default_permissions.php', + '/administrator/components/com_config/view/application/tmpl/default_proxy.php', + '/administrator/components/com_config/view/application/tmpl/default_seo.php', + '/administrator/components/com_config/view/application/tmpl/default_server.php', + '/administrator/components/com_config/view/application/tmpl/default_session.php', + '/administrator/components/com_config/view/application/tmpl/default_site.php', + '/administrator/components/com_config/view/application/tmpl/default_system.php', + '/administrator/components/com_config/view/component/html.php', + '/administrator/components/com_config/view/component/tmpl/default.php', + '/administrator/components/com_config/view/component/tmpl/default.xml', + '/administrator/components/com_config/view/component/tmpl/default_navigation.php', + '/administrator/components/com_contact/contact.php', + '/administrator/components/com_contact/controller.php', + '/administrator/components/com_contact/controllers/ajax.json.php', + '/administrator/components/com_contact/controllers/contact.php', + '/administrator/components/com_contact/controllers/contacts.php', + '/administrator/components/com_contact/helpers/associations.php', + '/administrator/components/com_contact/helpers/html/contact.php', + '/administrator/components/com_contact/models/contact.php', + '/administrator/components/com_contact/models/contacts.php', + '/administrator/components/com_contact/models/fields/modal/contact.php', + '/administrator/components/com_contact/models/forms/contact.xml', + '/administrator/components/com_contact/models/forms/fields/mail.xml', + '/administrator/components/com_contact/models/forms/filter_contacts.xml', + '/administrator/components/com_contact/tables/contact.php', + '/administrator/components/com_contact/views/contact/tmpl/edit.php', + '/administrator/components/com_contact/views/contact/tmpl/edit_associations.php', + '/administrator/components/com_contact/views/contact/tmpl/edit_metadata.php', + '/administrator/components/com_contact/views/contact/tmpl/edit_params.php', + '/administrator/components/com_contact/views/contact/tmpl/modal.php', + '/administrator/components/com_contact/views/contact/tmpl/modal_associations.php', + '/administrator/components/com_contact/views/contact/tmpl/modal_metadata.php', + '/administrator/components/com_contact/views/contact/tmpl/modal_params.php', + '/administrator/components/com_contact/views/contact/view.html.php', + '/administrator/components/com_contact/views/contacts/tmpl/default.php', + '/administrator/components/com_contact/views/contacts/tmpl/default_batch.php', + '/administrator/components/com_contact/views/contacts/tmpl/default_batch_body.php', + '/administrator/components/com_contact/views/contacts/tmpl/default_batch_footer.php', + '/administrator/components/com_contact/views/contacts/tmpl/modal.php', + '/administrator/components/com_contact/views/contacts/view.html.php', + '/administrator/components/com_content/content.php', + '/administrator/components/com_content/controller.php', + '/administrator/components/com_content/controllers/ajax.json.php', + '/administrator/components/com_content/controllers/article.php', + '/administrator/components/com_content/controllers/articles.php', + '/administrator/components/com_content/controllers/featured.php', + '/administrator/components/com_content/helpers/associations.php', + '/administrator/components/com_content/helpers/html/contentadministrator.php', + '/administrator/components/com_content/models/article.php', + '/administrator/components/com_content/models/articles.php', + '/administrator/components/com_content/models/feature.php', + '/administrator/components/com_content/models/featured.php', + '/administrator/components/com_content/models/fields/modal/article.php', + '/administrator/components/com_content/models/fields/voteradio.php', + '/administrator/components/com_content/models/forms/article.xml', + '/administrator/components/com_content/models/forms/filter_articles.xml', + '/administrator/components/com_content/models/forms/filter_featured.xml', + '/administrator/components/com_content/tables/featured.php', + '/administrator/components/com_content/views/article/tmpl/edit.php', + '/administrator/components/com_content/views/article/tmpl/edit.xml', + '/administrator/components/com_content/views/article/tmpl/edit_associations.php', + '/administrator/components/com_content/views/article/tmpl/edit_metadata.php', + '/administrator/components/com_content/views/article/tmpl/modal.php', + '/administrator/components/com_content/views/article/tmpl/modal_associations.php', + '/administrator/components/com_content/views/article/tmpl/modal_metadata.php', + '/administrator/components/com_content/views/article/tmpl/pagebreak.php', + '/administrator/components/com_content/views/article/view.html.php', + '/administrator/components/com_content/views/articles/tmpl/default.php', + '/administrator/components/com_content/views/articles/tmpl/default.xml', + '/administrator/components/com_content/views/articles/tmpl/default_batch_body.php', + '/administrator/components/com_content/views/articles/tmpl/default_batch_footer.php', + '/administrator/components/com_content/views/articles/tmpl/modal.php', + '/administrator/components/com_content/views/articles/view.html.php', + '/administrator/components/com_content/views/featured/tmpl/default.php', + '/administrator/components/com_content/views/featured/tmpl/default.xml', + '/administrator/components/com_content/views/featured/view.html.php', + '/administrator/components/com_contenthistory/contenthistory.php', + '/administrator/components/com_contenthistory/controller.php', + '/administrator/components/com_contenthistory/controllers/history.php', + '/administrator/components/com_contenthistory/controllers/preview.php', + '/administrator/components/com_contenthistory/helpers/html/textdiff.php', + '/administrator/components/com_contenthistory/models/compare.php', + '/administrator/components/com_contenthistory/models/history.php', + '/administrator/components/com_contenthistory/models/preview.php', + '/administrator/components/com_contenthistory/views/compare/tmpl/compare.php', + '/administrator/components/com_contenthistory/views/compare/view.html.php', + '/administrator/components/com_contenthistory/views/history/tmpl/modal.php', + '/administrator/components/com_contenthistory/views/history/view.html.php', + '/administrator/components/com_contenthistory/views/preview/tmpl/preview.php', + '/administrator/components/com_contenthistory/views/preview/view.html.php', + '/administrator/components/com_cpanel/controller.php', + '/administrator/components/com_cpanel/cpanel.php', + '/administrator/components/com_cpanel/views/cpanel/tmpl/default.php', + '/administrator/components/com_cpanel/views/cpanel/tmpl/default.xml', + '/administrator/components/com_cpanel/views/cpanel/view.html.php', + '/administrator/components/com_fields/controller.php', + '/administrator/components/com_fields/controllers/field.php', + '/administrator/components/com_fields/controllers/fields.php', + '/administrator/components/com_fields/controllers/group.php', + '/administrator/components/com_fields/controllers/groups.php', + '/administrator/components/com_fields/fields.php', + '/administrator/components/com_fields/libraries/fieldslistplugin.php', + '/administrator/components/com_fields/libraries/fieldsplugin.php', + '/administrator/components/com_fields/models/field.php', + '/administrator/components/com_fields/models/fields.php', + '/administrator/components/com_fields/models/fields/fieldcontexts.php', + '/administrator/components/com_fields/models/fields/fieldgroups.php', + '/administrator/components/com_fields/models/fields/fieldlayout.php', + '/administrator/components/com_fields/models/fields/section.php', + '/administrator/components/com_fields/models/fields/type.php', + '/administrator/components/com_fields/models/forms/field.xml', + '/administrator/components/com_fields/models/forms/filter_fields.xml', + '/administrator/components/com_fields/models/forms/filter_groups.xml', + '/administrator/components/com_fields/models/forms/group.xml', + '/administrator/components/com_fields/models/group.php', + '/administrator/components/com_fields/models/groups.php', + '/administrator/components/com_fields/tables/field.php', + '/administrator/components/com_fields/tables/group.php', + '/administrator/components/com_fields/views/field/tmpl/edit.php', + '/administrator/components/com_fields/views/field/view.html.php', + '/administrator/components/com_fields/views/fields/tmpl/default.php', + '/administrator/components/com_fields/views/fields/tmpl/default_batch_body.php', + '/administrator/components/com_fields/views/fields/tmpl/default_batch_footer.php', + '/administrator/components/com_fields/views/fields/tmpl/modal.php', + '/administrator/components/com_fields/views/fields/view.html.php', + '/administrator/components/com_fields/views/group/tmpl/edit.php', + '/administrator/components/com_fields/views/group/view.html.php', + '/administrator/components/com_fields/views/groups/tmpl/default.php', + '/administrator/components/com_fields/views/groups/tmpl/default_batch_body.php', + '/administrator/components/com_fields/views/groups/tmpl/default_batch_footer.php', + '/administrator/components/com_fields/views/groups/view.html.php', + '/administrator/components/com_finder/controller.php', + '/administrator/components/com_finder/controllers/filter.php', + '/administrator/components/com_finder/controllers/filters.php', + '/administrator/components/com_finder/controllers/index.php', + '/administrator/components/com_finder/controllers/indexer.json.php', + '/administrator/components/com_finder/controllers/maps.php', + '/administrator/components/com_finder/finder.php', + '/administrator/components/com_finder/helpers/finder.php', + '/administrator/components/com_finder/helpers/html/finder.php', + '/administrator/components/com_finder/helpers/indexer/driver/mysql.php', + '/administrator/components/com_finder/helpers/indexer/driver/postgresql.php', + '/administrator/components/com_finder/helpers/indexer/driver/sqlsrv.php', + '/administrator/components/com_finder/helpers/indexer/indexer.php', + '/administrator/components/com_finder/helpers/indexer/parser/html.php', + '/administrator/components/com_finder/helpers/indexer/parser/rtf.php', + '/administrator/components/com_finder/helpers/indexer/parser/txt.php', + '/administrator/components/com_finder/helpers/indexer/stemmer.php', + '/administrator/components/com_finder/helpers/indexer/stemmer/fr.php', + '/administrator/components/com_finder/helpers/indexer/stemmer/porter_en.php', + '/administrator/components/com_finder/helpers/indexer/stemmer/snowball.php', + '/administrator/components/com_finder/models/fields/branches.php', + '/administrator/components/com_finder/models/fields/contentmap.php', + '/administrator/components/com_finder/models/fields/contenttypes.php', + '/administrator/components/com_finder/models/fields/directories.php', + '/administrator/components/com_finder/models/fields/searchfilter.php', + '/administrator/components/com_finder/models/filter.php', + '/administrator/components/com_finder/models/filters.php', + '/administrator/components/com_finder/models/forms/filter.xml', + '/administrator/components/com_finder/models/forms/filter_filters.xml', + '/administrator/components/com_finder/models/forms/filter_index.xml', + '/administrator/components/com_finder/models/forms/filter_maps.xml', + '/administrator/components/com_finder/models/index.php', + '/administrator/components/com_finder/models/indexer.php', + '/administrator/components/com_finder/models/maps.php', + '/administrator/components/com_finder/models/statistics.php', + '/administrator/components/com_finder/tables/filter.php', + '/administrator/components/com_finder/tables/link.php', + '/administrator/components/com_finder/tables/map.php', + '/administrator/components/com_finder/views/filter/tmpl/edit.php', + '/administrator/components/com_finder/views/filter/view.html.php', + '/administrator/components/com_finder/views/filters/tmpl/default.php', + '/administrator/components/com_finder/views/filters/view.html.php', + '/administrator/components/com_finder/views/index/tmpl/default.php', + '/administrator/components/com_finder/views/index/view.html.php', + '/administrator/components/com_finder/views/indexer/tmpl/default.php', + '/administrator/components/com_finder/views/indexer/view.html.php', + '/administrator/components/com_finder/views/maps/tmpl/default.php', + '/administrator/components/com_finder/views/maps/view.html.php', + '/administrator/components/com_finder/views/statistics/tmpl/default.php', + '/administrator/components/com_finder/views/statistics/view.html.php', + '/administrator/components/com_installer/controller.php', + '/administrator/components/com_installer/controllers/database.php', + '/administrator/components/com_installer/controllers/discover.php', + '/administrator/components/com_installer/controllers/install.php', + '/administrator/components/com_installer/controllers/manage.php', + '/administrator/components/com_installer/controllers/update.php', + '/administrator/components/com_installer/controllers/updatesites.php', + '/administrator/components/com_installer/helpers/html/manage.php', + '/administrator/components/com_installer/helpers/html/updatesites.php', + '/administrator/components/com_installer/installer.php', + '/administrator/components/com_installer/models/database.php', + '/administrator/components/com_installer/models/discover.php', + '/administrator/components/com_installer/models/extension.php', + '/administrator/components/com_installer/models/fields/extensionstatus.php', + '/administrator/components/com_installer/models/fields/folder.php', + '/administrator/components/com_installer/models/fields/location.php', + '/administrator/components/com_installer/models/fields/type.php', + '/administrator/components/com_installer/models/forms/filter_discover.xml', + '/administrator/components/com_installer/models/forms/filter_languages.xml', + '/administrator/components/com_installer/models/forms/filter_manage.xml', + '/administrator/components/com_installer/models/forms/filter_update.xml', + '/administrator/components/com_installer/models/forms/filter_updatesites.xml', + '/administrator/components/com_installer/models/install.php', + '/administrator/components/com_installer/models/languages.php', + '/administrator/components/com_installer/models/manage.php', + '/administrator/components/com_installer/models/update.php', + '/administrator/components/com_installer/models/updatesites.php', + '/administrator/components/com_installer/models/warnings.php', + '/administrator/components/com_installer/views/database/tmpl/default.php', + '/administrator/components/com_installer/views/database/tmpl/default.xml', + '/administrator/components/com_installer/views/database/view.html.php', + '/administrator/components/com_installer/views/default/tmpl/default_ftp.php', + '/administrator/components/com_installer/views/default/tmpl/default_message.php', + '/administrator/components/com_installer/views/default/view.php', + '/administrator/components/com_installer/views/discover/tmpl/default.php', + '/administrator/components/com_installer/views/discover/tmpl/default.xml', + '/administrator/components/com_installer/views/discover/tmpl/default_item.php', + '/administrator/components/com_installer/views/discover/view.html.php', + '/administrator/components/com_installer/views/install/tmpl/default.php', + '/administrator/components/com_installer/views/install/tmpl/default.xml', + '/administrator/components/com_installer/views/install/view.html.php', + '/administrator/components/com_installer/views/languages/tmpl/default.php', + '/administrator/components/com_installer/views/languages/tmpl/default.xml', + '/administrator/components/com_installer/views/languages/view.html.php', + '/administrator/components/com_installer/views/manage/tmpl/default.php', + '/administrator/components/com_installer/views/manage/tmpl/default.xml', + '/administrator/components/com_installer/views/manage/view.html.php', + '/administrator/components/com_installer/views/update/tmpl/default.php', + '/administrator/components/com_installer/views/update/tmpl/default.xml', + '/administrator/components/com_installer/views/update/view.html.php', + '/administrator/components/com_installer/views/updatesites/tmpl/default.php', + '/administrator/components/com_installer/views/updatesites/tmpl/default.xml', + '/administrator/components/com_installer/views/updatesites/view.html.php', + '/administrator/components/com_installer/views/warnings/tmpl/default.php', + '/administrator/components/com_installer/views/warnings/tmpl/default.xml', + '/administrator/components/com_installer/views/warnings/view.html.php', + '/administrator/components/com_joomlaupdate/controller.php', + '/administrator/components/com_joomlaupdate/controllers/update.php', + '/administrator/components/com_joomlaupdate/helpers/joomlaupdate.php', + '/administrator/components/com_joomlaupdate/helpers/select.php', + '/administrator/components/com_joomlaupdate/joomlaupdate.php', + '/administrator/components/com_joomlaupdate/models/default.php', + '/administrator/components/com_joomlaupdate/restore.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/complete.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default.xml', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_nodownload.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_noupdate.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_preupdatecheck.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_reinstall.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_update.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_updatemefirst.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_upload.php', + '/administrator/components/com_joomlaupdate/views/default/view.html.php', + '/administrator/components/com_joomlaupdate/views/update/tmpl/default.php', + '/administrator/components/com_joomlaupdate/views/update/tmpl/finaliseconfirm.php', + '/administrator/components/com_joomlaupdate/views/update/view.html.php', + '/administrator/components/com_joomlaupdate/views/upload/tmpl/captive.php', + '/administrator/components/com_joomlaupdate/views/upload/view.html.php', + '/administrator/components/com_languages/controller.php', + '/administrator/components/com_languages/controllers/installed.php', + '/administrator/components/com_languages/controllers/language.php', + '/administrator/components/com_languages/controllers/languages.php', + '/administrator/components/com_languages/controllers/override.php', + '/administrator/components/com_languages/controllers/overrides.php', + '/administrator/components/com_languages/controllers/strings.json.php', + '/administrator/components/com_languages/helpers/html/languages.php', + '/administrator/components/com_languages/helpers/jsonresponse.php', + '/administrator/components/com_languages/helpers/languages.php', + '/administrator/components/com_languages/helpers/multilangstatus.php', + '/administrator/components/com_languages/languages.php', + '/administrator/components/com_languages/layouts/joomla/searchtools/default/bar.php', + '/administrator/components/com_languages/models/fields/languageclient.php', + '/administrator/components/com_languages/models/forms/filter_installed.xml', + '/administrator/components/com_languages/models/forms/filter_languages.xml', + '/administrator/components/com_languages/models/forms/filter_overrides.xml', + '/administrator/components/com_languages/models/forms/language.xml', + '/administrator/components/com_languages/models/forms/override.xml', + '/administrator/components/com_languages/models/installed.php', + '/administrator/components/com_languages/models/language.php', + '/administrator/components/com_languages/models/languages.php', + '/administrator/components/com_languages/models/override.php', + '/administrator/components/com_languages/models/overrides.php', + '/administrator/components/com_languages/models/strings.php', + '/administrator/components/com_languages/views/installed/tmpl/default.php', + '/administrator/components/com_languages/views/installed/tmpl/default.xml', + '/administrator/components/com_languages/views/installed/view.html.php', + '/administrator/components/com_languages/views/language/tmpl/edit.php', + '/administrator/components/com_languages/views/language/view.html.php', + '/administrator/components/com_languages/views/languages/tmpl/default.php', + '/administrator/components/com_languages/views/languages/tmpl/default.xml', + '/administrator/components/com_languages/views/languages/view.html.php', + '/administrator/components/com_languages/views/multilangstatus/tmpl/default.php', + '/administrator/components/com_languages/views/multilangstatus/view.html.php', + '/administrator/components/com_languages/views/override/tmpl/edit.php', + '/administrator/components/com_languages/views/override/view.html.php', + '/administrator/components/com_languages/views/overrides/tmpl/default.php', + '/administrator/components/com_languages/views/overrides/tmpl/default.xml', + '/administrator/components/com_languages/views/overrides/view.html.php', + '/administrator/components/com_login/controller.php', + '/administrator/components/com_login/login.php', + '/administrator/components/com_login/models/login.php', + '/administrator/components/com_login/views/login/tmpl/default.php', + '/administrator/components/com_login/views/login/view.html.php', + '/administrator/components/com_media/controller.php', + '/administrator/components/com_media/controllers/file.json.php', + '/administrator/components/com_media/controllers/file.php', + '/administrator/components/com_media/controllers/folder.php', + '/administrator/components/com_media/layouts/toolbar/deletemedia.php', + '/administrator/components/com_media/layouts/toolbar/newfolder.php', + '/administrator/components/com_media/layouts/toolbar/uploadmedia.php', + '/administrator/components/com_media/media.php', + '/administrator/components/com_media/models/list.php', + '/administrator/components/com_media/models/manager.php', + '/administrator/components/com_media/views/images/tmpl/default.php', + '/administrator/components/com_media/views/images/view.html.php', + '/administrator/components/com_media/views/imageslist/tmpl/default.php', + '/administrator/components/com_media/views/imageslist/tmpl/default_folder.php', + '/administrator/components/com_media/views/imageslist/tmpl/default_image.php', + '/administrator/components/com_media/views/imageslist/view.html.php', + '/administrator/components/com_media/views/media/tmpl/default.php', + '/administrator/components/com_media/views/media/tmpl/default.xml', + '/administrator/components/com_media/views/media/tmpl/default_folders.php', + '/administrator/components/com_media/views/media/tmpl/default_navigation.php', + '/administrator/components/com_media/views/media/view.html.php', + '/administrator/components/com_media/views/medialist/tmpl/default.php', + '/administrator/components/com_media/views/medialist/tmpl/details.php', + '/administrator/components/com_media/views/medialist/tmpl/details_doc.php', + '/administrator/components/com_media/views/medialist/tmpl/details_docs.php', + '/administrator/components/com_media/views/medialist/tmpl/details_folder.php', + '/administrator/components/com_media/views/medialist/tmpl/details_folders.php', + '/administrator/components/com_media/views/medialist/tmpl/details_img.php', + '/administrator/components/com_media/views/medialist/tmpl/details_imgs.php', + '/administrator/components/com_media/views/medialist/tmpl/details_up.php', + '/administrator/components/com_media/views/medialist/tmpl/details_video.php', + '/administrator/components/com_media/views/medialist/tmpl/details_videos.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_docs.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_folders.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_imgs.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_up.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_videos.php', + '/administrator/components/com_media/views/medialist/view.html.php', + '/administrator/components/com_menus/controller.php', + '/administrator/components/com_menus/controllers/ajax.json.php', + '/administrator/components/com_menus/controllers/item.php', + '/administrator/components/com_menus/controllers/items.php', + '/administrator/components/com_menus/controllers/menu.php', + '/administrator/components/com_menus/controllers/menus.php', + '/administrator/components/com_menus/helpers/associations.php', + '/administrator/components/com_menus/helpers/html/menus.php', + '/administrator/components/com_menus/layouts/joomla/searchtools/default/bar.php', + '/administrator/components/com_menus/menus.php', + '/administrator/components/com_menus/models/fields/componentscategory.php', + '/administrator/components/com_menus/models/fields/menuitembytype.php', + '/administrator/components/com_menus/models/fields/menuordering.php', + '/administrator/components/com_menus/models/fields/menuparent.php', + '/administrator/components/com_menus/models/fields/menupreset.php', + '/administrator/components/com_menus/models/fields/menutype.php', + '/administrator/components/com_menus/models/fields/modal/menu.php', + '/administrator/components/com_menus/models/forms/filter_items.xml', + '/administrator/components/com_menus/models/forms/filter_itemsadmin.xml', + '/administrator/components/com_menus/models/forms/filter_menus.xml', + '/administrator/components/com_menus/models/forms/item.xml', + '/administrator/components/com_menus/models/forms/item_alias.xml', + '/administrator/components/com_menus/models/forms/item_component.xml', + '/administrator/components/com_menus/models/forms/item_heading.xml', + '/administrator/components/com_menus/models/forms/item_separator.xml', + '/administrator/components/com_menus/models/forms/item_url.xml', + '/administrator/components/com_menus/models/forms/itemadmin.xml', + '/administrator/components/com_menus/models/forms/itemadmin_alias.xml', + '/administrator/components/com_menus/models/forms/itemadmin_component.xml', + '/administrator/components/com_menus/models/forms/itemadmin_container.xml', + '/administrator/components/com_menus/models/forms/itemadmin_heading.xml', + '/administrator/components/com_menus/models/forms/itemadmin_separator.xml', + '/administrator/components/com_menus/models/forms/itemadmin_url.xml', + '/administrator/components/com_menus/models/forms/menu.xml', + '/administrator/components/com_menus/models/item.php', + '/administrator/components/com_menus/models/items.php', + '/administrator/components/com_menus/models/menu.php', + '/administrator/components/com_menus/models/menus.php', + '/administrator/components/com_menus/models/menutypes.php', + '/administrator/components/com_menus/presets/joomla.xml', + '/administrator/components/com_menus/presets/modern.xml', + '/administrator/components/com_menus/tables/menu.php', + '/administrator/components/com_menus/views/item/tmpl/edit.php', + '/administrator/components/com_menus/views/item/tmpl/edit.xml', + '/administrator/components/com_menus/views/item/tmpl/edit_associations.php', + '/administrator/components/com_menus/views/item/tmpl/edit_container.php', + '/administrator/components/com_menus/views/item/tmpl/edit_modules.php', + '/administrator/components/com_menus/views/item/tmpl/edit_options.php', + '/administrator/components/com_menus/views/item/tmpl/modal.php', + '/administrator/components/com_menus/views/item/tmpl/modal_associations.php', + '/administrator/components/com_menus/views/item/tmpl/modal_options.php', + '/administrator/components/com_menus/views/item/view.html.php', + '/administrator/components/com_menus/views/items/tmpl/default.php', + '/administrator/components/com_menus/views/items/tmpl/default.xml', + '/administrator/components/com_menus/views/items/tmpl/default_batch_body.php', + '/administrator/components/com_menus/views/items/tmpl/default_batch_footer.php', + '/administrator/components/com_menus/views/items/tmpl/modal.php', + '/administrator/components/com_menus/views/items/view.html.php', + '/administrator/components/com_menus/views/menu/tmpl/edit.php', + '/administrator/components/com_menus/views/menu/tmpl/edit.xml', + '/administrator/components/com_menus/views/menu/view.html.php', + '/administrator/components/com_menus/views/menu/view.xml.php', + '/administrator/components/com_menus/views/menus/tmpl/default.php', + '/administrator/components/com_menus/views/menus/tmpl/default.xml', + '/administrator/components/com_menus/views/menus/view.html.php', + '/administrator/components/com_menus/views/menutypes/tmpl/default.php', + '/administrator/components/com_menus/views/menutypes/view.html.php', + '/administrator/components/com_messages/controller.php', + '/administrator/components/com_messages/controllers/config.php', + '/administrator/components/com_messages/controllers/message.php', + '/administrator/components/com_messages/controllers/messages.php', + '/administrator/components/com_messages/helpers/html/messages.php', + '/administrator/components/com_messages/helpers/messages.php', + '/administrator/components/com_messages/messages.php', + '/administrator/components/com_messages/models/config.php', + '/administrator/components/com_messages/models/fields/messagestates.php', + '/administrator/components/com_messages/models/fields/usermessages.php', + '/administrator/components/com_messages/models/forms/config.xml', + '/administrator/components/com_messages/models/forms/filter_messages.xml', + '/administrator/components/com_messages/models/forms/message.xml', + '/administrator/components/com_messages/models/message.php', + '/administrator/components/com_messages/models/messages.php', + '/administrator/components/com_messages/tables/message.php', + '/administrator/components/com_messages/views/config/tmpl/default.php', + '/administrator/components/com_messages/views/config/view.html.php', + '/administrator/components/com_messages/views/message/tmpl/default.php', + '/administrator/components/com_messages/views/message/tmpl/edit.php', + '/administrator/components/com_messages/views/message/view.html.php', + '/administrator/components/com_messages/views/messages/tmpl/default.php', + '/administrator/components/com_messages/views/messages/view.html.php', + '/administrator/components/com_modules/controller.php', + '/administrator/components/com_modules/controllers/module.php', + '/administrator/components/com_modules/controllers/modules.php', + '/administrator/components/com_modules/helpers/html/modules.php', + '/administrator/components/com_modules/helpers/xml.php', + '/administrator/components/com_modules/layouts/toolbar/newmodule.php', + '/administrator/components/com_modules/models/fields/modulesmodule.php', + '/administrator/components/com_modules/models/fields/modulesposition.php', + '/administrator/components/com_modules/models/forms/advanced.xml', + '/administrator/components/com_modules/models/forms/filter_modules.xml', + '/administrator/components/com_modules/models/forms/filter_modulesadmin.xml', + '/administrator/components/com_modules/models/forms/module.xml', + '/administrator/components/com_modules/models/forms/moduleadmin.xml', + '/administrator/components/com_modules/models/module.php', + '/administrator/components/com_modules/models/modules.php', + '/administrator/components/com_modules/models/positions.php', + '/administrator/components/com_modules/models/select.php', + '/administrator/components/com_modules/modules.php', + '/administrator/components/com_modules/views/module/tmpl/edit.php', + '/administrator/components/com_modules/views/module/tmpl/edit_assignment.php', + '/administrator/components/com_modules/views/module/tmpl/edit_options.php', + '/administrator/components/com_modules/views/module/tmpl/edit_positions.php', + '/administrator/components/com_modules/views/module/tmpl/modal.php', + '/administrator/components/com_modules/views/module/view.html.php', + '/administrator/components/com_modules/views/module/view.json.php', + '/administrator/components/com_modules/views/modules/tmpl/default.php', + '/administrator/components/com_modules/views/modules/tmpl/default.xml', + '/administrator/components/com_modules/views/modules/tmpl/default_batch_body.php', + '/administrator/components/com_modules/views/modules/tmpl/default_batch_footer.php', + '/administrator/components/com_modules/views/modules/tmpl/modal.php', + '/administrator/components/com_modules/views/modules/view.html.php', + '/administrator/components/com_modules/views/positions/tmpl/modal.php', + '/administrator/components/com_modules/views/positions/view.html.php', + '/administrator/components/com_modules/views/preview/tmpl/default.php', + '/administrator/components/com_modules/views/preview/view.html.php', + '/administrator/components/com_modules/views/select/tmpl/default.php', + '/administrator/components/com_modules/views/select/view.html.php', + '/administrator/components/com_newsfeeds/controller.php', + '/administrator/components/com_newsfeeds/controllers/ajax.json.php', + '/administrator/components/com_newsfeeds/controllers/newsfeed.php', + '/administrator/components/com_newsfeeds/controllers/newsfeeds.php', + '/administrator/components/com_newsfeeds/helpers/associations.php', + '/administrator/components/com_newsfeeds/helpers/html/newsfeed.php', + '/administrator/components/com_newsfeeds/models/fields/modal/newsfeed.php', + '/administrator/components/com_newsfeeds/models/fields/newsfeeds.php', + '/administrator/components/com_newsfeeds/models/forms/filter_newsfeeds.xml', + '/administrator/components/com_newsfeeds/models/forms/newsfeed.xml', + '/administrator/components/com_newsfeeds/models/newsfeed.php', + '/administrator/components/com_newsfeeds/models/newsfeeds.php', + '/administrator/components/com_newsfeeds/newsfeeds.php', + '/administrator/components/com_newsfeeds/tables/newsfeed.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_associations.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_display.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_metadata.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_params.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_associations.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_display.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_metadata.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_params.php', + '/administrator/components/com_newsfeeds/views/newsfeed/view.html.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default_batch_body.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default_batch_footer.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/modal.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/view.html.php', + '/administrator/components/com_plugins/controller.php', + '/administrator/components/com_plugins/controllers/plugin.php', + '/administrator/components/com_plugins/controllers/plugins.php', + '/administrator/components/com_plugins/models/fields/pluginelement.php', + '/administrator/components/com_plugins/models/fields/pluginordering.php', + '/administrator/components/com_plugins/models/fields/plugintype.php', + '/administrator/components/com_plugins/models/forms/filter_plugins.xml', + '/administrator/components/com_plugins/models/forms/plugin.xml', + '/administrator/components/com_plugins/models/plugin.php', + '/administrator/components/com_plugins/models/plugins.php', + '/administrator/components/com_plugins/plugins.php', + '/administrator/components/com_plugins/views/plugin/tmpl/edit.php', + '/administrator/components/com_plugins/views/plugin/tmpl/edit_options.php', + '/administrator/components/com_plugins/views/plugin/tmpl/modal.php', + '/administrator/components/com_plugins/views/plugin/view.html.php', + '/administrator/components/com_plugins/views/plugins/tmpl/default.php', + '/administrator/components/com_plugins/views/plugins/tmpl/default.xml', + '/administrator/components/com_plugins/views/plugins/view.html.php', + '/administrator/components/com_postinstall/controllers/message.php', + '/administrator/components/com_postinstall/fof.xml', + '/administrator/components/com_postinstall/models/messages.php', + '/administrator/components/com_postinstall/postinstall.php', + '/administrator/components/com_postinstall/toolbar.php', + '/administrator/components/com_postinstall/views/messages/tmpl/default.php', + '/administrator/components/com_postinstall/views/messages/tmpl/default.xml', + '/administrator/components/com_postinstall/views/messages/view.html.php', + '/administrator/components/com_privacy/controller.php', + '/administrator/components/com_privacy/controllers/consents.php', + '/administrator/components/com_privacy/controllers/request.php', + '/administrator/components/com_privacy/controllers/request.xml.php', + '/administrator/components/com_privacy/controllers/requests.php', + '/administrator/components/com_privacy/helpers/export/domain.php', + '/administrator/components/com_privacy/helpers/export/field.php', + '/administrator/components/com_privacy/helpers/export/item.php', + '/administrator/components/com_privacy/helpers/html/helper.php', + '/administrator/components/com_privacy/helpers/plugin.php', + '/administrator/components/com_privacy/helpers/privacy.php', + '/administrator/components/com_privacy/helpers/removal/status.php', + '/administrator/components/com_privacy/models/capabilities.php', + '/administrator/components/com_privacy/models/consents.php', + '/administrator/components/com_privacy/models/dashboard.php', + '/administrator/components/com_privacy/models/export.php', + '/administrator/components/com_privacy/models/fields/requeststatus.php', + '/administrator/components/com_privacy/models/fields/requesttype.php', + '/administrator/components/com_privacy/models/forms/filter_consents.xml', + '/administrator/components/com_privacy/models/forms/filter_requests.xml', + '/administrator/components/com_privacy/models/forms/request.xml', + '/administrator/components/com_privacy/models/remove.php', + '/administrator/components/com_privacy/models/request.php', + '/administrator/components/com_privacy/models/requests.php', + '/administrator/components/com_privacy/privacy.php', + '/administrator/components/com_privacy/tables/consent.php', + '/administrator/components/com_privacy/tables/request.php', + '/administrator/components/com_privacy/views/capabilities/tmpl/default.php', + '/administrator/components/com_privacy/views/capabilities/view.html.php', + '/administrator/components/com_privacy/views/consents/tmpl/default.php', + '/administrator/components/com_privacy/views/consents/tmpl/default.xml', + '/administrator/components/com_privacy/views/consents/view.html.php', + '/administrator/components/com_privacy/views/dashboard/tmpl/default.php', + '/administrator/components/com_privacy/views/dashboard/tmpl/default.xml', + '/administrator/components/com_privacy/views/dashboard/view.html.php', + '/administrator/components/com_privacy/views/export/view.xml.php', + '/administrator/components/com_privacy/views/request/tmpl/default.php', + '/administrator/components/com_privacy/views/request/tmpl/edit.php', + '/administrator/components/com_privacy/views/request/view.html.php', + '/administrator/components/com_privacy/views/requests/tmpl/default.php', + '/administrator/components/com_privacy/views/requests/tmpl/default.xml', + '/administrator/components/com_privacy/views/requests/view.html.php', + '/administrator/components/com_redirect/controller.php', + '/administrator/components/com_redirect/controllers/link.php', + '/administrator/components/com_redirect/controllers/links.php', + '/administrator/components/com_redirect/helpers/html/redirect.php', + '/administrator/components/com_redirect/models/fields/redirect.php', + '/administrator/components/com_redirect/models/forms/filter_links.xml', + '/administrator/components/com_redirect/models/forms/link.xml', + '/administrator/components/com_redirect/models/link.php', + '/administrator/components/com_redirect/models/links.php', + '/administrator/components/com_redirect/redirect.php', + '/administrator/components/com_redirect/tables/link.php', + '/administrator/components/com_redirect/views/link/tmpl/edit.php', + '/administrator/components/com_redirect/views/link/view.html.php', + '/administrator/components/com_redirect/views/links/tmpl/default.php', + '/administrator/components/com_redirect/views/links/tmpl/default.xml', + '/administrator/components/com_redirect/views/links/tmpl/default_addform.php', + '/administrator/components/com_redirect/views/links/tmpl/default_batch_body.php', + '/administrator/components/com_redirect/views/links/tmpl/default_batch_footer.php', + '/administrator/components/com_redirect/views/links/view.html.php', + '/administrator/components/com_tags/controller.php', + '/administrator/components/com_tags/controllers/tag.php', + '/administrator/components/com_tags/controllers/tags.php', + '/administrator/components/com_tags/helpers/tags.php', + '/administrator/components/com_tags/models/forms/filter_tags.xml', + '/administrator/components/com_tags/models/forms/tag.xml', + '/administrator/components/com_tags/models/tag.php', + '/administrator/components/com_tags/models/tags.php', + '/administrator/components/com_tags/tables/tag.php', + '/administrator/components/com_tags/tags.php', + '/administrator/components/com_tags/views/tag/tmpl/edit.php', + '/administrator/components/com_tags/views/tag/tmpl/edit_metadata.php', + '/administrator/components/com_tags/views/tag/tmpl/edit_options.php', + '/administrator/components/com_tags/views/tag/view.html.php', + '/administrator/components/com_tags/views/tags/tmpl/default.php', + '/administrator/components/com_tags/views/tags/tmpl/default.xml', + '/administrator/components/com_tags/views/tags/tmpl/default_batch_body.php', + '/administrator/components/com_tags/views/tags/tmpl/default_batch_footer.php', + '/administrator/components/com_tags/views/tags/view.html.php', + '/administrator/components/com_templates/controller.php', + '/administrator/components/com_templates/controllers/style.php', + '/administrator/components/com_templates/controllers/styles.php', + '/administrator/components/com_templates/controllers/template.php', + '/administrator/components/com_templates/helpers/html/templates.php', + '/administrator/components/com_templates/models/fields/templatelocation.php', + '/administrator/components/com_templates/models/fields/templatename.php', + '/administrator/components/com_templates/models/forms/filter_styles.xml', + '/administrator/components/com_templates/models/forms/filter_templates.xml', + '/administrator/components/com_templates/models/forms/source.xml', + '/administrator/components/com_templates/models/forms/style.xml', + '/administrator/components/com_templates/models/forms/style_administrator.xml', + '/administrator/components/com_templates/models/forms/style_site.xml', + '/administrator/components/com_templates/models/style.php', + '/administrator/components/com_templates/models/styles.php', + '/administrator/components/com_templates/models/template.php', + '/administrator/components/com_templates/models/templates.php', + '/administrator/components/com_templates/tables/style.php', + '/administrator/components/com_templates/templates.php', + '/administrator/components/com_templates/views/style/tmpl/edit.php', + '/administrator/components/com_templates/views/style/tmpl/edit_assignment.php', + '/administrator/components/com_templates/views/style/tmpl/edit_options.php', + '/administrator/components/com_templates/views/style/view.html.php', + '/administrator/components/com_templates/views/style/view.json.php', + '/administrator/components/com_templates/views/styles/tmpl/default.php', + '/administrator/components/com_templates/views/styles/tmpl/default.xml', + '/administrator/components/com_templates/views/styles/view.html.php', + '/administrator/components/com_templates/views/template/tmpl/default.php', + '/administrator/components/com_templates/views/template/tmpl/default_description.php', + '/administrator/components/com_templates/views/template/tmpl/default_folders.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_copy_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_copy_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_delete_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_delete_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_file_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_file_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_folder_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_folder_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_rename_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_rename_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_resize_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_resize_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_tree.php', + '/administrator/components/com_templates/views/template/tmpl/readonly.php', + '/administrator/components/com_templates/views/template/view.html.php', + '/administrator/components/com_templates/views/templates/tmpl/default.php', + '/administrator/components/com_templates/views/templates/tmpl/default.xml', + '/administrator/components/com_templates/views/templates/view.html.php', + '/administrator/components/com_users/controller.php', + '/administrator/components/com_users/controllers/group.php', + '/administrator/components/com_users/controllers/groups.php', + '/administrator/components/com_users/controllers/level.php', + '/administrator/components/com_users/controllers/levels.php', + '/administrator/components/com_users/controllers/mail.php', + '/administrator/components/com_users/controllers/note.php', + '/administrator/components/com_users/controllers/notes.php', + '/administrator/components/com_users/controllers/user.php', + '/administrator/components/com_users/controllers/users.php', + '/administrator/components/com_users/helpers/html/users.php', + '/administrator/components/com_users/models/debuggroup.php', + '/administrator/components/com_users/models/debuguser.php', + '/administrator/components/com_users/models/fields/groupparent.php', + '/administrator/components/com_users/models/fields/levels.php', + '/administrator/components/com_users/models/forms/config_domain.xml', + '/administrator/components/com_users/models/forms/fields/user.xml', + '/administrator/components/com_users/models/forms/filter_debuggroup.xml', + '/administrator/components/com_users/models/forms/filter_debuguser.xml', + '/administrator/components/com_users/models/forms/filter_groups.xml', + '/administrator/components/com_users/models/forms/filter_levels.xml', + '/administrator/components/com_users/models/forms/filter_notes.xml', + '/administrator/components/com_users/models/forms/filter_users.xml', + '/administrator/components/com_users/models/forms/group.xml', + '/administrator/components/com_users/models/forms/level.xml', + '/administrator/components/com_users/models/forms/mail.xml', + '/administrator/components/com_users/models/forms/note.xml', + '/administrator/components/com_users/models/forms/user.xml', + '/administrator/components/com_users/models/group.php', + '/administrator/components/com_users/models/groups.php', + '/administrator/components/com_users/models/level.php', + '/administrator/components/com_users/models/levels.php', + '/administrator/components/com_users/models/mail.php', + '/administrator/components/com_users/models/note.php', + '/administrator/components/com_users/models/notes.php', + '/administrator/components/com_users/models/user.php', + '/administrator/components/com_users/models/users.php', + '/administrator/components/com_users/tables/note.php', + '/administrator/components/com_users/users.php', + '/administrator/components/com_users/views/debuggroup/tmpl/default.php', + '/administrator/components/com_users/views/debuggroup/view.html.php', + '/administrator/components/com_users/views/debuguser/tmpl/default.php', + '/administrator/components/com_users/views/debuguser/view.html.php', + '/administrator/components/com_users/views/group/tmpl/edit.php', + '/administrator/components/com_users/views/group/tmpl/edit.xml', + '/administrator/components/com_users/views/group/view.html.php', + '/administrator/components/com_users/views/groups/tmpl/default.php', + '/administrator/components/com_users/views/groups/tmpl/default.xml', + '/administrator/components/com_users/views/groups/view.html.php', + '/administrator/components/com_users/views/level/tmpl/edit.php', + '/administrator/components/com_users/views/level/tmpl/edit.xml', + '/administrator/components/com_users/views/level/view.html.php', + '/administrator/components/com_users/views/levels/tmpl/default.php', + '/administrator/components/com_users/views/levels/tmpl/default.xml', + '/administrator/components/com_users/views/levels/view.html.php', + '/administrator/components/com_users/views/mail/tmpl/default.php', + '/administrator/components/com_users/views/mail/tmpl/default.xml', + '/administrator/components/com_users/views/mail/view.html.php', + '/administrator/components/com_users/views/note/tmpl/edit.php', + '/administrator/components/com_users/views/note/tmpl/edit.xml', + '/administrator/components/com_users/views/note/view.html.php', + '/administrator/components/com_users/views/notes/tmpl/default.php', + '/administrator/components/com_users/views/notes/tmpl/default.xml', + '/administrator/components/com_users/views/notes/tmpl/modal.php', + '/administrator/components/com_users/views/notes/view.html.php', + '/administrator/components/com_users/views/user/tmpl/edit.php', + '/administrator/components/com_users/views/user/tmpl/edit.xml', + '/administrator/components/com_users/views/user/tmpl/edit_groups.php', + '/administrator/components/com_users/views/user/view.html.php', + '/administrator/components/com_users/views/users/tmpl/default.php', + '/administrator/components/com_users/views/users/tmpl/default.xml', + '/administrator/components/com_users/views/users/tmpl/default_batch_body.php', + '/administrator/components/com_users/views/users/tmpl/default_batch_footer.php', + '/administrator/components/com_users/views/users/tmpl/modal.php', + '/administrator/components/com_users/views/users/view.html.php', + '/administrator/help/helpsites.xml', + '/administrator/includes/helper.php', + '/administrator/includes/subtoolbar.php', + '/administrator/language/en-GB/en-GB.com_actionlogs.ini', + '/administrator/language/en-GB/en-GB.com_actionlogs.sys.ini', + '/administrator/language/en-GB/en-GB.com_admin.ini', + '/administrator/language/en-GB/en-GB.com_admin.sys.ini', + '/administrator/language/en-GB/en-GB.com_ajax.ini', + '/administrator/language/en-GB/en-GB.com_ajax.sys.ini', + '/administrator/language/en-GB/en-GB.com_associations.ini', + '/administrator/language/en-GB/en-GB.com_associations.sys.ini', + '/administrator/language/en-GB/en-GB.com_banners.ini', + '/administrator/language/en-GB/en-GB.com_banners.sys.ini', + '/administrator/language/en-GB/en-GB.com_cache.ini', + '/administrator/language/en-GB/en-GB.com_cache.sys.ini', + '/administrator/language/en-GB/en-GB.com_categories.ini', + '/administrator/language/en-GB/en-GB.com_categories.sys.ini', + '/administrator/language/en-GB/en-GB.com_checkin.ini', + '/administrator/language/en-GB/en-GB.com_checkin.sys.ini', + '/administrator/language/en-GB/en-GB.com_config.ini', + '/administrator/language/en-GB/en-GB.com_config.sys.ini', + '/administrator/language/en-GB/en-GB.com_contact.ini', + '/administrator/language/en-GB/en-GB.com_contact.sys.ini', + '/administrator/language/en-GB/en-GB.com_content.ini', + '/administrator/language/en-GB/en-GB.com_content.sys.ini', + '/administrator/language/en-GB/en-GB.com_contenthistory.ini', + '/administrator/language/en-GB/en-GB.com_contenthistory.sys.ini', + '/administrator/language/en-GB/en-GB.com_cpanel.ini', + '/administrator/language/en-GB/en-GB.com_cpanel.sys.ini', + '/administrator/language/en-GB/en-GB.com_fields.ini', + '/administrator/language/en-GB/en-GB.com_fields.sys.ini', + '/administrator/language/en-GB/en-GB.com_finder.ini', + '/administrator/language/en-GB/en-GB.com_finder.sys.ini', + '/administrator/language/en-GB/en-GB.com_installer.ini', + '/administrator/language/en-GB/en-GB.com_installer.sys.ini', + '/administrator/language/en-GB/en-GB.com_joomlaupdate.ini', + '/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini', + '/administrator/language/en-GB/en-GB.com_languages.ini', + '/administrator/language/en-GB/en-GB.com_languages.sys.ini', + '/administrator/language/en-GB/en-GB.com_login.ini', + '/administrator/language/en-GB/en-GB.com_login.sys.ini', + '/administrator/language/en-GB/en-GB.com_mailto.sys.ini', + '/administrator/language/en-GB/en-GB.com_media.ini', + '/administrator/language/en-GB/en-GB.com_media.sys.ini', + '/administrator/language/en-GB/en-GB.com_menus.ini', + '/administrator/language/en-GB/en-GB.com_menus.sys.ini', + '/administrator/language/en-GB/en-GB.com_messages.ini', + '/administrator/language/en-GB/en-GB.com_messages.sys.ini', + '/administrator/language/en-GB/en-GB.com_modules.ini', + '/administrator/language/en-GB/en-GB.com_modules.sys.ini', + '/administrator/language/en-GB/en-GB.com_newsfeeds.ini', + '/administrator/language/en-GB/en-GB.com_newsfeeds.sys.ini', + '/administrator/language/en-GB/en-GB.com_plugins.ini', + '/administrator/language/en-GB/en-GB.com_plugins.sys.ini', + '/administrator/language/en-GB/en-GB.com_postinstall.ini', + '/administrator/language/en-GB/en-GB.com_postinstall.sys.ini', + '/administrator/language/en-GB/en-GB.com_privacy.ini', + '/administrator/language/en-GB/en-GB.com_privacy.sys.ini', + '/administrator/language/en-GB/en-GB.com_redirect.ini', + '/administrator/language/en-GB/en-GB.com_redirect.sys.ini', + '/administrator/language/en-GB/en-GB.com_tags.ini', + '/administrator/language/en-GB/en-GB.com_tags.sys.ini', + '/administrator/language/en-GB/en-GB.com_templates.ini', + '/administrator/language/en-GB/en-GB.com_templates.sys.ini', + '/administrator/language/en-GB/en-GB.com_users.ini', + '/administrator/language/en-GB/en-GB.com_users.sys.ini', + '/administrator/language/en-GB/en-GB.com_weblinks.ini', + '/administrator/language/en-GB/en-GB.com_weblinks.sys.ini', + '/administrator/language/en-GB/en-GB.com_wrapper.ini', + '/administrator/language/en-GB/en-GB.com_wrapper.sys.ini', + '/administrator/language/en-GB/en-GB.ini', + '/administrator/language/en-GB/en-GB.lib_joomla.ini', + '/administrator/language/en-GB/en-GB.localise.php', + '/administrator/language/en-GB/en-GB.mod_custom.ini', + '/administrator/language/en-GB/en-GB.mod_custom.sys.ini', + '/administrator/language/en-GB/en-GB.mod_feed.ini', + '/administrator/language/en-GB/en-GB.mod_feed.sys.ini', + '/administrator/language/en-GB/en-GB.mod_latest.ini', + '/administrator/language/en-GB/en-GB.mod_latest.sys.ini', + '/administrator/language/en-GB/en-GB.mod_latestactions.ini', + '/administrator/language/en-GB/en-GB.mod_latestactions.sys.ini', + '/administrator/language/en-GB/en-GB.mod_logged.ini', + '/administrator/language/en-GB/en-GB.mod_logged.sys.ini', + '/administrator/language/en-GB/en-GB.mod_login.ini', + '/administrator/language/en-GB/en-GB.mod_login.sys.ini', + '/administrator/language/en-GB/en-GB.mod_menu.ini', + '/administrator/language/en-GB/en-GB.mod_menu.sys.ini', + '/administrator/language/en-GB/en-GB.mod_multilangstatus.ini', + '/administrator/language/en-GB/en-GB.mod_multilangstatus.sys.ini', + '/administrator/language/en-GB/en-GB.mod_popular.ini', + '/administrator/language/en-GB/en-GB.mod_popular.sys.ini', + '/administrator/language/en-GB/en-GB.mod_privacy_dashboard.ini', + '/administrator/language/en-GB/en-GB.mod_privacy_dashboard.sys.ini', + '/administrator/language/en-GB/en-GB.mod_quickicon.ini', + '/administrator/language/en-GB/en-GB.mod_quickicon.sys.ini', + '/administrator/language/en-GB/en-GB.mod_sampledata.ini', + '/administrator/language/en-GB/en-GB.mod_sampledata.sys.ini', + '/administrator/language/en-GB/en-GB.mod_stats_admin.ini', + '/administrator/language/en-GB/en-GB.mod_stats_admin.sys.ini', + '/administrator/language/en-GB/en-GB.mod_status.ini', + '/administrator/language/en-GB/en-GB.mod_status.sys.ini', + '/administrator/language/en-GB/en-GB.mod_submenu.ini', + '/administrator/language/en-GB/en-GB.mod_submenu.sys.ini', + '/administrator/language/en-GB/en-GB.mod_title.ini', + '/administrator/language/en-GB/en-GB.mod_title.sys.ini', + '/administrator/language/en-GB/en-GB.mod_toolbar.ini', + '/administrator/language/en-GB/en-GB.mod_toolbar.sys.ini', + '/administrator/language/en-GB/en-GB.mod_version.ini', + '/administrator/language/en-GB/en-GB.mod_version.sys.ini', + '/administrator/language/en-GB/en-GB.plg_actionlog_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_actionlog_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_cookie.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_cookie.sys.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_gmail.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_gmail.sys.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_ldap.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_ldap.sys.ini', + '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.ini', + '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.sys.ini', + '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.ini', + '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_confirmconsent.ini', + '/administrator/language/en-GB/en-GB.plg_content_confirmconsent.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_contact.ini', + '/administrator/language/en-GB/en-GB.plg_content_contact.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_emailcloak.ini', + '/administrator/language/en-GB/en-GB.plg_content_emailcloak.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_fields.ini', + '/administrator/language/en-GB/en-GB.plg_content_fields.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_finder.ini', + '/administrator/language/en-GB/en-GB.plg_content_finder.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_content_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_loadmodule.ini', + '/administrator/language/en-GB/en-GB.plg_content_loadmodule.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_pagebreak.ini', + '/administrator/language/en-GB/en-GB.plg_content_pagebreak.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_pagenavigation.ini', + '/administrator/language/en-GB/en-GB.plg_content_pagenavigation.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_vote.ini', + '/administrator/language/en-GB/en-GB.plg_content_vote.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_article.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_article.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_image.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_image.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_module.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_module.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors_codemirror.ini', + '/administrator/language/en-GB/en-GB.plg_editors_codemirror.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors_none.ini', + '/administrator/language/en-GB/en-GB.plg_editors_none.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors_tinymce.ini', + '/administrator/language/en-GB/en-GB.plg_editors_tinymce.sys.ini', + '/administrator/language/en-GB/en-GB.plg_extension_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_extension_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_calendar.ini', + '/administrator/language/en-GB/en-GB.plg_fields_calendar.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_checkboxes.ini', + '/administrator/language/en-GB/en-GB.plg_fields_checkboxes.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_color.ini', + '/administrator/language/en-GB/en-GB.plg_fields_color.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_editor.ini', + '/administrator/language/en-GB/en-GB.plg_fields_editor.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_image.ini', + '/administrator/language/en-GB/en-GB.plg_fields_image.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_imagelist.ini', + '/administrator/language/en-GB/en-GB.plg_fields_imagelist.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_integer.ini', + '/administrator/language/en-GB/en-GB.plg_fields_integer.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_list.ini', + '/administrator/language/en-GB/en-GB.plg_fields_list.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_media.ini', + '/administrator/language/en-GB/en-GB.plg_fields_media.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_radio.ini', + '/administrator/language/en-GB/en-GB.plg_fields_radio.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_sql.ini', + '/administrator/language/en-GB/en-GB.plg_fields_sql.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_text.ini', + '/administrator/language/en-GB/en-GB.plg_fields_text.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_textarea.ini', + '/administrator/language/en-GB/en-GB.plg_fields_textarea.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_url.ini', + '/administrator/language/en-GB/en-GB.plg_fields_url.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_user.ini', + '/administrator/language/en-GB/en-GB.plg_fields_user.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.ini', + '/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_categories.ini', + '/administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_contacts.ini', + '/administrator/language/en-GB/en-GB.plg_finder_contacts.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_content.ini', + '/administrator/language/en-GB/en-GB.plg_finder_content.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.ini', + '/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_tags.ini', + '/administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_weblinks.ini', + '/administrator/language/en-GB/en-GB.plg_finder_weblinks.sys.ini', + '/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.ini', + '/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.sys.ini', + '/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.ini', + '/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.sys.ini', + '/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.ini', + '/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.sys.ini', + '/administrator/language/en-GB/en-GB.plg_installer_webinstaller.ini', + '/administrator/language/en-GB/en-GB.plg_installer_webinstaller.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_consents.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_consents.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_contact.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_contact.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_content.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_content.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_message.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_message.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_user.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_user.sys.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.sys.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.sys.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.sys.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.sys.ini', + '/administrator/language/en-GB/en-GB.plg_sampledata_blog.ini', + '/administrator/language/en-GB/en-GB.plg_sampledata_blog.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_actionlogs.ini', + '/administrator/language/en-GB/en-GB.plg_system_actionlogs.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_cache.ini', + '/administrator/language/en-GB/en-GB.plg_system_cache.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_debug.ini', + '/administrator/language/en-GB/en-GB.plg_system_debug.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_fields.ini', + '/administrator/language/en-GB/en-GB.plg_system_fields.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_highlight.ini', + '/administrator/language/en-GB/en-GB.plg_system_highlight.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_languagecode.ini', + '/administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_languagefilter.ini', + '/administrator/language/en-GB/en-GB.plg_system_languagefilter.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_log.ini', + '/administrator/language/en-GB/en-GB.plg_system_log.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_logout.ini', + '/administrator/language/en-GB/en-GB.plg_system_logout.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_logrotation.ini', + '/administrator/language/en-GB/en-GB.plg_system_logrotation.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_p3p.ini', + '/administrator/language/en-GB/en-GB.plg_system_p3p.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_privacyconsent.ini', + '/administrator/language/en-GB/en-GB.plg_system_privacyconsent.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_redirect.ini', + '/administrator/language/en-GB/en-GB.plg_system_redirect.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_remember.ini', + '/administrator/language/en-GB/en-GB.plg_system_remember.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_sef.ini', + '/administrator/language/en-GB/en-GB.plg_system_sef.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_sessiongc.ini', + '/administrator/language/en-GB/en-GB.plg_system_sessiongc.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_stats.ini', + '/administrator/language/en-GB/en-GB.plg_system_stats.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_updatenotification.ini', + '/administrator/language/en-GB/en-GB.plg_system_updatenotification.sys.ini', + '/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini', + '/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini', + '/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini', + '/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.sys.ini', + '/administrator/language/en-GB/en-GB.plg_user_contactcreator.ini', + '/administrator/language/en-GB/en-GB.plg_user_contactcreator.sys.ini', + '/administrator/language/en-GB/en-GB.plg_user_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_user_profile.ini', + '/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini', + '/administrator/language/en-GB/en-GB.plg_user_terms.ini', + '/administrator/language/en-GB/en-GB.plg_user_terms.sys.ini', + '/administrator/language/en-GB/en-GB.tpl_hathor.ini', + '/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini', + '/administrator/language/en-GB/en-GB.tpl_isis.ini', + '/administrator/language/en-GB/en-GB.tpl_isis.sys.ini', + '/administrator/language/en-GB/en-GB.xml', + '/administrator/manifests/libraries/fof.xml', + '/administrator/manifests/libraries/idna_convert.xml', + '/administrator/manifests/libraries/phputf8.xml', + '/administrator/modules/mod_feed/helper.php', + '/administrator/modules/mod_latest/helper.php', + '/administrator/modules/mod_latestactions/helper.php', + '/administrator/modules/mod_logged/helper.php', + '/administrator/modules/mod_login/helper.php', + '/administrator/modules/mod_menu/helper.php', + '/administrator/modules/mod_menu/menu.php', + '/administrator/modules/mod_multilangstatus/language/en-GB/en-GB.mod_multilangstatus.ini', + '/administrator/modules/mod_multilangstatus/language/en-GB/en-GB.mod_multilangstatus.sys.ini', + '/administrator/modules/mod_popular/helper.php', + '/administrator/modules/mod_privacy_dashboard/helper.php', + '/administrator/modules/mod_quickicon/helper.php', + '/administrator/modules/mod_quickicon/mod_quickicon.php', + '/administrator/modules/mod_sampledata/helper.php', + '/administrator/modules/mod_stats_admin/helper.php', + '/administrator/modules/mod_stats_admin/language/en-GB.mod_stats_admin.ini', + '/administrator/modules/mod_stats_admin/language/en-GB.mod_stats_admin.sys.ini', + '/administrator/modules/mod_status/mod_status.php', + '/administrator/modules/mod_status/mod_status.xml', + '/administrator/modules/mod_status/tmpl/default.php', + '/administrator/modules/mod_version/helper.php', + '/administrator/modules/mod_version/language/en-GB/en-GB.mod_version.ini', + '/administrator/modules/mod_version/language/en-GB/en-GB.mod_version.sys.ini', + '/administrator/templates/hathor/LICENSE.txt', + '/administrator/templates/hathor/component.php', + '/administrator/templates/hathor/cpanel.php', + '/administrator/templates/hathor/css/boldtext.css', + '/administrator/templates/hathor/css/colour_blue.css', + '/administrator/templates/hathor/css/colour_blue_rtl.css', + '/administrator/templates/hathor/css/colour_brown.css', + '/administrator/templates/hathor/css/colour_brown_rtl.css', + '/administrator/templates/hathor/css/colour_highcontrast.css', + '/administrator/templates/hathor/css/colour_highcontrast_rtl.css', + '/administrator/templates/hathor/css/colour_standard.css', + '/administrator/templates/hathor/css/colour_standard_rtl.css', + '/administrator/templates/hathor/css/error.css', + '/administrator/templates/hathor/css/ie7.css', + '/administrator/templates/hathor/css/ie8.css', + '/administrator/templates/hathor/css/template.css', + '/administrator/templates/hathor/css/template_rtl.css', + '/administrator/templates/hathor/css/theme.css', + '/administrator/templates/hathor/error.php', + '/administrator/templates/hathor/favicon.ico', + '/administrator/templates/hathor/html/com_admin/help/default.php', + '/administrator/templates/hathor/html/com_admin/profile/edit.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_config.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_directory.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_navigation.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_phpsettings.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_system.php', + '/administrator/templates/hathor/html/com_associations/associations/default.php', + '/administrator/templates/hathor/html/com_banners/banner/edit.php', + '/administrator/templates/hathor/html/com_banners/banners/default.php', + '/administrator/templates/hathor/html/com_banners/client/edit.php', + '/administrator/templates/hathor/html/com_banners/clients/default.php', + '/administrator/templates/hathor/html/com_banners/download/default.php', + '/administrator/templates/hathor/html/com_banners/tracks/default.php', + '/administrator/templates/hathor/html/com_cache/cache/default.php', + '/administrator/templates/hathor/html/com_cache/purge/default.php', + '/administrator/templates/hathor/html/com_categories/categories/default.php', + '/administrator/templates/hathor/html/com_categories/category/edit.php', + '/administrator/templates/hathor/html/com_categories/category/edit_options.php', + '/administrator/templates/hathor/html/com_checkin/checkin/default.php', + '/administrator/templates/hathor/html/com_config/application/default.php', + '/administrator/templates/hathor/html/com_config/application/default_cache.php', + '/administrator/templates/hathor/html/com_config/application/default_cookie.php', + '/administrator/templates/hathor/html/com_config/application/default_database.php', + '/administrator/templates/hathor/html/com_config/application/default_debug.php', + '/administrator/templates/hathor/html/com_config/application/default_filters.php', + '/administrator/templates/hathor/html/com_config/application/default_ftp.php', + '/administrator/templates/hathor/html/com_config/application/default_ftplogin.php', + '/administrator/templates/hathor/html/com_config/application/default_locale.php', + '/administrator/templates/hathor/html/com_config/application/default_mail.php', + '/administrator/templates/hathor/html/com_config/application/default_metadata.php', + '/administrator/templates/hathor/html/com_config/application/default_navigation.php', + '/administrator/templates/hathor/html/com_config/application/default_permissions.php', + '/administrator/templates/hathor/html/com_config/application/default_seo.php', + '/administrator/templates/hathor/html/com_config/application/default_server.php', + '/administrator/templates/hathor/html/com_config/application/default_session.php', + '/administrator/templates/hathor/html/com_config/application/default_site.php', + '/administrator/templates/hathor/html/com_config/application/default_system.php', + '/administrator/templates/hathor/html/com_config/component/default.php', + '/administrator/templates/hathor/html/com_contact/contact/edit.php', + '/administrator/templates/hathor/html/com_contact/contact/edit_params.php', + '/administrator/templates/hathor/html/com_contact/contacts/default.php', + '/administrator/templates/hathor/html/com_contact/contacts/modal.php', + '/administrator/templates/hathor/html/com_content/article/edit.php', + '/administrator/templates/hathor/html/com_content/articles/default.php', + '/administrator/templates/hathor/html/com_content/articles/modal.php', + '/administrator/templates/hathor/html/com_content/featured/default.php', + '/administrator/templates/hathor/html/com_contenthistory/history/modal.php', + '/administrator/templates/hathor/html/com_cpanel/cpanel/default.php', + '/administrator/templates/hathor/html/com_fields/field/edit.php', + '/administrator/templates/hathor/html/com_fields/fields/default.php', + '/administrator/templates/hathor/html/com_fields/group/edit.php', + '/administrator/templates/hathor/html/com_fields/groups/default.php', + '/administrator/templates/hathor/html/com_finder/filters/default.php', + '/administrator/templates/hathor/html/com_finder/index/default.php', + '/administrator/templates/hathor/html/com_finder/maps/default.php', + '/administrator/templates/hathor/html/com_installer/database/default.php', + '/administrator/templates/hathor/html/com_installer/default/default_ftp.php', + '/administrator/templates/hathor/html/com_installer/discover/default.php', + '/administrator/templates/hathor/html/com_installer/install/default.php', + '/administrator/templates/hathor/html/com_installer/install/default_form.php', + '/administrator/templates/hathor/html/com_installer/languages/default.php', + '/administrator/templates/hathor/html/com_installer/languages/default_filter.php', + '/administrator/templates/hathor/html/com_installer/manage/default.php', + '/administrator/templates/hathor/html/com_installer/manage/default_filter.php', + '/administrator/templates/hathor/html/com_installer/update/default.php', + '/administrator/templates/hathor/html/com_installer/warnings/default.php', + '/administrator/templates/hathor/html/com_joomlaupdate/default/default.php', + '/administrator/templates/hathor/html/com_languages/installed/default.php', + '/administrator/templates/hathor/html/com_languages/installed/default_ftp.php', + '/administrator/templates/hathor/html/com_languages/languages/default.php', + '/administrator/templates/hathor/html/com_languages/overrides/default.php', + '/administrator/templates/hathor/html/com_menus/item/edit.php', + '/administrator/templates/hathor/html/com_menus/item/edit_options.php', + '/administrator/templates/hathor/html/com_menus/items/default.php', + '/administrator/templates/hathor/html/com_menus/menu/edit.php', + '/administrator/templates/hathor/html/com_menus/menus/default.php', + '/administrator/templates/hathor/html/com_menus/menutypes/default.php', + '/administrator/templates/hathor/html/com_messages/message/edit.php', + '/administrator/templates/hathor/html/com_messages/messages/default.php', + '/administrator/templates/hathor/html/com_modules/module/edit.php', + '/administrator/templates/hathor/html/com_modules/module/edit_assignment.php', + '/administrator/templates/hathor/html/com_modules/module/edit_options.php', + '/administrator/templates/hathor/html/com_modules/modules/default.php', + '/administrator/templates/hathor/html/com_modules/positions/modal.php', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeed/edit.php', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeed/edit_params.php', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds/default.php', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds/modal.php', + '/administrator/templates/hathor/html/com_plugins/plugin/edit.php', + '/administrator/templates/hathor/html/com_plugins/plugin/edit_options.php', + '/administrator/templates/hathor/html/com_plugins/plugins/default.php', + '/administrator/templates/hathor/html/com_postinstall/messages/default.php', + '/administrator/templates/hathor/html/com_redirect/links/default.php', + '/administrator/templates/hathor/html/com_search/searches/default.php', + '/administrator/templates/hathor/html/com_tags/tag/edit.php', + '/administrator/templates/hathor/html/com_tags/tag/edit_metadata.php', + '/administrator/templates/hathor/html/com_tags/tag/edit_options.php', + '/administrator/templates/hathor/html/com_tags/tags/default.php', + '/administrator/templates/hathor/html/com_templates/style/edit.php', + '/administrator/templates/hathor/html/com_templates/style/edit_assignment.php', + '/administrator/templates/hathor/html/com_templates/style/edit_options.php', + '/administrator/templates/hathor/html/com_templates/styles/default.php', + '/administrator/templates/hathor/html/com_templates/template/default.php', + '/administrator/templates/hathor/html/com_templates/template/default_description.php', + '/administrator/templates/hathor/html/com_templates/template/default_folders.php', + '/administrator/templates/hathor/html/com_templates/template/default_tree.php', + '/administrator/templates/hathor/html/com_templates/templates/default.php', + '/administrator/templates/hathor/html/com_users/debuggroup/default.php', + '/administrator/templates/hathor/html/com_users/debuguser/default.php', + '/administrator/templates/hathor/html/com_users/groups/default.php', + '/administrator/templates/hathor/html/com_users/levels/default.php', + '/administrator/templates/hathor/html/com_users/note/edit.php', + '/administrator/templates/hathor/html/com_users/notes/default.php', + '/administrator/templates/hathor/html/com_users/user/edit.php', + '/administrator/templates/hathor/html/com_users/users/default.php', + '/administrator/templates/hathor/html/com_users/users/modal.php', + '/administrator/templates/hathor/html/com_weblinks/weblink/edit.php', + '/administrator/templates/hathor/html/com_weblinks/weblink/edit_params.php', + '/administrator/templates/hathor/html/com_weblinks/weblinks/default.php', + '/administrator/templates/hathor/html/layouts/com_media/toolbar/deletemedia.php', + '/administrator/templates/hathor/html/layouts/com_media/toolbar/newfolder.php', + '/administrator/templates/hathor/html/layouts/com_media/toolbar/uploadmedia.php', + '/administrator/templates/hathor/html/layouts/com_messages/toolbar/mysettings.php', + '/administrator/templates/hathor/html/layouts/com_modules/toolbar/cancelselect.php', + '/administrator/templates/hathor/html/layouts/com_modules/toolbar/newmodule.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/details.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/fieldset.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/global.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/metadata.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/params.php', + '/administrator/templates/hathor/html/layouts/joomla/quickicons/icon.php', + '/administrator/templates/hathor/html/layouts/joomla/sidebars/submenu.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/base.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/batch.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/confirm.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/containerclose.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/containeropen.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/help.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/iconclass.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/link.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/modal.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/popup.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/separator.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/slider.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/standard.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/title.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/versions.php', + '/administrator/templates/hathor/html/layouts/plugins/user/profile/fields/dob.php', + '/administrator/templates/hathor/html/mod_login/default.php', + '/administrator/templates/hathor/html/mod_quickicon/default.php', + '/administrator/templates/hathor/html/modules.php', + '/administrator/templates/hathor/html/pagination.php', + '/administrator/templates/hathor/images/admin/blank.png', + '/administrator/templates/hathor/images/admin/checked_out.png', + '/administrator/templates/hathor/images/admin/collapseall.png', + '/administrator/templates/hathor/images/admin/disabled.png', + '/administrator/templates/hathor/images/admin/downarrow-1.png', + '/administrator/templates/hathor/images/admin/downarrow.png', + '/administrator/templates/hathor/images/admin/downarrow0.png', + '/administrator/templates/hathor/images/admin/expandall.png', + '/administrator/templates/hathor/images/admin/featured.png', + '/administrator/templates/hathor/images/admin/filesave.png', + '/administrator/templates/hathor/images/admin/filter_16.png', + '/administrator/templates/hathor/images/admin/icon-16-allow.png', + '/administrator/templates/hathor/images/admin/icon-16-allowinactive.png', + '/administrator/templates/hathor/images/admin/icon-16-deny.png', + '/administrator/templates/hathor/images/admin/icon-16-denyinactive.png', + '/administrator/templates/hathor/images/admin/icon-16-links.png', + '/administrator/templates/hathor/images/admin/icon-16-notice-note.png', + '/administrator/templates/hathor/images/admin/icon-16-protected.png', + '/administrator/templates/hathor/images/admin/menu_divider.png', + '/administrator/templates/hathor/images/admin/note_add_16.png', + '/administrator/templates/hathor/images/admin/publish_g.png', + '/administrator/templates/hathor/images/admin/publish_r.png', + '/administrator/templates/hathor/images/admin/publish_x.png', + '/administrator/templates/hathor/images/admin/publish_y.png', + '/administrator/templates/hathor/images/admin/sort_asc.png', + '/administrator/templates/hathor/images/admin/sort_desc.png', + '/administrator/templates/hathor/images/admin/tick.png', + '/administrator/templates/hathor/images/admin/trash.png', + '/administrator/templates/hathor/images/admin/uparrow-1.png', + '/administrator/templates/hathor/images/admin/uparrow.png', + '/administrator/templates/hathor/images/admin/uparrow0.png', + '/administrator/templates/hathor/images/arrow.png', + '/administrator/templates/hathor/images/bg-menu.gif', + '/administrator/templates/hathor/images/calendar.png', + '/administrator/templates/hathor/images/header/icon-48-alert.png', + '/administrator/templates/hathor/images/header/icon-48-apply.png', + '/administrator/templates/hathor/images/header/icon-48-archive.png', + '/administrator/templates/hathor/images/header/icon-48-article-add.png', + '/administrator/templates/hathor/images/header/icon-48-article-edit.png', + '/administrator/templates/hathor/images/header/icon-48-article.png', + '/administrator/templates/hathor/images/header/icon-48-assoc.png', + '/administrator/templates/hathor/images/header/icon-48-banner-categories.png', + '/administrator/templates/hathor/images/header/icon-48-banner-client.png', + '/administrator/templates/hathor/images/header/icon-48-banner-tracks.png', + '/administrator/templates/hathor/images/header/icon-48-banner.png', + '/administrator/templates/hathor/images/header/icon-48-calendar.png', + '/administrator/templates/hathor/images/header/icon-48-category-add.png', + '/administrator/templates/hathor/images/header/icon-48-category.png', + '/administrator/templates/hathor/images/header/icon-48-checkin.png', + '/administrator/templates/hathor/images/header/icon-48-clear.png', + '/administrator/templates/hathor/images/header/icon-48-component.png', + '/administrator/templates/hathor/images/header/icon-48-config.png', + '/administrator/templates/hathor/images/header/icon-48-contacts-categories.png', + '/administrator/templates/hathor/images/header/icon-48-contacts.png', + '/administrator/templates/hathor/images/header/icon-48-content.png', + '/administrator/templates/hathor/images/header/icon-48-cpanel.png', + '/administrator/templates/hathor/images/header/icon-48-default.png', + '/administrator/templates/hathor/images/header/icon-48-deny.png', + '/administrator/templates/hathor/images/header/icon-48-download.png', + '/administrator/templates/hathor/images/header/icon-48-edit.png', + '/administrator/templates/hathor/images/header/icon-48-extension.png', + '/administrator/templates/hathor/images/header/icon-48-featured.png', + '/administrator/templates/hathor/images/header/icon-48-frontpage.png', + '/administrator/templates/hathor/images/header/icon-48-generic.png', + '/administrator/templates/hathor/images/header/icon-48-groups-add.png', + '/administrator/templates/hathor/images/header/icon-48-groups.png', + '/administrator/templates/hathor/images/header/icon-48-help-forum.png', + '/administrator/templates/hathor/images/header/icon-48-help-this.png', + '/administrator/templates/hathor/images/header/icon-48-help_header.png', + '/administrator/templates/hathor/images/header/icon-48-inbox.png', + '/administrator/templates/hathor/images/header/icon-48-info.png', + '/administrator/templates/hathor/images/header/icon-48-install.png', + '/administrator/templates/hathor/images/header/icon-48-jupdate-updatefound.png', + '/administrator/templates/hathor/images/header/icon-48-jupdate-uptodate.png', + '/administrator/templates/hathor/images/header/icon-48-language.png', + '/administrator/templates/hathor/images/header/icon-48-levels-add.png', + '/administrator/templates/hathor/images/header/icon-48-levels.png', + '/administrator/templates/hathor/images/header/icon-48-links-cat.png', + '/administrator/templates/hathor/images/header/icon-48-links.png', + '/administrator/templates/hathor/images/header/icon-48-massmail.png', + '/administrator/templates/hathor/images/header/icon-48-media.png', + '/administrator/templates/hathor/images/header/icon-48-menu-add.png', + '/administrator/templates/hathor/images/header/icon-48-menu.png', + '/administrator/templates/hathor/images/header/icon-48-menumgr.png', + '/administrator/templates/hathor/images/header/icon-48-module.png', + '/administrator/templates/hathor/images/header/icon-48-move.png', + '/administrator/templates/hathor/images/header/icon-48-new-privatemessage.png', + '/administrator/templates/hathor/images/header/icon-48-newcategory.png', + '/administrator/templates/hathor/images/header/icon-48-newsfeeds-cat.png', + '/administrator/templates/hathor/images/header/icon-48-newsfeeds.png', + '/administrator/templates/hathor/images/header/icon-48-notice.png', + '/administrator/templates/hathor/images/header/icon-48-plugin.png', + '/administrator/templates/hathor/images/header/icon-48-preview.png', + '/administrator/templates/hathor/images/header/icon-48-print.png', + '/administrator/templates/hathor/images/header/icon-48-purge.png', + '/administrator/templates/hathor/images/header/icon-48-puzzle.png', + '/administrator/templates/hathor/images/header/icon-48-read-privatemessage.png', + '/administrator/templates/hathor/images/header/icon-48-readmess.png', + '/administrator/templates/hathor/images/header/icon-48-redirect.png', + '/administrator/templates/hathor/images/header/icon-48-revert.png', + '/administrator/templates/hathor/images/header/icon-48-search.png', + '/administrator/templates/hathor/images/header/icon-48-section.png', + '/administrator/templates/hathor/images/header/icon-48-send.png', + '/administrator/templates/hathor/images/header/icon-48-static.png', + '/administrator/templates/hathor/images/header/icon-48-stats.png', + '/administrator/templates/hathor/images/header/icon-48-tags.png', + '/administrator/templates/hathor/images/header/icon-48-themes.png', + '/administrator/templates/hathor/images/header/icon-48-trash.png', + '/administrator/templates/hathor/images/header/icon-48-unarchive.png', + '/administrator/templates/hathor/images/header/icon-48-upload.png', + '/administrator/templates/hathor/images/header/icon-48-user-add.png', + '/administrator/templates/hathor/images/header/icon-48-user-edit.png', + '/administrator/templates/hathor/images/header/icon-48-user-profile.png', + '/administrator/templates/hathor/images/header/icon-48-user.png', + '/administrator/templates/hathor/images/header/icon-48-writemess.png', + '/administrator/templates/hathor/images/header/icon-messaging.png', + '/administrator/templates/hathor/images/j_arrow.png', + '/administrator/templates/hathor/images/j_arrow_down.png', + '/administrator/templates/hathor/images/j_arrow_left.png', + '/administrator/templates/hathor/images/j_arrow_right.png', + '/administrator/templates/hathor/images/j_login_lock.png', + '/administrator/templates/hathor/images/j_logo.png', + '/administrator/templates/hathor/images/logo.png', + '/administrator/templates/hathor/images/menu/icon-16-alert.png', + '/administrator/templates/hathor/images/menu/icon-16-apply.png', + '/administrator/templates/hathor/images/menu/icon-16-archive.png', + '/administrator/templates/hathor/images/menu/icon-16-article.png', + '/administrator/templates/hathor/images/menu/icon-16-assoc.png', + '/administrator/templates/hathor/images/menu/icon-16-back-user.png', + '/administrator/templates/hathor/images/menu/icon-16-banner-categories.png', + '/administrator/templates/hathor/images/menu/icon-16-banner-client.png', + '/administrator/templates/hathor/images/menu/icon-16-banner-tracks.png', + '/administrator/templates/hathor/images/menu/icon-16-banner.png', + '/administrator/templates/hathor/images/menu/icon-16-calendar.png', + '/administrator/templates/hathor/images/menu/icon-16-category.png', + '/administrator/templates/hathor/images/menu/icon-16-checkin.png', + '/administrator/templates/hathor/images/menu/icon-16-clear.png', + '/administrator/templates/hathor/images/menu/icon-16-component.png', + '/administrator/templates/hathor/images/menu/icon-16-config.png', + '/administrator/templates/hathor/images/menu/icon-16-contacts-categories.png', + '/administrator/templates/hathor/images/menu/icon-16-contacts.png', + '/administrator/templates/hathor/images/menu/icon-16-content.png', + '/administrator/templates/hathor/images/menu/icon-16-cpanel.png', + '/administrator/templates/hathor/images/menu/icon-16-default.png', + '/administrator/templates/hathor/images/menu/icon-16-delete.png', + '/administrator/templates/hathor/images/menu/icon-16-deny.png', + '/administrator/templates/hathor/images/menu/icon-16-download.png', + '/administrator/templates/hathor/images/menu/icon-16-edit.png', + '/administrator/templates/hathor/images/menu/icon-16-featured.png', + '/administrator/templates/hathor/images/menu/icon-16-frontpage.png', + '/administrator/templates/hathor/images/menu/icon-16-generic.png', + '/administrator/templates/hathor/images/menu/icon-16-groups.png', + '/administrator/templates/hathor/images/menu/icon-16-help-community.png', + '/administrator/templates/hathor/images/menu/icon-16-help-dev.png', + '/administrator/templates/hathor/images/menu/icon-16-help-docs.png', + '/administrator/templates/hathor/images/menu/icon-16-help-forum.png', + '/administrator/templates/hathor/images/menu/icon-16-help-jed.png', + '/administrator/templates/hathor/images/menu/icon-16-help-jrd.png', + '/administrator/templates/hathor/images/menu/icon-16-help-security.png', + '/administrator/templates/hathor/images/menu/icon-16-help-shop.png', + '/administrator/templates/hathor/images/menu/icon-16-help-this.png', + '/administrator/templates/hathor/images/menu/icon-16-help-trans.png', + '/administrator/templates/hathor/images/menu/icon-16-help.png', + '/administrator/templates/hathor/images/menu/icon-16-inbox.png', + '/administrator/templates/hathor/images/menu/icon-16-info.png', + '/administrator/templates/hathor/images/menu/icon-16-install.png', + '/administrator/templates/hathor/images/menu/icon-16-language.png', + '/administrator/templates/hathor/images/menu/icon-16-levels.png', + '/administrator/templates/hathor/images/menu/icon-16-links-cat.png', + '/administrator/templates/hathor/images/menu/icon-16-links.png', + '/administrator/templates/hathor/images/menu/icon-16-logout.png', + '/administrator/templates/hathor/images/menu/icon-16-maintenance.png', + '/administrator/templates/hathor/images/menu/icon-16-massmail.png', + '/administrator/templates/hathor/images/menu/icon-16-media.png', + '/administrator/templates/hathor/images/menu/icon-16-menu.png', + '/administrator/templates/hathor/images/menu/icon-16-menumgr.png', + '/administrator/templates/hathor/images/menu/icon-16-messages.png', + '/administrator/templates/hathor/images/menu/icon-16-messaging.png', + '/administrator/templates/hathor/images/menu/icon-16-module.png', + '/administrator/templates/hathor/images/menu/icon-16-move.png', + '/administrator/templates/hathor/images/menu/icon-16-new-privatemessage.png', + '/administrator/templates/hathor/images/menu/icon-16-new.png', + '/administrator/templates/hathor/images/menu/icon-16-newarticle.png', + '/administrator/templates/hathor/images/menu/icon-16-newcategory.png', + '/administrator/templates/hathor/images/menu/icon-16-newgroup.png', + '/administrator/templates/hathor/images/menu/icon-16-newlevel.png', + '/administrator/templates/hathor/images/menu/icon-16-newsfeeds-cat.png', + '/administrator/templates/hathor/images/menu/icon-16-newsfeeds.png', + '/administrator/templates/hathor/images/menu/icon-16-newuser.png', + '/administrator/templates/hathor/images/menu/icon-16-nopreview.png', + '/administrator/templates/hathor/images/menu/icon-16-notdefault.png', + '/administrator/templates/hathor/images/menu/icon-16-notice.png', + '/administrator/templates/hathor/images/menu/icon-16-plugin.png', + '/administrator/templates/hathor/images/menu/icon-16-preview.png', + '/administrator/templates/hathor/images/menu/icon-16-print.png', + '/administrator/templates/hathor/images/menu/icon-16-purge.png', + '/administrator/templates/hathor/images/menu/icon-16-puzzle.png', + '/administrator/templates/hathor/images/menu/icon-16-read-privatemessage.png', + '/administrator/templates/hathor/images/menu/icon-16-readmess.png', + '/administrator/templates/hathor/images/menu/icon-16-redirect.png', + '/administrator/templates/hathor/images/menu/icon-16-revert.png', + '/administrator/templates/hathor/images/menu/icon-16-search.png', + '/administrator/templates/hathor/images/menu/icon-16-send.png', + '/administrator/templates/hathor/images/menu/icon-16-stats.png', + '/administrator/templates/hathor/images/menu/icon-16-tags.png', + '/administrator/templates/hathor/images/menu/icon-16-themes.png', + '/administrator/templates/hathor/images/menu/icon-16-trash.png', + '/administrator/templates/hathor/images/menu/icon-16-unarticle.png', + '/administrator/templates/hathor/images/menu/icon-16-upload.png', + '/administrator/templates/hathor/images/menu/icon-16-user-dd.png', + '/administrator/templates/hathor/images/menu/icon-16-user-note.png', + '/administrator/templates/hathor/images/menu/icon-16-user.png', + '/administrator/templates/hathor/images/menu/icon-16-viewsite.png', + '/administrator/templates/hathor/images/menu/icon-16-writemess.png', + '/administrator/templates/hathor/images/mini_icon.png', + '/administrator/templates/hathor/images/notice-alert.png', + '/administrator/templates/hathor/images/notice-info.png', + '/administrator/templates/hathor/images/notice-note.png', + '/administrator/templates/hathor/images/required.png', + '/administrator/templates/hathor/images/selector-arrow-hc.png', + '/administrator/templates/hathor/images/selector-arrow-rtl.png', + '/administrator/templates/hathor/images/selector-arrow-std.png', + '/administrator/templates/hathor/images/selector-arrow.png', + '/administrator/templates/hathor/images/system/calendar.png', + '/administrator/templates/hathor/images/system/selector-arrow.png', + '/administrator/templates/hathor/images/toolbar/icon-32-adduser.png', + '/administrator/templates/hathor/images/toolbar/icon-32-alert.png', + '/administrator/templates/hathor/images/toolbar/icon-32-apply.png', + '/administrator/templates/hathor/images/toolbar/icon-32-archive.png', + '/administrator/templates/hathor/images/toolbar/icon-32-article-add.png', + '/administrator/templates/hathor/images/toolbar/icon-32-article.png', + '/administrator/templates/hathor/images/toolbar/icon-32-back.png', + '/administrator/templates/hathor/images/toolbar/icon-32-banner-categories.png', + '/administrator/templates/hathor/images/toolbar/icon-32-banner-client.png', + '/administrator/templates/hathor/images/toolbar/icon-32-banner-tracks.png', + '/administrator/templates/hathor/images/toolbar/icon-32-banner.png', + '/administrator/templates/hathor/images/toolbar/icon-32-batch.png', + '/administrator/templates/hathor/images/toolbar/icon-32-calendar.png', + '/administrator/templates/hathor/images/toolbar/icon-32-cancel.png', + '/administrator/templates/hathor/images/toolbar/icon-32-checkin.png', + '/administrator/templates/hathor/images/toolbar/icon-32-cog.png', + '/administrator/templates/hathor/images/toolbar/icon-32-component.png', + '/administrator/templates/hathor/images/toolbar/icon-32-config.png', + '/administrator/templates/hathor/images/toolbar/icon-32-contacts-categories.png', + '/administrator/templates/hathor/images/toolbar/icon-32-contacts.png', + '/administrator/templates/hathor/images/toolbar/icon-32-copy.png', + '/administrator/templates/hathor/images/toolbar/icon-32-css.png', + '/administrator/templates/hathor/images/toolbar/icon-32-default.png', + '/administrator/templates/hathor/images/toolbar/icon-32-delete-style.png', + '/administrator/templates/hathor/images/toolbar/icon-32-delete.png', + '/administrator/templates/hathor/images/toolbar/icon-32-deny.png', + '/administrator/templates/hathor/images/toolbar/icon-32-download.png', + '/administrator/templates/hathor/images/toolbar/icon-32-edit.png', + '/administrator/templates/hathor/images/toolbar/icon-32-error.png', + '/administrator/templates/hathor/images/toolbar/icon-32-export.png', + '/administrator/templates/hathor/images/toolbar/icon-32-extension.png', + '/administrator/templates/hathor/images/toolbar/icon-32-featured.png', + '/administrator/templates/hathor/images/toolbar/icon-32-forward.png', + '/administrator/templates/hathor/images/toolbar/icon-32-help.png', + '/administrator/templates/hathor/images/toolbar/icon-32-html.png', + '/administrator/templates/hathor/images/toolbar/icon-32-inbox.png', + '/administrator/templates/hathor/images/toolbar/icon-32-info.png', + '/administrator/templates/hathor/images/toolbar/icon-32-links.png', + '/administrator/templates/hathor/images/toolbar/icon-32-lock.png', + '/administrator/templates/hathor/images/toolbar/icon-32-menu.png', + '/administrator/templates/hathor/images/toolbar/icon-32-messaging.png', + '/administrator/templates/hathor/images/toolbar/icon-32-messanging.png', + '/administrator/templates/hathor/images/toolbar/icon-32-module.png', + '/administrator/templates/hathor/images/toolbar/icon-32-move.png', + '/administrator/templates/hathor/images/toolbar/icon-32-new-privatemessage.png', + '/administrator/templates/hathor/images/toolbar/icon-32-new-style.png', + '/administrator/templates/hathor/images/toolbar/icon-32-new.png', + '/administrator/templates/hathor/images/toolbar/icon-32-notice.png', + '/administrator/templates/hathor/images/toolbar/icon-32-preview.png', + '/administrator/templates/hathor/images/toolbar/icon-32-print.png', + '/administrator/templates/hathor/images/toolbar/icon-32-publish.png', + '/administrator/templates/hathor/images/toolbar/icon-32-purge.png', + '/administrator/templates/hathor/images/toolbar/icon-32-read-privatemessage.png', + '/administrator/templates/hathor/images/toolbar/icon-32-refresh.png', + '/administrator/templates/hathor/images/toolbar/icon-32-remove.png', + '/administrator/templates/hathor/images/toolbar/icon-32-revert.png', + '/administrator/templates/hathor/images/toolbar/icon-32-save-copy.png', + '/administrator/templates/hathor/images/toolbar/icon-32-save-new.png', + '/administrator/templates/hathor/images/toolbar/icon-32-save.png', + '/administrator/templates/hathor/images/toolbar/icon-32-search.png', + '/administrator/templates/hathor/images/toolbar/icon-32-send.png', + '/administrator/templates/hathor/images/toolbar/icon-32-stats.png', + '/administrator/templates/hathor/images/toolbar/icon-32-trash.png', + '/administrator/templates/hathor/images/toolbar/icon-32-unarchive.png', + '/administrator/templates/hathor/images/toolbar/icon-32-unblock.png', + '/administrator/templates/hathor/images/toolbar/icon-32-unpublish.png', + '/administrator/templates/hathor/images/toolbar/icon-32-upload.png', + '/administrator/templates/hathor/images/toolbar/icon-32-user-add.png', + '/administrator/templates/hathor/images/toolbar/icon-32-xml.png', + '/administrator/templates/hathor/index.php', + '/administrator/templates/hathor/js/template.js', + '/administrator/templates/hathor/language/en-GB/en-GB.tpl_hathor.ini', + '/administrator/templates/hathor/language/en-GB/en-GB.tpl_hathor.sys.ini', + '/administrator/templates/hathor/less/buttons.less', + '/administrator/templates/hathor/less/colour_baseline.less', + '/administrator/templates/hathor/less/colour_blue.less', + '/administrator/templates/hathor/less/colour_brown.less', + '/administrator/templates/hathor/less/colour_standard.less', + '/administrator/templates/hathor/less/forms.less', + '/administrator/templates/hathor/less/hathor_variables.less', + '/administrator/templates/hathor/less/icomoon.less', + '/administrator/templates/hathor/less/modals.less', + '/administrator/templates/hathor/less/template.less', + '/administrator/templates/hathor/less/variables.less', + '/administrator/templates/hathor/login.php', + '/administrator/templates/hathor/postinstall/hathormessage.php', + '/administrator/templates/hathor/templateDetails.xml', + '/administrator/templates/hathor/template_preview.png', + '/administrator/templates/hathor/template_thumbnail.png', + '/administrator/templates/isis/component.php', + '/administrator/templates/isis/cpanel.php', + '/administrator/templates/isis/css/template-rtl.css', + '/administrator/templates/isis/css/template.css', + '/administrator/templates/isis/error.php', + '/administrator/templates/isis/favicon.ico', + '/administrator/templates/isis/html/com_media/imageslist/default_folder.php', + '/administrator/templates/isis/html/com_media/imageslist/default_image.php', + '/administrator/templates/isis/html/com_media/medialist/thumbs_folders.php', + '/administrator/templates/isis/html/com_media/medialist/thumbs_imgs.php', + '/administrator/templates/isis/html/editor_content.css', + '/administrator/templates/isis/html/layouts/joomla/form/field/media.php', + '/administrator/templates/isis/html/layouts/joomla/form/field/user.php', + '/administrator/templates/isis/html/layouts/joomla/pagination/link.php', + '/administrator/templates/isis/html/layouts/joomla/pagination/links.php', + '/administrator/templates/isis/html/layouts/joomla/system/message.php', + '/administrator/templates/isis/html/layouts/joomla/toolbar/versions.php', + '/administrator/templates/isis/html/mod_version/default.php', + '/administrator/templates/isis/html/modules.php', + '/administrator/templates/isis/html/pagination.php', + '/administrator/templates/isis/images/admin/blank.png', + '/administrator/templates/isis/images/admin/checked_out.png', + '/administrator/templates/isis/images/admin/collapseall.png', + '/administrator/templates/isis/images/admin/disabled.png', + '/administrator/templates/isis/images/admin/downarrow-1.png', + '/administrator/templates/isis/images/admin/downarrow.png', + '/administrator/templates/isis/images/admin/downarrow0.png', + '/administrator/templates/isis/images/admin/expandall.png', + '/administrator/templates/isis/images/admin/featured.png', + '/administrator/templates/isis/images/admin/filesave.png', + '/administrator/templates/isis/images/admin/filter_16.png', + '/administrator/templates/isis/images/admin/icon-16-add.png', + '/administrator/templates/isis/images/admin/icon-16-allow.png', + '/administrator/templates/isis/images/admin/icon-16-allowinactive.png', + '/administrator/templates/isis/images/admin/icon-16-deny.png', + '/administrator/templates/isis/images/admin/icon-16-denyinactive.png', + '/administrator/templates/isis/images/admin/icon-16-links.png', + '/administrator/templates/isis/images/admin/icon-16-notice-note.png', + '/administrator/templates/isis/images/admin/icon-16-protected.png', + '/administrator/templates/isis/images/admin/menu_divider.png', + '/administrator/templates/isis/images/admin/note_add_16.png', + '/administrator/templates/isis/images/admin/publish_g.png', + '/administrator/templates/isis/images/admin/publish_r.png', + '/administrator/templates/isis/images/admin/publish_x.png', + '/administrator/templates/isis/images/admin/publish_y.png', + '/administrator/templates/isis/images/admin/sort_asc.png', + '/administrator/templates/isis/images/admin/sort_desc.png', + '/administrator/templates/isis/images/admin/tick.png', + '/administrator/templates/isis/images/admin/trash.png', + '/administrator/templates/isis/images/admin/uparrow-1.png', + '/administrator/templates/isis/images/admin/uparrow.png', + '/administrator/templates/isis/images/admin/uparrow0.png', + '/administrator/templates/isis/images/emailButton.png', + '/administrator/templates/isis/images/joomla.png', + '/administrator/templates/isis/images/login-joomla-inverse.png', + '/administrator/templates/isis/images/login-joomla.png', + '/administrator/templates/isis/images/logo-inverse.png', + '/administrator/templates/isis/images/logo.png', + '/administrator/templates/isis/images/pdf_button.png', + '/administrator/templates/isis/images/printButton.png', + '/administrator/templates/isis/images/system/sort_asc.png', + '/administrator/templates/isis/images/system/sort_desc.png', + '/administrator/templates/isis/img/glyphicons-halflings-white.png', + '/administrator/templates/isis/img/glyphicons-halflings.png', + '/administrator/templates/isis/index.php', + '/administrator/templates/isis/js/application.js', + '/administrator/templates/isis/js/classes.js', + '/administrator/templates/isis/js/template.js', + '/administrator/templates/isis/language/en-GB/en-GB.tpl_isis.ini', + '/administrator/templates/isis/language/en-GB/en-GB.tpl_isis.sys.ini', + '/administrator/templates/isis/less/blocks/_chzn-override.less', + '/administrator/templates/isis/less/blocks/_custom.less', + '/administrator/templates/isis/less/blocks/_editors.less', + '/administrator/templates/isis/less/blocks/_forms.less', + '/administrator/templates/isis/less/blocks/_global.less', + '/administrator/templates/isis/less/blocks/_header.less', + '/administrator/templates/isis/less/blocks/_login.less', + '/administrator/templates/isis/less/blocks/_media.less', + '/administrator/templates/isis/less/blocks/_modals.less', + '/administrator/templates/isis/less/blocks/_navbar.less', + '/administrator/templates/isis/less/blocks/_quickicons.less', + '/administrator/templates/isis/less/blocks/_sidebar.less', + '/administrator/templates/isis/less/blocks/_status.less', + '/administrator/templates/isis/less/blocks/_tables.less', + '/administrator/templates/isis/less/blocks/_toolbar.less', + '/administrator/templates/isis/less/blocks/_treeselect.less', + '/administrator/templates/isis/less/blocks/_utility-classes.less', + '/administrator/templates/isis/less/bootstrap/button-groups.less', + '/administrator/templates/isis/less/bootstrap/buttons.less', + '/administrator/templates/isis/less/bootstrap/mixins.less', + '/administrator/templates/isis/less/bootstrap/responsive-1200px-min.less', + '/administrator/templates/isis/less/bootstrap/responsive-768px-979px.less', + '/administrator/templates/isis/less/bootstrap/wells.less', + '/administrator/templates/isis/less/icomoon.less', + '/administrator/templates/isis/less/pages/_com_cpanel.less', + '/administrator/templates/isis/less/pages/_com_postinstall.less', + '/administrator/templates/isis/less/pages/_com_privacy.less', + '/administrator/templates/isis/less/pages/_com_templates.less', + '/administrator/templates/isis/less/template-rtl.less', + '/administrator/templates/isis/less/template.less', + '/administrator/templates/isis/less/variables.less', + '/administrator/templates/isis/login.php', + '/administrator/templates/isis/templateDetails.xml', + '/administrator/templates/isis/template_preview.png', + '/administrator/templates/isis/template_thumbnail.png', + '/administrator/templates/system/html/modules.php', + '/bin/index.html', + '/bin/keychain.php', + '/cli/deletefiles.php', + '/cli/finder_indexer.php', + '/cli/garbagecron.php', + '/cli/sessionGc.php', + '/cli/sessionMetadataGc.php', + '/cli/update_cron.php', + '/components/com_banners/banners.php', + '/components/com_banners/controller.php', + '/components/com_banners/helpers/banner.php', + '/components/com_banners/helpers/category.php', + '/components/com_banners/models/banner.php', + '/components/com_banners/models/banners.php', + '/components/com_banners/router.php', + '/components/com_config/config.php', + '/components/com_config/controller/cancel.php', + '/components/com_config/controller/canceladmin.php', + '/components/com_config/controller/cmsbase.php', + '/components/com_config/controller/config/display.php', + '/components/com_config/controller/config/save.php', + '/components/com_config/controller/display.php', + '/components/com_config/controller/helper.php', + '/components/com_config/controller/modules/cancel.php', + '/components/com_config/controller/modules/display.php', + '/components/com_config/controller/modules/save.php', + '/components/com_config/controller/templates/display.php', + '/components/com_config/controller/templates/save.php', + '/components/com_config/model/cms.php', + '/components/com_config/model/config.php', + '/components/com_config/model/form.php', + '/components/com_config/model/form/config.xml', + '/components/com_config/model/form/modules.xml', + '/components/com_config/model/form/modules_advanced.xml', + '/components/com_config/model/form/templates.xml', + '/components/com_config/model/modules.php', + '/components/com_config/model/templates.php', + '/components/com_config/view/cms/html.php', + '/components/com_config/view/cms/json.php', + '/components/com_config/view/config/html.php', + '/components/com_config/view/config/tmpl/default.php', + '/components/com_config/view/config/tmpl/default.xml', + '/components/com_config/view/config/tmpl/default_metadata.php', + '/components/com_config/view/config/tmpl/default_seo.php', + '/components/com_config/view/config/tmpl/default_site.php', + '/components/com_config/view/modules/html.php', + '/components/com_config/view/modules/tmpl/default.php', + '/components/com_config/view/modules/tmpl/default_options.php', + '/components/com_config/view/modules/tmpl/default_positions.php', + '/components/com_config/view/templates/html.php', + '/components/com_config/view/templates/tmpl/default.php', + '/components/com_config/view/templates/tmpl/default.xml', + '/components/com_config/view/templates/tmpl/default_options.php', + '/components/com_contact/contact.php', + '/components/com_contact/controller.php', + '/components/com_contact/controllers/contact.php', + '/components/com_contact/helpers/association.php', + '/components/com_contact/helpers/category.php', + '/components/com_contact/helpers/legacyrouter.php', + '/components/com_contact/layouts/joomla/form/renderfield.php', + '/components/com_contact/models/categories.php', + '/components/com_contact/models/category.php', + '/components/com_contact/models/contact.php', + '/components/com_contact/models/featured.php', + '/components/com_contact/models/forms/contact.xml', + '/components/com_contact/models/forms/filter_contacts.xml', + '/components/com_contact/models/forms/form.xml', + '/components/com_contact/models/rules/contactemail.php', + '/components/com_contact/models/rules/contactemailmessage.php', + '/components/com_contact/models/rules/contactemailsubject.php', + '/components/com_contact/router.php', + '/components/com_contact/views/categories/tmpl/default.php', + '/components/com_contact/views/categories/tmpl/default.xml', + '/components/com_contact/views/categories/tmpl/default_items.php', + '/components/com_contact/views/categories/view.html.php', + '/components/com_contact/views/category/tmpl/default.php', + '/components/com_contact/views/category/tmpl/default.xml', + '/components/com_contact/views/category/tmpl/default_children.php', + '/components/com_contact/views/category/tmpl/default_items.php', + '/components/com_contact/views/category/view.feed.php', + '/components/com_contact/views/category/view.html.php', + '/components/com_contact/views/contact/tmpl/default.php', + '/components/com_contact/views/contact/tmpl/default.xml', + '/components/com_contact/views/contact/tmpl/default_address.php', + '/components/com_contact/views/contact/tmpl/default_articles.php', + '/components/com_contact/views/contact/tmpl/default_form.php', + '/components/com_contact/views/contact/tmpl/default_links.php', + '/components/com_contact/views/contact/tmpl/default_profile.php', + '/components/com_contact/views/contact/tmpl/default_user_custom_fields.php', + '/components/com_contact/views/contact/view.html.php', + '/components/com_contact/views/contact/view.vcf.php', + '/components/com_contact/views/featured/tmpl/default.php', + '/components/com_contact/views/featured/tmpl/default.xml', + '/components/com_contact/views/featured/tmpl/default_items.php', + '/components/com_contact/views/featured/view.html.php', + '/components/com_content/content.php', + '/components/com_content/controller.php', + '/components/com_content/controllers/article.php', + '/components/com_content/helpers/association.php', + '/components/com_content/helpers/category.php', + '/components/com_content/helpers/legacyrouter.php', + '/components/com_content/helpers/query.php', + '/components/com_content/helpers/route.php', + '/components/com_content/models/archive.php', + '/components/com_content/models/article.php', + '/components/com_content/models/articles.php', + '/components/com_content/models/categories.php', + '/components/com_content/models/category.php', + '/components/com_content/models/featured.php', + '/components/com_content/models/form.php', + '/components/com_content/models/forms/article.xml', + '/components/com_content/models/forms/filter_articles.xml', + '/components/com_content/router.php', + '/components/com_content/views/archive/tmpl/default.php', + '/components/com_content/views/archive/tmpl/default.xml', + '/components/com_content/views/archive/tmpl/default_items.php', + '/components/com_content/views/archive/view.html.php', + '/components/com_content/views/article/tmpl/default.php', + '/components/com_content/views/article/tmpl/default.xml', + '/components/com_content/views/article/tmpl/default_links.php', + '/components/com_content/views/article/view.html.php', + '/components/com_content/views/categories/tmpl/default.php', + '/components/com_content/views/categories/tmpl/default.xml', + '/components/com_content/views/categories/tmpl/default_items.php', + '/components/com_content/views/categories/view.html.php', + '/components/com_content/views/category/tmpl/blog.php', + '/components/com_content/views/category/tmpl/blog.xml', + '/components/com_content/views/category/tmpl/blog_children.php', + '/components/com_content/views/category/tmpl/blog_item.php', + '/components/com_content/views/category/tmpl/blog_links.php', + '/components/com_content/views/category/tmpl/default.php', + '/components/com_content/views/category/tmpl/default.xml', + '/components/com_content/views/category/tmpl/default_articles.php', + '/components/com_content/views/category/tmpl/default_children.php', + '/components/com_content/views/category/view.feed.php', + '/components/com_content/views/category/view.html.php', + '/components/com_content/views/featured/tmpl/default.php', + '/components/com_content/views/featured/tmpl/default.xml', + '/components/com_content/views/featured/tmpl/default_item.php', + '/components/com_content/views/featured/tmpl/default_links.php', + '/components/com_content/views/featured/view.feed.php', + '/components/com_content/views/featured/view.html.php', + '/components/com_content/views/form/tmpl/edit.php', + '/components/com_content/views/form/tmpl/edit.xml', + '/components/com_content/views/form/view.html.php', + '/components/com_contenthistory/contenthistory.php', + '/components/com_fields/controller.php', + '/components/com_fields/fields.php', + '/components/com_fields/models/forms/filter_fields.xml', + '/components/com_finder/controller.php', + '/components/com_finder/controllers/suggestions.json.php', + '/components/com_finder/finder.php', + '/components/com_finder/helpers/html/filter.php', + '/components/com_finder/helpers/html/query.php', + '/components/com_finder/models/search.php', + '/components/com_finder/models/suggestions.php', + '/components/com_finder/router.php', + '/components/com_finder/views/search/tmpl/default.php', + '/components/com_finder/views/search/tmpl/default.xml', + '/components/com_finder/views/search/tmpl/default_form.php', + '/components/com_finder/views/search/tmpl/default_result.php', + '/components/com_finder/views/search/tmpl/default_results.php', + '/components/com_finder/views/search/view.feed.php', + '/components/com_finder/views/search/view.html.php', + '/components/com_finder/views/search/view.opensearch.php', + '/components/com_mailto/controller.php', + '/components/com_mailto/helpers/mailto.php', + '/components/com_mailto/mailto.php', + '/components/com_mailto/mailto.xml', + '/components/com_mailto/models/forms/mailto.xml', + '/components/com_mailto/models/mailto.php', + '/components/com_mailto/views/mailto/tmpl/default.php', + '/components/com_mailto/views/mailto/view.html.php', + '/components/com_mailto/views/sent/tmpl/default.php', + '/components/com_mailto/views/sent/view.html.php', + '/components/com_media/media.php', + '/components/com_menus/controller.php', + '/components/com_menus/menus.php', + '/components/com_menus/models/forms/filter_items.xml', + '/components/com_modules/controller.php', + '/components/com_modules/models/forms/filter_modules.xml', + '/components/com_modules/modules.php', + '/components/com_newsfeeds/controller.php', + '/components/com_newsfeeds/helpers/association.php', + '/components/com_newsfeeds/helpers/category.php', + '/components/com_newsfeeds/helpers/legacyrouter.php', + '/components/com_newsfeeds/models/categories.php', + '/components/com_newsfeeds/models/category.php', + '/components/com_newsfeeds/models/newsfeed.php', + '/components/com_newsfeeds/newsfeeds.php', + '/components/com_newsfeeds/router.php', + '/components/com_newsfeeds/views/categories/tmpl/default.php', + '/components/com_newsfeeds/views/categories/tmpl/default.xml', + '/components/com_newsfeeds/views/categories/tmpl/default_items.php', + '/components/com_newsfeeds/views/categories/view.html.php', + '/components/com_newsfeeds/views/category/tmpl/default.php', + '/components/com_newsfeeds/views/category/tmpl/default.xml', + '/components/com_newsfeeds/views/category/tmpl/default_children.php', + '/components/com_newsfeeds/views/category/tmpl/default_items.php', + '/components/com_newsfeeds/views/category/view.html.php', + '/components/com_newsfeeds/views/newsfeed/tmpl/default.php', + '/components/com_newsfeeds/views/newsfeed/tmpl/default.xml', + '/components/com_newsfeeds/views/newsfeed/view.html.php', + '/components/com_privacy/controller.php', + '/components/com_privacy/controllers/request.php', + '/components/com_privacy/models/confirm.php', + '/components/com_privacy/models/forms/confirm.xml', + '/components/com_privacy/models/forms/remind.xml', + '/components/com_privacy/models/forms/request.xml', + '/components/com_privacy/models/remind.php', + '/components/com_privacy/models/request.php', + '/components/com_privacy/privacy.php', + '/components/com_privacy/router.php', + '/components/com_privacy/views/confirm/tmpl/default.php', + '/components/com_privacy/views/confirm/tmpl/default.xml', + '/components/com_privacy/views/confirm/view.html.php', + '/components/com_privacy/views/remind/tmpl/default.php', + '/components/com_privacy/views/remind/tmpl/default.xml', + '/components/com_privacy/views/remind/view.html.php', + '/components/com_privacy/views/request/tmpl/default.php', + '/components/com_privacy/views/request/tmpl/default.xml', + '/components/com_privacy/views/request/view.html.php', + '/components/com_tags/controller.php', + '/components/com_tags/controllers/tags.php', + '/components/com_tags/models/tag.php', + '/components/com_tags/models/tags.php', + '/components/com_tags/router.php', + '/components/com_tags/tags.php', + '/components/com_tags/views/tag/tmpl/default.php', + '/components/com_tags/views/tag/tmpl/default.xml', + '/components/com_tags/views/tag/tmpl/default_items.php', + '/components/com_tags/views/tag/tmpl/list.php', + '/components/com_tags/views/tag/tmpl/list.xml', + '/components/com_tags/views/tag/tmpl/list_items.php', + '/components/com_tags/views/tag/view.feed.php', + '/components/com_tags/views/tag/view.html.php', + '/components/com_tags/views/tags/tmpl/default.php', + '/components/com_tags/views/tags/tmpl/default.xml', + '/components/com_tags/views/tags/tmpl/default_items.php', + '/components/com_tags/views/tags/view.feed.php', + '/components/com_tags/views/tags/view.html.php', + '/components/com_users/controller.php', + '/components/com_users/controllers/profile.php', + '/components/com_users/controllers/registration.php', + '/components/com_users/controllers/remind.php', + '/components/com_users/controllers/reset.php', + '/components/com_users/controllers/user.php', + '/components/com_users/helpers/html/users.php', + '/components/com_users/helpers/legacyrouter.php', + '/components/com_users/helpers/route.php', + '/components/com_users/layouts/joomla/form/renderfield.php', + '/components/com_users/models/forms/frontend.xml', + '/components/com_users/models/forms/frontend_admin.xml', + '/components/com_users/models/forms/login.xml', + '/components/com_users/models/forms/profile.xml', + '/components/com_users/models/forms/registration.xml', + '/components/com_users/models/forms/remind.xml', + '/components/com_users/models/forms/reset_complete.xml', + '/components/com_users/models/forms/reset_confirm.xml', + '/components/com_users/models/forms/reset_request.xml', + '/components/com_users/models/forms/sitelang.xml', + '/components/com_users/models/login.php', + '/components/com_users/models/profile.php', + '/components/com_users/models/registration.php', + '/components/com_users/models/remind.php', + '/components/com_users/models/reset.php', + '/components/com_users/models/rules/loginuniquefield.php', + '/components/com_users/models/rules/logoutuniquefield.php', + '/components/com_users/router.php', + '/components/com_users/users.php', + '/components/com_users/views/login/tmpl/default.php', + '/components/com_users/views/login/tmpl/default.xml', + '/components/com_users/views/login/tmpl/default_login.php', + '/components/com_users/views/login/tmpl/default_logout.php', + '/components/com_users/views/login/tmpl/logout.xml', + '/components/com_users/views/login/view.html.php', + '/components/com_users/views/profile/tmpl/default.php', + '/components/com_users/views/profile/tmpl/default.xml', + '/components/com_users/views/profile/tmpl/default_core.php', + '/components/com_users/views/profile/tmpl/default_custom.php', + '/components/com_users/views/profile/tmpl/default_params.php', + '/components/com_users/views/profile/tmpl/edit.php', + '/components/com_users/views/profile/tmpl/edit.xml', + '/components/com_users/views/profile/view.html.php', + '/components/com_users/views/registration/tmpl/complete.php', + '/components/com_users/views/registration/tmpl/default.php', + '/components/com_users/views/registration/tmpl/default.xml', + '/components/com_users/views/registration/view.html.php', + '/components/com_users/views/remind/tmpl/default.php', + '/components/com_users/views/remind/tmpl/default.xml', + '/components/com_users/views/remind/view.html.php', + '/components/com_users/views/reset/tmpl/complete.php', + '/components/com_users/views/reset/tmpl/confirm.php', + '/components/com_users/views/reset/tmpl/default.php', + '/components/com_users/views/reset/tmpl/default.xml', + '/components/com_users/views/reset/view.html.php', + '/components/com_wrapper/controller.php', + '/components/com_wrapper/router.php', + '/components/com_wrapper/views/wrapper/tmpl/default.php', + '/components/com_wrapper/views/wrapper/tmpl/default.xml', + '/components/com_wrapper/views/wrapper/view.html.php', + '/components/com_wrapper/wrapper.php', + '/components/com_wrapper/wrapper.xml', + '/language/en-GB/en-GB.com_ajax.ini', + '/language/en-GB/en-GB.com_config.ini', + '/language/en-GB/en-GB.com_contact.ini', + '/language/en-GB/en-GB.com_content.ini', + '/language/en-GB/en-GB.com_finder.ini', + '/language/en-GB/en-GB.com_mailto.ini', + '/language/en-GB/en-GB.com_media.ini', + '/language/en-GB/en-GB.com_messages.ini', + '/language/en-GB/en-GB.com_newsfeeds.ini', + '/language/en-GB/en-GB.com_privacy.ini', + '/language/en-GB/en-GB.com_tags.ini', + '/language/en-GB/en-GB.com_users.ini', + '/language/en-GB/en-GB.com_weblinks.ini', + '/language/en-GB/en-GB.com_wrapper.ini', + '/language/en-GB/en-GB.files_joomla.sys.ini', + '/language/en-GB/en-GB.finder_cli.ini', + '/language/en-GB/en-GB.ini', + '/language/en-GB/en-GB.lib_fof.ini', + '/language/en-GB/en-GB.lib_fof.sys.ini', + '/language/en-GB/en-GB.lib_idna_convert.sys.ini', + '/language/en-GB/en-GB.lib_joomla.ini', + '/language/en-GB/en-GB.lib_joomla.sys.ini', + '/language/en-GB/en-GB.lib_phpass.sys.ini', + '/language/en-GB/en-GB.lib_phputf8.sys.ini', + '/language/en-GB/en-GB.lib_simplepie.sys.ini', + '/language/en-GB/en-GB.localise.php', + '/language/en-GB/en-GB.mod_articles_archive.ini', + '/language/en-GB/en-GB.mod_articles_archive.sys.ini', + '/language/en-GB/en-GB.mod_articles_categories.ini', + '/language/en-GB/en-GB.mod_articles_categories.sys.ini', + '/language/en-GB/en-GB.mod_articles_category.ini', + '/language/en-GB/en-GB.mod_articles_category.sys.ini', + '/language/en-GB/en-GB.mod_articles_latest.ini', + '/language/en-GB/en-GB.mod_articles_latest.sys.ini', + '/language/en-GB/en-GB.mod_articles_news.ini', + '/language/en-GB/en-GB.mod_articles_news.sys.ini', + '/language/en-GB/en-GB.mod_articles_popular.ini', + '/language/en-GB/en-GB.mod_articles_popular.sys.ini', + '/language/en-GB/en-GB.mod_banners.ini', + '/language/en-GB/en-GB.mod_banners.sys.ini', + '/language/en-GB/en-GB.mod_breadcrumbs.ini', + '/language/en-GB/en-GB.mod_breadcrumbs.sys.ini', + '/language/en-GB/en-GB.mod_custom.ini', + '/language/en-GB/en-GB.mod_custom.sys.ini', + '/language/en-GB/en-GB.mod_feed.ini', + '/language/en-GB/en-GB.mod_feed.sys.ini', + '/language/en-GB/en-GB.mod_finder.ini', + '/language/en-GB/en-GB.mod_finder.sys.ini', + '/language/en-GB/en-GB.mod_footer.ini', + '/language/en-GB/en-GB.mod_footer.sys.ini', + '/language/en-GB/en-GB.mod_languages.ini', + '/language/en-GB/en-GB.mod_languages.sys.ini', + '/language/en-GB/en-GB.mod_login.ini', + '/language/en-GB/en-GB.mod_login.sys.ini', + '/language/en-GB/en-GB.mod_menu.ini', + '/language/en-GB/en-GB.mod_menu.sys.ini', + '/language/en-GB/en-GB.mod_random_image.ini', + '/language/en-GB/en-GB.mod_random_image.sys.ini', + '/language/en-GB/en-GB.mod_related_items.ini', + '/language/en-GB/en-GB.mod_related_items.sys.ini', + '/language/en-GB/en-GB.mod_stats.ini', + '/language/en-GB/en-GB.mod_stats.sys.ini', + '/language/en-GB/en-GB.mod_syndicate.ini', + '/language/en-GB/en-GB.mod_syndicate.sys.ini', + '/language/en-GB/en-GB.mod_tags_popular.ini', + '/language/en-GB/en-GB.mod_tags_popular.sys.ini', + '/language/en-GB/en-GB.mod_tags_similar.ini', + '/language/en-GB/en-GB.mod_tags_similar.sys.ini', + '/language/en-GB/en-GB.mod_users_latest.ini', + '/language/en-GB/en-GB.mod_users_latest.sys.ini', + '/language/en-GB/en-GB.mod_weblinks.ini', + '/language/en-GB/en-GB.mod_weblinks.sys.ini', + '/language/en-GB/en-GB.mod_whosonline.ini', + '/language/en-GB/en-GB.mod_whosonline.sys.ini', + '/language/en-GB/en-GB.mod_wrapper.ini', + '/language/en-GB/en-GB.mod_wrapper.sys.ini', + '/language/en-GB/en-GB.tpl_beez3.ini', + '/language/en-GB/en-GB.tpl_beez3.sys.ini', + '/language/en-GB/en-GB.tpl_protostar.ini', + '/language/en-GB/en-GB.tpl_protostar.sys.ini', + '/language/en-GB/en-GB.xml', + '/layouts/joomla/content/blog_style_default_links.php', + '/layouts/joomla/content/icons/email.php', + '/layouts/joomla/content/icons/print_popup.php', + '/layouts/joomla/content/icons/print_screen.php', + '/layouts/joomla/content/info_block/block.php', + '/layouts/joomla/edit/details.php', + '/layouts/joomla/edit/item_title.php', + '/layouts/joomla/form/field/radio.php', + '/layouts/joomla/html/formbehavior/ajaxchosen.php', + '/layouts/joomla/html/formbehavior/chosen.php', + '/layouts/joomla/html/sortablelist.php', + '/layouts/joomla/html/tag.php', + '/layouts/joomla/modal/body.php', + '/layouts/joomla/modal/footer.php', + '/layouts/joomla/modal/header.php', + '/layouts/joomla/modal/iframe.php', + '/layouts/joomla/modal/main.php', + '/layouts/joomla/sidebars/toggle.php', + '/layouts/joomla/tinymce/buttons.php', + '/layouts/joomla/tinymce/buttons/button.php', + '/layouts/joomla/toolbar/confirm.php', + '/layouts/joomla/toolbar/help.php', + '/layouts/joomla/toolbar/modal.php', + '/layouts/joomla/toolbar/slider.php', + '/layouts/libraries/cms/html/bootstrap/addtab.php', + '/layouts/libraries/cms/html/bootstrap/addtabscript.php', + '/layouts/libraries/cms/html/bootstrap/endtab.php', + '/layouts/libraries/cms/html/bootstrap/endtabset.php', + '/layouts/libraries/cms/html/bootstrap/starttabset.php', + '/layouts/libraries/cms/html/bootstrap/starttabsetscript.php', + '/libraries/cms/class/loader.php', + '/libraries/cms/html/access.php', + '/libraries/cms/html/actionsdropdown.php', + '/libraries/cms/html/adminlanguage.php', + '/libraries/cms/html/batch.php', + '/libraries/cms/html/behavior.php', + '/libraries/cms/html/bootstrap.php', + '/libraries/cms/html/category.php', + '/libraries/cms/html/content.php', + '/libraries/cms/html/contentlanguage.php', + '/libraries/cms/html/date.php', + '/libraries/cms/html/debug.php', + '/libraries/cms/html/dropdown.php', + '/libraries/cms/html/email.php', + '/libraries/cms/html/form.php', + '/libraries/cms/html/formbehavior.php', + '/libraries/cms/html/grid.php', + '/libraries/cms/html/icons.php', + '/libraries/cms/html/jgrid.php', + '/libraries/cms/html/jquery.php', + '/libraries/cms/html/language/en-GB/en-GB.jhtmldate.ini', + '/libraries/cms/html/links.php', + '/libraries/cms/html/list.php', + '/libraries/cms/html/menu.php', + '/libraries/cms/html/number.php', + '/libraries/cms/html/rules.php', + '/libraries/cms/html/searchtools.php', + '/libraries/cms/html/select.php', + '/libraries/cms/html/sidebar.php', + '/libraries/cms/html/sliders.php', + '/libraries/cms/html/sortablelist.php', + '/libraries/cms/html/string.php', + '/libraries/cms/html/tabs.php', + '/libraries/cms/html/tag.php', + '/libraries/cms/html/tel.php', + '/libraries/cms/html/user.php', + '/libraries/cms/less/formatter/joomla.php', + '/libraries/cms/less/less.php', + '/libraries/fof/LICENSE.txt', + '/libraries/fof/autoloader/component.php', + '/libraries/fof/autoloader/fof.php', + '/libraries/fof/config/domain/dispatcher.php', + '/libraries/fof/config/domain/interface.php', + '/libraries/fof/config/domain/tables.php', + '/libraries/fof/config/domain/views.php', + '/libraries/fof/config/provider.php', + '/libraries/fof/controller/controller.php', + '/libraries/fof/database/database.php', + '/libraries/fof/database/driver.php', + '/libraries/fof/database/driver/joomla.php', + '/libraries/fof/database/driver/mysql.php', + '/libraries/fof/database/driver/mysqli.php', + '/libraries/fof/database/driver/oracle.php', + '/libraries/fof/database/driver/pdo.php', + '/libraries/fof/database/driver/pdomysql.php', + '/libraries/fof/database/driver/postgresql.php', + '/libraries/fof/database/driver/sqlazure.php', + '/libraries/fof/database/driver/sqlite.php', + '/libraries/fof/database/driver/sqlsrv.php', + '/libraries/fof/database/factory.php', + '/libraries/fof/database/installer.php', + '/libraries/fof/database/interface.php', + '/libraries/fof/database/iterator.php', + '/libraries/fof/database/iterator/azure.php', + '/libraries/fof/database/iterator/mysql.php', + '/libraries/fof/database/iterator/mysqli.php', + '/libraries/fof/database/iterator/oracle.php', + '/libraries/fof/database/iterator/pdo.php', + '/libraries/fof/database/iterator/pdomysql.php', + '/libraries/fof/database/iterator/postgresql.php', + '/libraries/fof/database/iterator/sqlite.php', + '/libraries/fof/database/iterator/sqlsrv.php', + '/libraries/fof/database/query.php', + '/libraries/fof/database/query/element.php', + '/libraries/fof/database/query/limitable.php', + '/libraries/fof/database/query/mysql.php', + '/libraries/fof/database/query/mysqli.php', + '/libraries/fof/database/query/oracle.php', + '/libraries/fof/database/query/pdo.php', + '/libraries/fof/database/query/pdomysql.php', + '/libraries/fof/database/query/postgresql.php', + '/libraries/fof/database/query/preparable.php', + '/libraries/fof/database/query/sqlazure.php', + '/libraries/fof/database/query/sqlite.php', + '/libraries/fof/database/query/sqlsrv.php', + '/libraries/fof/dispatcher/dispatcher.php', + '/libraries/fof/download/adapter/abstract.php', + '/libraries/fof/download/adapter/cacert.pem', + '/libraries/fof/download/adapter/curl.php', + '/libraries/fof/download/adapter/fopen.php', + '/libraries/fof/download/download.php', + '/libraries/fof/download/interface.php', + '/libraries/fof/encrypt/aes.php', + '/libraries/fof/encrypt/aes/abstract.php', + '/libraries/fof/encrypt/aes/interface.php', + '/libraries/fof/encrypt/aes/mcrypt.php', + '/libraries/fof/encrypt/aes/openssl.php', + '/libraries/fof/encrypt/base32.php', + '/libraries/fof/encrypt/randval.php', + '/libraries/fof/encrypt/randvalinterface.php', + '/libraries/fof/encrypt/totp.php', + '/libraries/fof/form/field.php', + '/libraries/fof/form/field/accesslevel.php', + '/libraries/fof/form/field/actions.php', + '/libraries/fof/form/field/button.php', + '/libraries/fof/form/field/cachehandler.php', + '/libraries/fof/form/field/calendar.php', + '/libraries/fof/form/field/captcha.php', + '/libraries/fof/form/field/checkbox.php', + '/libraries/fof/form/field/checkboxes.php', + '/libraries/fof/form/field/components.php', + '/libraries/fof/form/field/editor.php', + '/libraries/fof/form/field/email.php', + '/libraries/fof/form/field/groupedbutton.php', + '/libraries/fof/form/field/groupedlist.php', + '/libraries/fof/form/field/hidden.php', + '/libraries/fof/form/field/image.php', + '/libraries/fof/form/field/imagelist.php', + '/libraries/fof/form/field/integer.php', + '/libraries/fof/form/field/language.php', + '/libraries/fof/form/field/list.php', + '/libraries/fof/form/field/media.php', + '/libraries/fof/form/field/model.php', + '/libraries/fof/form/field/ordering.php', + '/libraries/fof/form/field/password.php', + '/libraries/fof/form/field/plugins.php', + '/libraries/fof/form/field/published.php', + '/libraries/fof/form/field/radio.php', + '/libraries/fof/form/field/relation.php', + '/libraries/fof/form/field/rules.php', + '/libraries/fof/form/field/selectrow.php', + '/libraries/fof/form/field/sessionhandler.php', + '/libraries/fof/form/field/spacer.php', + '/libraries/fof/form/field/sql.php', + '/libraries/fof/form/field/tag.php', + '/libraries/fof/form/field/tel.php', + '/libraries/fof/form/field/text.php', + '/libraries/fof/form/field/textarea.php', + '/libraries/fof/form/field/timezone.php', + '/libraries/fof/form/field/title.php', + '/libraries/fof/form/field/url.php', + '/libraries/fof/form/field/user.php', + '/libraries/fof/form/field/usergroup.php', + '/libraries/fof/form/form.php', + '/libraries/fof/form/header.php', + '/libraries/fof/form/header/accesslevel.php', + '/libraries/fof/form/header/field.php', + '/libraries/fof/form/header/fielddate.php', + '/libraries/fof/form/header/fieldfilterable.php', + '/libraries/fof/form/header/fieldsearchable.php', + '/libraries/fof/form/header/fieldselectable.php', + '/libraries/fof/form/header/fieldsql.php', + '/libraries/fof/form/header/filterdate.php', + '/libraries/fof/form/header/filterfilterable.php', + '/libraries/fof/form/header/filtersearchable.php', + '/libraries/fof/form/header/filterselectable.php', + '/libraries/fof/form/header/filtersql.php', + '/libraries/fof/form/header/language.php', + '/libraries/fof/form/header/model.php', + '/libraries/fof/form/header/ordering.php', + '/libraries/fof/form/header/published.php', + '/libraries/fof/form/header/rowselect.php', + '/libraries/fof/form/helper.php', + '/libraries/fof/hal/document.php', + '/libraries/fof/hal/link.php', + '/libraries/fof/hal/links.php', + '/libraries/fof/hal/render/interface.php', + '/libraries/fof/hal/render/json.php', + '/libraries/fof/include.php', + '/libraries/fof/inflector/inflector.php', + '/libraries/fof/input/input.php', + '/libraries/fof/input/jinput/cli.php', + '/libraries/fof/input/jinput/cookie.php', + '/libraries/fof/input/jinput/files.php', + '/libraries/fof/input/jinput/input.php', + '/libraries/fof/input/jinput/json.php', + '/libraries/fof/integration/joomla/filesystem/filesystem.php', + '/libraries/fof/integration/joomla/platform.php', + '/libraries/fof/layout/file.php', + '/libraries/fof/layout/helper.php', + '/libraries/fof/less/formatter/classic.php', + '/libraries/fof/less/formatter/compressed.php', + '/libraries/fof/less/formatter/joomla.php', + '/libraries/fof/less/formatter/lessjs.php', + '/libraries/fof/less/less.php', + '/libraries/fof/less/parser/parser.php', + '/libraries/fof/model/behavior.php', + '/libraries/fof/model/behavior/access.php', + '/libraries/fof/model/behavior/emptynonzero.php', + '/libraries/fof/model/behavior/enabled.php', + '/libraries/fof/model/behavior/filters.php', + '/libraries/fof/model/behavior/language.php', + '/libraries/fof/model/behavior/private.php', + '/libraries/fof/model/dispatcher/behavior.php', + '/libraries/fof/model/field.php', + '/libraries/fof/model/field/boolean.php', + '/libraries/fof/model/field/date.php', + '/libraries/fof/model/field/number.php', + '/libraries/fof/model/field/text.php', + '/libraries/fof/model/model.php', + '/libraries/fof/platform/filesystem/filesystem.php', + '/libraries/fof/platform/filesystem/interface.php', + '/libraries/fof/platform/interface.php', + '/libraries/fof/platform/platform.php', + '/libraries/fof/query/abstract.php', + '/libraries/fof/render/abstract.php', + '/libraries/fof/render/joomla.php', + '/libraries/fof/render/joomla3.php', + '/libraries/fof/render/strapper.php', + '/libraries/fof/string/utils.php', + '/libraries/fof/table/behavior.php', + '/libraries/fof/table/behavior/assets.php', + '/libraries/fof/table/behavior/contenthistory.php', + '/libraries/fof/table/behavior/tags.php', + '/libraries/fof/table/dispatcher/behavior.php', + '/libraries/fof/table/nested.php', + '/libraries/fof/table/relations.php', + '/libraries/fof/table/table.php', + '/libraries/fof/template/utils.php', + '/libraries/fof/toolbar/toolbar.php', + '/libraries/fof/utils/array/array.php', + '/libraries/fof/utils/cache/cleaner.php', + '/libraries/fof/utils/config/helper.php', + '/libraries/fof/utils/filescheck/filescheck.php', + '/libraries/fof/utils/ini/parser.php', + '/libraries/fof/utils/installscript/installscript.php', + '/libraries/fof/utils/ip/ip.php', + '/libraries/fof/utils/object/object.php', + '/libraries/fof/utils/observable/dispatcher.php', + '/libraries/fof/utils/observable/event.php', + '/libraries/fof/utils/phpfunc/phpfunc.php', + '/libraries/fof/utils/timer/timer.php', + '/libraries/fof/utils/update/collection.php', + '/libraries/fof/utils/update/extension.php', + '/libraries/fof/utils/update/joomla.php', + '/libraries/fof/utils/update/update.php', + '/libraries/fof/version.txt', + '/libraries/fof/view/csv.php', + '/libraries/fof/view/form.php', + '/libraries/fof/view/html.php', + '/libraries/fof/view/json.php', + '/libraries/fof/view/raw.php', + '/libraries/fof/view/view.php', + '/libraries/idna_convert/LICENCE', + '/libraries/idna_convert/ReadMe.txt', + '/libraries/idna_convert/idna_convert.class.php', + '/libraries/idna_convert/transcode_wrapper.php', + '/libraries/idna_convert/uctc.php', + '/libraries/joomla/application/web/router.php', + '/libraries/joomla/application/web/router/base.php', + '/libraries/joomla/application/web/router/rest.php', + '/libraries/joomla/archive/archive.php', + '/libraries/joomla/archive/bzip2.php', + '/libraries/joomla/archive/extractable.php', + '/libraries/joomla/archive/gzip.php', + '/libraries/joomla/archive/tar.php', + '/libraries/joomla/archive/wrapper/archive.php', + '/libraries/joomla/archive/zip.php', + '/libraries/joomla/controller/base.php', + '/libraries/joomla/controller/controller.php', + '/libraries/joomla/database/database.php', + '/libraries/joomla/database/driver.php', + '/libraries/joomla/database/driver/mysql.php', + '/libraries/joomla/database/driver/mysqli.php', + '/libraries/joomla/database/driver/oracle.php', + '/libraries/joomla/database/driver/pdo.php', + '/libraries/joomla/database/driver/pdomysql.php', + '/libraries/joomla/database/driver/pgsql.php', + '/libraries/joomla/database/driver/postgresql.php', + '/libraries/joomla/database/driver/sqlazure.php', + '/libraries/joomla/database/driver/sqlite.php', + '/libraries/joomla/database/driver/sqlsrv.php', + '/libraries/joomla/database/exception/connecting.php', + '/libraries/joomla/database/exception/executing.php', + '/libraries/joomla/database/exception/unsupported.php', + '/libraries/joomla/database/exporter.php', + '/libraries/joomla/database/exporter/mysql.php', + '/libraries/joomla/database/exporter/mysqli.php', + '/libraries/joomla/database/exporter/pdomysql.php', + '/libraries/joomla/database/exporter/pgsql.php', + '/libraries/joomla/database/exporter/postgresql.php', + '/libraries/joomla/database/factory.php', + '/libraries/joomla/database/importer.php', + '/libraries/joomla/database/importer/mysql.php', + '/libraries/joomla/database/importer/mysqli.php', + '/libraries/joomla/database/importer/pdomysql.php', + '/libraries/joomla/database/importer/pgsql.php', + '/libraries/joomla/database/importer/postgresql.php', + '/libraries/joomla/database/interface.php', + '/libraries/joomla/database/iterator.php', + '/libraries/joomla/database/iterator/mysql.php', + '/libraries/joomla/database/iterator/mysqli.php', + '/libraries/joomla/database/iterator/oracle.php', + '/libraries/joomla/database/iterator/pdo.php', + '/libraries/joomla/database/iterator/pdomysql.php', + '/libraries/joomla/database/iterator/pgsql.php', + '/libraries/joomla/database/iterator/postgresql.php', + '/libraries/joomla/database/iterator/sqlazure.php', + '/libraries/joomla/database/iterator/sqlite.php', + '/libraries/joomla/database/iterator/sqlsrv.php', + '/libraries/joomla/database/query.php', + '/libraries/joomla/database/query/element.php', + '/libraries/joomla/database/query/limitable.php', + '/libraries/joomla/database/query/mysql.php', + '/libraries/joomla/database/query/mysqli.php', + '/libraries/joomla/database/query/oracle.php', + '/libraries/joomla/database/query/pdo.php', + '/libraries/joomla/database/query/pdomysql.php', + '/libraries/joomla/database/query/pgsql.php', + '/libraries/joomla/database/query/postgresql.php', + '/libraries/joomla/database/query/preparable.php', + '/libraries/joomla/database/query/sqlazure.php', + '/libraries/joomla/database/query/sqlite.php', + '/libraries/joomla/database/query/sqlsrv.php', + '/libraries/joomla/event/dispatcher.php', + '/libraries/joomla/event/event.php', + '/libraries/joomla/facebook/album.php', + '/libraries/joomla/facebook/checkin.php', + '/libraries/joomla/facebook/comment.php', + '/libraries/joomla/facebook/event.php', + '/libraries/joomla/facebook/facebook.php', + '/libraries/joomla/facebook/group.php', + '/libraries/joomla/facebook/link.php', + '/libraries/joomla/facebook/note.php', + '/libraries/joomla/facebook/oauth.php', + '/libraries/joomla/facebook/object.php', + '/libraries/joomla/facebook/photo.php', + '/libraries/joomla/facebook/post.php', + '/libraries/joomla/facebook/status.php', + '/libraries/joomla/facebook/user.php', + '/libraries/joomla/facebook/video.php', + '/libraries/joomla/form/fields/accesslevel.php', + '/libraries/joomla/form/fields/aliastag.php', + '/libraries/joomla/form/fields/cachehandler.php', + '/libraries/joomla/form/fields/calendar.php', + '/libraries/joomla/form/fields/checkbox.php', + '/libraries/joomla/form/fields/checkboxes.php', + '/libraries/joomla/form/fields/color.php', + '/libraries/joomla/form/fields/combo.php', + '/libraries/joomla/form/fields/components.php', + '/libraries/joomla/form/fields/databaseconnection.php', + '/libraries/joomla/form/fields/email.php', + '/libraries/joomla/form/fields/file.php', + '/libraries/joomla/form/fields/filelist.php', + '/libraries/joomla/form/fields/folderlist.php', + '/libraries/joomla/form/fields/groupedlist.php', + '/libraries/joomla/form/fields/hidden.php', + '/libraries/joomla/form/fields/imagelist.php', + '/libraries/joomla/form/fields/integer.php', + '/libraries/joomla/form/fields/language.php', + '/libraries/joomla/form/fields/list.php', + '/libraries/joomla/form/fields/meter.php', + '/libraries/joomla/form/fields/note.php', + '/libraries/joomla/form/fields/number.php', + '/libraries/joomla/form/fields/password.php', + '/libraries/joomla/form/fields/plugins.php', + '/libraries/joomla/form/fields/predefinedlist.php', + '/libraries/joomla/form/fields/radio.php', + '/libraries/joomla/form/fields/range.php', + '/libraries/joomla/form/fields/repeatable.php', + '/libraries/joomla/form/fields/rules.php', + '/libraries/joomla/form/fields/sessionhandler.php', + '/libraries/joomla/form/fields/spacer.php', + '/libraries/joomla/form/fields/sql.php', + '/libraries/joomla/form/fields/subform.php', + '/libraries/joomla/form/fields/tel.php', + '/libraries/joomla/form/fields/text.php', + '/libraries/joomla/form/fields/textarea.php', + '/libraries/joomla/form/fields/timezone.php', + '/libraries/joomla/form/fields/url.php', + '/libraries/joomla/form/fields/usergroup.php', + '/libraries/joomla/github/account.php', + '/libraries/joomla/github/commits.php', + '/libraries/joomla/github/forks.php', + '/libraries/joomla/github/github.php', + '/libraries/joomla/github/hooks.php', + '/libraries/joomla/github/http.php', + '/libraries/joomla/github/meta.php', + '/libraries/joomla/github/milestones.php', + '/libraries/joomla/github/object.php', + '/libraries/joomla/github/package.php', + '/libraries/joomla/github/package/activity.php', + '/libraries/joomla/github/package/activity/events.php', + '/libraries/joomla/github/package/activity/notifications.php', + '/libraries/joomla/github/package/activity/starring.php', + '/libraries/joomla/github/package/activity/watching.php', + '/libraries/joomla/github/package/authorization.php', + '/libraries/joomla/github/package/data.php', + '/libraries/joomla/github/package/data/blobs.php', + '/libraries/joomla/github/package/data/commits.php', + '/libraries/joomla/github/package/data/refs.php', + '/libraries/joomla/github/package/data/tags.php', + '/libraries/joomla/github/package/data/trees.php', + '/libraries/joomla/github/package/gists.php', + '/libraries/joomla/github/package/gists/comments.php', + '/libraries/joomla/github/package/gitignore.php', + '/libraries/joomla/github/package/issues.php', + '/libraries/joomla/github/package/issues/assignees.php', + '/libraries/joomla/github/package/issues/comments.php', + '/libraries/joomla/github/package/issues/events.php', + '/libraries/joomla/github/package/issues/labels.php', + '/libraries/joomla/github/package/issues/milestones.php', + '/libraries/joomla/github/package/markdown.php', + '/libraries/joomla/github/package/orgs.php', + '/libraries/joomla/github/package/orgs/members.php', + '/libraries/joomla/github/package/orgs/teams.php', + '/libraries/joomla/github/package/pulls.php', + '/libraries/joomla/github/package/pulls/comments.php', + '/libraries/joomla/github/package/repositories.php', + '/libraries/joomla/github/package/repositories/collaborators.php', + '/libraries/joomla/github/package/repositories/comments.php', + '/libraries/joomla/github/package/repositories/commits.php', + '/libraries/joomla/github/package/repositories/contents.php', + '/libraries/joomla/github/package/repositories/downloads.php', + '/libraries/joomla/github/package/repositories/forks.php', + '/libraries/joomla/github/package/repositories/hooks.php', + '/libraries/joomla/github/package/repositories/keys.php', + '/libraries/joomla/github/package/repositories/merging.php', + '/libraries/joomla/github/package/repositories/statistics.php', + '/libraries/joomla/github/package/repositories/statuses.php', + '/libraries/joomla/github/package/search.php', + '/libraries/joomla/github/package/users.php', + '/libraries/joomla/github/package/users/emails.php', + '/libraries/joomla/github/package/users/followers.php', + '/libraries/joomla/github/package/users/keys.php', + '/libraries/joomla/github/refs.php', + '/libraries/joomla/github/statuses.php', + '/libraries/joomla/google/auth.php', + '/libraries/joomla/google/auth/oauth2.php', + '/libraries/joomla/google/data.php', + '/libraries/joomla/google/data/adsense.php', + '/libraries/joomla/google/data/calendar.php', + '/libraries/joomla/google/data/picasa.php', + '/libraries/joomla/google/data/picasa/album.php', + '/libraries/joomla/google/data/picasa/photo.php', + '/libraries/joomla/google/data/plus.php', + '/libraries/joomla/google/data/plus/activities.php', + '/libraries/joomla/google/data/plus/comments.php', + '/libraries/joomla/google/data/plus/people.php', + '/libraries/joomla/google/embed.php', + '/libraries/joomla/google/embed/analytics.php', + '/libraries/joomla/google/embed/maps.php', + '/libraries/joomla/google/google.php', + '/libraries/joomla/grid/grid.php', + '/libraries/joomla/keychain/keychain.php', + '/libraries/joomla/linkedin/communications.php', + '/libraries/joomla/linkedin/companies.php', + '/libraries/joomla/linkedin/groups.php', + '/libraries/joomla/linkedin/jobs.php', + '/libraries/joomla/linkedin/linkedin.php', + '/libraries/joomla/linkedin/oauth.php', + '/libraries/joomla/linkedin/object.php', + '/libraries/joomla/linkedin/people.php', + '/libraries/joomla/linkedin/stream.php', + '/libraries/joomla/mediawiki/categories.php', + '/libraries/joomla/mediawiki/http.php', + '/libraries/joomla/mediawiki/images.php', + '/libraries/joomla/mediawiki/links.php', + '/libraries/joomla/mediawiki/mediawiki.php', + '/libraries/joomla/mediawiki/object.php', + '/libraries/joomla/mediawiki/pages.php', + '/libraries/joomla/mediawiki/search.php', + '/libraries/joomla/mediawiki/sites.php', + '/libraries/joomla/mediawiki/users.php', + '/libraries/joomla/model/base.php', + '/libraries/joomla/model/database.php', + '/libraries/joomla/model/model.php', + '/libraries/joomla/oauth1/client.php', + '/libraries/joomla/oauth2/client.php', + '/libraries/joomla/observable/interface.php', + '/libraries/joomla/observer/interface.php', + '/libraries/joomla/observer/mapper.php', + '/libraries/joomla/observer/updater.php', + '/libraries/joomla/observer/updater/interface.php', + '/libraries/joomla/observer/wrapper/mapper.php', + '/libraries/joomla/openstreetmap/changesets.php', + '/libraries/joomla/openstreetmap/elements.php', + '/libraries/joomla/openstreetmap/gps.php', + '/libraries/joomla/openstreetmap/info.php', + '/libraries/joomla/openstreetmap/oauth.php', + '/libraries/joomla/openstreetmap/object.php', + '/libraries/joomla/openstreetmap/openstreetmap.php', + '/libraries/joomla/openstreetmap/user.php', + '/libraries/joomla/platform.php', + '/libraries/joomla/route/wrapper/route.php', + '/libraries/joomla/session/handler/interface.php', + '/libraries/joomla/session/handler/joomla.php', + '/libraries/joomla/session/handler/native.php', + '/libraries/joomla/session/storage.php', + '/libraries/joomla/session/storage/apc.php', + '/libraries/joomla/session/storage/apcu.php', + '/libraries/joomla/session/storage/database.php', + '/libraries/joomla/session/storage/memcache.php', + '/libraries/joomla/session/storage/memcached.php', + '/libraries/joomla/session/storage/none.php', + '/libraries/joomla/session/storage/redis.php', + '/libraries/joomla/session/storage/wincache.php', + '/libraries/joomla/session/storage/xcache.php', + '/libraries/joomla/string/string.php', + '/libraries/joomla/string/wrapper/normalise.php', + '/libraries/joomla/string/wrapper/punycode.php', + '/libraries/joomla/twitter/block.php', + '/libraries/joomla/twitter/directmessages.php', + '/libraries/joomla/twitter/favorites.php', + '/libraries/joomla/twitter/friends.php', + '/libraries/joomla/twitter/help.php', + '/libraries/joomla/twitter/lists.php', + '/libraries/joomla/twitter/oauth.php', + '/libraries/joomla/twitter/object.php', + '/libraries/joomla/twitter/places.php', + '/libraries/joomla/twitter/profile.php', + '/libraries/joomla/twitter/search.php', + '/libraries/joomla/twitter/statuses.php', + '/libraries/joomla/twitter/trends.php', + '/libraries/joomla/twitter/twitter.php', + '/libraries/joomla/twitter/users.php', + '/libraries/joomla/utilities/arrayhelper.php', + '/libraries/joomla/view/base.php', + '/libraries/joomla/view/html.php', + '/libraries/joomla/view/view.php', + '/libraries/legacy/application/application.php', + '/libraries/legacy/base/node.php', + '/libraries/legacy/base/observable.php', + '/libraries/legacy/base/observer.php', + '/libraries/legacy/base/tree.php', + '/libraries/legacy/database/exception.php', + '/libraries/legacy/database/mysql.php', + '/libraries/legacy/database/mysqli.php', + '/libraries/legacy/database/sqlazure.php', + '/libraries/legacy/database/sqlsrv.php', + '/libraries/legacy/dispatcher/dispatcher.php', + '/libraries/legacy/error/error.php', + '/libraries/legacy/exception/exception.php', + '/libraries/legacy/form/field/category.php', + '/libraries/legacy/form/field/componentlayout.php', + '/libraries/legacy/form/field/modulelayout.php', + '/libraries/legacy/log/logexception.php', + '/libraries/legacy/request/request.php', + '/libraries/legacy/response/response.php', + '/libraries/legacy/simplecrypt/simplecrypt.php', + '/libraries/legacy/simplepie/factory.php', + '/libraries/legacy/table/session.php', + '/libraries/legacy/utilities/xmlelement.php', + '/libraries/phputf8/LICENSE', + '/libraries/phputf8/README', + '/libraries/phputf8/mbstring/core.php', + '/libraries/phputf8/native/core.php', + '/libraries/phputf8/ord.php', + '/libraries/phputf8/str_ireplace.php', + '/libraries/phputf8/str_pad.php', + '/libraries/phputf8/str_split.php', + '/libraries/phputf8/strcasecmp.php', + '/libraries/phputf8/strcspn.php', + '/libraries/phputf8/stristr.php', + '/libraries/phputf8/strrev.php', + '/libraries/phputf8/strspn.php', + '/libraries/phputf8/substr_replace.php', + '/libraries/phputf8/trim.php', + '/libraries/phputf8/ucfirst.php', + '/libraries/phputf8/ucwords.php', + '/libraries/phputf8/utf8.php', + '/libraries/phputf8/utils/ascii.php', + '/libraries/phputf8/utils/bad.php', + '/libraries/phputf8/utils/patterns.php', + '/libraries/phputf8/utils/position.php', + '/libraries/phputf8/utils/specials.php', + '/libraries/phputf8/utils/unicode.php', + '/libraries/phputf8/utils/validation.php', + '/libraries/src/Access/Wrapper/Access.php', + '/libraries/src/Cache/Storage/ApcStorage.php', + '/libraries/src/Cache/Storage/CacheliteStorage.php', + '/libraries/src/Cache/Storage/MemcacheStorage.php', + '/libraries/src/Cache/Storage/XcacheStorage.php', + '/libraries/src/Client/ClientWrapper.php', + '/libraries/src/Crypt/Cipher/BlowfishCipher.php', + '/libraries/src/Crypt/Cipher/McryptCipher.php', + '/libraries/src/Crypt/Cipher/Rijndael256Cipher.php', + '/libraries/src/Crypt/Cipher/SimpleCipher.php', + '/libraries/src/Crypt/Cipher/TripleDesCipher.php', + '/libraries/src/Crypt/CipherInterface.php', + '/libraries/src/Crypt/CryptPassword.php', + '/libraries/src/Crypt/Key.php', + '/libraries/src/Crypt/Password/SimpleCryptPassword.php', + '/libraries/src/Crypt/README.md', + '/libraries/src/Filesystem/Wrapper/FileWrapper.php', + '/libraries/src/Filesystem/Wrapper/FolderWrapper.php', + '/libraries/src/Filesystem/Wrapper/PathWrapper.php', + '/libraries/src/Filter/Wrapper/OutputFilterWrapper.php', + '/libraries/src/Form/Field/HelpsiteField.php', + '/libraries/src/Form/FormWrapper.php', + '/libraries/src/Helper/ContentHistoryHelper.php', + '/libraries/src/Helper/SearchHelper.php', + '/libraries/src/Http/Transport/cacert.pem', + '/libraries/src/Http/Wrapper/FactoryWrapper.php', + '/libraries/src/Language/LanguageStemmer.php', + '/libraries/src/Language/Stemmer/Porteren.php', + '/libraries/src/Language/Wrapper/JTextWrapper.php', + '/libraries/src/Language/Wrapper/LanguageHelperWrapper.php', + '/libraries/src/Language/Wrapper/TransliterateWrapper.php', + '/libraries/src/Mail/MailWrapper.php', + '/libraries/src/Menu/MenuHelper.php', + '/libraries/src/Menu/Node.php', + '/libraries/src/Menu/Node/Component.php', + '/libraries/src/Menu/Node/Container.php', + '/libraries/src/Menu/Node/Heading.php', + '/libraries/src/Menu/Node/Separator.php', + '/libraries/src/Menu/Node/Url.php', + '/libraries/src/Menu/Tree.php', + '/libraries/src/Table/Observer/AbstractObserver.php', + '/libraries/src/Table/Observer/ContentHistory.php', + '/libraries/src/Table/Observer/Tags.php', + '/libraries/src/Toolbar/Button/SliderButton.php', + '/libraries/src/User/UserWrapper.php', + '/libraries/vendor/.htaccess', + '/libraries/vendor/brumann/polyfill-unserialize/LICENSE', + '/libraries/vendor/brumann/polyfill-unserialize/composer.json', + '/libraries/vendor/brumann/polyfill-unserialize/src/DisallowedClassesSubstitutor.php', + '/libraries/vendor/brumann/polyfill-unserialize/src/Unserialize.php', + '/libraries/vendor/ircmaxell/password-compat/LICENSE.md', + '/libraries/vendor/ircmaxell/password-compat/lib/password.php', + '/libraries/vendor/joomla/application/src/AbstractCliApplication.php', + '/libraries/vendor/joomla/application/src/AbstractDaemonApplication.php', + '/libraries/vendor/joomla/application/src/Cli/CliInput.php', + '/libraries/vendor/joomla/application/src/Cli/CliOutput.php', + '/libraries/vendor/joomla/application/src/Cli/ColorProcessor.php', + '/libraries/vendor/joomla/application/src/Cli/ColorStyle.php', + '/libraries/vendor/joomla/application/src/Cli/Output/Processor/ColorProcessor.php', + '/libraries/vendor/joomla/application/src/Cli/Output/Processor/ProcessorInterface.php', + '/libraries/vendor/joomla/application/src/Cli/Output/Stdout.php', + '/libraries/vendor/joomla/application/src/Cli/Output/Xml.php', + '/libraries/vendor/joomla/compat/LICENSE', + '/libraries/vendor/joomla/compat/src/CallbackFilterIterator.php', + '/libraries/vendor/joomla/compat/src/JsonSerializable.php', + '/libraries/vendor/joomla/event/src/DelegatingDispatcher.php', + '/libraries/vendor/joomla/filesystem/src/Stream/String.php', + '/libraries/vendor/joomla/image/LICENSE', + '/libraries/vendor/joomla/image/src/Filter/Backgroundfill.php', + '/libraries/vendor/joomla/image/src/Filter/Brightness.php', + '/libraries/vendor/joomla/image/src/Filter/Contrast.php', + '/libraries/vendor/joomla/image/src/Filter/Edgedetect.php', + '/libraries/vendor/joomla/image/src/Filter/Emboss.php', + '/libraries/vendor/joomla/image/src/Filter/Grayscale.php', + '/libraries/vendor/joomla/image/src/Filter/Negate.php', + '/libraries/vendor/joomla/image/src/Filter/Sketchy.php', + '/libraries/vendor/joomla/image/src/Filter/Smooth.php', + '/libraries/vendor/joomla/image/src/Image.php', + '/libraries/vendor/joomla/image/src/ImageFilter.php', + '/libraries/vendor/joomla/input/src/Cli.php', + '/libraries/vendor/joomla/registry/src/AbstractRegistryFormat.php', + '/libraries/vendor/joomla/session/Joomla/Session/LICENSE', + '/libraries/vendor/joomla/session/Joomla/Session/Session.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Apc.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Apcu.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Database.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Memcache.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Memcached.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/None.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Wincache.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Xcache.php', + '/libraries/vendor/joomla/string/src/String.php', + '/libraries/vendor/leafo/lessphp/LICENSE', + '/libraries/vendor/leafo/lessphp/lessc.inc.php', + '/libraries/vendor/leafo/lessphp/lessify', + '/libraries/vendor/leafo/lessphp/lessify.inc.php', + '/libraries/vendor/leafo/lessphp/plessc', + '/libraries/vendor/paragonie/random_compat/LICENSE', + '/libraries/vendor/paragonie/random_compat/lib/byte_safe_strings.php', + '/libraries/vendor/paragonie/random_compat/lib/cast_to_int.php', + '/libraries/vendor/paragonie/random_compat/lib/error_polyfill.php', + '/libraries/vendor/paragonie/random_compat/lib/random.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_com_dotnet.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_dev_urandom.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_libsodium.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_libsodium_legacy.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_mcrypt.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_openssl.php', + '/libraries/vendor/paragonie/random_compat/lib/random_int.php', + '/libraries/vendor/paragonie/sodium_compat/src/Core32/Curve25519/README.md', + '/libraries/vendor/phpmailer/phpmailer/PHPMailerAutoload.php', + '/libraries/vendor/phpmailer/phpmailer/class.phpmailer.php', + '/libraries/vendor/phpmailer/phpmailer/class.phpmaileroauth.php', + '/libraries/vendor/phpmailer/phpmailer/class.phpmaileroauthgoogle.php', + '/libraries/vendor/phpmailer/phpmailer/class.pop3.php', + '/libraries/vendor/phpmailer/phpmailer/class.smtp.php', + '/libraries/vendor/phpmailer/phpmailer/extras/EasyPeasyICS.php', + '/libraries/vendor/phpmailer/phpmailer/extras/htmlfilter.php', + '/libraries/vendor/phpmailer/phpmailer/extras/ntlm_sasl_client.php', + '/libraries/vendor/simplepie/simplepie/LICENSE.txt', + '/libraries/vendor/simplepie/simplepie/autoloader.php', + '/libraries/vendor/simplepie/simplepie/db.sql', + '/libraries/vendor/simplepie/simplepie/idn/LICENCE', + '/libraries/vendor/simplepie/simplepie/idn/idna_convert.class.php', + '/libraries/vendor/simplepie/simplepie/idn/npdata.ser', + '/libraries/vendor/simplepie/simplepie/library/SimplePie.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Author.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/Base.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/DB.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/File.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/Memcache.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/MySQL.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Caption.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Category.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content/Type/Sniffer.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Copyright.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Core.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Credit.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML/Entities.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Enclosure.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Exception.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/File.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/HTTP/Parser.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/IRI.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Item.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Locator.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Misc.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Net/IPv6.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parse/Date.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parser.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Rating.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Registry.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Restriction.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Sanitize.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Source.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration/Parser.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/gzdecode.php', + '/libraries/vendor/symfony/polyfill-php55/LICENSE', + '/libraries/vendor/symfony/polyfill-php55/Php55.php', + '/libraries/vendor/symfony/polyfill-php55/Php55ArrayColumn.php', + '/libraries/vendor/symfony/polyfill-php55/bootstrap.php', + '/libraries/vendor/symfony/polyfill-php56/LICENSE', + '/libraries/vendor/symfony/polyfill-php56/Php56.php', + '/libraries/vendor/symfony/polyfill-php56/bootstrap.php', + '/libraries/vendor/symfony/polyfill-php71/LICENSE', + '/libraries/vendor/symfony/polyfill-php71/Php71.php', + '/libraries/vendor/symfony/polyfill-php71/bootstrap.php', + '/libraries/vendor/symfony/polyfill-util/Binary.php', + '/libraries/vendor/symfony/polyfill-util/BinaryNoFuncOverload.php', + '/libraries/vendor/symfony/polyfill-util/BinaryOnFuncOverload.php', + '/libraries/vendor/symfony/polyfill-util/LICENSE', + '/libraries/vendor/typo3/phar-stream-wrapper/composer.json', + '/libraries/vendor/web.config', + '/media/cms/css/debug.css', + '/media/com_associations/js/sidebyside-uncompressed.js', + '/media/com_contenthistory/css/jquery.pretty-text-diff.css', + '/media/com_contenthistory/js/diff_match_patch.js', + '/media/com_contenthistory/js/jquery.pretty-text-diff.js', + '/media/com_contenthistory/js/jquery.pretty-text-diff.min.js', + '/media/com_finder/js/autocompleter.js', + '/media/com_joomlaupdate/js/encryption.js', + '/media/com_joomlaupdate/js/encryption.min.js', + '/media/com_joomlaupdate/js/json2.js', + '/media/com_joomlaupdate/js/json2.min.js', + '/media/com_joomlaupdate/js/update.js', + '/media/com_joomlaupdate/js/update.min.js', + '/media/contacts/images/con_address.png', + '/media/contacts/images/con_fax.png', + '/media/contacts/images/con_info.png', + '/media/contacts/images/con_mobile.png', + '/media/contacts/images/con_tel.png', + '/media/contacts/images/emailButton.png', + '/media/editors/codemirror/LICENSE', + '/media/editors/codemirror/addon/comment/comment.js', + '/media/editors/codemirror/addon/comment/comment.min.js', + '/media/editors/codemirror/addon/comment/continuecomment.js', + '/media/editors/codemirror/addon/comment/continuecomment.min.js', + '/media/editors/codemirror/addon/dialog/dialog.css', + '/media/editors/codemirror/addon/dialog/dialog.js', + '/media/editors/codemirror/addon/dialog/dialog.min.css', + '/media/editors/codemirror/addon/dialog/dialog.min.js', + '/media/editors/codemirror/addon/display/autorefresh.js', + '/media/editors/codemirror/addon/display/autorefresh.min.js', + '/media/editors/codemirror/addon/display/fullscreen.css', + '/media/editors/codemirror/addon/display/fullscreen.js', + '/media/editors/codemirror/addon/display/fullscreen.min.css', + '/media/editors/codemirror/addon/display/fullscreen.min.js', + '/media/editors/codemirror/addon/display/panel.js', + '/media/editors/codemirror/addon/display/panel.min.js', + '/media/editors/codemirror/addon/display/placeholder.js', + '/media/editors/codemirror/addon/display/placeholder.min.js', + '/media/editors/codemirror/addon/display/rulers.js', + '/media/editors/codemirror/addon/display/rulers.min.js', + '/media/editors/codemirror/addon/edit/closebrackets.js', + '/media/editors/codemirror/addon/edit/closebrackets.min.js', + '/media/editors/codemirror/addon/edit/closetag.js', + '/media/editors/codemirror/addon/edit/closetag.min.js', + '/media/editors/codemirror/addon/edit/continuelist.js', + '/media/editors/codemirror/addon/edit/continuelist.min.js', + '/media/editors/codemirror/addon/edit/matchbrackets.js', + '/media/editors/codemirror/addon/edit/matchbrackets.min.js', + '/media/editors/codemirror/addon/edit/matchtags.js', + '/media/editors/codemirror/addon/edit/matchtags.min.js', + '/media/editors/codemirror/addon/edit/trailingspace.js', + '/media/editors/codemirror/addon/edit/trailingspace.min.js', + '/media/editors/codemirror/addon/fold/brace-fold.js', + '/media/editors/codemirror/addon/fold/brace-fold.min.js', + '/media/editors/codemirror/addon/fold/comment-fold.js', + '/media/editors/codemirror/addon/fold/comment-fold.min.js', + '/media/editors/codemirror/addon/fold/foldcode.js', + '/media/editors/codemirror/addon/fold/foldcode.min.js', + '/media/editors/codemirror/addon/fold/foldgutter.css', + '/media/editors/codemirror/addon/fold/foldgutter.js', + '/media/editors/codemirror/addon/fold/foldgutter.min.css', + '/media/editors/codemirror/addon/fold/foldgutter.min.js', + '/media/editors/codemirror/addon/fold/indent-fold.js', + '/media/editors/codemirror/addon/fold/indent-fold.min.js', + '/media/editors/codemirror/addon/fold/markdown-fold.js', + '/media/editors/codemirror/addon/fold/markdown-fold.min.js', + '/media/editors/codemirror/addon/fold/xml-fold.js', + '/media/editors/codemirror/addon/fold/xml-fold.min.js', + '/media/editors/codemirror/addon/hint/anyword-hint.js', + '/media/editors/codemirror/addon/hint/anyword-hint.min.js', + '/media/editors/codemirror/addon/hint/css-hint.js', + '/media/editors/codemirror/addon/hint/css-hint.min.js', + '/media/editors/codemirror/addon/hint/html-hint.js', + '/media/editors/codemirror/addon/hint/html-hint.min.js', + '/media/editors/codemirror/addon/hint/javascript-hint.js', + '/media/editors/codemirror/addon/hint/javascript-hint.min.js', + '/media/editors/codemirror/addon/hint/show-hint.css', + '/media/editors/codemirror/addon/hint/show-hint.js', + '/media/editors/codemirror/addon/hint/show-hint.min.css', + '/media/editors/codemirror/addon/hint/show-hint.min.js', + '/media/editors/codemirror/addon/hint/sql-hint.js', + '/media/editors/codemirror/addon/hint/sql-hint.min.js', + '/media/editors/codemirror/addon/hint/xml-hint.js', + '/media/editors/codemirror/addon/hint/xml-hint.min.js', + '/media/editors/codemirror/addon/lint/coffeescript-lint.js', + '/media/editors/codemirror/addon/lint/coffeescript-lint.min.js', + '/media/editors/codemirror/addon/lint/css-lint.js', + '/media/editors/codemirror/addon/lint/css-lint.min.js', + '/media/editors/codemirror/addon/lint/html-lint.js', + '/media/editors/codemirror/addon/lint/html-lint.min.js', + '/media/editors/codemirror/addon/lint/javascript-lint.js', + '/media/editors/codemirror/addon/lint/javascript-lint.min.js', + '/media/editors/codemirror/addon/lint/json-lint.js', + '/media/editors/codemirror/addon/lint/json-lint.min.js', + '/media/editors/codemirror/addon/lint/lint.css', + '/media/editors/codemirror/addon/lint/lint.js', + '/media/editors/codemirror/addon/lint/lint.min.css', + '/media/editors/codemirror/addon/lint/lint.min.js', + '/media/editors/codemirror/addon/lint/yaml-lint.js', + '/media/editors/codemirror/addon/lint/yaml-lint.min.js', + '/media/editors/codemirror/addon/merge/merge.css', + '/media/editors/codemirror/addon/merge/merge.js', + '/media/editors/codemirror/addon/merge/merge.min.css', + '/media/editors/codemirror/addon/merge/merge.min.js', + '/media/editors/codemirror/addon/mode/loadmode.js', + '/media/editors/codemirror/addon/mode/loadmode.min.js', + '/media/editors/codemirror/addon/mode/multiplex.js', + '/media/editors/codemirror/addon/mode/multiplex.min.js', + '/media/editors/codemirror/addon/mode/multiplex_test.js', + '/media/editors/codemirror/addon/mode/multiplex_test.min.js', + '/media/editors/codemirror/addon/mode/overlay.js', + '/media/editors/codemirror/addon/mode/overlay.min.js', + '/media/editors/codemirror/addon/mode/simple.js', + '/media/editors/codemirror/addon/mode/simple.min.js', + '/media/editors/codemirror/addon/runmode/colorize.js', + '/media/editors/codemirror/addon/runmode/colorize.min.js', + '/media/editors/codemirror/addon/runmode/runmode-standalone.js', + '/media/editors/codemirror/addon/runmode/runmode-standalone.min.js', + '/media/editors/codemirror/addon/runmode/runmode.js', + '/media/editors/codemirror/addon/runmode/runmode.min.js', + '/media/editors/codemirror/addon/runmode/runmode.node.js', + '/media/editors/codemirror/addon/scroll/annotatescrollbar.js', + '/media/editors/codemirror/addon/scroll/annotatescrollbar.min.js', + '/media/editors/codemirror/addon/scroll/scrollpastend.js', + '/media/editors/codemirror/addon/scroll/scrollpastend.min.js', + '/media/editors/codemirror/addon/scroll/simplescrollbars.css', + '/media/editors/codemirror/addon/scroll/simplescrollbars.js', + '/media/editors/codemirror/addon/scroll/simplescrollbars.min.css', + '/media/editors/codemirror/addon/scroll/simplescrollbars.min.js', + '/media/editors/codemirror/addon/search/jump-to-line.js', + '/media/editors/codemirror/addon/search/jump-to-line.min.js', + '/media/editors/codemirror/addon/search/match-highlighter.js', + '/media/editors/codemirror/addon/search/match-highlighter.min.js', + '/media/editors/codemirror/addon/search/matchesonscrollbar.css', + '/media/editors/codemirror/addon/search/matchesonscrollbar.js', + '/media/editors/codemirror/addon/search/matchesonscrollbar.min.css', + '/media/editors/codemirror/addon/search/matchesonscrollbar.min.js', + '/media/editors/codemirror/addon/search/search.js', + '/media/editors/codemirror/addon/search/search.min.js', + '/media/editors/codemirror/addon/search/searchcursor.js', + '/media/editors/codemirror/addon/search/searchcursor.min.js', + '/media/editors/codemirror/addon/selection/active-line.js', + '/media/editors/codemirror/addon/selection/active-line.min.js', + '/media/editors/codemirror/addon/selection/mark-selection.js', + '/media/editors/codemirror/addon/selection/mark-selection.min.js', + '/media/editors/codemirror/addon/selection/selection-pointer.js', + '/media/editors/codemirror/addon/selection/selection-pointer.min.js', + '/media/editors/codemirror/addon/tern/tern.css', + '/media/editors/codemirror/addon/tern/tern.js', + '/media/editors/codemirror/addon/tern/tern.min.css', + '/media/editors/codemirror/addon/tern/tern.min.js', + '/media/editors/codemirror/addon/tern/worker.js', + '/media/editors/codemirror/addon/tern/worker.min.js', + '/media/editors/codemirror/addon/wrap/hardwrap.js', + '/media/editors/codemirror/addon/wrap/hardwrap.min.js', + '/media/editors/codemirror/keymap/emacs.js', + '/media/editors/codemirror/keymap/emacs.min.js', + '/media/editors/codemirror/keymap/sublime.js', + '/media/editors/codemirror/keymap/sublime.min.js', + '/media/editors/codemirror/keymap/vim.js', + '/media/editors/codemirror/keymap/vim.min.js', + '/media/editors/codemirror/lib/addons.css', + '/media/editors/codemirror/lib/addons.js', + '/media/editors/codemirror/lib/addons.min.css', + '/media/editors/codemirror/lib/addons.min.js', + '/media/editors/codemirror/lib/codemirror.css', + '/media/editors/codemirror/lib/codemirror.js', + '/media/editors/codemirror/lib/codemirror.min.css', + '/media/editors/codemirror/lib/codemirror.min.js', + '/media/editors/codemirror/mode/apl/apl.js', + '/media/editors/codemirror/mode/apl/apl.min.js', + '/media/editors/codemirror/mode/asciiarmor/asciiarmor.js', + '/media/editors/codemirror/mode/asciiarmor/asciiarmor.min.js', + '/media/editors/codemirror/mode/asn.1/asn.1.js', + '/media/editors/codemirror/mode/asn.1/asn.min.js', + '/media/editors/codemirror/mode/asterisk/asterisk.js', + '/media/editors/codemirror/mode/asterisk/asterisk.min.js', + '/media/editors/codemirror/mode/brainfuck/brainfuck.js', + '/media/editors/codemirror/mode/brainfuck/brainfuck.min.js', + '/media/editors/codemirror/mode/clike/clike.js', + '/media/editors/codemirror/mode/clike/clike.min.js', + '/media/editors/codemirror/mode/clojure/clojure.js', + '/media/editors/codemirror/mode/clojure/clojure.min.js', + '/media/editors/codemirror/mode/cmake/cmake.js', + '/media/editors/codemirror/mode/cmake/cmake.min.js', + '/media/editors/codemirror/mode/cobol/cobol.js', + '/media/editors/codemirror/mode/cobol/cobol.min.js', + '/media/editors/codemirror/mode/coffeescript/coffeescript.js', + '/media/editors/codemirror/mode/coffeescript/coffeescript.min.js', + '/media/editors/codemirror/mode/commonlisp/commonlisp.js', + '/media/editors/codemirror/mode/commonlisp/commonlisp.min.js', + '/media/editors/codemirror/mode/crystal/crystal.js', + '/media/editors/codemirror/mode/crystal/crystal.min.js', + '/media/editors/codemirror/mode/css/css.js', + '/media/editors/codemirror/mode/css/css.min.js', + '/media/editors/codemirror/mode/cypher/cypher.js', + '/media/editors/codemirror/mode/cypher/cypher.min.js', + '/media/editors/codemirror/mode/d/d.js', + '/media/editors/codemirror/mode/d/d.min.js', + '/media/editors/codemirror/mode/dart/dart.js', + '/media/editors/codemirror/mode/dart/dart.min.js', + '/media/editors/codemirror/mode/diff/diff.js', + '/media/editors/codemirror/mode/diff/diff.min.js', + '/media/editors/codemirror/mode/django/django.js', + '/media/editors/codemirror/mode/django/django.min.js', + '/media/editors/codemirror/mode/dockerfile/dockerfile.js', + '/media/editors/codemirror/mode/dockerfile/dockerfile.min.js', + '/media/editors/codemirror/mode/dtd/dtd.js', + '/media/editors/codemirror/mode/dtd/dtd.min.js', + '/media/editors/codemirror/mode/dylan/dylan.js', + '/media/editors/codemirror/mode/dylan/dylan.min.js', + '/media/editors/codemirror/mode/ebnf/ebnf.js', + '/media/editors/codemirror/mode/ebnf/ebnf.min.js', + '/media/editors/codemirror/mode/ecl/ecl.js', + '/media/editors/codemirror/mode/ecl/ecl.min.js', + '/media/editors/codemirror/mode/eiffel/eiffel.js', + '/media/editors/codemirror/mode/eiffel/eiffel.min.js', + '/media/editors/codemirror/mode/elm/elm.js', + '/media/editors/codemirror/mode/elm/elm.min.js', + '/media/editors/codemirror/mode/erlang/erlang.js', + '/media/editors/codemirror/mode/erlang/erlang.min.js', + '/media/editors/codemirror/mode/factor/factor.js', + '/media/editors/codemirror/mode/factor/factor.min.js', + '/media/editors/codemirror/mode/fcl/fcl.js', + '/media/editors/codemirror/mode/fcl/fcl.min.js', + '/media/editors/codemirror/mode/forth/forth.js', + '/media/editors/codemirror/mode/forth/forth.min.js', + '/media/editors/codemirror/mode/fortran/fortran.js', + '/media/editors/codemirror/mode/fortran/fortran.min.js', + '/media/editors/codemirror/mode/gas/gas.js', + '/media/editors/codemirror/mode/gas/gas.min.js', + '/media/editors/codemirror/mode/gfm/gfm.js', + '/media/editors/codemirror/mode/gfm/gfm.min.js', + '/media/editors/codemirror/mode/gherkin/gherkin.js', + '/media/editors/codemirror/mode/gherkin/gherkin.min.js', + '/media/editors/codemirror/mode/go/go.js', + '/media/editors/codemirror/mode/go/go.min.js', + '/media/editors/codemirror/mode/groovy/groovy.js', + '/media/editors/codemirror/mode/groovy/groovy.min.js', + '/media/editors/codemirror/mode/haml/haml.js', + '/media/editors/codemirror/mode/haml/haml.min.js', + '/media/editors/codemirror/mode/handlebars/handlebars.js', + '/media/editors/codemirror/mode/handlebars/handlebars.min.js', + '/media/editors/codemirror/mode/haskell-literate/haskell-literate.js', + '/media/editors/codemirror/mode/haskell-literate/haskell-literate.min.js', + '/media/editors/codemirror/mode/haskell/haskell.js', + '/media/editors/codemirror/mode/haskell/haskell.min.js', + '/media/editors/codemirror/mode/haxe/haxe.js', + '/media/editors/codemirror/mode/haxe/haxe.min.js', + '/media/editors/codemirror/mode/htmlembedded/htmlembedded.js', + '/media/editors/codemirror/mode/htmlembedded/htmlembedded.min.js', + '/media/editors/codemirror/mode/htmlmixed/htmlmixed.js', + '/media/editors/codemirror/mode/htmlmixed/htmlmixed.min.js', + '/media/editors/codemirror/mode/http/http.js', + '/media/editors/codemirror/mode/http/http.min.js', + '/media/editors/codemirror/mode/idl/idl.js', + '/media/editors/codemirror/mode/idl/idl.min.js', + '/media/editors/codemirror/mode/javascript/javascript.js', + '/media/editors/codemirror/mode/javascript/javascript.min.js', + '/media/editors/codemirror/mode/jinja2/jinja2.js', + '/media/editors/codemirror/mode/jinja2/jinja2.min.js', + '/media/editors/codemirror/mode/jsx/jsx.js', + '/media/editors/codemirror/mode/jsx/jsx.min.js', + '/media/editors/codemirror/mode/julia/julia.js', + '/media/editors/codemirror/mode/julia/julia.min.js', + '/media/editors/codemirror/mode/livescript/livescript.js', + '/media/editors/codemirror/mode/livescript/livescript.min.js', + '/media/editors/codemirror/mode/lua/lua.js', + '/media/editors/codemirror/mode/lua/lua.min.js', + '/media/editors/codemirror/mode/markdown/markdown.js', + '/media/editors/codemirror/mode/markdown/markdown.min.js', + '/media/editors/codemirror/mode/mathematica/mathematica.js', + '/media/editors/codemirror/mode/mathematica/mathematica.min.js', + '/media/editors/codemirror/mode/mbox/mbox.js', + '/media/editors/codemirror/mode/mbox/mbox.min.js', + '/media/editors/codemirror/mode/meta.js', + '/media/editors/codemirror/mode/meta.min.js', + '/media/editors/codemirror/mode/mirc/mirc.js', + '/media/editors/codemirror/mode/mirc/mirc.min.js', + '/media/editors/codemirror/mode/mllike/mllike.js', + '/media/editors/codemirror/mode/mllike/mllike.min.js', + '/media/editors/codemirror/mode/modelica/modelica.js', + '/media/editors/codemirror/mode/modelica/modelica.min.js', + '/media/editors/codemirror/mode/mscgen/mscgen.js', + '/media/editors/codemirror/mode/mscgen/mscgen.min.js', + '/media/editors/codemirror/mode/mumps/mumps.js', + '/media/editors/codemirror/mode/mumps/mumps.min.js', + '/media/editors/codemirror/mode/nginx/nginx.js', + '/media/editors/codemirror/mode/nginx/nginx.min.js', + '/media/editors/codemirror/mode/nsis/nsis.js', + '/media/editors/codemirror/mode/nsis/nsis.min.js', + '/media/editors/codemirror/mode/ntriples/ntriples.js', + '/media/editors/codemirror/mode/ntriples/ntriples.min.js', + '/media/editors/codemirror/mode/octave/octave.js', + '/media/editors/codemirror/mode/octave/octave.min.js', + '/media/editors/codemirror/mode/oz/oz.js', + '/media/editors/codemirror/mode/oz/oz.min.js', + '/media/editors/codemirror/mode/pascal/pascal.js', + '/media/editors/codemirror/mode/pascal/pascal.min.js', + '/media/editors/codemirror/mode/pegjs/pegjs.js', + '/media/editors/codemirror/mode/pegjs/pegjs.min.js', + '/media/editors/codemirror/mode/perl/perl.js', + '/media/editors/codemirror/mode/perl/perl.min.js', + '/media/editors/codemirror/mode/php/php.js', + '/media/editors/codemirror/mode/php/php.min.js', + '/media/editors/codemirror/mode/pig/pig.js', + '/media/editors/codemirror/mode/pig/pig.min.js', + '/media/editors/codemirror/mode/powershell/powershell.js', + '/media/editors/codemirror/mode/powershell/powershell.min.js', + '/media/editors/codemirror/mode/properties/properties.js', + '/media/editors/codemirror/mode/properties/properties.min.js', + '/media/editors/codemirror/mode/protobuf/protobuf.js', + '/media/editors/codemirror/mode/protobuf/protobuf.min.js', + '/media/editors/codemirror/mode/pug/pug.js', + '/media/editors/codemirror/mode/pug/pug.min.js', + '/media/editors/codemirror/mode/puppet/puppet.js', + '/media/editors/codemirror/mode/puppet/puppet.min.js', + '/media/editors/codemirror/mode/python/python.js', + '/media/editors/codemirror/mode/python/python.min.js', + '/media/editors/codemirror/mode/q/q.js', + '/media/editors/codemirror/mode/q/q.min.js', + '/media/editors/codemirror/mode/r/r.js', + '/media/editors/codemirror/mode/r/r.min.js', + '/media/editors/codemirror/mode/rpm/changes/index.html', + '/media/editors/codemirror/mode/rpm/rpm.js', + '/media/editors/codemirror/mode/rpm/rpm.min.js', + '/media/editors/codemirror/mode/rst/rst.js', + '/media/editors/codemirror/mode/rst/rst.min.js', + '/media/editors/codemirror/mode/ruby/ruby.js', + '/media/editors/codemirror/mode/ruby/ruby.min.js', + '/media/editors/codemirror/mode/rust/rust.js', + '/media/editors/codemirror/mode/rust/rust.min.js', + '/media/editors/codemirror/mode/sas/sas.js', + '/media/editors/codemirror/mode/sas/sas.min.js', + '/media/editors/codemirror/mode/sass/sass.js', + '/media/editors/codemirror/mode/sass/sass.min.js', + '/media/editors/codemirror/mode/scheme/scheme.js', + '/media/editors/codemirror/mode/scheme/scheme.min.js', + '/media/editors/codemirror/mode/shell/shell.js', + '/media/editors/codemirror/mode/shell/shell.min.js', + '/media/editors/codemirror/mode/sieve/sieve.js', + '/media/editors/codemirror/mode/sieve/sieve.min.js', + '/media/editors/codemirror/mode/slim/slim.js', + '/media/editors/codemirror/mode/slim/slim.min.js', + '/media/editors/codemirror/mode/smalltalk/smalltalk.js', + '/media/editors/codemirror/mode/smalltalk/smalltalk.min.js', + '/media/editors/codemirror/mode/smarty/smarty.js', + '/media/editors/codemirror/mode/smarty/smarty.min.js', + '/media/editors/codemirror/mode/solr/solr.js', + '/media/editors/codemirror/mode/solr/solr.min.js', + '/media/editors/codemirror/mode/soy/soy.js', + '/media/editors/codemirror/mode/soy/soy.min.js', + '/media/editors/codemirror/mode/sparql/sparql.js', + '/media/editors/codemirror/mode/sparql/sparql.min.js', + '/media/editors/codemirror/mode/spreadsheet/spreadsheet.js', + '/media/editors/codemirror/mode/spreadsheet/spreadsheet.min.js', + '/media/editors/codemirror/mode/sql/sql.js', + '/media/editors/codemirror/mode/sql/sql.min.js', + '/media/editors/codemirror/mode/stex/stex.js', + '/media/editors/codemirror/mode/stex/stex.min.js', + '/media/editors/codemirror/mode/stylus/stylus.js', + '/media/editors/codemirror/mode/stylus/stylus.min.js', + '/media/editors/codemirror/mode/swift/swift.js', + '/media/editors/codemirror/mode/swift/swift.min.js', + '/media/editors/codemirror/mode/tcl/tcl.js', + '/media/editors/codemirror/mode/tcl/tcl.min.js', + '/media/editors/codemirror/mode/textile/textile.js', + '/media/editors/codemirror/mode/textile/textile.min.js', + '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.css', + '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.js', + '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.min.css', + '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.min.js', + '/media/editors/codemirror/mode/tiki/tiki.css', + '/media/editors/codemirror/mode/tiki/tiki.js', + '/media/editors/codemirror/mode/tiki/tiki.min.css', + '/media/editors/codemirror/mode/tiki/tiki.min.js', + '/media/editors/codemirror/mode/toml/toml.js', + '/media/editors/codemirror/mode/toml/toml.min.js', + '/media/editors/codemirror/mode/tornado/tornado.js', + '/media/editors/codemirror/mode/tornado/tornado.min.js', + '/media/editors/codemirror/mode/troff/troff.js', + '/media/editors/codemirror/mode/troff/troff.min.js', + '/media/editors/codemirror/mode/ttcn-cfg/ttcn-cfg.js', + '/media/editors/codemirror/mode/ttcn-cfg/ttcn-cfg.min.js', + '/media/editors/codemirror/mode/ttcn/ttcn.js', + '/media/editors/codemirror/mode/ttcn/ttcn.min.js', + '/media/editors/codemirror/mode/turtle/turtle.js', + '/media/editors/codemirror/mode/turtle/turtle.min.js', + '/media/editors/codemirror/mode/twig/twig.js', + '/media/editors/codemirror/mode/twig/twig.min.js', + '/media/editors/codemirror/mode/vb/vb.js', + '/media/editors/codemirror/mode/vb/vb.min.js', + '/media/editors/codemirror/mode/vbscript/vbscript.js', + '/media/editors/codemirror/mode/vbscript/vbscript.min.js', + '/media/editors/codemirror/mode/velocity/velocity.js', + '/media/editors/codemirror/mode/velocity/velocity.min.js', + '/media/editors/codemirror/mode/verilog/verilog.js', + '/media/editors/codemirror/mode/verilog/verilog.min.js', + '/media/editors/codemirror/mode/vhdl/vhdl.js', + '/media/editors/codemirror/mode/vhdl/vhdl.min.js', + '/media/editors/codemirror/mode/vue/vue.js', + '/media/editors/codemirror/mode/vue/vue.min.js', + '/media/editors/codemirror/mode/wast/wast.js', + '/media/editors/codemirror/mode/wast/wast.min.js', + '/media/editors/codemirror/mode/webidl/webidl.js', + '/media/editors/codemirror/mode/webidl/webidl.min.js', + '/media/editors/codemirror/mode/xml/xml.js', + '/media/editors/codemirror/mode/xml/xml.min.js', + '/media/editors/codemirror/mode/xquery/xquery.js', + '/media/editors/codemirror/mode/xquery/xquery.min.js', + '/media/editors/codemirror/mode/yacas/yacas.js', + '/media/editors/codemirror/mode/yacas/yacas.min.js', + '/media/editors/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js', + '/media/editors/codemirror/mode/yaml-frontmatter/yaml-frontmatter.min.js', + '/media/editors/codemirror/mode/yaml/yaml.js', + '/media/editors/codemirror/mode/yaml/yaml.min.js', + '/media/editors/codemirror/mode/z80/z80.js', + '/media/editors/codemirror/mode/z80/z80.min.js', + '/media/editors/codemirror/theme/3024-day.css', + '/media/editors/codemirror/theme/3024-night.css', + '/media/editors/codemirror/theme/abcdef.css', + '/media/editors/codemirror/theme/ambiance-mobile.css', + '/media/editors/codemirror/theme/ambiance.css', + '/media/editors/codemirror/theme/ayu-dark.css', + '/media/editors/codemirror/theme/ayu-mirage.css', + '/media/editors/codemirror/theme/base16-dark.css', + '/media/editors/codemirror/theme/base16-light.css', + '/media/editors/codemirror/theme/bespin.css', + '/media/editors/codemirror/theme/blackboard.css', + '/media/editors/codemirror/theme/cobalt.css', + '/media/editors/codemirror/theme/colorforth.css', + '/media/editors/codemirror/theme/darcula.css', + '/media/editors/codemirror/theme/dracula.css', + '/media/editors/codemirror/theme/duotone-dark.css', + '/media/editors/codemirror/theme/duotone-light.css', + '/media/editors/codemirror/theme/eclipse.css', + '/media/editors/codemirror/theme/elegant.css', + '/media/editors/codemirror/theme/erlang-dark.css', + '/media/editors/codemirror/theme/gruvbox-dark.css', + '/media/editors/codemirror/theme/hopscotch.css', + '/media/editors/codemirror/theme/icecoder.css', + '/media/editors/codemirror/theme/idea.css', + '/media/editors/codemirror/theme/isotope.css', + '/media/editors/codemirror/theme/lesser-dark.css', + '/media/editors/codemirror/theme/liquibyte.css', + '/media/editors/codemirror/theme/lucario.css', + '/media/editors/codemirror/theme/material-darker.css', + '/media/editors/codemirror/theme/material-ocean.css', + '/media/editors/codemirror/theme/material-palenight.css', + '/media/editors/codemirror/theme/material.css', + '/media/editors/codemirror/theme/mbo.css', + '/media/editors/codemirror/theme/mdn-like.css', + '/media/editors/codemirror/theme/midnight.css', + '/media/editors/codemirror/theme/monokai.css', + '/media/editors/codemirror/theme/moxer.css', + '/media/editors/codemirror/theme/neat.css', + '/media/editors/codemirror/theme/neo.css', + '/media/editors/codemirror/theme/night.css', + '/media/editors/codemirror/theme/nord.css', + '/media/editors/codemirror/theme/oceanic-next.css', + '/media/editors/codemirror/theme/panda-syntax.css', + '/media/editors/codemirror/theme/paraiso-dark.css', + '/media/editors/codemirror/theme/paraiso-light.css', + '/media/editors/codemirror/theme/pastel-on-dark.css', + '/media/editors/codemirror/theme/railscasts.css', + '/media/editors/codemirror/theme/rubyblue.css', + '/media/editors/codemirror/theme/seti.css', + '/media/editors/codemirror/theme/shadowfox.css', + '/media/editors/codemirror/theme/solarized.css', + '/media/editors/codemirror/theme/ssms.css', + '/media/editors/codemirror/theme/the-matrix.css', + '/media/editors/codemirror/theme/tomorrow-night-bright.css', + '/media/editors/codemirror/theme/tomorrow-night-eighties.css', + '/media/editors/codemirror/theme/ttcn.css', + '/media/editors/codemirror/theme/twilight.css', + '/media/editors/codemirror/theme/vibrant-ink.css', + '/media/editors/codemirror/theme/xq-dark.css', + '/media/editors/codemirror/theme/xq-light.css', + '/media/editors/codemirror/theme/yeti.css', + '/media/editors/codemirror/theme/yonce.css', + '/media/editors/codemirror/theme/zenburn.css', + '/media/editors/none/js/none.js', + '/media/editors/none/js/none.min.js', + '/media/editors/tinymce/changelog.txt', + '/media/editors/tinymce/js/plugins/dragdrop/plugin.js', + '/media/editors/tinymce/js/plugins/dragdrop/plugin.min.js', + '/media/editors/tinymce/js/tiny-close.js', + '/media/editors/tinymce/js/tiny-close.min.js', + '/media/editors/tinymce/js/tinymce-builder.js', + '/media/editors/tinymce/js/tinymce.js', + '/media/editors/tinymce/js/tinymce.min.js', + '/media/editors/tinymce/langs/af.js', + '/media/editors/tinymce/langs/ar.js', + '/media/editors/tinymce/langs/be.js', + '/media/editors/tinymce/langs/bg.js', + '/media/editors/tinymce/langs/bs.js', + '/media/editors/tinymce/langs/ca.js', + '/media/editors/tinymce/langs/cs.js', + '/media/editors/tinymce/langs/cy.js', + '/media/editors/tinymce/langs/da.js', + '/media/editors/tinymce/langs/de.js', + '/media/editors/tinymce/langs/el.js', + '/media/editors/tinymce/langs/es.js', + '/media/editors/tinymce/langs/et.js', + '/media/editors/tinymce/langs/eu.js', + '/media/editors/tinymce/langs/fa.js', + '/media/editors/tinymce/langs/fi.js', + '/media/editors/tinymce/langs/fo.js', + '/media/editors/tinymce/langs/fr.js', + '/media/editors/tinymce/langs/ga.js', + '/media/editors/tinymce/langs/gl.js', + '/media/editors/tinymce/langs/he.js', + '/media/editors/tinymce/langs/hr.js', + '/media/editors/tinymce/langs/hu.js', + '/media/editors/tinymce/langs/id.js', + '/media/editors/tinymce/langs/it.js', + '/media/editors/tinymce/langs/ja.js', + '/media/editors/tinymce/langs/ka.js', + '/media/editors/tinymce/langs/kk.js', + '/media/editors/tinymce/langs/km.js', + '/media/editors/tinymce/langs/ko.js', + '/media/editors/tinymce/langs/lb.js', + '/media/editors/tinymce/langs/lt.js', + '/media/editors/tinymce/langs/lv.js', + '/media/editors/tinymce/langs/mk.js', + '/media/editors/tinymce/langs/ms.js', + '/media/editors/tinymce/langs/nb.js', + '/media/editors/tinymce/langs/nl.js', + '/media/editors/tinymce/langs/pl.js', + '/media/editors/tinymce/langs/pt-BR.js', + '/media/editors/tinymce/langs/pt-PT.js', + '/media/editors/tinymce/langs/readme.md', + '/media/editors/tinymce/langs/ro.js', + '/media/editors/tinymce/langs/ru.js', + '/media/editors/tinymce/langs/si-LK.js', + '/media/editors/tinymce/langs/sk.js', + '/media/editors/tinymce/langs/sl.js', + '/media/editors/tinymce/langs/sr.js', + '/media/editors/tinymce/langs/sv.js', + '/media/editors/tinymce/langs/sw.js', + '/media/editors/tinymce/langs/sy.js', + '/media/editors/tinymce/langs/ta.js', + '/media/editors/tinymce/langs/th.js', + '/media/editors/tinymce/langs/tr.js', + '/media/editors/tinymce/langs/ug.js', + '/media/editors/tinymce/langs/uk.js', + '/media/editors/tinymce/langs/vi.js', + '/media/editors/tinymce/langs/zh-CN.js', + '/media/editors/tinymce/langs/zh-TW.js', + '/media/editors/tinymce/license.txt', + '/media/editors/tinymce/plugins/advlist/plugin.min.js', + '/media/editors/tinymce/plugins/anchor/plugin.min.js', + '/media/editors/tinymce/plugins/autolink/plugin.min.js', + '/media/editors/tinymce/plugins/autoresize/plugin.min.js', + '/media/editors/tinymce/plugins/autosave/plugin.min.js', + '/media/editors/tinymce/plugins/bbcode/plugin.min.js', + '/media/editors/tinymce/plugins/charmap/plugin.min.js', + '/media/editors/tinymce/plugins/code/plugin.min.js', + '/media/editors/tinymce/plugins/codesample/css/prism.css', + '/media/editors/tinymce/plugins/codesample/plugin.min.js', + '/media/editors/tinymce/plugins/colorpicker/plugin.min.js', + '/media/editors/tinymce/plugins/contextmenu/plugin.min.js', + '/media/editors/tinymce/plugins/directionality/plugin.min.js', + '/media/editors/tinymce/plugins/emoticons/img/smiley-cool.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-cry.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-embarassed.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-foot-in-mouth.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-frown.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-innocent.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-kiss.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-laughing.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-money-mouth.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-sealed.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-smile.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-surprised.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-tongue-out.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-undecided.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-wink.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-yell.gif', + '/media/editors/tinymce/plugins/emoticons/plugin.min.js', + '/media/editors/tinymce/plugins/example/dialog.html', + '/media/editors/tinymce/plugins/example/plugin.min.js', + '/media/editors/tinymce/plugins/example_dependency/plugin.min.js', + '/media/editors/tinymce/plugins/fullpage/plugin.min.js', + '/media/editors/tinymce/plugins/fullscreen/plugin.min.js', + '/media/editors/tinymce/plugins/hr/plugin.min.js', + '/media/editors/tinymce/plugins/image/plugin.min.js', + '/media/editors/tinymce/plugins/imagetools/plugin.min.js', + '/media/editors/tinymce/plugins/importcss/plugin.min.js', + '/media/editors/tinymce/plugins/insertdatetime/plugin.min.js', + '/media/editors/tinymce/plugins/layer/plugin.min.js', + '/media/editors/tinymce/plugins/legacyoutput/plugin.min.js', + '/media/editors/tinymce/plugins/link/plugin.min.js', + '/media/editors/tinymce/plugins/lists/plugin.min.js', + '/media/editors/tinymce/plugins/media/plugin.min.js', + '/media/editors/tinymce/plugins/nonbreaking/plugin.min.js', + '/media/editors/tinymce/plugins/noneditable/plugin.min.js', + '/media/editors/tinymce/plugins/pagebreak/plugin.min.js', + '/media/editors/tinymce/plugins/paste/plugin.min.js', + '/media/editors/tinymce/plugins/preview/plugin.min.js', + '/media/editors/tinymce/plugins/print/plugin.min.js', + '/media/editors/tinymce/plugins/save/plugin.min.js', + '/media/editors/tinymce/plugins/searchreplace/plugin.min.js', + '/media/editors/tinymce/plugins/spellchecker/plugin.min.js', + '/media/editors/tinymce/plugins/tabfocus/plugin.min.js', + '/media/editors/tinymce/plugins/table/plugin.min.js', + '/media/editors/tinymce/plugins/template/plugin.min.js', + '/media/editors/tinymce/plugins/textcolor/plugin.min.js', + '/media/editors/tinymce/plugins/textpattern/plugin.min.js', + '/media/editors/tinymce/plugins/toc/plugin.min.js', + '/media/editors/tinymce/plugins/visualblocks/css/visualblocks.css', + '/media/editors/tinymce/plugins/visualblocks/plugin.min.js', + '/media/editors/tinymce/plugins/visualchars/plugin.min.js', + '/media/editors/tinymce/plugins/wordcount/plugin.min.js', + '/media/editors/tinymce/skins/lightgray/content.inline.min.css', + '/media/editors/tinymce/skins/lightgray/content.min.css', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.eot', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.svg', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.ttf', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.woff', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce.eot', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce.svg', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce.ttf', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce.woff', + '/media/editors/tinymce/skins/lightgray/img/anchor.gif', + '/media/editors/tinymce/skins/lightgray/img/loader.gif', + '/media/editors/tinymce/skins/lightgray/img/object.gif', + '/media/editors/tinymce/skins/lightgray/img/trans.gif', + '/media/editors/tinymce/skins/lightgray/skin.ie7.min.css', + '/media/editors/tinymce/skins/lightgray/skin.min.css', + '/media/editors/tinymce/templates/layout1.html', + '/media/editors/tinymce/templates/snippet1.html', + '/media/editors/tinymce/themes/modern/theme.min.js', + '/media/editors/tinymce/tinymce.min.js', + '/media/jui/css/bootstrap-extended.css', + '/media/jui/css/bootstrap-responsive.css', + '/media/jui/css/bootstrap-responsive.min.css', + '/media/jui/css/bootstrap-rtl.css', + '/media/jui/css/bootstrap-tooltip-extended.css', + '/media/jui/css/bootstrap.css', + '/media/jui/css/bootstrap.min.css', + '/media/jui/css/chosen-sprite.png', + '/media/jui/css/chosen-sprite@2x.png', + '/media/jui/css/chosen.css', + '/media/jui/css/icomoon.css', + '/media/jui/css/jquery.minicolors.css', + '/media/jui/css/jquery.searchtools.css', + '/media/jui/css/jquery.simplecolors.css', + '/media/jui/css/sortablelist.css', + '/media/jui/fonts/IcoMoon.dev.commented.svg', + '/media/jui/fonts/IcoMoon.dev.svg', + '/media/jui/fonts/IcoMoon.eot', + '/media/jui/fonts/IcoMoon.svg', + '/media/jui/fonts/IcoMoon.ttf', + '/media/jui/fonts/IcoMoon.woff', + '/media/jui/fonts/icomoon-license.txt', + '/media/jui/images/ajax-loader.gif', + '/media/jui/img/ajax-loader.gif', + '/media/jui/img/alpha.png', + '/media/jui/img/bg-overlay.png', + '/media/jui/img/glyphicons-halflings-white.png', + '/media/jui/img/glyphicons-halflings.png', + '/media/jui/img/hue.png', + '/media/jui/img/joomla.png', + '/media/jui/img/jquery.minicolors.png', + '/media/jui/img/saturation.png', + '/media/jui/js/ajax-chosen.js', + '/media/jui/js/ajax-chosen.min.js', + '/media/jui/js/bootstrap-tooltip-extended.js', + '/media/jui/js/bootstrap-tooltip-extended.min.js', + '/media/jui/js/bootstrap.js', + '/media/jui/js/bootstrap.min.js', + '/media/jui/js/chosen.jquery.js', + '/media/jui/js/chosen.jquery.min.js', + '/media/jui/js/cms-uncompressed.js', + '/media/jui/js/cms.js', + '/media/jui/js/fielduser.js', + '/media/jui/js/fielduser.min.js', + '/media/jui/js/html5-uncompressed.js', + '/media/jui/js/html5.js', + '/media/jui/js/icomoon-lte-ie7.js', + '/media/jui/js/jquery-migrate.js', + '/media/jui/js/jquery-migrate.min.js', + '/media/jui/js/jquery-noconflict.js', + '/media/jui/js/jquery.autocomplete.js', + '/media/jui/js/jquery.autocomplete.min.js', + '/media/jui/js/jquery.js', + '/media/jui/js/jquery.min.js', + '/media/jui/js/jquery.minicolors.js', + '/media/jui/js/jquery.minicolors.min.js', + '/media/jui/js/jquery.searchtools.js', + '/media/jui/js/jquery.searchtools.min.js', + '/media/jui/js/jquery.simplecolors.js', + '/media/jui/js/jquery.simplecolors.min.js', + '/media/jui/js/jquery.ui.core.js', + '/media/jui/js/jquery.ui.core.min.js', + '/media/jui/js/jquery.ui.sortable.js', + '/media/jui/js/jquery.ui.sortable.min.js', + '/media/jui/js/sortablelist.js', + '/media/jui/js/treeselectmenu.jquery.js', + '/media/jui/js/treeselectmenu.jquery.min.js', + '/media/jui/less/accordion.less', + '/media/jui/less/alerts.less', + '/media/jui/less/bootstrap-extended.less', + '/media/jui/less/bootstrap-rtl.less', + '/media/jui/less/bootstrap.less', + '/media/jui/less/breadcrumbs.less', + '/media/jui/less/button-groups.less', + '/media/jui/less/buttons.less', + '/media/jui/less/carousel.less', + '/media/jui/less/close.less', + '/media/jui/less/code.less', + '/media/jui/less/component-animations.less', + '/media/jui/less/dropdowns.less', + '/media/jui/less/forms.less', + '/media/jui/less/grid.less', + '/media/jui/less/hero-unit.less', + '/media/jui/less/icomoon.less', + '/media/jui/less/labels-badges.less', + '/media/jui/less/layouts.less', + '/media/jui/less/media.less', + '/media/jui/less/mixins.less', + '/media/jui/less/modals.joomla.less', + '/media/jui/less/modals.less', + '/media/jui/less/navbar.less', + '/media/jui/less/navs.less', + '/media/jui/less/pager.less', + '/media/jui/less/pagination.less', + '/media/jui/less/popovers.less', + '/media/jui/less/progress-bars.less', + '/media/jui/less/reset.less', + '/media/jui/less/responsive-1200px-min.less', + '/media/jui/less/responsive-767px-max.joomla.less', + '/media/jui/less/responsive-767px-max.less', + '/media/jui/less/responsive-768px-979px.less', + '/media/jui/less/responsive-navbar.less', + '/media/jui/less/responsive-utilities.less', + '/media/jui/less/responsive.less', + '/media/jui/less/scaffolding.less', + '/media/jui/less/sprites.less', + '/media/jui/less/tables.less', + '/media/jui/less/thumbnails.less', + '/media/jui/less/tooltip.less', + '/media/jui/less/type.less', + '/media/jui/less/utilities.less', + '/media/jui/less/variables.less', + '/media/jui/less/wells.less', + '/media/media/css/background.png', + '/media/media/css/bigplay.fw.png', + '/media/media/css/bigplay.png', + '/media/media/css/bigplay.svg', + '/media/media/css/controls-ted.png', + '/media/media/css/controls-wmp-bg.png', + '/media/media/css/controls-wmp.png', + '/media/media/css/controls.fw.png', + '/media/media/css/controls.png', + '/media/media/css/controls.svg', + '/media/media/css/jumpforward.png', + '/media/media/css/loading.gif', + '/media/media/css/mediaelementplayer.css', + '/media/media/css/mediaelementplayer.min.css', + '/media/media/css/medialist-details.css', + '/media/media/css/medialist-details_rtl.css', + '/media/media/css/medialist-thumbs.css', + '/media/media/css/medialist-thumbs_rtl.css', + '/media/media/css/mediamanager.css', + '/media/media/css/mediamanager_rtl.css', + '/media/media/css/mejs-skins.css', + '/media/media/css/popup-imagelist.css', + '/media/media/css/popup-imagelist_rtl.css', + '/media/media/css/popup-imagemanager.css', + '/media/media/css/popup-imagemanager_rtl.css', + '/media/media/css/skipback.png', + '/media/media/images/bar.gif', + '/media/media/images/con_info.png', + '/media/media/images/delete.png', + '/media/media/images/dots.gif', + '/media/media/images/failed.png', + '/media/media/images/folder.gif', + '/media/media/images/folder.png', + '/media/media/images/folder_sm.png', + '/media/media/images/folderup_16.png', + '/media/media/images/folderup_32.png', + '/media/media/images/mime-icon-16/avi.png', + '/media/media/images/mime-icon-16/doc.png', + '/media/media/images/mime-icon-16/mov.png', + '/media/media/images/mime-icon-16/mp3.png', + '/media/media/images/mime-icon-16/mp4.png', + '/media/media/images/mime-icon-16/odc.png', + '/media/media/images/mime-icon-16/odd.png', + '/media/media/images/mime-icon-16/odt.png', + '/media/media/images/mime-icon-16/ogg.png', + '/media/media/images/mime-icon-16/pdf.png', + '/media/media/images/mime-icon-16/ppt.png', + '/media/media/images/mime-icon-16/rar.png', + '/media/media/images/mime-icon-16/rtf.png', + '/media/media/images/mime-icon-16/svg.png', + '/media/media/images/mime-icon-16/sxd.png', + '/media/media/images/mime-icon-16/tar.png', + '/media/media/images/mime-icon-16/tgz.png', + '/media/media/images/mime-icon-16/wma.png', + '/media/media/images/mime-icon-16/wmv.png', + '/media/media/images/mime-icon-16/xls.png', + '/media/media/images/mime-icon-16/zip.png', + '/media/media/images/mime-icon-32/avi.png', + '/media/media/images/mime-icon-32/doc.png', + '/media/media/images/mime-icon-32/mov.png', + '/media/media/images/mime-icon-32/mp3.png', + '/media/media/images/mime-icon-32/mp4.png', + '/media/media/images/mime-icon-32/odc.png', + '/media/media/images/mime-icon-32/odd.png', + '/media/media/images/mime-icon-32/odt.png', + '/media/media/images/mime-icon-32/ogg.png', + '/media/media/images/mime-icon-32/pdf.png', + '/media/media/images/mime-icon-32/ppt.png', + '/media/media/images/mime-icon-32/rar.png', + '/media/media/images/mime-icon-32/rtf.png', + '/media/media/images/mime-icon-32/svg.png', + '/media/media/images/mime-icon-32/sxd.png', + '/media/media/images/mime-icon-32/tar.png', + '/media/media/images/mime-icon-32/tgz.png', + '/media/media/images/mime-icon-32/wma.png', + '/media/media/images/mime-icon-32/wmv.png', + '/media/media/images/mime-icon-32/xls.png', + '/media/media/images/mime-icon-32/zip.png', + '/media/media/images/progress.gif', + '/media/media/images/remove.png', + '/media/media/images/success.png', + '/media/media/images/upload.png', + '/media/media/images/uploading.png', + '/media/media/js/flashmediaelement-cdn.swf', + '/media/media/js/flashmediaelement.swf', + '/media/media/js/mediaelement-and-player.js', + '/media/media/js/mediaelement-and-player.min.js', + '/media/media/js/mediafield-mootools.js', + '/media/media/js/mediafield-mootools.min.js', + '/media/media/js/mediafield.js', + '/media/media/js/mediafield.min.js', + '/media/media/js/mediamanager.js', + '/media/media/js/mediamanager.min.js', + '/media/media/js/popup-imagemanager.js', + '/media/media/js/popup-imagemanager.min.js', + '/media/media/js/silverlightmediaelement.xap', + '/media/overrider/css/overrider.css', + '/media/overrider/js/overrider.js', + '/media/overrider/js/overrider.min.js', + '/media/plg_system_highlight/highlight.css', + '/media/plg_twofactorauth_totp/js/qrcode.js', + '/media/plg_twofactorauth_totp/js/qrcode.min.js', + '/media/plg_twofactorauth_totp/js/qrcode_SJIS.js', + '/media/plg_twofactorauth_totp/js/qrcode_UTF8.js', + '/media/system/css/adminlist.css', + '/media/system/css/jquery.Jcrop.min.css', + '/media/system/css/modal.css', + '/media/system/css/system.css', + '/media/system/js/associations-edit-uncompressed.js', + '/media/system/js/associations-edit.js', + '/media/system/js/calendar-setup-uncompressed.js', + '/media/system/js/calendar-setup.js', + '/media/system/js/calendar-uncompressed.js', + '/media/system/js/calendar.js', + '/media/system/js/caption-uncompressed.js', + '/media/system/js/caption.js', + '/media/system/js/color-field-adv-init.js', + '/media/system/js/color-field-adv-init.min.js', + '/media/system/js/color-field-init.js', + '/media/system/js/color-field-init.min.js', + '/media/system/js/combobox-uncompressed.js', + '/media/system/js/combobox.js', + '/media/system/js/core-uncompressed.js', + '/media/system/js/fields/calendar-locales/af.js', + '/media/system/js/fields/calendar-locales/ar.js', + '/media/system/js/fields/calendar-locales/bg.js', + '/media/system/js/fields/calendar-locales/bn.js', + '/media/system/js/fields/calendar-locales/bs.js', + '/media/system/js/fields/calendar-locales/ca.js', + '/media/system/js/fields/calendar-locales/cs.js', + '/media/system/js/fields/calendar-locales/cy.js', + '/media/system/js/fields/calendar-locales/da.js', + '/media/system/js/fields/calendar-locales/de.js', + '/media/system/js/fields/calendar-locales/el.js', + '/media/system/js/fields/calendar-locales/en.js', + '/media/system/js/fields/calendar-locales/es.js', + '/media/system/js/fields/calendar-locales/eu.js', + '/media/system/js/fields/calendar-locales/fa-ir.js', + '/media/system/js/fields/calendar-locales/fi.js', + '/media/system/js/fields/calendar-locales/fr.js', + '/media/system/js/fields/calendar-locales/ga.js', + '/media/system/js/fields/calendar-locales/hr.js', + '/media/system/js/fields/calendar-locales/hu.js', + '/media/system/js/fields/calendar-locales/it.js', + '/media/system/js/fields/calendar-locales/ja.js', + '/media/system/js/fields/calendar-locales/ka.js', + '/media/system/js/fields/calendar-locales/kk.js', + '/media/system/js/fields/calendar-locales/ko.js', + '/media/system/js/fields/calendar-locales/lt.js', + '/media/system/js/fields/calendar-locales/mk.js', + '/media/system/js/fields/calendar-locales/nb.js', + '/media/system/js/fields/calendar-locales/nl.js', + '/media/system/js/fields/calendar-locales/pl.js', + '/media/system/js/fields/calendar-locales/prs-af.js', + '/media/system/js/fields/calendar-locales/pt.js', + '/media/system/js/fields/calendar-locales/ru.js', + '/media/system/js/fields/calendar-locales/sk.js', + '/media/system/js/fields/calendar-locales/sl.js', + '/media/system/js/fields/calendar-locales/sr-rs.js', + '/media/system/js/fields/calendar-locales/sr-yu.js', + '/media/system/js/fields/calendar-locales/sv.js', + '/media/system/js/fields/calendar-locales/sw.js', + '/media/system/js/fields/calendar-locales/ta.js', + '/media/system/js/fields/calendar-locales/th.js', + '/media/system/js/fields/calendar-locales/uk.js', + '/media/system/js/fields/calendar-locales/zh-CN.js', + '/media/system/js/fields/calendar-locales/zh-TW.js', + '/media/system/js/frontediting-uncompressed.js', + '/media/system/js/frontediting.js', + '/media/system/js/helpsite.js', + '/media/system/js/highlighter-uncompressed.js', + '/media/system/js/highlighter.js', + '/media/system/js/html5fallback-uncompressed.js', + '/media/system/js/html5fallback.js', + '/media/system/js/jquery.Jcrop.js', + '/media/system/js/jquery.Jcrop.min.js', + '/media/system/js/keepalive-uncompressed.js', + '/media/system/js/modal-fields-uncompressed.js', + '/media/system/js/modal-fields.js', + '/media/system/js/modal-uncompressed.js', + '/media/system/js/modal.js', + '/media/system/js/moduleorder.js', + '/media/system/js/mootools-core-uncompressed.js', + '/media/system/js/mootools-core.js', + '/media/system/js/mootools-more-uncompressed.js', + '/media/system/js/mootools-more.js', + '/media/system/js/mootree-uncompressed.js', + '/media/system/js/mootree.js', + '/media/system/js/multiselect-uncompressed.js', + '/media/system/js/passwordstrength.js', + '/media/system/js/permissions-uncompressed.js', + '/media/system/js/permissions.js', + '/media/system/js/polyfill.classlist-uncompressed.js', + '/media/system/js/polyfill.classlist.js', + '/media/system/js/polyfill.event-uncompressed.js', + '/media/system/js/polyfill.event.js', + '/media/system/js/polyfill.filter-uncompressed.js', + '/media/system/js/polyfill.filter.js', + '/media/system/js/polyfill.map-uncompressed.js', + '/media/system/js/polyfill.map.js', + '/media/system/js/polyfill.xpath-uncompressed.js', + '/media/system/js/polyfill.xpath.js', + '/media/system/js/progressbar-uncompressed.js', + '/media/system/js/progressbar.js', + '/media/system/js/punycode-uncompressed.js', + '/media/system/js/punycode.js', + '/media/system/js/repeatable-uncompressed.js', + '/media/system/js/repeatable.js', + '/media/system/js/sendtestmail-uncompressed.js', + '/media/system/js/sendtestmail.js', + '/media/system/js/subform-repeatable-uncompressed.js', + '/media/system/js/subform-repeatable.js', + '/media/system/js/switcher-uncompressed.js', + '/media/system/js/switcher.js', + '/media/system/js/tabs-state-uncompressed.js', + '/media/system/js/tabs-state.js', + '/media/system/js/tabs.js', + '/media/system/js/validate-uncompressed.js', + '/media/system/js/validate.js', + '/modules/mod_articles_archive/helper.php', + '/modules/mod_articles_categories/helper.php', + '/modules/mod_articles_category/helper.php', + '/modules/mod_articles_latest/helper.php', + '/modules/mod_articles_news/helper.php', + '/modules/mod_articles_popular/helper.php', + '/modules/mod_banners/helper.php', + '/modules/mod_breadcrumbs/helper.php', + '/modules/mod_feed/helper.php', + '/modules/mod_finder/helper.php', + '/modules/mod_languages/helper.php', + '/modules/mod_login/helper.php', + '/modules/mod_menu/helper.php', + '/modules/mod_random_image/helper.php', + '/modules/mod_related_items/helper.php', + '/modules/mod_stats/helper.php', + '/modules/mod_syndicate/helper.php', + '/modules/mod_tags_popular/helper.php', + '/modules/mod_tags_similar/helper.php', + '/modules/mod_users_latest/helper.php', + '/modules/mod_whosonline/helper.php', + '/modules/mod_wrapper/helper.php', + '/plugins/authentication/gmail/gmail.php', + '/plugins/authentication/gmail/gmail.xml', + '/plugins/captcha/recaptcha/postinstall/actions.php', + '/plugins/content/confirmconsent/fields/consentbox.php', + '/plugins/editors/codemirror/fonts.php', + '/plugins/editors/codemirror/layouts/editors/codemirror/init.php', + '/plugins/editors/tinymce/field/skins.php', + '/plugins/editors/tinymce/field/tinymcebuilder.php', + '/plugins/editors/tinymce/field/uploaddirs.php', + '/plugins/editors/tinymce/form/setoptions.xml', + '/plugins/quickicon/joomlaupdate/joomlaupdate.php', + '/plugins/system/languagecode/language/en-GB/en-GB.plg_system_languagecode.ini', + '/plugins/system/languagecode/language/en-GB/en-GB.plg_system_languagecode.sys.ini', + '/plugins/system/p3p/p3p.php', + '/plugins/system/p3p/p3p.xml', + '/plugins/system/privacyconsent/field/privacy.php', + '/plugins/system/privacyconsent/privacyconsent/privacyconsent.xml', + '/plugins/system/stats/field/base.php', + '/plugins/system/stats/field/data.php', + '/plugins/system/stats/field/uniqueid.php', + '/plugins/user/profile/field/dob.php', + '/plugins/user/profile/field/tos.php', + '/plugins/user/profile/profiles/profile.xml', + '/plugins/user/terms/field/terms.php', + '/plugins/user/terms/terms/terms.xml', + '/templates/beez3/component.php', + '/templates/beez3/css/general.css', + '/templates/beez3/css/ie7only.css', + '/templates/beez3/css/ieonly.css', + '/templates/beez3/css/layout.css', + '/templates/beez3/css/nature.css', + '/templates/beez3/css/nature_rtl.css', + '/templates/beez3/css/personal.css', + '/templates/beez3/css/personal_rtl.css', + '/templates/beez3/css/position.css', + '/templates/beez3/css/print.css', + '/templates/beez3/css/red.css', + '/templates/beez3/css/template.css', + '/templates/beez3/css/template_rtl.css', + '/templates/beez3/css/turq.css', + '/templates/beez3/css/turq.less', + '/templates/beez3/error.php', + '/templates/beez3/favicon.ico', + '/templates/beez3/html/com_contact/categories/default.php', + '/templates/beez3/html/com_contact/categories/default_items.php', + '/templates/beez3/html/com_contact/category/default.php', + '/templates/beez3/html/com_contact/category/default_children.php', + '/templates/beez3/html/com_contact/category/default_items.php', + '/templates/beez3/html/com_contact/contact/default.php', + '/templates/beez3/html/com_contact/contact/default_address.php', + '/templates/beez3/html/com_contact/contact/default_articles.php', + '/templates/beez3/html/com_contact/contact/default_form.php', + '/templates/beez3/html/com_contact/contact/default_links.php', + '/templates/beez3/html/com_contact/contact/default_profile.php', + '/templates/beez3/html/com_contact/contact/default_user_custom_fields.php', + '/templates/beez3/html/com_contact/contact/encyclopedia.php', + '/templates/beez3/html/com_content/archive/default.php', + '/templates/beez3/html/com_content/archive/default_items.php', + '/templates/beez3/html/com_content/article/default.php', + '/templates/beez3/html/com_content/article/default_links.php', + '/templates/beez3/html/com_content/categories/default.php', + '/templates/beez3/html/com_content/categories/default_items.php', + '/templates/beez3/html/com_content/category/blog.php', + '/templates/beez3/html/com_content/category/blog_children.php', + '/templates/beez3/html/com_content/category/blog_item.php', + '/templates/beez3/html/com_content/category/blog_links.php', + '/templates/beez3/html/com_content/category/default.php', + '/templates/beez3/html/com_content/category/default_articles.php', + '/templates/beez3/html/com_content/category/default_children.php', + '/templates/beez3/html/com_content/featured/default.php', + '/templates/beez3/html/com_content/featured/default_item.php', + '/templates/beez3/html/com_content/featured/default_links.php', + '/templates/beez3/html/com_content/form/edit.php', + '/templates/beez3/html/com_newsfeeds/categories/default.php', + '/templates/beez3/html/com_newsfeeds/categories/default_items.php', + '/templates/beez3/html/com_newsfeeds/category/default.php', + '/templates/beez3/html/com_newsfeeds/category/default_children.php', + '/templates/beez3/html/com_newsfeeds/category/default_items.php', + '/templates/beez3/html/com_weblinks/categories/default.php', + '/templates/beez3/html/com_weblinks/categories/default_items.php', + '/templates/beez3/html/com_weblinks/category/default.php', + '/templates/beez3/html/com_weblinks/category/default_children.php', + '/templates/beez3/html/com_weblinks/category/default_items.php', + '/templates/beez3/html/com_weblinks/form/edit.php', + '/templates/beez3/html/layouts/joomla/system/message.php', + '/templates/beez3/html/mod_breadcrumbs/default.php', + '/templates/beez3/html/mod_languages/default.php', + '/templates/beez3/html/mod_login/default.php', + '/templates/beez3/html/mod_login/default_logout.php', + '/templates/beez3/html/modules.php', + '/templates/beez3/images/all_bg.gif', + '/templates/beez3/images/arrow.png', + '/templates/beez3/images/arrow2_grey.png', + '/templates/beez3/images/arrow_white_grey.png', + '/templates/beez3/images/blog_more.gif', + '/templates/beez3/images/blog_more_hover.gif', + '/templates/beez3/images/close.png', + '/templates/beez3/images/content_bg.gif', + '/templates/beez3/images/footer_bg.gif', + '/templates/beez3/images/footer_bg.png', + '/templates/beez3/images/header-bg.gif', + '/templates/beez3/images/minus.png', + '/templates/beez3/images/nature/arrow1.gif', + '/templates/beez3/images/nature/arrow1_rtl.gif', + '/templates/beez3/images/nature/arrow2.gif', + '/templates/beez3/images/nature/arrow2_grey.png', + '/templates/beez3/images/nature/arrow2_rtl.gif', + '/templates/beez3/images/nature/arrow_nav.gif', + '/templates/beez3/images/nature/arrow_small.png', + '/templates/beez3/images/nature/arrow_small_rtl.png', + '/templates/beez3/images/nature/blog_more.gif', + '/templates/beez3/images/nature/box.png', + '/templates/beez3/images/nature/box1.png', + '/templates/beez3/images/nature/grey_bg.png', + '/templates/beez3/images/nature/headingback.png', + '/templates/beez3/images/nature/karo.gif', + '/templates/beez3/images/nature/level4.png', + '/templates/beez3/images/nature/nav_level1_a.gif', + '/templates/beez3/images/nature/nav_level_1.gif', + '/templates/beez3/images/nature/pfeil.gif', + '/templates/beez3/images/nature/readmore_arrow.png', + '/templates/beez3/images/nature/searchbutton.png', + '/templates/beez3/images/nature/tabs.gif', + '/templates/beez3/images/nav_level_1.gif', + '/templates/beez3/images/news.gif', + '/templates/beez3/images/personal/arrow2_grey.jpg', + '/templates/beez3/images/personal/arrow2_grey.png', + '/templates/beez3/images/personal/bg2.png', + '/templates/beez3/images/personal/button.png', + '/templates/beez3/images/personal/dot.png', + '/templates/beez3/images/personal/ecke.gif', + '/templates/beez3/images/personal/footer.jpg', + '/templates/beez3/images/personal/grey_bg.png', + '/templates/beez3/images/personal/navi_active.png', + '/templates/beez3/images/personal/personal2.png', + '/templates/beez3/images/personal/readmore_arrow.png', + '/templates/beez3/images/personal/readmore_arrow_hover.png', + '/templates/beez3/images/personal/tabs_back.png', + '/templates/beez3/images/plus.png', + '/templates/beez3/images/req.png', + '/templates/beez3/images/slider_minus.png', + '/templates/beez3/images/slider_minus_rtl.png', + '/templates/beez3/images/slider_plus.png', + '/templates/beez3/images/slider_plus_rtl.png', + '/templates/beez3/images/system/arrow.png', + '/templates/beez3/images/system/arrow_rtl.png', + '/templates/beez3/images/system/calendar.png', + '/templates/beez3/images/system/j_button2_blank.png', + '/templates/beez3/images/system/j_button2_image.png', + '/templates/beez3/images/system/j_button2_left.png', + '/templates/beez3/images/system/j_button2_pagebreak.png', + '/templates/beez3/images/system/j_button2_readmore.png', + '/templates/beez3/images/system/notice-alert.png', + '/templates/beez3/images/system/notice-alert_rtl.png', + '/templates/beez3/images/system/notice-info.png', + '/templates/beez3/images/system/notice-info_rtl.png', + '/templates/beez3/images/system/notice-note.png', + '/templates/beez3/images/system/notice-note_rtl.png', + '/templates/beez3/images/system/selector-arrow.png', + '/templates/beez3/images/table_footer.gif', + '/templates/beez3/images/trans.gif', + '/templates/beez3/index.php', + '/templates/beez3/javascript/hide.js', + '/templates/beez3/javascript/md_stylechanger.js', + '/templates/beez3/javascript/respond.js', + '/templates/beez3/javascript/respond.src.js', + '/templates/beez3/javascript/template.js', + '/templates/beez3/jsstrings.php', + '/templates/beez3/language/en-GB/en-GB.tpl_beez3.ini', + '/templates/beez3/language/en-GB/en-GB.tpl_beez3.sys.ini', + '/templates/beez3/templateDetails.xml', + '/templates/beez3/template_preview.png', + '/templates/beez3/template_thumbnail.png', + '/templates/protostar/component.php', + '/templates/protostar/css/offline.css', + '/templates/protostar/css/template.css', + '/templates/protostar/error.php', + '/templates/protostar/favicon.ico', + '/templates/protostar/html/com_media/imageslist/default_folder.php', + '/templates/protostar/html/com_media/imageslist/default_image.php', + '/templates/protostar/html/layouts/joomla/form/field/contenthistory.php', + '/templates/protostar/html/layouts/joomla/form/field/media.php', + '/templates/protostar/html/layouts/joomla/form/field/user.php', + '/templates/protostar/html/layouts/joomla/system/message.php', + '/templates/protostar/html/modules.php', + '/templates/protostar/html/pagination.php', + '/templates/protostar/images/logo.png', + '/templates/protostar/images/system/rating_star.png', + '/templates/protostar/images/system/rating_star_blank.png', + '/templates/protostar/images/system/sort_asc.png', + '/templates/protostar/images/system/sort_desc.png', + '/templates/protostar/img/glyphicons-halflings-white.png', + '/templates/protostar/img/glyphicons-halflings.png', + '/templates/protostar/index.php', + '/templates/protostar/js/application.js', + '/templates/protostar/js/classes.js', + '/templates/protostar/js/template.js', + '/templates/protostar/language/en-GB/en-GB.tpl_protostar.ini', + '/templates/protostar/language/en-GB/en-GB.tpl_protostar.sys.ini', + '/templates/protostar/less/icomoon.less', + '/templates/protostar/less/template.less', + '/templates/protostar/less/template_rtl.less', + '/templates/protostar/less/variables.less', + '/templates/protostar/offline.php', + '/templates/protostar/templateDetails.xml', + '/templates/protostar/template_preview.png', + '/templates/protostar/template_thumbnail.png', + '/templates/system/css/system.css', + '/templates/system/css/toolbar.css', + '/templates/system/html/modules.php', + '/templates/system/images/calendar.png', + '/templates/system/images/j_button2_blank.png', + '/templates/system/images/j_button2_image.png', + '/templates/system/images/j_button2_left.png', + '/templates/system/images/j_button2_pagebreak.png', + '/templates/system/images/j_button2_readmore.png', + '/templates/system/images/j_button2_right.png', + '/templates/system/images/selector-arrow.png', + // 4.0 from Beta 1 to Beta 2 + '/administrator/components/com_finder/src/Indexer/Driver/Mysql.php', + '/administrator/components/com_finder/src/Indexer/Driver/Postgresql.php', + '/administrator/components/com_workflow/access.xml', + '/api/components/com_installer/src/Controller/LanguagesController.php', + '/api/components/com_installer/src/View/Languages/JsonapiView.php', + '/libraries/vendor/joomla/controller/LICENSE', + '/libraries/vendor/joomla/controller/src/AbstractController.php', + '/libraries/vendor/joomla/controller/src/ControllerInterface.php', + '/media/com_users/js/admin-users-user.es6.js', + '/media/com_users/js/admin-users-user.es6.min.js', + '/media/com_users/js/admin-users-user.es6.min.js.gz', + '/media/com_users/js/admin-users-user.js', + '/media/com_users/js/admin-users-user.min.js', + '/media/com_users/js/admin-users-user.min.js.gz', + // 4.0 from Beta 2 to Beta 3 + '/administrator/templates/atum/images/logo-blue.svg', + '/administrator/templates/atum/images/logo-joomla-blue.svg', + '/administrator/templates/atum/images/logo-joomla-white.svg', + '/administrator/templates/atum/images/logo.svg', + // 4.0 from Beta 3 to Beta 4 + '/components/com_config/src/Model/CmsModel.php', + // 4.0 from Beta 4 to Beta 5 + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-18.sql', + '/administrator/components/com_config/tmpl/application/default_system.php', + '/administrator/language/en-GB/plg_content_imagelazyload.sys.ini', + '/administrator/language/en-GB/plg_fields_image.ini', + '/administrator/language/en-GB/plg_fields_image.sys.ini', + '/administrator/templates/atum/scss/vendor/bootstrap/_nav.scss', + '/libraries/vendor/spomky-labs/base64url/phpstan.neon', + '/media/plg_system_webauthn/images/webauthn-black.png', + '/media/plg_system_webauthn/images/webauthn-color.png', + '/media/plg_system_webauthn/images/webauthn-white.png', + '/media/system/css/system.min.css', + '/media/system/css/system.min.css.gz', + '/plugins/content/imagelazyload/imagelazyload.php', + '/plugins/content/imagelazyload/imagelazyload.xml', + '/templates/cassiopeia/html/layouts/chromes/cardGrey.php', + '/templates/cassiopeia/html/layouts/chromes/default.php', + '/templates/cassiopeia/scss/vendor/bootstrap/_card.scss', + // 4.0 from Beta 5 to Beta 6 + '/administrator/modules/mod_multilangstatus/src/Helper/MultilangstatusAdminHelper.php', + '/administrator/templates/atum/favicon.ico', + '/libraries/vendor/nyholm/psr7/phpstan.baseline.dist', + '/libraries/vendor/spomky-labs/base64url/.php_cs.dist', + '/libraries/vendor/spomky-labs/base64url/infection.json.dist', + '/media/layouts/js/joomla/html/batch/batch-language.es6.js', + '/media/layouts/js/joomla/html/batch/batch-language.es6.min.js', + '/media/layouts/js/joomla/html/batch/batch-language.es6.min.js.gz', + '/media/layouts/js/joomla/html/batch/batch-language.js', + '/media/layouts/js/joomla/html/batch/batch-language.min.js', + '/media/layouts/js/joomla/html/batch/batch-language.min.js.gz', + '/media/plg_system_webauthn/images/webauthn-black.svg', + '/media/plg_system_webauthn/images/webauthn-white.svg', + '/media/system/js/core.es6/ajax.es6', + '/media/system/js/core.es6/customevent.es6', + '/media/system/js/core.es6/event.es6', + '/media/system/js/core.es6/form.es6', + '/media/system/js/core.es6/message.es6', + '/media/system/js/core.es6/options.es6', + '/media/system/js/core.es6/text.es6', + '/media/system/js/core.es6/token.es6', + '/media/system/js/core.es6/webcomponent.es6', + '/templates/cassiopeia/favicon.ico', + '/templates/cassiopeia/scss/_mixin.scss', + '/templates/cassiopeia/scss/_variables.scss', + '/templates/cassiopeia/scss/blocks/_demo-styling.scss', + // 4.0 from Beta 6 to Beta 7 + '/media/legacy/js/bootstrap-init.js', + '/media/legacy/js/bootstrap-init.min.js', + '/media/legacy/js/bootstrap-init.min.js.gz', + '/media/legacy/js/frontediting.js', + '/media/legacy/js/frontediting.min.js', + '/media/legacy/js/frontediting.min.js.gz', + '/media/vendor/bootstrap/js/bootstrap.bundle.js', + '/media/vendor/bootstrap/js/bootstrap.bundle.min.js', + '/media/vendor/bootstrap/js/bootstrap.bundle.min.js.gz', + '/media/vendor/bootstrap/js/bootstrap.bundle.min.js.map', + '/media/vendor/bootstrap/js/bootstrap.js', + '/media/vendor/bootstrap/js/bootstrap.min.js', + '/media/vendor/bootstrap/js/bootstrap.min.js.gz', + '/media/vendor/bootstrap/scss/_code.scss', + '/media/vendor/bootstrap/scss/_custom-forms.scss', + '/media/vendor/bootstrap/scss/_input-group.scss', + '/media/vendor/bootstrap/scss/_jumbotron.scss', + '/media/vendor/bootstrap/scss/_media.scss', + '/media/vendor/bootstrap/scss/_print.scss', + '/media/vendor/bootstrap/scss/mixins/_background-variant.scss', + '/media/vendor/bootstrap/scss/mixins/_badge.scss', + '/media/vendor/bootstrap/scss/mixins/_float.scss', + '/media/vendor/bootstrap/scss/mixins/_grid-framework.scss', + '/media/vendor/bootstrap/scss/mixins/_hover.scss', + '/media/vendor/bootstrap/scss/mixins/_nav-divider.scss', + '/media/vendor/bootstrap/scss/mixins/_screen-reader.scss', + '/media/vendor/bootstrap/scss/mixins/_size.scss', + '/media/vendor/bootstrap/scss/mixins/_table-row.scss', + '/media/vendor/bootstrap/scss/mixins/_text-emphasis.scss', + '/media/vendor/bootstrap/scss/mixins/_text-hide.scss', + '/media/vendor/bootstrap/scss/mixins/_visibility.scss', + '/media/vendor/bootstrap/scss/utilities/_align.scss', + '/media/vendor/bootstrap/scss/utilities/_background.scss', + '/media/vendor/bootstrap/scss/utilities/_borders.scss', + '/media/vendor/bootstrap/scss/utilities/_clearfix.scss', + '/media/vendor/bootstrap/scss/utilities/_display.scss', + '/media/vendor/bootstrap/scss/utilities/_embed.scss', + '/media/vendor/bootstrap/scss/utilities/_flex.scss', + '/media/vendor/bootstrap/scss/utilities/_float.scss', + '/media/vendor/bootstrap/scss/utilities/_interactions.scss', + '/media/vendor/bootstrap/scss/utilities/_overflow.scss', + '/media/vendor/bootstrap/scss/utilities/_position.scss', + '/media/vendor/bootstrap/scss/utilities/_screenreaders.scss', + '/media/vendor/bootstrap/scss/utilities/_shadows.scss', + '/media/vendor/bootstrap/scss/utilities/_sizing.scss', + '/media/vendor/bootstrap/scss/utilities/_spacing.scss', + '/media/vendor/bootstrap/scss/utilities/_stretched-link.scss', + '/media/vendor/bootstrap/scss/utilities/_text.scss', + '/media/vendor/bootstrap/scss/utilities/_visibility.scss', + '/media/vendor/skipto/css/SkipTo.css', + '/media/vendor/skipto/js/dropMenu.js', + // 4.0 from Beta 7 to RC 1 + '/administrator/components/com_admin/forms/profile.xml', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-07-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-09-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-09-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-10-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-03-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-04-25.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-05-31.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-06-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-10-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-02-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-07-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-08-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-09-12.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-10-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-02-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-31.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-05-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-06-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-23.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-25.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-27.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-10-13.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-10-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-11-07.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-11-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-05-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-09-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-09-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-02-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-07.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-07-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-09-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-09-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-10-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-03-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-04-25.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-05-31.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-06-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-10-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-02-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-08-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-09-12.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-10-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-02-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-31.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-05-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-06-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-23.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-25.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-27.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-10-13.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-10-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-11-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-11-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-05-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-09-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-09-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-02-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-21.sql', + '/administrator/components/com_admin/src/Controller/ProfileController.php', + '/administrator/components/com_admin/src/Model/ProfileModel.php', + '/administrator/components/com_admin/src/View/Profile/HtmlView.php', + '/administrator/components/com_admin/tmpl/profile/edit.php', + '/administrator/components/com_config/tmpl/application/default_ftp.php', + '/administrator/components/com_config/tmpl/application/default_ftplogin.php', + '/administrator/components/com_csp/access.xml', + '/administrator/components/com_csp/config.xml', + '/administrator/components/com_csp/csp.xml', + '/administrator/components/com_csp/forms/filter_reports.xml', + '/administrator/components/com_csp/services/provider.php', + '/administrator/components/com_csp/src/Controller/DisplayController.php', + '/administrator/components/com_csp/src/Controller/ReportsController.php', + '/administrator/components/com_csp/src/Helper/ReporterHelper.php', + '/administrator/components/com_csp/src/Model/ReportModel.php', + '/administrator/components/com_csp/src/Model/ReportsModel.php', + '/administrator/components/com_csp/src/Table/ReportTable.php', + '/administrator/components/com_csp/src/View/Reports/HtmlView.php', + '/administrator/components/com_csp/tmpl/reports/default.php', + '/administrator/components/com_csp/tmpl/reports/default.xml', + '/administrator/components/com_fields/src/Field/SubfieldstypeField.php', + '/administrator/components/com_installer/tmpl/installer/default_ftp.php', + '/administrator/components/com_joomlaupdate/src/Helper/Select.php', + '/administrator/language/en-GB/com_csp.ini', + '/administrator/language/en-GB/com_csp.sys.ini', + '/administrator/language/en-GB/plg_fields_subfields.ini', + '/administrator/language/en-GB/plg_fields_subfields.sys.ini', + '/administrator/templates/atum/Service/HTML/Atum.php', + '/components/com_csp/src/Controller/ReportController.php', + '/components/com_menus/src/Controller/DisplayController.php', + '/libraries/vendor/algo26-matthias/idna-convert/CODE_OF_CONDUCT.md', + '/libraries/vendor/algo26-matthias/idna-convert/UPGRADING.md', + '/libraries/vendor/algo26-matthias/idna-convert/docker-compose.yml', + '/libraries/vendor/beberlei/assert/phpstan-code.neon', + '/libraries/vendor/beberlei/assert/phpstan-tests.neon', + '/libraries/vendor/bin/generate-defuse-key', + '/libraries/vendor/bin/var-dump-server', + '/libraries/vendor/bin/yaml-lint', + '/libraries/vendor/brick/math/psalm-baseline.xml', + '/libraries/vendor/doctrine/inflector/phpstan.neon.dist', + '/libraries/vendor/jakeasmith/http_build_url/readme.md', + '/libraries/vendor/nyholm/psr7/src/LowercaseTrait.php', + '/libraries/vendor/ozdemirburak/iris/LICENSE.md', + '/libraries/vendor/ozdemirburak/iris/src/BaseColor.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Factory.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Hex.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Hsl.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Hsla.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Hsv.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Rgb.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Rgba.php', + '/libraries/vendor/ozdemirburak/iris/src/Exceptions/AmbiguousColorString.php', + '/libraries/vendor/ozdemirburak/iris/src/Exceptions/InvalidColorException.php', + '/libraries/vendor/ozdemirburak/iris/src/Helpers/DefinedColor.php', + '/libraries/vendor/ozdemirburak/iris/src/Traits/AlphaTrait.php', + '/libraries/vendor/ozdemirburak/iris/src/Traits/HsTrait.php', + '/libraries/vendor/ozdemirburak/iris/src/Traits/HslTrait.php', + '/libraries/vendor/ozdemirburak/iris/src/Traits/RgbTrait.php', + '/libraries/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey', + '/libraries/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc', + '/libraries/vendor/psr/http-factory/.pullapprove.yml', + '/libraries/vendor/spomky-labs/cbor-php/.php_cs.dist', + '/libraries/vendor/spomky-labs/cbor-php/CODE_OF_CONDUCT.md', + '/libraries/vendor/spomky-labs/cbor-php/infection.json.dist', + '/libraries/vendor/spomky-labs/cbor-php/phpstan.neon', + '/libraries/vendor/typo3/phar-stream-wrapper/_config.yml', + '/libraries/vendor/voku/portable-utf8/SUMMARY.md', + '/libraries/vendor/willdurand/negotiation/src/Negotiation/Match.php', + '/media/com_actionlogs/js/admin-actionlogs-default.es6.js', + '/media/com_actionlogs/js/admin-actionlogs-default.es6.min.js', + '/media/com_actionlogs/js/admin-actionlogs-default.es6.min.js.gz', + '/media/com_associations/js/admin-associations-default.es6.js', + '/media/com_associations/js/admin-associations-default.es6.min.js', + '/media/com_associations/js/admin-associations-default.es6.min.js.gz', + '/media/com_associations/js/admin-associations-modal.es6.js', + '/media/com_associations/js/admin-associations-modal.es6.min.js', + '/media/com_associations/js/admin-associations-modal.es6.min.js.gz', + '/media/com_associations/js/associations-edit.es6.js', + '/media/com_associations/js/associations-edit.es6.min.js', + '/media/com_associations/js/associations-edit.es6.min.js.gz', + '/media/com_banners/js/admin-banner-edit.es6.js', + '/media/com_banners/js/admin-banner-edit.es6.min.js', + '/media/com_banners/js/admin-banner-edit.es6.min.js.gz', + '/media/com_cache/js/admin-cache-default.es6.js', + '/media/com_cache/js/admin-cache-default.es6.min.js', + '/media/com_cache/js/admin-cache-default.es6.min.js.gz', + '/media/com_categories/js/shared-categories-accordion.es6.js', + '/media/com_categories/js/shared-categories-accordion.es6.min.js', + '/media/com_categories/js/shared-categories-accordion.es6.min.js.gz', + '/media/com_config/js/config-default.es6.js', + '/media/com_config/js/config-default.es6.min.js', + '/media/com_config/js/config-default.es6.min.js.gz', + '/media/com_config/js/modules-default.es6.js', + '/media/com_config/js/modules-default.es6.min.js', + '/media/com_config/js/modules-default.es6.min.js.gz', + '/media/com_config/js/templates-default.es6.js', + '/media/com_config/js/templates-default.es6.min.js', + '/media/com_config/js/templates-default.es6.min.js.gz', + '/media/com_contact/js/admin-contacts-modal.es6.js', + '/media/com_contact/js/admin-contacts-modal.es6.min.js', + '/media/com_contact/js/admin-contacts-modal.es6.min.js.gz', + '/media/com_contact/js/contacts-list.es6.js', + '/media/com_contact/js/contacts-list.es6.min.js', + '/media/com_contact/js/contacts-list.es6.min.js.gz', + '/media/com_content/js/admin-article-pagebreak.es6.js', + '/media/com_content/js/admin-article-pagebreak.es6.min.js', + '/media/com_content/js/admin-article-pagebreak.es6.min.js.gz', + '/media/com_content/js/admin-article-readmore.es6.js', + '/media/com_content/js/admin-article-readmore.es6.min.js', + '/media/com_content/js/admin-article-readmore.es6.min.js.gz', + '/media/com_content/js/admin-articles-default-batch-footer.es6.js', + '/media/com_content/js/admin-articles-default-batch-footer.es6.min.js', + '/media/com_content/js/admin-articles-default-batch-footer.es6.min.js.gz', + '/media/com_content/js/admin-articles-default-stage-footer.es6.js', + '/media/com_content/js/admin-articles-default-stage-footer.es6.min.js', + '/media/com_content/js/admin-articles-default-stage-footer.es6.min.js.gz', + '/media/com_content/js/admin-articles-modal.es6.js', + '/media/com_content/js/admin-articles-modal.es6.min.js', + '/media/com_content/js/admin-articles-modal.es6.min.js.gz', + '/media/com_content/js/articles-list.es6.js', + '/media/com_content/js/articles-list.es6.min.js', + '/media/com_content/js/articles-list.es6.min.js.gz', + '/media/com_content/js/form-edit.es6.js', + '/media/com_content/js/form-edit.es6.min.js', + '/media/com_content/js/form-edit.es6.min.js.gz', + '/media/com_contenthistory/js/admin-compare-compare.es6.js', + '/media/com_contenthistory/js/admin-compare-compare.es6.min.js', + '/media/com_contenthistory/js/admin-compare-compare.es6.min.js.gz', + '/media/com_contenthistory/js/admin-history-modal.es6.js', + '/media/com_contenthistory/js/admin-history-modal.es6.min.js', + '/media/com_contenthistory/js/admin-history-modal.es6.min.js.gz', + '/media/com_contenthistory/js/admin-history-versions.es6.js', + '/media/com_contenthistory/js/admin-history-versions.es6.min.js', + '/media/com_contenthistory/js/admin-history-versions.es6.min.js.gz', + '/media/com_cpanel/js/admin-add_module.es6.js', + '/media/com_cpanel/js/admin-add_module.es6.min.js', + '/media/com_cpanel/js/admin-add_module.es6.min.js.gz', + '/media/com_cpanel/js/admin-cpanel-default.es6.js', + '/media/com_cpanel/js/admin-cpanel-default.es6.min.js', + '/media/com_cpanel/js/admin-cpanel-default.es6.min.js.gz', + '/media/com_cpanel/js/admin-system-loader.es6.js', + '/media/com_cpanel/js/admin-system-loader.es6.min.js', + '/media/com_cpanel/js/admin-system-loader.es6.min.js.gz', + '/media/com_fields/js/admin-field-changecontext.es6.js', + '/media/com_fields/js/admin-field-changecontext.es6.min.js', + '/media/com_fields/js/admin-field-changecontext.es6.min.js.gz', + '/media/com_fields/js/admin-field-edit-modal.es6.js', + '/media/com_fields/js/admin-field-edit-modal.es6.min.js', + '/media/com_fields/js/admin-field-edit-modal.es6.min.js.gz', + '/media/com_fields/js/admin-field-edit.es6.js', + '/media/com_fields/js/admin-field-edit.es6.min.js', + '/media/com_fields/js/admin-field-edit.es6.min.js.gz', + '/media/com_fields/js/admin-field-typehaschanged.es6.js', + '/media/com_fields/js/admin-field-typehaschanged.es6.min.js', + '/media/com_fields/js/admin-field-typehaschanged.es6.min.js.gz', + '/media/com_fields/js/admin-fields-default-batch.es6.js', + '/media/com_fields/js/admin-fields-default-batch.es6.min.js', + '/media/com_fields/js/admin-fields-default-batch.es6.min.js.gz', + '/media/com_fields/js/admin-fields-modal.es6.js', + '/media/com_fields/js/admin-fields-modal.es6.min.js', + '/media/com_fields/js/admin-fields-modal.es6.min.js.gz', + '/media/com_finder/js/filters.es6.js', + '/media/com_finder/js/filters.es6.min.js', + '/media/com_finder/js/filters.es6.min.js.gz', + '/media/com_finder/js/finder-edit.es6.js', + '/media/com_finder/js/finder-edit.es6.min.js', + '/media/com_finder/js/finder-edit.es6.min.js.gz', + '/media/com_finder/js/finder.es6.js', + '/media/com_finder/js/finder.es6.min.js', + '/media/com_finder/js/finder.es6.min.js.gz', + '/media/com_finder/js/index.es6.js', + '/media/com_finder/js/index.es6.min.js', + '/media/com_finder/js/index.es6.min.js.gz', + '/media/com_finder/js/indexer.es6.js', + '/media/com_finder/js/indexer.es6.min.js', + '/media/com_finder/js/indexer.es6.min.js.gz', + '/media/com_finder/js/maps.es6.js', + '/media/com_finder/js/maps.es6.min.js', + '/media/com_finder/js/maps.es6.min.js.gz', + '/media/com_installer/js/changelog.es6.js', + '/media/com_installer/js/changelog.es6.min.js', + '/media/com_installer/js/changelog.es6.min.js.gz', + '/media/com_installer/js/installer.es6.js', + '/media/com_installer/js/installer.es6.min.js', + '/media/com_installer/js/installer.es6.min.js.gz', + '/media/com_joomlaupdate/js/admin-update-default.es6.js', + '/media/com_joomlaupdate/js/admin-update-default.es6.min.js', + '/media/com_joomlaupdate/js/admin-update-default.es6.min.js.gz', + '/media/com_languages/js/admin-language-edit-change-flag.es6.js', + '/media/com_languages/js/admin-language-edit-change-flag.es6.min.js', + '/media/com_languages/js/admin-language-edit-change-flag.es6.min.js.gz', + '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.js', + '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.min.js', + '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.min.js.gz', + '/media/com_languages/js/overrider.es6.js', + '/media/com_languages/js/overrider.es6.min.js', + '/media/com_languages/js/overrider.es6.min.js.gz', + '/media/com_mails/js/admin-email-template-edit.es6.js', + '/media/com_mails/js/admin-email-template-edit.es6.min.js', + '/media/com_mails/js/admin-email-template-edit.es6.min.js.gz', + '/media/com_media/css/mediamanager.min.css', + '/media/com_media/css/mediamanager.min.css.gz', + '/media/com_media/css/mediamanager.min.css.map', + '/media/com_media/js/edit-images.es6.js', + '/media/com_media/js/edit-images.es6.min.js', + '/media/com_media/js/mediamanager.min.js', + '/media/com_media/js/mediamanager.min.js.gz', + '/media/com_media/js/mediamanager.min.js.map', + '/media/com_menus/js/admin-item-edit.es6.js', + '/media/com_menus/js/admin-item-edit.es6.min.js', + '/media/com_menus/js/admin-item-edit.es6.min.js.gz', + '/media/com_menus/js/admin-item-edit_container.es6.js', + '/media/com_menus/js/admin-item-edit_container.es6.min.js', + '/media/com_menus/js/admin-item-edit_container.es6.min.js.gz', + '/media/com_menus/js/admin-item-edit_modules.es6.js', + '/media/com_menus/js/admin-item-edit_modules.es6.min.js', + '/media/com_menus/js/admin-item-edit_modules.es6.min.js.gz', + '/media/com_menus/js/admin-item-modal.es6.js', + '/media/com_menus/js/admin-item-modal.es6.min.js', + '/media/com_menus/js/admin-item-modal.es6.min.js.gz', + '/media/com_menus/js/admin-items-modal.es6.js', + '/media/com_menus/js/admin-items-modal.es6.min.js', + '/media/com_menus/js/admin-items-modal.es6.min.js.gz', + '/media/com_menus/js/admin-menus-default.es6.js', + '/media/com_menus/js/admin-menus-default.es6.min.js', + '/media/com_menus/js/admin-menus-default.es6.min.js.gz', + '/media/com_menus/js/default-batch-body.es6.js', + '/media/com_menus/js/default-batch-body.es6.min.js', + '/media/com_menus/js/default-batch-body.es6.min.js.gz', + '/media/com_modules/js/admin-module-edit.es6.js', + '/media/com_modules/js/admin-module-edit.es6.min.js', + '/media/com_modules/js/admin-module-edit.es6.min.js.gz', + '/media/com_modules/js/admin-module-edit_assignment.es6.js', + '/media/com_modules/js/admin-module-edit_assignment.es6.min.js', + '/media/com_modules/js/admin-module-edit_assignment.es6.min.js.gz', + '/media/com_modules/js/admin-module-search.es6.js', + '/media/com_modules/js/admin-module-search.es6.min.js', + '/media/com_modules/js/admin-module-search.es6.min.js.gz', + '/media/com_modules/js/admin-modules-modal.es6.js', + '/media/com_modules/js/admin-modules-modal.es6.min.js', + '/media/com_modules/js/admin-modules-modal.es6.min.js.gz', + '/media/com_modules/js/admin-select-modal.es6.js', + '/media/com_modules/js/admin-select-modal.es6.min.js', + '/media/com_modules/js/admin-select-modal.es6.min.js.gz', + '/media/com_tags/js/tag-default.es6.js', + '/media/com_tags/js/tag-default.es6.min.js', + '/media/com_tags/js/tag-default.es6.min.js.gz', + '/media/com_tags/js/tag-list.es6.js', + '/media/com_tags/js/tag-list.es6.min.js', + '/media/com_tags/js/tag-list.es6.min.js.gz', + '/media/com_tags/js/tags-default.es6.js', + '/media/com_tags/js/tags-default.es6.min.js', + '/media/com_tags/js/tags-default.es6.min.js.gz', + '/media/com_templates/js/admin-template-compare.es6.js', + '/media/com_templates/js/admin-template-compare.es6.min.js', + '/media/com_templates/js/admin-template-compare.es6.min.js.gz', + '/media/com_templates/js/admin-template-toggle-assignment.es6.js', + '/media/com_templates/js/admin-template-toggle-assignment.es6.min.js', + '/media/com_templates/js/admin-template-toggle-assignment.es6.min.js.gz', + '/media/com_templates/js/admin-template-toggle-switch.es6.js', + '/media/com_templates/js/admin-template-toggle-switch.es6.min.js', + '/media/com_templates/js/admin-template-toggle-switch.es6.min.js.gz', + '/media/com_templates/js/admin-templates-default.es6.js', + '/media/com_templates/js/admin-templates-default.es6.min.js', + '/media/com_templates/js/admin-templates-default.es6.min.js.gz', + '/media/com_users/js/admin-users-groups.es6.js', + '/media/com_users/js/admin-users-groups.es6.min.js', + '/media/com_users/js/admin-users-groups.es6.min.js.gz', + '/media/com_users/js/admin-users-mail.es6.js', + '/media/com_users/js/admin-users-mail.es6.min.js', + '/media/com_users/js/admin-users-mail.es6.min.js.gz', + '/media/com_users/js/two-factor-switcher.es6.js', + '/media/com_users/js/two-factor-switcher.es6.min.js', + '/media/com_users/js/two-factor-switcher.es6.min.js.gz', + '/media/com_workflow/js/admin-items-workflow-buttons.es6.js', + '/media/com_workflow/js/admin-items-workflow-buttons.es6.min.js', + '/media/com_workflow/js/admin-items-workflow-buttons.es6.min.js.gz', + '/media/com_wrapper/js/iframe-height.es6.js', + '/media/com_wrapper/js/iframe-height.es6.min.js', + '/media/com_wrapper/js/iframe-height.es6.min.js.gz', + '/media/layouts/js/joomla/form/field/category-change.es6.js', + '/media/layouts/js/joomla/form/field/category-change.es6.min.js', + '/media/layouts/js/joomla/form/field/category-change.es6.min.js.gz', + '/media/layouts/js/joomla/html/batch/batch-copymove.es6.js', + '/media/layouts/js/joomla/html/batch/batch-copymove.es6.min.js', + '/media/layouts/js/joomla/html/batch/batch-copymove.es6.min.js.gz', + '/media/legacy/js/highlighter.js', + '/media/legacy/js/highlighter.min.js', + '/media/legacy/js/highlighter.min.js.gz', + '/media/mod_login/js/admin-login.es6.js', + '/media/mod_login/js/admin-login.es6.min.js', + '/media/mod_login/js/admin-login.es6.min.js.gz', + '/media/mod_menu/js/admin-menu.es6.js', + '/media/mod_menu/js/admin-menu.es6.min.js', + '/media/mod_menu/js/admin-menu.es6.min.js.gz', + '/media/mod_menu/js/menu.es6.js', + '/media/mod_menu/js/menu.es6.min.js', + '/media/mod_menu/js/menu.es6.min.js.gz', + '/media/mod_multilangstatus/js/admin-multilangstatus.es6.js', + '/media/mod_multilangstatus/js/admin-multilangstatus.es6.min.js', + '/media/mod_multilangstatus/js/admin-multilangstatus.es6.min.js.gz', + '/media/mod_quickicon/js/quickicon.es6.js', + '/media/mod_quickicon/js/quickicon.es6.min.js', + '/media/mod_quickicon/js/quickicon.es6.min.js.gz', + '/media/mod_sampledata/js/sampledata-process.es6.js', + '/media/mod_sampledata/js/sampledata-process.es6.min.js', + '/media/mod_sampledata/js/sampledata-process.es6.min.js.gz', + '/media/plg_captcha_recaptcha/js/recaptcha.es6.js', + '/media/plg_captcha_recaptcha/js/recaptcha.es6.min.js', + '/media/plg_captcha_recaptcha/js/recaptcha.es6.min.js.gz', + '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.js', + '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.min.js', + '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.min.js.gz', + '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.js', + '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.min.js', + '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.min.js.gz', + '/media/plg_editors_tinymce/js/tinymce-builder.es6.js', + '/media/plg_editors_tinymce/js/tinymce-builder.es6.min.js', + '/media/plg_editors_tinymce/js/tinymce-builder.es6.min.js.gz', + '/media/plg_editors_tinymce/js/tinymce.es6.js', + '/media/plg_editors_tinymce/js/tinymce.es6.min.js', + '/media/plg_editors_tinymce/js/tinymce.es6.min.js.gz', + '/media/plg_installer_folderinstaller/js/folderinstaller.es6.js', + '/media/plg_installer_folderinstaller/js/folderinstaller.es6.min.js', + '/media/plg_installer_folderinstaller/js/folderinstaller.es6.min.js.gz', + '/media/plg_installer_packageinstaller/js/packageinstaller.es6.js', + '/media/plg_installer_packageinstaller/js/packageinstaller.es6.min.js', + '/media/plg_installer_packageinstaller/js/packageinstaller.es6.min.js.gz', + '/media/plg_installer_urlinstaller/js/urlinstaller.es6.js', + '/media/plg_installer_urlinstaller/js/urlinstaller.es6.min.js', + '/media/plg_installer_urlinstaller/js/urlinstaller.es6.min.js.gz', + '/media/plg_installer_webinstaller/js/client.es6.js', + '/media/plg_installer_webinstaller/js/client.es6.min.js', + '/media/plg_installer_webinstaller/js/client.es6.min.js.gz', + '/media/plg_media-action_crop/js/crop.es6.js', + '/media/plg_media-action_crop/js/crop.es6.min.js', + '/media/plg_media-action_crop/js/crop.es6.min.js.gz', + '/media/plg_media-action_resize/js/resize.es6.js', + '/media/plg_media-action_resize/js/resize.es6.min.js', + '/media/plg_media-action_resize/js/resize.es6.min.js.gz', + '/media/plg_media-action_rotate/js/rotate.es6.js', + '/media/plg_media-action_rotate/js/rotate.es6.min.js', + '/media/plg_media-action_rotate/js/rotate.es6.min.js.gz', + '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.js', + '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.min.js', + '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.min.js.gz', + '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.js', + '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.min.js', + '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.min.js.gz', + '/media/plg_quickicon_overridecheck/js/overridecheck.es6.js', + '/media/plg_quickicon_overridecheck/js/overridecheck.es6.min.js', + '/media/plg_quickicon_overridecheck/js/overridecheck.es6.min.js.gz', + '/media/plg_quickicon_privacycheck/js/privacycheck.es6.js', + '/media/plg_quickicon_privacycheck/js/privacycheck.es6.min.js', + '/media/plg_quickicon_privacycheck/js/privacycheck.es6.min.js.gz', + '/media/plg_system_debug/js/debug.es6.js', + '/media/plg_system_debug/js/debug.es6.min.js', + '/media/plg_system_debug/js/debug.es6.min.js.gz', + '/media/plg_system_highlight/highlight.min.css', + '/media/plg_system_highlight/highlight.min.css.gz', + '/media/plg_system_stats/js/stats-message.es6.js', + '/media/plg_system_stats/js/stats-message.es6.min.js', + '/media/plg_system_stats/js/stats-message.es6.min.js.gz', + '/media/plg_system_stats/js/stats.es6.js', + '/media/plg_system_stats/js/stats.es6.min.js', + '/media/plg_system_stats/js/stats.es6.min.js.gz', + '/media/plg_system_webauthn/js/login.es6.js', + '/media/plg_system_webauthn/js/login.es6.min.js', + '/media/plg_system_webauthn/js/login.es6.min.js.gz', + '/media/plg_system_webauthn/js/management.es6.js', + '/media/plg_system_webauthn/js/management.es6.min.js', + '/media/plg_system_webauthn/js/management.es6.min.js.gz', + '/media/plg_user_token/js/token.es6.js', + '/media/plg_user_token/js/token.es6.min.js', + '/media/plg_user_token/js/token.es6.min.js.gz', + '/media/system/js/core.es6.js', + '/media/system/js/core.es6.min.js', + '/media/system/js/core.es6.min.js.gz', + '/media/system/js/draggable.es6.js', + '/media/system/js/draggable.es6.min.js', + '/media/system/js/draggable.es6.min.js.gz', + '/media/system/js/fields/joomla-field-color-slider.es6.js', + '/media/system/js/fields/joomla-field-color-slider.es6.min.js', + '/media/system/js/fields/joomla-field-color-slider.es6.min.js.gz', + '/media/system/js/fields/passwordstrength.es6.js', + '/media/system/js/fields/passwordstrength.es6.min.js', + '/media/system/js/fields/passwordstrength.es6.min.js.gz', + '/media/system/js/fields/passwordview.es6.js', + '/media/system/js/fields/passwordview.es6.min.js', + '/media/system/js/fields/passwordview.es6.min.js.gz', + '/media/system/js/fields/select-colour.es6.js', + '/media/system/js/fields/select-colour.es6.min.js', + '/media/system/js/fields/select-colour.es6.min.js.gz', + '/media/system/js/fields/validate.es6.js', + '/media/system/js/fields/validate.es6.min.js', + '/media/system/js/fields/validate.es6.min.js.gz', + '/media/system/js/keepalive.es6.js', + '/media/system/js/keepalive.es6.min.js', + '/media/system/js/keepalive.es6.min.js.gz', + '/media/system/js/multiselect.es6.js', + '/media/system/js/multiselect.es6.min.js', + '/media/system/js/multiselect.es6.min.js.gz', + '/media/system/js/searchtools.es6.js', + '/media/system/js/searchtools.es6.min.js', + '/media/system/js/searchtools.es6.min.js.gz', + '/media/system/js/showon.es6.js', + '/media/system/js/showon.es6.min.js', + '/media/system/js/showon.es6.min.js.gz', + '/media/templates/atum/js/template.es6.js', + '/media/templates/atum/js/template.es6.min.js', + '/media/templates/atum/js/template.es6.min.js.gz', + '/media/templates/atum/js/template.js', + '/media/templates/atum/js/template.min.js', + '/media/templates/atum/js/template.min.js.gz', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.min.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.min.js.gz', + '/media/vendor/bootstrap/js/alert.es6.js', + '/media/vendor/bootstrap/js/alert.es6.min.js', + '/media/vendor/bootstrap/js/alert.es6.min.js.gz', + '/media/vendor/bootstrap/js/bootstrap.es5.js', + '/media/vendor/bootstrap/js/bootstrap.es5.min.js', + '/media/vendor/bootstrap/js/bootstrap.es5.min.js.gz', + '/media/vendor/bootstrap/js/button.es6.js', + '/media/vendor/bootstrap/js/button.es6.min.js', + '/media/vendor/bootstrap/js/button.es6.min.js.gz', + '/media/vendor/bootstrap/js/carousel.es6.js', + '/media/vendor/bootstrap/js/carousel.es6.min.js', + '/media/vendor/bootstrap/js/carousel.es6.min.js.gz', + '/media/vendor/bootstrap/js/collapse.es6.js', + '/media/vendor/bootstrap/js/collapse.es6.min.js', + '/media/vendor/bootstrap/js/collapse.es6.min.js.gz', + '/media/vendor/bootstrap/js/dom-8eef6b5f.js', + '/media/vendor/bootstrap/js/dropdown.es6.js', + '/media/vendor/bootstrap/js/dropdown.es6.min.js', + '/media/vendor/bootstrap/js/dropdown.es6.min.js.gz', + '/media/vendor/bootstrap/js/modal.es6.js', + '/media/vendor/bootstrap/js/modal.es6.min.js', + '/media/vendor/bootstrap/js/modal.es6.min.js.gz', + '/media/vendor/bootstrap/js/popover.es6.js', + '/media/vendor/bootstrap/js/popover.es6.min.js', + '/media/vendor/bootstrap/js/popover.es6.min.js.gz', + '/media/vendor/bootstrap/js/popper-5304749a.js', + '/media/vendor/bootstrap/js/scrollspy.es6.js', + '/media/vendor/bootstrap/js/scrollspy.es6.min.js', + '/media/vendor/bootstrap/js/scrollspy.es6.min.js.gz', + '/media/vendor/bootstrap/js/tab.es6.js', + '/media/vendor/bootstrap/js/tab.es6.min.js', + '/media/vendor/bootstrap/js/tab.es6.min.js.gz', + '/media/vendor/bootstrap/js/toast.es6.js', + '/media/vendor/bootstrap/js/toast.es6.min.js', + '/media/vendor/bootstrap/js/toast.es6.min.js.gz', + '/media/vendor/codemirror/lib/codemirror-ce.js', + '/media/vendor/codemirror/lib/codemirror-ce.min.js', + '/media/vendor/codemirror/lib/codemirror-ce.min.js.gz', + '/media/vendor/punycode/js/punycode.js', + '/media/vendor/punycode/js/punycode.min.js', + '/media/vendor/punycode/js/punycode.min.js.gz', + '/media/vendor/tinymce/changelog.txt', + '/media/vendor/webcomponentsjs/js/webcomponents-ce.js', + '/media/vendor/webcomponentsjs/js/webcomponents-ce.min.js', + '/media/vendor/webcomponentsjs/js/webcomponents-ce.min.js.gz', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.min.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.min.js.gz', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.min.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.min.js.gz', + '/media/vendor/webcomponentsjs/js/webcomponents-sd.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd.min.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd.min.js.gz', + '/plugins/fields/subfields/params/subfields.xml', + '/plugins/fields/subfields/subfields.php', + '/plugins/fields/subfields/subfields.xml', + '/plugins/fields/subfields/tmpl/subfields.php', + '/templates/cassiopeia/images/system/rating_star.png', + '/templates/cassiopeia/images/system/rating_star_blank.png', + '/templates/cassiopeia/scss/tools/mixins/_margin.scss', + '/templates/cassiopeia/scss/tools/mixins/_visually-hidden.scss', + '/templates/system/js/error-locales.js', + // 4.0 from RC 1 to RC 2 + '/administrator/components/com_fields/tmpl/field/modal.php', + '/administrator/templates/atum/scss/pages/_com_admin.scss', + '/administrator/templates/atum/scss/pages/_com_finder.scss', + '/libraries/src/Error/JsonApi/InstallLanguageExceptionHandler.php', + '/libraries/src/MVC/Controller/Exception/InstallLanguage.php', + '/media/com_fields/js/admin-field-edit-modal-es5.js', + '/media/com_fields/js/admin-field-edit-modal-es5.min.js', + '/media/com_fields/js/admin-field-edit-modal-es5.min.js.gz', + '/media/com_fields/js/admin-field-edit-modal.js', + '/media/com_fields/js/admin-field-edit-modal.min.js', + '/media/com_fields/js/admin-field-edit-modal.min.js.gz', + // 4.0 from RC 3 to RC 4 + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_nodownload.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_noupdate.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_preupdatecheck.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_reinstall.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_update.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_updatemefirst.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_upload.php', + '/language/en-GB/com_messages.ini', + '/media/system/css/fields/joomla-image-select.css', + '/media/system/css/fields/joomla-image-select.min.css', + '/media/system/css/fields/joomla-image-select.min.css.gz', + '/media/system/js/fields/joomla-image-select-es5.js', + '/media/system/js/fields/joomla-image-select-es5.min.js', + '/media/system/js/fields/joomla-image-select-es5.min.js.gz', + '/media/system/js/fields/joomla-image-select.js', + '/media/system/js/fields/joomla-image-select.min.js', + '/media/system/js/fields/joomla-image-select.min.js.gz', + // 4.0 from RC 4 to RC 5 + '/media/system/js/fields/calendar-locales/af.min.js', + '/media/system/js/fields/calendar-locales/af.min.js.gz', + '/media/system/js/fields/calendar-locales/ar.min.js', + '/media/system/js/fields/calendar-locales/ar.min.js.gz', + '/media/system/js/fields/calendar-locales/bg.min.js', + '/media/system/js/fields/calendar-locales/bg.min.js.gz', + '/media/system/js/fields/calendar-locales/bn.min.js', + '/media/system/js/fields/calendar-locales/bn.min.js.gz', + '/media/system/js/fields/calendar-locales/bs.min.js', + '/media/system/js/fields/calendar-locales/bs.min.js.gz', + '/media/system/js/fields/calendar-locales/ca.min.js', + '/media/system/js/fields/calendar-locales/ca.min.js.gz', + '/media/system/js/fields/calendar-locales/cs.min.js', + '/media/system/js/fields/calendar-locales/cs.min.js.gz', + '/media/system/js/fields/calendar-locales/cy.min.js', + '/media/system/js/fields/calendar-locales/cy.min.js.gz', + '/media/system/js/fields/calendar-locales/da.min.js', + '/media/system/js/fields/calendar-locales/da.min.js.gz', + '/media/system/js/fields/calendar-locales/de.min.js', + '/media/system/js/fields/calendar-locales/de.min.js.gz', + '/media/system/js/fields/calendar-locales/el.min.js', + '/media/system/js/fields/calendar-locales/el.min.js.gz', + '/media/system/js/fields/calendar-locales/en.min.js', + '/media/system/js/fields/calendar-locales/en.min.js.gz', + '/media/system/js/fields/calendar-locales/es.min.js', + '/media/system/js/fields/calendar-locales/es.min.js.gz', + '/media/system/js/fields/calendar-locales/eu.min.js', + '/media/system/js/fields/calendar-locales/eu.min.js.gz', + '/media/system/js/fields/calendar-locales/fa-ir.min.js', + '/media/system/js/fields/calendar-locales/fa-ir.min.js.gz', + '/media/system/js/fields/calendar-locales/fi.min.js', + '/media/system/js/fields/calendar-locales/fi.min.js.gz', + '/media/system/js/fields/calendar-locales/fr.min.js', + '/media/system/js/fields/calendar-locales/fr.min.js.gz', + '/media/system/js/fields/calendar-locales/ga.min.js', + '/media/system/js/fields/calendar-locales/ga.min.js.gz', + '/media/system/js/fields/calendar-locales/hr.min.js', + '/media/system/js/fields/calendar-locales/hr.min.js.gz', + '/media/system/js/fields/calendar-locales/hu.min.js', + '/media/system/js/fields/calendar-locales/hu.min.js.gz', + '/media/system/js/fields/calendar-locales/it.min.js', + '/media/system/js/fields/calendar-locales/it.min.js.gz', + '/media/system/js/fields/calendar-locales/ja.min.js', + '/media/system/js/fields/calendar-locales/ja.min.js.gz', + '/media/system/js/fields/calendar-locales/ka.min.js', + '/media/system/js/fields/calendar-locales/ka.min.js.gz', + '/media/system/js/fields/calendar-locales/kk.min.js', + '/media/system/js/fields/calendar-locales/kk.min.js.gz', + '/media/system/js/fields/calendar-locales/ko.min.js', + '/media/system/js/fields/calendar-locales/ko.min.js.gz', + '/media/system/js/fields/calendar-locales/lt.min.js', + '/media/system/js/fields/calendar-locales/lt.min.js.gz', + '/media/system/js/fields/calendar-locales/mk.min.js', + '/media/system/js/fields/calendar-locales/mk.min.js.gz', + '/media/system/js/fields/calendar-locales/nb.min.js', + '/media/system/js/fields/calendar-locales/nb.min.js.gz', + '/media/system/js/fields/calendar-locales/nl.min.js', + '/media/system/js/fields/calendar-locales/nl.min.js.gz', + '/media/system/js/fields/calendar-locales/pl.min.js', + '/media/system/js/fields/calendar-locales/pl.min.js.gz', + '/media/system/js/fields/calendar-locales/prs-af.min.js', + '/media/system/js/fields/calendar-locales/prs-af.min.js.gz', + '/media/system/js/fields/calendar-locales/pt.min.js', + '/media/system/js/fields/calendar-locales/pt.min.js.gz', + '/media/system/js/fields/calendar-locales/ru.min.js', + '/media/system/js/fields/calendar-locales/ru.min.js.gz', + '/media/system/js/fields/calendar-locales/sk.min.js', + '/media/system/js/fields/calendar-locales/sk.min.js.gz', + '/media/system/js/fields/calendar-locales/sl.min.js', + '/media/system/js/fields/calendar-locales/sl.min.js.gz', + '/media/system/js/fields/calendar-locales/sr-rs.min.js', + '/media/system/js/fields/calendar-locales/sr-rs.min.js.gz', + '/media/system/js/fields/calendar-locales/sr-yu.min.js', + '/media/system/js/fields/calendar-locales/sr-yu.min.js.gz', + '/media/system/js/fields/calendar-locales/sv.min.js', + '/media/system/js/fields/calendar-locales/sv.min.js.gz', + '/media/system/js/fields/calendar-locales/sw.min.js', + '/media/system/js/fields/calendar-locales/sw.min.js.gz', + '/media/system/js/fields/calendar-locales/ta.min.js', + '/media/system/js/fields/calendar-locales/ta.min.js.gz', + '/media/system/js/fields/calendar-locales/th.min.js', + '/media/system/js/fields/calendar-locales/th.min.js.gz', + '/media/system/js/fields/calendar-locales/uk.min.js', + '/media/system/js/fields/calendar-locales/uk.min.js.gz', + '/media/system/js/fields/calendar-locales/zh-CN.min.js', + '/media/system/js/fields/calendar-locales/zh-CN.min.js.gz', + '/media/system/js/fields/calendar-locales/zh-TW.min.js', + '/media/system/js/fields/calendar-locales/zh-TW.min.js.gz', + // 4.0 from RC 5 to RC 6 + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.min.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.min.js.gz', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.min.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.min.js.gz', + '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.css', + '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.min.css', + '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.min.css.gz', + '/templates/cassiopeia/scss/vendor/fontawesome-free/fontawesome.scss', + // 4.0 from RC 6 to 4.0.0 (stable) + '/libraries/vendor/algo26-matthias/idna-convert/tests/integration/ToIdnTest.php', + '/libraries/vendor/algo26-matthias/idna-convert/tests/integration/ToUnicodeTest.php', + '/libraries/vendor/algo26-matthias/idna-convert/tests/unit/.gitkeep', + '/libraries/vendor/algo26-matthias/idna-convert/tests/unit/namePrepTest.php', + '/libraries/vendor/doctrine/inflector/docs/en/index.rst', + '/libraries/vendor/jakeasmith/http_build_url/tests/HttpBuildUrlTest.php', + '/libraries/vendor/jakeasmith/http_build_url/tests/bootstrap.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/AcceptLanguageTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/AcceptTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/BaseAcceptTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/CharsetNegotiatorTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/EncodingNegotiatorTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/LanguageNegotiatorTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/MatchTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/NegotiatorTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/TestCase.php', + '/libraries/vendor/willdurand/negotiation/tests/bootstrap.php', + // From 4.0.2 to 4.0.3 + '/templates/cassiopeia/css/global/fonts-web_fira-sans.css', + '/templates/cassiopeia/css/global/fonts-web_fira-sans.min.css', + '/templates/cassiopeia/css/global/fonts-web_fira-sans.min.css.gz', + '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.css', + '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.min.css', + '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.min.css.gz', + '/templates/cassiopeia/scss/global/fonts-web_fira-sans.scss', + '/templates/cassiopeia/scss/global/fonts-web_roboto+noto-sans.scss', + // From 4.0.3 to 4.0.4 + '/administrator/templates/atum/scss/_mixin.scss', + '/media/com_joomlaupdate/js/encryption.min.js.gz', + '/media/com_joomlaupdate/js/update.min.js.gz', + '/templates/cassiopeia/images/system/sort_asc.png', + '/templates/cassiopeia/images/system/sort_desc.png', + // From 4.0.4 to 4.0.5 + '/media/vendor/codemirror/lib/#codemirror.js#', + // From 4.0.5 to 4.0.6 + '/media/vendor/mediaelement/css/mejs-controls.png', + // From 4.0.x to 4.1.0-beta1 + '/administrator/templates/atum/css/system/searchtools/searchtools.css', + '/administrator/templates/atum/css/system/searchtools/searchtools.min.css', + '/administrator/templates/atum/css/system/searchtools/searchtools.min.css.gz', + '/administrator/templates/atum/css/template-rtl.css', + '/administrator/templates/atum/css/template-rtl.min.css', + '/administrator/templates/atum/css/template-rtl.min.css.gz', + '/administrator/templates/atum/css/template.css', + '/administrator/templates/atum/css/template.min.css', + '/administrator/templates/atum/css/template.min.css.gz', + '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.css', + '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.min.css', + '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.min.css.gz', + '/administrator/templates/atum/css/vendor/choicesjs/choices.css', + '/administrator/templates/atum/css/vendor/choicesjs/choices.min.css', + '/administrator/templates/atum/css/vendor/choicesjs/choices.min.css.gz', + '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.css', + '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.min.css', + '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.min.css.gz', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.css', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.min.css', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.min.css.gz', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.css', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.min.css', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.min.css.gz', + '/administrator/templates/atum/css/vendor/minicolors/minicolors.css', + '/administrator/templates/atum/css/vendor/minicolors/minicolors.min.css', + '/administrator/templates/atum/css/vendor/minicolors/minicolors.min.css.gz', + '/administrator/templates/atum/images/joomla-pattern.svg', + '/administrator/templates/atum/images/logos/brand-large.svg', + '/administrator/templates/atum/images/logos/brand-small.svg', + '/administrator/templates/atum/images/logos/login.svg', + '/administrator/templates/atum/images/select-bg-active-rtl.svg', + '/administrator/templates/atum/images/select-bg-active.svg', + '/administrator/templates/atum/images/select-bg-rtl.svg', + '/administrator/templates/atum/images/select-bg.svg', + '/administrator/templates/atum/scss/_root.scss', + '/administrator/templates/atum/scss/_variables.scss', + '/administrator/templates/atum/scss/blocks/_alerts.scss', + '/administrator/templates/atum/scss/blocks/_edit.scss', + '/administrator/templates/atum/scss/blocks/_form.scss', + '/administrator/templates/atum/scss/blocks/_global.scss', + '/administrator/templates/atum/scss/blocks/_header.scss', + '/administrator/templates/atum/scss/blocks/_icons.scss', + '/administrator/templates/atum/scss/blocks/_iframe.scss', + '/administrator/templates/atum/scss/blocks/_layout.scss', + '/administrator/templates/atum/scss/blocks/_lists.scss', + '/administrator/templates/atum/scss/blocks/_login.scss', + '/administrator/templates/atum/scss/blocks/_modals.scss', + '/administrator/templates/atum/scss/blocks/_quickicons.scss', + '/administrator/templates/atum/scss/blocks/_sidebar-nav.scss', + '/administrator/templates/atum/scss/blocks/_sidebar.scss', + '/administrator/templates/atum/scss/blocks/_switcher.scss', + '/administrator/templates/atum/scss/blocks/_toolbar.scss', + '/administrator/templates/atum/scss/blocks/_treeselect.scss', + '/administrator/templates/atum/scss/blocks/_utilities.scss', + '/administrator/templates/atum/scss/pages/_com_config.scss', + '/administrator/templates/atum/scss/pages/_com_content.scss', + '/administrator/templates/atum/scss/pages/_com_cpanel.scss', + '/administrator/templates/atum/scss/pages/_com_joomlaupdate.scss', + '/administrator/templates/atum/scss/pages/_com_modules.scss', + '/administrator/templates/atum/scss/pages/_com_privacy.scss', + '/administrator/templates/atum/scss/pages/_com_tags.scss', + '/administrator/templates/atum/scss/pages/_com_templates.scss', + '/administrator/templates/atum/scss/pages/_com_users.scss', + '/administrator/templates/atum/scss/system/searchtools/searchtools.scss', + '/administrator/templates/atum/scss/template-rtl.scss', + '/administrator/templates/atum/scss/template.scss', + '/administrator/templates/atum/scss/vendor/_bootstrap.scss', + '/administrator/templates/atum/scss/vendor/_codemirror.scss', + '/administrator/templates/atum/scss/vendor/_dragula.scss', + '/administrator/templates/atum/scss/vendor/_tinymce.scss', + '/administrator/templates/atum/scss/vendor/awesomplete/awesomplete.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_badge.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_bootstrap-rtl.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_buttons.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_card.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_collapse.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_custom-forms.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_dropdown.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_form.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_lists.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_modal.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_pagination.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_reboot.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_table.scss', + '/administrator/templates/atum/scss/vendor/choicesjs/choices.scss', + '/administrator/templates/atum/scss/vendor/fontawesome-free/fontawesome.scss', + '/administrator/templates/atum/scss/vendor/joomla-custom-elements/joomla-alert.scss', + '/administrator/templates/atum/scss/vendor/joomla-custom-elements/joomla-tab.scss', + '/administrator/templates/atum/scss/vendor/minicolors/minicolors.scss', + '/administrator/templates/atum/template_preview.png', + '/administrator/templates/atum/template_thumbnail.png', + '/administrator/templates/system/css/error.css', + '/administrator/templates/system/css/error.min.css', + '/administrator/templates/system/css/error.min.css.gz', + '/administrator/templates/system/css/system.css', + '/administrator/templates/system/css/system.min.css', + '/administrator/templates/system/css/system.min.css.gz', + '/administrator/templates/system/images/calendar.png', + '/administrator/templates/system/scss/error.scss', + '/administrator/templates/system/scss/system.scss', + '/templates/cassiopeia/css/editor.css', + '/templates/cassiopeia/css/editor.min.css', + '/templates/cassiopeia/css/editor.min.css.gz', + '/templates/cassiopeia/css/global/colors_alternative.css', + '/templates/cassiopeia/css/global/colors_alternative.min.css', + '/templates/cassiopeia/css/global/colors_alternative.min.css.gz', + '/templates/cassiopeia/css/global/colors_standard.css', + '/templates/cassiopeia/css/global/colors_standard.min.css', + '/templates/cassiopeia/css/global/colors_standard.min.css.gz', + '/templates/cassiopeia/css/global/fonts-local_roboto.css', + '/templates/cassiopeia/css/global/fonts-local_roboto.min.css', + '/templates/cassiopeia/css/global/fonts-local_roboto.min.css.gz', + '/templates/cassiopeia/css/offline.css', + '/templates/cassiopeia/css/offline.min.css', + '/templates/cassiopeia/css/offline.min.css.gz', + '/templates/cassiopeia/css/system/searchtools/searchtools.css', + '/templates/cassiopeia/css/system/searchtools/searchtools.min.css', + '/templates/cassiopeia/css/system/searchtools/searchtools.min.css.gz', + '/templates/cassiopeia/css/template-rtl.css', + '/templates/cassiopeia/css/template-rtl.min.css', + '/templates/cassiopeia/css/template-rtl.min.css.gz', + '/templates/cassiopeia/css/template.css', + '/templates/cassiopeia/css/template.min.css', + '/templates/cassiopeia/css/template.min.css.gz', + '/templates/cassiopeia/css/vendor/choicesjs/choices.css', + '/templates/cassiopeia/css/vendor/choicesjs/choices.min.css', + '/templates/cassiopeia/css/vendor/choicesjs/choices.min.css.gz', + '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.css', + '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.min.css', + '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.min.css.gz', + '/templates/cassiopeia/images/logo.svg', + '/templates/cassiopeia/images/select-bg-active-rtl.svg', + '/templates/cassiopeia/images/select-bg-active.svg', + '/templates/cassiopeia/images/select-bg-rtl.svg', + '/templates/cassiopeia/images/select-bg.svg', + '/templates/cassiopeia/js/template.es5.js', + '/templates/cassiopeia/js/template.js', + '/templates/cassiopeia/js/template.min.js', + '/templates/cassiopeia/js/template.min.js.gz', + '/templates/cassiopeia/scss/blocks/_alerts.scss', + '/templates/cassiopeia/scss/blocks/_back-to-top.scss', + '/templates/cassiopeia/scss/blocks/_banner.scss', + '/templates/cassiopeia/scss/blocks/_css-grid.scss', + '/templates/cassiopeia/scss/blocks/_footer.scss', + '/templates/cassiopeia/scss/blocks/_form.scss', + '/templates/cassiopeia/scss/blocks/_frontend-edit.scss', + '/templates/cassiopeia/scss/blocks/_global.scss', + '/templates/cassiopeia/scss/blocks/_header.scss', + '/templates/cassiopeia/scss/blocks/_icons.scss', + '/templates/cassiopeia/scss/blocks/_iframe.scss', + '/templates/cassiopeia/scss/blocks/_layout.scss', + '/templates/cassiopeia/scss/blocks/_legacy.scss', + '/templates/cassiopeia/scss/blocks/_modals.scss', + '/templates/cassiopeia/scss/blocks/_modifiers.scss', + '/templates/cassiopeia/scss/blocks/_tags.scss', + '/templates/cassiopeia/scss/blocks/_toolbar.scss', + '/templates/cassiopeia/scss/blocks/_utilities.scss', + '/templates/cassiopeia/scss/editor.scss', + '/templates/cassiopeia/scss/global/colors_alternative.scss', + '/templates/cassiopeia/scss/global/colors_standard.scss', + '/templates/cassiopeia/scss/global/fonts-local_roboto.scss', + '/templates/cassiopeia/scss/offline.scss', + '/templates/cassiopeia/scss/system/searchtools/searchtools.scss', + '/templates/cassiopeia/scss/template-rtl.scss', + '/templates/cassiopeia/scss/template.scss', + '/templates/cassiopeia/scss/tools/_tools.scss', + '/templates/cassiopeia/scss/tools/functions/_max-width.scss', + '/templates/cassiopeia/scss/tools/variables/_variables.scss', + '/templates/cassiopeia/scss/vendor/_awesomplete.scss', + '/templates/cassiopeia/scss/vendor/_chosen.scss', + '/templates/cassiopeia/scss/vendor/_dragula.scss', + '/templates/cassiopeia/scss/vendor/_minicolors.scss', + '/templates/cassiopeia/scss/vendor/_tinymce.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_bootstrap-rtl.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_buttons.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_collapse.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_custom-forms.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_dropdown.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_forms.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_lists.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_modal.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_nav.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_pagination.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_table.scss', + '/templates/cassiopeia/scss/vendor/choicesjs/choices.scss', + '/templates/cassiopeia/scss/vendor/joomla-custom-elements/joomla-alert.scss', + '/templates/cassiopeia/scss/vendor/metismenu/_metismenu.scss', + '/templates/cassiopeia/template_preview.png', + '/templates/cassiopeia/template_thumbnail.png', + '/templates/system/css/editor.css', + '/templates/system/css/editor.min.css', + '/templates/system/css/editor.min.css.gz', + '/templates/system/css/error.css', + '/templates/system/css/error.min.css', + '/templates/system/css/error.min.css.gz', + '/templates/system/css/error_rtl.css', + '/templates/system/css/error_rtl.min.css', + '/templates/system/css/error_rtl.min.css.gz', + '/templates/system/css/general.css', + '/templates/system/css/general.min.css', + '/templates/system/css/general.min.css.gz', + '/templates/system/css/offline.css', + '/templates/system/css/offline.min.css', + '/templates/system/css/offline.min.css.gz', + '/templates/system/css/offline_rtl.css', + '/templates/system/css/offline_rtl.min.css', + '/templates/system/css/offline_rtl.min.css.gz', + '/templates/system/scss/editor.scss', + '/templates/system/scss/error.scss', + '/templates/system/scss/error_rtl.scss', + '/templates/system/scss/general.scss', + '/templates/system/scss/offline.scss', + '/templates/system/scss/offline_rtl.scss', + // From 4.1.0-beta3 to 4.1.0-rc1 + '/api/components/com_media/src/Helper/AdapterTrait.php', + // From 4.1.0 to 4.1.1 + '/libraries/vendor/tobscure/json-api/.git/HEAD', + '/libraries/vendor/tobscure/json-api/.git/ORIG_HEAD', + '/libraries/vendor/tobscure/json-api/.git/config', + '/libraries/vendor/tobscure/json-api/.git/description', + '/libraries/vendor/tobscure/json-api/.git/hooks/applypatch-msg.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/commit-msg.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/fsmonitor-watchman.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/post-update.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-applypatch.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-commit.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-merge-commit.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-push.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-rebase.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-receive.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/prepare-commit-msg.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/push-to-checkout.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/update.sample', + '/libraries/vendor/tobscure/json-api/.git/index', + '/libraries/vendor/tobscure/json-api/.git/info/exclude', + '/libraries/vendor/tobscure/json-api/.git/info/refs', + '/libraries/vendor/tobscure/json-api/.git/logs/HEAD', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/heads/joomla-backports', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes/origin/HEAD', + '/libraries/vendor/tobscure/json-api/.git/objects/info/packs', + '/libraries/vendor/tobscure/json-api/.git/objects/pack/pack-51530cba04703b17f3c11b9e8458a171092cf5e3.idx', + '/libraries/vendor/tobscure/json-api/.git/objects/pack/pack-51530cba04703b17f3c11b9e8458a171092cf5e3.pack', + '/libraries/vendor/tobscure/json-api/.git/packed-refs', + '/libraries/vendor/tobscure/json-api/.git/refs/heads/joomla-backports', + '/libraries/vendor/tobscure/json-api/.git/refs/remotes/origin/HEAD', + '/libraries/vendor/tobscure/json-api/.php_cs', + '/libraries/vendor/tobscure/json-api/tests/AbstractSerializerTest.php', + '/libraries/vendor/tobscure/json-api/tests/AbstractTestCase.php', + '/libraries/vendor/tobscure/json-api/tests/CollectionTest.php', + '/libraries/vendor/tobscure/json-api/tests/DocumentTest.php', + '/libraries/vendor/tobscure/json-api/tests/ErrorHandlerTest.php', + '/libraries/vendor/tobscure/json-api/tests/Exception/Handler/FallbackExceptionHandlerTest.php', + '/libraries/vendor/tobscure/json-api/tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php', + '/libraries/vendor/tobscure/json-api/tests/LinksTraitTest.php', + '/libraries/vendor/tobscure/json-api/tests/ParametersTest.php', + '/libraries/vendor/tobscure/json-api/tests/ResourceTest.php', + '/libraries/vendor/tobscure/json-api/tests/UtilTest.php', + // From 4.1.1 to 4.1.2 + '/administrator/components/com_users/src/Field/PrimaryauthprovidersField.php', + // From 4.1.2 to 4.1.3 + '/libraries/vendor/webmozart/assert/.php_cs', + // From 4.1.3 to 4.1.4 + '/libraries/vendor/maximebf/debugbar/.bowerrc', + '/libraries/vendor/maximebf/debugbar/bower.json', + '/libraries/vendor/maximebf/debugbar/build/namespaceFontAwesome.php', + '/libraries/vendor/maximebf/debugbar/demo/ajax.php', + '/libraries/vendor/maximebf/debugbar/demo/ajax_exception.php', + '/libraries/vendor/maximebf/debugbar/demo/bootstrap.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/cachecache/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/bootstrap.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/build.sh', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/cli-config.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src/Demo/Product.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/monolog/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/build.properties', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/build.sh', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/runtime-conf.xml', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/schema.xml', + '/libraries/vendor/maximebf/debugbar/demo/bridge/slim/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/swiftmailer/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/foobar.html', + '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/hello.html', + '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/index.php', + '/libraries/vendor/maximebf/debugbar/demo/dump_assets.php', + '/libraries/vendor/maximebf/debugbar/demo/index.php', + '/libraries/vendor/maximebf/debugbar/demo/open.php', + '/libraries/vendor/maximebf/debugbar/demo/pdo.php', + '/libraries/vendor/maximebf/debugbar/demo/stack.php', + '/libraries/vendor/maximebf/debugbar/docs/ajax_and_stack.md', + '/libraries/vendor/maximebf/debugbar/docs/base_collectors.md', + '/libraries/vendor/maximebf/debugbar/docs/bridge_collectors.md', + '/libraries/vendor/maximebf/debugbar/docs/data_collectors.md', + '/libraries/vendor/maximebf/debugbar/docs/data_formatter.md', + '/libraries/vendor/maximebf/debugbar/docs/http_drivers.md', + '/libraries/vendor/maximebf/debugbar/docs/javascript_bar.md', + '/libraries/vendor/maximebf/debugbar/docs/manifest.json', + '/libraries/vendor/maximebf/debugbar/docs/openhandler.md', + '/libraries/vendor/maximebf/debugbar/docs/rendering.md', + '/libraries/vendor/maximebf/debugbar/docs/screenshot.png', + '/libraries/vendor/maximebf/debugbar/docs/storage.md', + '/libraries/vendor/maximebf/debugbar/docs/style.css', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/AggregatedCollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/ConfigCollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/MessagesCollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/MockCollector.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/Propel2CollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/TimeDataCollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter/DataFormatterTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter/DebugBarVarDumperTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DebugBarTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DebugBarTestCase.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/JavascriptRendererTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/MockHttpDriver.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/OpenHandlerTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage/FileStorageTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage/MockStorage.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/TracedStatementTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/full_init.html', + '/libraries/vendor/maximebf/debugbar/tests/bootstrap.php', + // From 4.1 to 4.2.0-beta1 + '/libraries/src/Service/Provider/ApiRouter.php', + '/libraries/vendor/nyholm/psr7/doc/final.md', + '/media/com_finder/js/index-es5.js', + '/media/com_finder/js/index-es5.min.js', + '/media/com_finder/js/index-es5.min.js.gz', + '/media/com_finder/js/index.js', + '/media/com_finder/js/index.min.js', + '/media/com_finder/js/index.min.js.gz', + '/media/com_users/js/two-factor-switcher-es5.js', + '/media/com_users/js/two-factor-switcher-es5.min.js', + '/media/com_users/js/two-factor-switcher-es5.min.js.gz', + '/media/com_users/js/two-factor-switcher.js', + '/media/com_users/js/two-factor-switcher.min.js', + '/media/com_users/js/two-factor-switcher.min.js.gz', + '/modules/mod_articles_news/mod_articles_news.php', + '/plugins/actionlog/joomla/joomla.php', + '/plugins/api-authentication/basic/basic.php', + '/plugins/api-authentication/token/token.php', + '/plugins/system/cache/cache.php', + '/plugins/twofactorauth/totp/postinstall/actions.php', + '/plugins/twofactorauth/totp/tmpl/form.php', + '/plugins/twofactorauth/totp/totp.php', + '/plugins/twofactorauth/totp/totp.xml', + '/plugins/twofactorauth/yubikey/tmpl/form.php', + '/plugins/twofactorauth/yubikey/yubikey.php', + '/plugins/twofactorauth/yubikey/yubikey.xml', + // From 4.2.0-beta1 to 4.2.0-beta2 + '/layouts/plugins/user/profile/fields/dob.php', + '/modules/mod_articles_latest/mod_articles_latest.php', + '/plugins/behaviour/taggable/taggable.php', + '/plugins/behaviour/versionable/versionable.php', + '/plugins/task/requests/requests.php', + '/plugins/task/sitestatus/sitestatus.php', + '/plugins/user/profile/src/Field/DobField.php', + // From 4.2.0-beta2 to 4.2.0-beta3 + '/plugins/system/webauthn/src/Exception/AjaxNonCmsAppException.php', + '/plugins/system/webauthn/src/Helper/CredentialsCreation.php', + '/plugins/system/webauthn/src/Helper/Joomla.php', + '/plugins/system/webauthn/webauthn.php', + '/plugins/task/checkfiles/checkfiles.php', + '/plugins/task/demotasks/demotasks.php', + // From 4.2.0-rc1 to 4.2.0 + '/administrator/language/en-GB/plg_fields_menuitem.ini', + '/administrator/language/en-GB/plg_fields_menuitem.sys.ini', + '/plugins/fields/menuitem/menuitem.php', + '/plugins/fields/menuitem/menuitem.xml', + '/plugins/fields/menuitem/tmpl/menuitem.php', + // From 4.2.0 to 4.2.1 + '/media/vendor/hotkeys.js/js/hotkeys.js', + '/media/vendor/hotkeys.js/js/hotkeys.min.js', + '/media/vendor/hotkeys.js/js/hotkeys.min.js.gz', + '/media/vendor/hotkeys.js/LICENSE', + // From 4.2.1 to 4.2.2 + '/administrator/cache/fido.jwt', + ); + + $folders = array( + // From 3.10 to 4.1 + '/templates/system/images', + '/templates/system/html', + '/templates/protostar/less', + '/templates/protostar/language/en-GB', + '/templates/protostar/language', + '/templates/protostar/js', + '/templates/protostar/img', + '/templates/protostar/images/system', + '/templates/protostar/images', + '/templates/protostar/html/layouts/joomla/system', + '/templates/protostar/html/layouts/joomla/form/field', + '/templates/protostar/html/layouts/joomla/form', + '/templates/protostar/html/layouts/joomla', + '/templates/protostar/html/layouts', + '/templates/protostar/html/com_media/imageslist', + '/templates/protostar/html/com_media', + '/templates/protostar/html', + '/templates/protostar/css', + '/templates/protostar', + '/templates/beez3/language/en-GB', + '/templates/beez3/language', + '/templates/beez3/javascript', + '/templates/beez3/images/system', + '/templates/beez3/images/personal', + '/templates/beez3/images/nature', + '/templates/beez3/images', + '/templates/beez3/html/mod_login', + '/templates/beez3/html/mod_languages', + '/templates/beez3/html/mod_breadcrumbs', + '/templates/beez3/html/layouts/joomla/system', + '/templates/beez3/html/layouts/joomla', + '/templates/beez3/html/layouts', + '/templates/beez3/html/com_weblinks/form', + '/templates/beez3/html/com_weblinks/category', + '/templates/beez3/html/com_weblinks/categories', + '/templates/beez3/html/com_weblinks', + '/templates/beez3/html/com_newsfeeds/category', + '/templates/beez3/html/com_newsfeeds/categories', + '/templates/beez3/html/com_newsfeeds', + '/templates/beez3/html/com_content/form', + '/templates/beez3/html/com_content/featured', + '/templates/beez3/html/com_content/category', + '/templates/beez3/html/com_content/categories', + '/templates/beez3/html/com_content/article', + '/templates/beez3/html/com_content/archive', + '/templates/beez3/html/com_content', + '/templates/beez3/html/com_contact/contact', + '/templates/beez3/html/com_contact/category', + '/templates/beez3/html/com_contact/categories', + '/templates/beez3/html/com_contact', + '/templates/beez3/html', + '/templates/beez3/css', + '/templates/beez3', + '/plugins/user/terms/terms', + '/plugins/user/terms/field', + '/plugins/user/profile/profiles', + '/plugins/user/profile/field', + '/plugins/system/stats/field', + '/plugins/system/privacyconsent/privacyconsent', + '/plugins/system/privacyconsent/field', + '/plugins/system/p3p', + '/plugins/system/languagecode/language/en-GB', + '/plugins/system/languagecode/language', + '/plugins/editors/tinymce/form', + '/plugins/editors/tinymce/field', + '/plugins/content/confirmconsent/fields', + '/plugins/captcha/recaptcha/postinstall', + '/plugins/authentication/gmail', + '/media/plg_twofactorauth_totp/js', + '/media/plg_twofactorauth_totp', + '/media/plg_system_highlight', + '/media/overrider/js', + '/media/overrider/css', + '/media/overrider', + '/media/media/js', + '/media/media/images/mime-icon-32', + '/media/media/images/mime-icon-16', + '/media/media/images', + '/media/media/css', + '/media/media', + '/media/jui/less', + '/media/jui/js', + '/media/jui/img', + '/media/jui/images', + '/media/jui/fonts', + '/media/jui/css', + '/media/jui', + '/media/editors/tinymce/themes/modern', + '/media/editors/tinymce/themes', + '/media/editors/tinymce/templates', + '/media/editors/tinymce/skins/lightgray/img', + '/media/editors/tinymce/skins/lightgray/fonts', + '/media/editors/tinymce/skins/lightgray', + '/media/editors/tinymce/skins', + '/media/editors/tinymce/plugins/wordcount', + '/media/editors/tinymce/plugins/visualchars', + '/media/editors/tinymce/plugins/visualblocks/css', + '/media/editors/tinymce/plugins/visualblocks', + '/media/editors/tinymce/plugins/toc', + '/media/editors/tinymce/plugins/textpattern', + '/media/editors/tinymce/plugins/textcolor', + '/media/editors/tinymce/plugins/template', + '/media/editors/tinymce/plugins/table', + '/media/editors/tinymce/plugins/tabfocus', + '/media/editors/tinymce/plugins/spellchecker', + '/media/editors/tinymce/plugins/searchreplace', + '/media/editors/tinymce/plugins/save', + '/media/editors/tinymce/plugins/print', + '/media/editors/tinymce/plugins/preview', + '/media/editors/tinymce/plugins/paste', + '/media/editors/tinymce/plugins/pagebreak', + '/media/editors/tinymce/plugins/noneditable', + '/media/editors/tinymce/plugins/nonbreaking', + '/media/editors/tinymce/plugins/media', + '/media/editors/tinymce/plugins/lists', + '/media/editors/tinymce/plugins/link', + '/media/editors/tinymce/plugins/legacyoutput', + '/media/editors/tinymce/plugins/layer', + '/media/editors/tinymce/plugins/insertdatetime', + '/media/editors/tinymce/plugins/importcss', + '/media/editors/tinymce/plugins/imagetools', + '/media/editors/tinymce/plugins/image', + '/media/editors/tinymce/plugins/hr', + '/media/editors/tinymce/plugins/fullscreen', + '/media/editors/tinymce/plugins/fullpage', + '/media/editors/tinymce/plugins/example_dependency', + '/media/editors/tinymce/plugins/example', + '/media/editors/tinymce/plugins/emoticons/img', + '/media/editors/tinymce/plugins/emoticons', + '/media/editors/tinymce/plugins/directionality', + '/media/editors/tinymce/plugins/contextmenu', + '/media/editors/tinymce/plugins/colorpicker', + '/media/editors/tinymce/plugins/codesample/css', + '/media/editors/tinymce/plugins/codesample', + '/media/editors/tinymce/plugins/code', + '/media/editors/tinymce/plugins/charmap', + '/media/editors/tinymce/plugins/bbcode', + '/media/editors/tinymce/plugins/autosave', + '/media/editors/tinymce/plugins/autoresize', + '/media/editors/tinymce/plugins/autolink', + '/media/editors/tinymce/plugins/anchor', + '/media/editors/tinymce/plugins/advlist', + '/media/editors/tinymce/plugins', + '/media/editors/tinymce/langs', + '/media/editors/tinymce/js/plugins/dragdrop', + '/media/editors/tinymce/js/plugins', + '/media/editors/tinymce/js', + '/media/editors/tinymce', + '/media/editors/none/js', + '/media/editors/none', + '/media/editors/codemirror/theme', + '/media/editors/codemirror/mode/z80', + '/media/editors/codemirror/mode/yaml-frontmatter', + '/media/editors/codemirror/mode/yaml', + '/media/editors/codemirror/mode/yacas', + '/media/editors/codemirror/mode/xquery', + '/media/editors/codemirror/mode/xml', + '/media/editors/codemirror/mode/webidl', + '/media/editors/codemirror/mode/wast', + '/media/editors/codemirror/mode/vue', + '/media/editors/codemirror/mode/vhdl', + '/media/editors/codemirror/mode/verilog', + '/media/editors/codemirror/mode/velocity', + '/media/editors/codemirror/mode/vbscript', + '/media/editors/codemirror/mode/vb', + '/media/editors/codemirror/mode/twig', + '/media/editors/codemirror/mode/turtle', + '/media/editors/codemirror/mode/ttcn-cfg', + '/media/editors/codemirror/mode/ttcn', + '/media/editors/codemirror/mode/troff', + '/media/editors/codemirror/mode/tornado', + '/media/editors/codemirror/mode/toml', + '/media/editors/codemirror/mode/tiki', + '/media/editors/codemirror/mode/tiddlywiki', + '/media/editors/codemirror/mode/textile', + '/media/editors/codemirror/mode/tcl', + '/media/editors/codemirror/mode/swift', + '/media/editors/codemirror/mode/stylus', + '/media/editors/codemirror/mode/stex', + '/media/editors/codemirror/mode/sql', + '/media/editors/codemirror/mode/spreadsheet', + '/media/editors/codemirror/mode/sparql', + '/media/editors/codemirror/mode/soy', + '/media/editors/codemirror/mode/solr', + '/media/editors/codemirror/mode/smarty', + '/media/editors/codemirror/mode/smalltalk', + '/media/editors/codemirror/mode/slim', + '/media/editors/codemirror/mode/sieve', + '/media/editors/codemirror/mode/shell', + '/media/editors/codemirror/mode/scheme', + '/media/editors/codemirror/mode/sass', + '/media/editors/codemirror/mode/sas', + '/media/editors/codemirror/mode/rust', + '/media/editors/codemirror/mode/ruby', + '/media/editors/codemirror/mode/rst', + '/media/editors/codemirror/mode/rpm/changes', + '/media/editors/codemirror/mode/rpm', + '/media/editors/codemirror/mode/r', + '/media/editors/codemirror/mode/q', + '/media/editors/codemirror/mode/python', + '/media/editors/codemirror/mode/puppet', + '/media/editors/codemirror/mode/pug', + '/media/editors/codemirror/mode/protobuf', + '/media/editors/codemirror/mode/properties', + '/media/editors/codemirror/mode/powershell', + '/media/editors/codemirror/mode/pig', + '/media/editors/codemirror/mode/php', + '/media/editors/codemirror/mode/perl', + '/media/editors/codemirror/mode/pegjs', + '/media/editors/codemirror/mode/pascal', + '/media/editors/codemirror/mode/oz', + '/media/editors/codemirror/mode/octave', + '/media/editors/codemirror/mode/ntriples', + '/media/editors/codemirror/mode/nsis', + '/media/editors/codemirror/mode/nginx', + '/media/editors/codemirror/mode/mumps', + '/media/editors/codemirror/mode/mscgen', + '/media/editors/codemirror/mode/modelica', + '/media/editors/codemirror/mode/mllike', + '/media/editors/codemirror/mode/mirc', + '/media/editors/codemirror/mode/mbox', + '/media/editors/codemirror/mode/mathematica', + '/media/editors/codemirror/mode/markdown', + '/media/editors/codemirror/mode/lua', + '/media/editors/codemirror/mode/livescript', + '/media/editors/codemirror/mode/julia', + '/media/editors/codemirror/mode/jsx', + '/media/editors/codemirror/mode/jinja2', + '/media/editors/codemirror/mode/javascript', + '/media/editors/codemirror/mode/idl', + '/media/editors/codemirror/mode/http', + '/media/editors/codemirror/mode/htmlmixed', + '/media/editors/codemirror/mode/htmlembedded', + '/media/editors/codemirror/mode/haxe', + '/media/editors/codemirror/mode/haskell-literate', + '/media/editors/codemirror/mode/haskell', + '/media/editors/codemirror/mode/handlebars', + '/media/editors/codemirror/mode/haml', + '/media/editors/codemirror/mode/groovy', + '/media/editors/codemirror/mode/go', + '/media/editors/codemirror/mode/gherkin', + '/media/editors/codemirror/mode/gfm', + '/media/editors/codemirror/mode/gas', + '/media/editors/codemirror/mode/fortran', + '/media/editors/codemirror/mode/forth', + '/media/editors/codemirror/mode/fcl', + '/media/editors/codemirror/mode/factor', + '/media/editors/codemirror/mode/erlang', + '/media/editors/codemirror/mode/elm', + '/media/editors/codemirror/mode/eiffel', + '/media/editors/codemirror/mode/ecl', + '/media/editors/codemirror/mode/ebnf', + '/media/editors/codemirror/mode/dylan', + '/media/editors/codemirror/mode/dtd', + '/media/editors/codemirror/mode/dockerfile', + '/media/editors/codemirror/mode/django', + '/media/editors/codemirror/mode/diff', + '/media/editors/codemirror/mode/dart', + '/media/editors/codemirror/mode/d', + '/media/editors/codemirror/mode/cypher', + '/media/editors/codemirror/mode/css', + '/media/editors/codemirror/mode/crystal', + '/media/editors/codemirror/mode/commonlisp', + '/media/editors/codemirror/mode/coffeescript', + '/media/editors/codemirror/mode/cobol', + '/media/editors/codemirror/mode/cmake', + '/media/editors/codemirror/mode/clojure', + '/media/editors/codemirror/mode/clike', + '/media/editors/codemirror/mode/brainfuck', + '/media/editors/codemirror/mode/asterisk', + '/media/editors/codemirror/mode/asn.1', + '/media/editors/codemirror/mode/asciiarmor', + '/media/editors/codemirror/mode/apl', + '/media/editors/codemirror/mode', + '/media/editors/codemirror/lib', + '/media/editors/codemirror/keymap', + '/media/editors/codemirror/addon/wrap', + '/media/editors/codemirror/addon/tern', + '/media/editors/codemirror/addon/selection', + '/media/editors/codemirror/addon/search', + '/media/editors/codemirror/addon/scroll', + '/media/editors/codemirror/addon/runmode', + '/media/editors/codemirror/addon/mode', + '/media/editors/codemirror/addon/merge', + '/media/editors/codemirror/addon/lint', + '/media/editors/codemirror/addon/hint', + '/media/editors/codemirror/addon/fold', + '/media/editors/codemirror/addon/edit', + '/media/editors/codemirror/addon/display', + '/media/editors/codemirror/addon/dialog', + '/media/editors/codemirror/addon/comment', + '/media/editors/codemirror/addon', + '/media/editors/codemirror', + '/media/editors', + '/media/contacts/images', + '/media/contacts', + '/media/com_contenthistory/css', + '/media/cms/css', + '/media/cms', + '/libraries/vendor/symfony/polyfill-util', + '/libraries/vendor/symfony/polyfill-php71', + '/libraries/vendor/symfony/polyfill-php56', + '/libraries/vendor/symfony/polyfill-php55', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parse', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Net', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/HTTP', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content/Type', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache', + '/libraries/vendor/simplepie/simplepie/library/SimplePie', + '/libraries/vendor/simplepie/simplepie/library', + '/libraries/vendor/simplepie/simplepie/idn', + '/libraries/vendor/simplepie/simplepie', + '/libraries/vendor/simplepie', + '/libraries/vendor/phpmailer/phpmailer/extras', + '/libraries/vendor/paragonie/random_compat/lib', + '/libraries/vendor/leafo/lessphp', + '/libraries/vendor/leafo', + '/libraries/vendor/joomla/session/Joomla/Session/Storage', + '/libraries/vendor/joomla/session/Joomla/Session', + '/libraries/vendor/joomla/session/Joomla', + '/libraries/vendor/joomla/image/src/Filter', + '/libraries/vendor/joomla/image/src', + '/libraries/vendor/joomla/image', + '/libraries/vendor/joomla/compat/src', + '/libraries/vendor/joomla/compat', + '/libraries/vendor/joomla/application/src/Cli/Output/Processor', + '/libraries/vendor/joomla/application/src/Cli/Output', + '/libraries/vendor/joomla/application/src/Cli', + '/libraries/vendor/ircmaxell/password-compat/lib', + '/libraries/vendor/ircmaxell/password-compat', + '/libraries/vendor/ircmaxell', + '/libraries/vendor/brumann/polyfill-unserialize/src', + '/libraries/vendor/brumann/polyfill-unserialize', + '/libraries/vendor/brumann', + '/libraries/src/Table/Observer', + '/libraries/src/Menu/Node', + '/libraries/src/Language/Wrapper', + '/libraries/src/Language/Stemmer', + '/libraries/src/Http/Wrapper', + '/libraries/src/Filter/Wrapper', + '/libraries/src/Filesystem/Wrapper', + '/libraries/src/Crypt/Password', + '/libraries/src/Access/Wrapper', + '/libraries/phputf8/utils', + '/libraries/phputf8/native', + '/libraries/phputf8/mbstring', + '/libraries/phputf8', + '/libraries/legacy/utilities', + '/libraries/legacy/table', + '/libraries/legacy/simplepie', + '/libraries/legacy/simplecrypt', + '/libraries/legacy/response', + '/libraries/legacy/request', + '/libraries/legacy/log', + '/libraries/legacy/form/field', + '/libraries/legacy/form', + '/libraries/legacy/exception', + '/libraries/legacy/error', + '/libraries/legacy/dispatcher', + '/libraries/legacy/database', + '/libraries/legacy/base', + '/libraries/legacy/application', + '/libraries/legacy', + '/libraries/joomla/view', + '/libraries/joomla/utilities', + '/libraries/joomla/twitter', + '/libraries/joomla/string/wrapper', + '/libraries/joomla/string', + '/libraries/joomla/session/storage', + '/libraries/joomla/session/handler', + '/libraries/joomla/session', + '/libraries/joomla/route/wrapper', + '/libraries/joomla/route', + '/libraries/joomla/openstreetmap', + '/libraries/joomla/observer/wrapper', + '/libraries/joomla/observer/updater', + '/libraries/joomla/observer', + '/libraries/joomla/observable', + '/libraries/joomla/oauth2', + '/libraries/joomla/oauth1', + '/libraries/joomla/model', + '/libraries/joomla/mediawiki', + '/libraries/joomla/linkedin', + '/libraries/joomla/keychain', + '/libraries/joomla/grid', + '/libraries/joomla/google/embed', + '/libraries/joomla/google/data/plus', + '/libraries/joomla/google/data/picasa', + '/libraries/joomla/google/data', + '/libraries/joomla/google/auth', + '/libraries/joomla/google', + '/libraries/joomla/github/package/users', + '/libraries/joomla/github/package/repositories', + '/libraries/joomla/github/package/pulls', + '/libraries/joomla/github/package/orgs', + '/libraries/joomla/github/package/issues', + '/libraries/joomla/github/package/gists', + '/libraries/joomla/github/package/data', + '/libraries/joomla/github/package/activity', + '/libraries/joomla/github/package', + '/libraries/joomla/github', + '/libraries/joomla/form/fields', + '/libraries/joomla/form', + '/libraries/joomla/facebook', + '/libraries/joomla/event', + '/libraries/joomla/database/query', + '/libraries/joomla/database/iterator', + '/libraries/joomla/database/importer', + '/libraries/joomla/database/exporter', + '/libraries/joomla/database/exception', + '/libraries/joomla/database/driver', + '/libraries/joomla/database', + '/libraries/joomla/controller', + '/libraries/joomla/archive/wrapper', + '/libraries/joomla/archive', + '/libraries/joomla/application/web/router', + '/libraries/joomla/application/web', + '/libraries/joomla/application', + '/libraries/joomla', + '/libraries/idna_convert', + '/libraries/fof/view', + '/libraries/fof/utils/update', + '/libraries/fof/utils/timer', + '/libraries/fof/utils/phpfunc', + '/libraries/fof/utils/observable', + '/libraries/fof/utils/object', + '/libraries/fof/utils/ip', + '/libraries/fof/utils/installscript', + '/libraries/fof/utils/ini', + '/libraries/fof/utils/filescheck', + '/libraries/fof/utils/config', + '/libraries/fof/utils/cache', + '/libraries/fof/utils/array', + '/libraries/fof/utils', + '/libraries/fof/toolbar', + '/libraries/fof/template', + '/libraries/fof/table/dispatcher', + '/libraries/fof/table/behavior', + '/libraries/fof/table', + '/libraries/fof/string', + '/libraries/fof/render', + '/libraries/fof/query', + '/libraries/fof/platform/filesystem', + '/libraries/fof/platform', + '/libraries/fof/model/field', + '/libraries/fof/model/dispatcher', + '/libraries/fof/model/behavior', + '/libraries/fof/model', + '/libraries/fof/less/parser', + '/libraries/fof/less/formatter', + '/libraries/fof/less', + '/libraries/fof/layout', + '/libraries/fof/integration/joomla/filesystem', + '/libraries/fof/integration/joomla', + '/libraries/fof/integration', + '/libraries/fof/input/jinput', + '/libraries/fof/input', + '/libraries/fof/inflector', + '/libraries/fof/hal/render', + '/libraries/fof/hal', + '/libraries/fof/form/header', + '/libraries/fof/form/field', + '/libraries/fof/form', + '/libraries/fof/encrypt/aes', + '/libraries/fof/encrypt', + '/libraries/fof/download/adapter', + '/libraries/fof/download', + '/libraries/fof/dispatcher', + '/libraries/fof/database/query', + '/libraries/fof/database/iterator', + '/libraries/fof/database/driver', + '/libraries/fof/database', + '/libraries/fof/controller', + '/libraries/fof/config/domain', + '/libraries/fof/config', + '/libraries/fof/autoloader', + '/libraries/fof', + '/libraries/cms/less/formatter', + '/libraries/cms/less', + '/libraries/cms/html/language/en-GB', + '/libraries/cms/html/language', + '/libraries/cms/html', + '/libraries/cms/class', + '/libraries/cms', + '/layouts/libraries/cms/html/bootstrap', + '/layouts/libraries/cms/html', + '/layouts/libraries/cms', + '/layouts/joomla/tinymce/buttons', + '/layouts/joomla/modal', + '/layouts/joomla/html/formbehavior', + '/components/com_wrapper/views/wrapper/tmpl', + '/components/com_wrapper/views/wrapper', + '/components/com_wrapper/views', + '/components/com_users/views/reset/tmpl', + '/components/com_users/views/reset', + '/components/com_users/views/remind/tmpl', + '/components/com_users/views/remind', + '/components/com_users/views/registration/tmpl', + '/components/com_users/views/registration', + '/components/com_users/views/profile/tmpl', + '/components/com_users/views/profile', + '/components/com_users/views/login/tmpl', + '/components/com_users/views/login', + '/components/com_users/views', + '/components/com_users/models/rules', + '/components/com_users/models/forms', + '/components/com_users/models', + '/components/com_users/layouts/joomla/form', + '/components/com_users/layouts/joomla', + '/components/com_users/layouts', + '/components/com_users/helpers/html', + '/components/com_users/helpers', + '/components/com_users/controllers', + '/components/com_tags/views/tags/tmpl', + '/components/com_tags/views/tags', + '/components/com_tags/views/tag/tmpl', + '/components/com_tags/views/tag', + '/components/com_tags/views', + '/components/com_tags/models', + '/components/com_tags/controllers', + '/components/com_privacy/views/request/tmpl', + '/components/com_privacy/views/request', + '/components/com_privacy/views/remind/tmpl', + '/components/com_privacy/views/remind', + '/components/com_privacy/views/confirm/tmpl', + '/components/com_privacy/views/confirm', + '/components/com_privacy/views', + '/components/com_privacy/models/forms', + '/components/com_privacy/models', + '/components/com_privacy/controllers', + '/components/com_newsfeeds/views/newsfeed/tmpl', + '/components/com_newsfeeds/views/newsfeed', + '/components/com_newsfeeds/views/category/tmpl', + '/components/com_newsfeeds/views/category', + '/components/com_newsfeeds/views/categories/tmpl', + '/components/com_newsfeeds/views/categories', + '/components/com_newsfeeds/views', + '/components/com_newsfeeds/models', + '/components/com_modules/models/forms', + '/components/com_modules/models', + '/components/com_menus/models/forms', + '/components/com_menus/models', + '/components/com_mailto/views/sent/tmpl', + '/components/com_mailto/views/sent', + '/components/com_mailto/views/mailto/tmpl', + '/components/com_mailto/views/mailto', + '/components/com_mailto/views', + '/components/com_mailto/models/forms', + '/components/com_mailto/models', + '/components/com_mailto/helpers', + '/components/com_mailto', + '/components/com_finder/views/search/tmpl', + '/components/com_finder/views/search', + '/components/com_finder/views', + '/components/com_finder/models', + '/components/com_finder/helpers/html', + '/components/com_finder/controllers', + '/components/com_fields/models/forms', + '/components/com_fields/models', + '/components/com_content/views/form/tmpl', + '/components/com_content/views/form', + '/components/com_content/views/featured/tmpl', + '/components/com_content/views/featured', + '/components/com_content/views/category/tmpl', + '/components/com_content/views/category', + '/components/com_content/views/categories/tmpl', + '/components/com_content/views/categories', + '/components/com_content/views/article/tmpl', + '/components/com_content/views/article', + '/components/com_content/views/archive/tmpl', + '/components/com_content/views/archive', + '/components/com_content/views', + '/components/com_content/models/forms', + '/components/com_content/models', + '/components/com_content/controllers', + '/components/com_contact/views/featured/tmpl', + '/components/com_contact/views/featured', + '/components/com_contact/views/contact/tmpl', + '/components/com_contact/views/contact', + '/components/com_contact/views/category/tmpl', + '/components/com_contact/views/category', + '/components/com_contact/views/categories/tmpl', + '/components/com_contact/views/categories', + '/components/com_contact/views', + '/components/com_contact/models/rules', + '/components/com_contact/models/forms', + '/components/com_contact/models', + '/components/com_contact/layouts/joomla/form', + '/components/com_contact/layouts/joomla', + '/components/com_contact/controllers', + '/components/com_config/view/templates/tmpl', + '/components/com_config/view/templates', + '/components/com_config/view/modules/tmpl', + '/components/com_config/view/modules', + '/components/com_config/view/config/tmpl', + '/components/com_config/view/config', + '/components/com_config/view/cms', + '/components/com_config/view', + '/components/com_config/model/form', + '/components/com_config/model', + '/components/com_config/controller/templates', + '/components/com_config/controller/modules', + '/components/com_config/controller/config', + '/components/com_config/controller', + '/components/com_banners/models', + '/components/com_banners/helpers', + '/administrator/templates/system/html', + '/administrator/templates/isis/less/pages', + '/administrator/templates/isis/less/bootstrap', + '/administrator/templates/isis/less/blocks', + '/administrator/templates/isis/less', + '/administrator/templates/isis/language/en-GB', + '/administrator/templates/isis/language', + '/administrator/templates/isis/js', + '/administrator/templates/isis/img', + '/administrator/templates/isis/images/system', + '/administrator/templates/isis/images/admin', + '/administrator/templates/isis/images', + '/administrator/templates/isis/html/mod_version', + '/administrator/templates/isis/html/layouts/joomla/toolbar', + '/administrator/templates/isis/html/layouts/joomla/system', + '/administrator/templates/isis/html/layouts/joomla/pagination', + '/administrator/templates/isis/html/layouts/joomla/form/field', + '/administrator/templates/isis/html/layouts/joomla/form', + '/administrator/templates/isis/html/layouts/joomla', + '/administrator/templates/isis/html/layouts', + '/administrator/templates/isis/html/com_media/medialist', + '/administrator/templates/isis/html/com_media/imageslist', + '/administrator/templates/isis/html/com_media', + '/administrator/templates/isis/html', + '/administrator/templates/isis/css', + '/administrator/templates/isis', + '/administrator/templates/hathor/postinstall', + '/administrator/templates/hathor/less', + '/administrator/templates/hathor/language/en-GB', + '/administrator/templates/hathor/language', + '/administrator/templates/hathor/js', + '/administrator/templates/hathor/images/toolbar', + '/administrator/templates/hathor/images/system', + '/administrator/templates/hathor/images/menu', + '/administrator/templates/hathor/images/header', + '/administrator/templates/hathor/images/admin', + '/administrator/templates/hathor/images', + '/administrator/templates/hathor/html/mod_quickicon', + '/administrator/templates/hathor/html/mod_login', + '/administrator/templates/hathor/html/layouts/plugins/user/profile/fields', + '/administrator/templates/hathor/html/layouts/plugins/user/profile', + '/administrator/templates/hathor/html/layouts/plugins/user', + '/administrator/templates/hathor/html/layouts/plugins', + '/administrator/templates/hathor/html/layouts/joomla/toolbar', + '/administrator/templates/hathor/html/layouts/joomla/sidebars', + '/administrator/templates/hathor/html/layouts/joomla/quickicons', + '/administrator/templates/hathor/html/layouts/joomla/edit', + '/administrator/templates/hathor/html/layouts/joomla', + '/administrator/templates/hathor/html/layouts/com_modules/toolbar', + '/administrator/templates/hathor/html/layouts/com_modules', + '/administrator/templates/hathor/html/layouts/com_messages/toolbar', + '/administrator/templates/hathor/html/layouts/com_messages', + '/administrator/templates/hathor/html/layouts/com_media/toolbar', + '/administrator/templates/hathor/html/layouts/com_media', + '/administrator/templates/hathor/html/layouts', + '/administrator/templates/hathor/html/com_weblinks/weblinks', + '/administrator/templates/hathor/html/com_weblinks/weblink', + '/administrator/templates/hathor/html/com_weblinks', + '/administrator/templates/hathor/html/com_users/users', + '/administrator/templates/hathor/html/com_users/user', + '/administrator/templates/hathor/html/com_users/notes', + '/administrator/templates/hathor/html/com_users/note', + '/administrator/templates/hathor/html/com_users/levels', + '/administrator/templates/hathor/html/com_users/groups', + '/administrator/templates/hathor/html/com_users/debuguser', + '/administrator/templates/hathor/html/com_users/debuggroup', + '/administrator/templates/hathor/html/com_users', + '/administrator/templates/hathor/html/com_templates/templates', + '/administrator/templates/hathor/html/com_templates/template', + '/administrator/templates/hathor/html/com_templates/styles', + '/administrator/templates/hathor/html/com_templates/style', + '/administrator/templates/hathor/html/com_templates', + '/administrator/templates/hathor/html/com_tags/tags', + '/administrator/templates/hathor/html/com_tags/tag', + '/administrator/templates/hathor/html/com_tags', + '/administrator/templates/hathor/html/com_search/searches', + '/administrator/templates/hathor/html/com_search', + '/administrator/templates/hathor/html/com_redirect/links', + '/administrator/templates/hathor/html/com_redirect', + '/administrator/templates/hathor/html/com_postinstall/messages', + '/administrator/templates/hathor/html/com_postinstall', + '/administrator/templates/hathor/html/com_plugins/plugins', + '/administrator/templates/hathor/html/com_plugins/plugin', + '/administrator/templates/hathor/html/com_plugins', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeed', + '/administrator/templates/hathor/html/com_newsfeeds', + '/administrator/templates/hathor/html/com_modules/positions', + '/administrator/templates/hathor/html/com_modules/modules', + '/administrator/templates/hathor/html/com_modules/module', + '/administrator/templates/hathor/html/com_modules', + '/administrator/templates/hathor/html/com_messages/messages', + '/administrator/templates/hathor/html/com_messages/message', + '/administrator/templates/hathor/html/com_messages', + '/administrator/templates/hathor/html/com_menus/menutypes', + '/administrator/templates/hathor/html/com_menus/menus', + '/administrator/templates/hathor/html/com_menus/menu', + '/administrator/templates/hathor/html/com_menus/items', + '/administrator/templates/hathor/html/com_menus/item', + '/administrator/templates/hathor/html/com_menus', + '/administrator/templates/hathor/html/com_languages/overrides', + '/administrator/templates/hathor/html/com_languages/languages', + '/administrator/templates/hathor/html/com_languages/installed', + '/administrator/templates/hathor/html/com_languages', + '/administrator/templates/hathor/html/com_joomlaupdate/default', + '/administrator/templates/hathor/html/com_joomlaupdate', + '/administrator/templates/hathor/html/com_installer/warnings', + '/administrator/templates/hathor/html/com_installer/update', + '/administrator/templates/hathor/html/com_installer/manage', + '/administrator/templates/hathor/html/com_installer/languages', + '/administrator/templates/hathor/html/com_installer/install', + '/administrator/templates/hathor/html/com_installer/discover', + '/administrator/templates/hathor/html/com_installer/default', + '/administrator/templates/hathor/html/com_installer/database', + '/administrator/templates/hathor/html/com_installer', + '/administrator/templates/hathor/html/com_finder/maps', + '/administrator/templates/hathor/html/com_finder/index', + '/administrator/templates/hathor/html/com_finder/filters', + '/administrator/templates/hathor/html/com_finder', + '/administrator/templates/hathor/html/com_fields/groups', + '/administrator/templates/hathor/html/com_fields/group', + '/administrator/templates/hathor/html/com_fields/fields', + '/administrator/templates/hathor/html/com_fields/field', + '/administrator/templates/hathor/html/com_fields', + '/administrator/templates/hathor/html/com_cpanel/cpanel', + '/administrator/templates/hathor/html/com_cpanel', + '/administrator/templates/hathor/html/com_contenthistory/history', + '/administrator/templates/hathor/html/com_contenthistory', + '/administrator/templates/hathor/html/com_content/featured', + '/administrator/templates/hathor/html/com_content/articles', + '/administrator/templates/hathor/html/com_content/article', + '/administrator/templates/hathor/html/com_content', + '/administrator/templates/hathor/html/com_contact/contacts', + '/administrator/templates/hathor/html/com_contact/contact', + '/administrator/templates/hathor/html/com_contact', + '/administrator/templates/hathor/html/com_config/component', + '/administrator/templates/hathor/html/com_config/application', + '/administrator/templates/hathor/html/com_config', + '/administrator/templates/hathor/html/com_checkin/checkin', + '/administrator/templates/hathor/html/com_checkin', + '/administrator/templates/hathor/html/com_categories/category', + '/administrator/templates/hathor/html/com_categories/categories', + '/administrator/templates/hathor/html/com_categories', + '/administrator/templates/hathor/html/com_cache/purge', + '/administrator/templates/hathor/html/com_cache/cache', + '/administrator/templates/hathor/html/com_cache', + '/administrator/templates/hathor/html/com_banners/tracks', + '/administrator/templates/hathor/html/com_banners/download', + '/administrator/templates/hathor/html/com_banners/clients', + '/administrator/templates/hathor/html/com_banners/client', + '/administrator/templates/hathor/html/com_banners/banners', + '/administrator/templates/hathor/html/com_banners/banner', + '/administrator/templates/hathor/html/com_banners', + '/administrator/templates/hathor/html/com_associations/associations', + '/administrator/templates/hathor/html/com_associations', + '/administrator/templates/hathor/html/com_admin/sysinfo', + '/administrator/templates/hathor/html/com_admin/profile', + '/administrator/templates/hathor/html/com_admin/help', + '/administrator/templates/hathor/html/com_admin', + '/administrator/templates/hathor/html', + '/administrator/templates/hathor/css', + '/administrator/templates/hathor', + '/administrator/modules/mod_version/language/en-GB', + '/administrator/modules/mod_version/language', + '/administrator/modules/mod_status/tmpl', + '/administrator/modules/mod_status', + '/administrator/modules/mod_stats_admin/language', + '/administrator/modules/mod_multilangstatus/language/en-GB', + '/administrator/modules/mod_multilangstatus/language', + '/administrator/components/com_users/views/users/tmpl', + '/administrator/components/com_users/views/users', + '/administrator/components/com_users/views/user/tmpl', + '/administrator/components/com_users/views/user', + '/administrator/components/com_users/views/notes/tmpl', + '/administrator/components/com_users/views/notes', + '/administrator/components/com_users/views/note/tmpl', + '/administrator/components/com_users/views/note', + '/administrator/components/com_users/views/mail/tmpl', + '/administrator/components/com_users/views/mail', + '/administrator/components/com_users/views/levels/tmpl', + '/administrator/components/com_users/views/levels', + '/administrator/components/com_users/views/level/tmpl', + '/administrator/components/com_users/views/level', + '/administrator/components/com_users/views/groups/tmpl', + '/administrator/components/com_users/views/groups', + '/administrator/components/com_users/views/group/tmpl', + '/administrator/components/com_users/views/group', + '/administrator/components/com_users/views/debuguser/tmpl', + '/administrator/components/com_users/views/debuguser', + '/administrator/components/com_users/views/debuggroup/tmpl', + '/administrator/components/com_users/views/debuggroup', + '/administrator/components/com_users/views', + '/administrator/components/com_users/tables', + '/administrator/components/com_users/models/forms/fields', + '/administrator/components/com_users/models/forms', + '/administrator/components/com_users/models/fields', + '/administrator/components/com_users/models', + '/administrator/components/com_users/helpers/html', + '/administrator/components/com_users/controllers', + '/administrator/components/com_templates/views/templates/tmpl', + '/administrator/components/com_templates/views/templates', + '/administrator/components/com_templates/views/template/tmpl', + '/administrator/components/com_templates/views/template', + '/administrator/components/com_templates/views/styles/tmpl', + '/administrator/components/com_templates/views/styles', + '/administrator/components/com_templates/views/style/tmpl', + '/administrator/components/com_templates/views/style', + '/administrator/components/com_templates/views', + '/administrator/components/com_templates/tables', + '/administrator/components/com_templates/models/forms', + '/administrator/components/com_templates/models/fields', + '/administrator/components/com_templates/models', + '/administrator/components/com_templates/helpers/html', + '/administrator/components/com_templates/controllers', + '/administrator/components/com_tags/views/tags/tmpl', + '/administrator/components/com_tags/views/tags', + '/administrator/components/com_tags/views/tag/tmpl', + '/administrator/components/com_tags/views/tag', + '/administrator/components/com_tags/views', + '/administrator/components/com_tags/tables', + '/administrator/components/com_tags/models/forms', + '/administrator/components/com_tags/models', + '/administrator/components/com_tags/helpers', + '/administrator/components/com_tags/controllers', + '/administrator/components/com_redirect/views/links/tmpl', + '/administrator/components/com_redirect/views/links', + '/administrator/components/com_redirect/views/link/tmpl', + '/administrator/components/com_redirect/views/link', + '/administrator/components/com_redirect/views', + '/administrator/components/com_redirect/tables', + '/administrator/components/com_redirect/models/forms', + '/administrator/components/com_redirect/models/fields', + '/administrator/components/com_redirect/models', + '/administrator/components/com_redirect/helpers/html', + '/administrator/components/com_redirect/controllers', + '/administrator/components/com_privacy/views/requests/tmpl', + '/administrator/components/com_privacy/views/requests', + '/administrator/components/com_privacy/views/request/tmpl', + '/administrator/components/com_privacy/views/request', + '/administrator/components/com_privacy/views/export', + '/administrator/components/com_privacy/views/dashboard/tmpl', + '/administrator/components/com_privacy/views/dashboard', + '/administrator/components/com_privacy/views/consents/tmpl', + '/administrator/components/com_privacy/views/consents', + '/administrator/components/com_privacy/views/capabilities/tmpl', + '/administrator/components/com_privacy/views/capabilities', + '/administrator/components/com_privacy/views', + '/administrator/components/com_privacy/tables', + '/administrator/components/com_privacy/models/forms', + '/administrator/components/com_privacy/models/fields', + '/administrator/components/com_privacy/models', + '/administrator/components/com_privacy/helpers/removal', + '/administrator/components/com_privacy/helpers/html', + '/administrator/components/com_privacy/helpers/export', + '/administrator/components/com_privacy/helpers', + '/administrator/components/com_privacy/controllers', + '/administrator/components/com_postinstall/views/messages/tmpl', + '/administrator/components/com_postinstall/views/messages', + '/administrator/components/com_postinstall/views', + '/administrator/components/com_postinstall/models', + '/administrator/components/com_postinstall/controllers', + '/administrator/components/com_plugins/views/plugins/tmpl', + '/administrator/components/com_plugins/views/plugins', + '/administrator/components/com_plugins/views/plugin/tmpl', + '/administrator/components/com_plugins/views/plugin', + '/administrator/components/com_plugins/views', + '/administrator/components/com_plugins/models/forms', + '/administrator/components/com_plugins/models/fields', + '/administrator/components/com_plugins/models', + '/administrator/components/com_plugins/controllers', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl', + '/administrator/components/com_newsfeeds/views/newsfeeds', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl', + '/administrator/components/com_newsfeeds/views/newsfeed', + '/administrator/components/com_newsfeeds/views', + '/administrator/components/com_newsfeeds/tables', + '/administrator/components/com_newsfeeds/models/forms', + '/administrator/components/com_newsfeeds/models/fields/modal', + '/administrator/components/com_newsfeeds/models/fields', + '/administrator/components/com_newsfeeds/models', + '/administrator/components/com_newsfeeds/helpers/html', + '/administrator/components/com_newsfeeds/controllers', + '/administrator/components/com_modules/views/select/tmpl', + '/administrator/components/com_modules/views/select', + '/administrator/components/com_modules/views/preview/tmpl', + '/administrator/components/com_modules/views/preview', + '/administrator/components/com_modules/views/positions/tmpl', + '/administrator/components/com_modules/views/positions', + '/administrator/components/com_modules/views/modules/tmpl', + '/administrator/components/com_modules/views/modules', + '/administrator/components/com_modules/views/module/tmpl', + '/administrator/components/com_modules/views/module', + '/administrator/components/com_modules/views', + '/administrator/components/com_modules/models/forms', + '/administrator/components/com_modules/models/fields', + '/administrator/components/com_modules/models', + '/administrator/components/com_modules/helpers/html', + '/administrator/components/com_modules/controllers', + '/administrator/components/com_messages/views/messages/tmpl', + '/administrator/components/com_messages/views/messages', + '/administrator/components/com_messages/views/message/tmpl', + '/administrator/components/com_messages/views/message', + '/administrator/components/com_messages/views/config/tmpl', + '/administrator/components/com_messages/views/config', + '/administrator/components/com_messages/views', + '/administrator/components/com_messages/tables', + '/administrator/components/com_messages/models/forms', + '/administrator/components/com_messages/models/fields', + '/administrator/components/com_messages/models', + '/administrator/components/com_messages/helpers/html', + '/administrator/components/com_messages/helpers', + '/administrator/components/com_messages/controllers', + '/administrator/components/com_menus/views/menutypes/tmpl', + '/administrator/components/com_menus/views/menutypes', + '/administrator/components/com_menus/views/menus/tmpl', + '/administrator/components/com_menus/views/menus', + '/administrator/components/com_menus/views/menu/tmpl', + '/administrator/components/com_menus/views/menu', + '/administrator/components/com_menus/views/items/tmpl', + '/administrator/components/com_menus/views/items', + '/administrator/components/com_menus/views/item/tmpl', + '/administrator/components/com_menus/views/item', + '/administrator/components/com_menus/views', + '/administrator/components/com_menus/tables', + '/administrator/components/com_menus/models/forms', + '/administrator/components/com_menus/models/fields/modal', + '/administrator/components/com_menus/models/fields', + '/administrator/components/com_menus/models', + '/administrator/components/com_menus/layouts/joomla/searchtools/default', + '/administrator/components/com_menus/helpers/html', + '/administrator/components/com_menus/controllers', + '/administrator/components/com_media/views/medialist/tmpl', + '/administrator/components/com_media/views/medialist', + '/administrator/components/com_media/views/media/tmpl', + '/administrator/components/com_media/views/media', + '/administrator/components/com_media/views/imageslist/tmpl', + '/administrator/components/com_media/views/imageslist', + '/administrator/components/com_media/views/images/tmpl', + '/administrator/components/com_media/views/images', + '/administrator/components/com_media/views', + '/administrator/components/com_media/models', + '/administrator/components/com_media/controllers', + '/administrator/components/com_login/views/login/tmpl', + '/administrator/components/com_login/views/login', + '/administrator/components/com_login/views', + '/administrator/components/com_login/models', + '/administrator/components/com_languages/views/overrides/tmpl', + '/administrator/components/com_languages/views/overrides', + '/administrator/components/com_languages/views/override/tmpl', + '/administrator/components/com_languages/views/override', + '/administrator/components/com_languages/views/multilangstatus/tmpl', + '/administrator/components/com_languages/views/multilangstatus', + '/administrator/components/com_languages/views/languages/tmpl', + '/administrator/components/com_languages/views/languages', + '/administrator/components/com_languages/views/language/tmpl', + '/administrator/components/com_languages/views/language', + '/administrator/components/com_languages/views/installed/tmpl', + '/administrator/components/com_languages/views/installed', + '/administrator/components/com_languages/views', + '/administrator/components/com_languages/models/forms', + '/administrator/components/com_languages/models/fields', + '/administrator/components/com_languages/models', + '/administrator/components/com_languages/layouts/joomla/searchtools/default', + '/administrator/components/com_languages/layouts/joomla/searchtools', + '/administrator/components/com_languages/layouts/joomla', + '/administrator/components/com_languages/layouts', + '/administrator/components/com_languages/helpers/html', + '/administrator/components/com_languages/helpers', + '/administrator/components/com_languages/controllers', + '/administrator/components/com_joomlaupdate/views/upload/tmpl', + '/administrator/components/com_joomlaupdate/views/upload', + '/administrator/components/com_joomlaupdate/views/update/tmpl', + '/administrator/components/com_joomlaupdate/views/update', + '/administrator/components/com_joomlaupdate/views/default/tmpl', + '/administrator/components/com_joomlaupdate/views/default', + '/administrator/components/com_joomlaupdate/views', + '/administrator/components/com_joomlaupdate/models', + '/administrator/components/com_joomlaupdate/helpers', + '/administrator/components/com_joomlaupdate/controllers', + '/administrator/components/com_installer/views/warnings/tmpl', + '/administrator/components/com_installer/views/warnings', + '/administrator/components/com_installer/views/updatesites/tmpl', + '/administrator/components/com_installer/views/updatesites', + '/administrator/components/com_installer/views/update/tmpl', + '/administrator/components/com_installer/views/update', + '/administrator/components/com_installer/views/manage/tmpl', + '/administrator/components/com_installer/views/manage', + '/administrator/components/com_installer/views/languages/tmpl', + '/administrator/components/com_installer/views/languages', + '/administrator/components/com_installer/views/install/tmpl', + '/administrator/components/com_installer/views/install', + '/administrator/components/com_installer/views/discover/tmpl', + '/administrator/components/com_installer/views/discover', + '/administrator/components/com_installer/views/default/tmpl', + '/administrator/components/com_installer/views/default', + '/administrator/components/com_installer/views/database/tmpl', + '/administrator/components/com_installer/views/database', + '/administrator/components/com_installer/views', + '/administrator/components/com_installer/models/forms', + '/administrator/components/com_installer/models/fields', + '/administrator/components/com_installer/models', + '/administrator/components/com_installer/helpers/html', + '/administrator/components/com_installer/controllers', + '/administrator/components/com_finder/views/statistics/tmpl', + '/administrator/components/com_finder/views/statistics', + '/administrator/components/com_finder/views/maps/tmpl', + '/administrator/components/com_finder/views/maps', + '/administrator/components/com_finder/views/indexer/tmpl', + '/administrator/components/com_finder/views/indexer', + '/administrator/components/com_finder/views/index/tmpl', + '/administrator/components/com_finder/views/index', + '/administrator/components/com_finder/views/filters/tmpl', + '/administrator/components/com_finder/views/filters', + '/administrator/components/com_finder/views/filter/tmpl', + '/administrator/components/com_finder/views/filter', + '/administrator/components/com_finder/views', + '/administrator/components/com_finder/tables', + '/administrator/components/com_finder/models/forms', + '/administrator/components/com_finder/models/fields', + '/administrator/components/com_finder/models', + '/administrator/components/com_finder/helpers/indexer/stemmer', + '/administrator/components/com_finder/helpers/indexer/parser', + '/administrator/components/com_finder/helpers/indexer/driver', + '/administrator/components/com_finder/helpers/html', + '/administrator/components/com_finder/controllers', + '/administrator/components/com_fields/views/groups/tmpl', + '/administrator/components/com_fields/views/groups', + '/administrator/components/com_fields/views/group/tmpl', + '/administrator/components/com_fields/views/group', + '/administrator/components/com_fields/views/fields/tmpl', + '/administrator/components/com_fields/views/fields', + '/administrator/components/com_fields/views/field/tmpl', + '/administrator/components/com_fields/views/field', + '/administrator/components/com_fields/views', + '/administrator/components/com_fields/tables', + '/administrator/components/com_fields/models/forms', + '/administrator/components/com_fields/models/fields', + '/administrator/components/com_fields/models', + '/administrator/components/com_fields/libraries', + '/administrator/components/com_fields/controllers', + '/administrator/components/com_cpanel/views/cpanel/tmpl', + '/administrator/components/com_cpanel/views/cpanel', + '/administrator/components/com_cpanel/views', + '/administrator/components/com_contenthistory/views/preview/tmpl', + '/administrator/components/com_contenthistory/views/preview', + '/administrator/components/com_contenthistory/views/history/tmpl', + '/administrator/components/com_contenthistory/views/history', + '/administrator/components/com_contenthistory/views/compare/tmpl', + '/administrator/components/com_contenthistory/views/compare', + '/administrator/components/com_contenthistory/views', + '/administrator/components/com_contenthistory/models', + '/administrator/components/com_contenthistory/helpers/html', + '/administrator/components/com_contenthistory/controllers', + '/administrator/components/com_content/views/featured/tmpl', + '/administrator/components/com_content/views/featured', + '/administrator/components/com_content/views/articles/tmpl', + '/administrator/components/com_content/views/articles', + '/administrator/components/com_content/views/article/tmpl', + '/administrator/components/com_content/views/article', + '/administrator/components/com_content/views', + '/administrator/components/com_content/tables', + '/administrator/components/com_content/models/forms', + '/administrator/components/com_content/models/fields/modal', + '/administrator/components/com_content/models/fields', + '/administrator/components/com_content/models', + '/administrator/components/com_content/helpers/html', + '/administrator/components/com_content/controllers', + '/administrator/components/com_contact/views/contacts/tmpl', + '/administrator/components/com_contact/views/contacts', + '/administrator/components/com_contact/views/contact/tmpl', + '/administrator/components/com_contact/views/contact', + '/administrator/components/com_contact/views', + '/administrator/components/com_contact/tables', + '/administrator/components/com_contact/models/forms/fields', + '/administrator/components/com_contact/models/forms', + '/administrator/components/com_contact/models/fields/modal', + '/administrator/components/com_contact/models/fields', + '/administrator/components/com_contact/models', + '/administrator/components/com_contact/helpers/html', + '/administrator/components/com_contact/controllers', + '/administrator/components/com_config/view/component/tmpl', + '/administrator/components/com_config/view/component', + '/administrator/components/com_config/view/application/tmpl', + '/administrator/components/com_config/view/application', + '/administrator/components/com_config/view', + '/administrator/components/com_config/models', + '/administrator/components/com_config/model/form', + '/administrator/components/com_config/model/field', + '/administrator/components/com_config/model', + '/administrator/components/com_config/helper', + '/administrator/components/com_config/controllers', + '/administrator/components/com_config/controller/component', + '/administrator/components/com_config/controller/application', + '/administrator/components/com_config/controller', + '/administrator/components/com_checkin/views/checkin/tmpl', + '/administrator/components/com_checkin/views/checkin', + '/administrator/components/com_checkin/views', + '/administrator/components/com_checkin/models/forms', + '/administrator/components/com_checkin/models', + '/administrator/components/com_categories/views/category/tmpl', + '/administrator/components/com_categories/views/category', + '/administrator/components/com_categories/views/categories/tmpl', + '/administrator/components/com_categories/views/categories', + '/administrator/components/com_categories/views', + '/administrator/components/com_categories/tables', + '/administrator/components/com_categories/models/forms', + '/administrator/components/com_categories/models/fields/modal', + '/administrator/components/com_categories/models/fields', + '/administrator/components/com_categories/models', + '/administrator/components/com_categories/helpers/html', + '/administrator/components/com_categories/controllers', + '/administrator/components/com_cache/views/purge/tmpl', + '/administrator/components/com_cache/views/purge', + '/administrator/components/com_cache/views/cache/tmpl', + '/administrator/components/com_cache/views/cache', + '/administrator/components/com_cache/views', + '/administrator/components/com_cache/models/forms', + '/administrator/components/com_cache/models', + '/administrator/components/com_cache/helpers', + '/administrator/components/com_banners/views/tracks/tmpl', + '/administrator/components/com_banners/views/tracks', + '/administrator/components/com_banners/views/download/tmpl', + '/administrator/components/com_banners/views/download', + '/administrator/components/com_banners/views/clients/tmpl', + '/administrator/components/com_banners/views/clients', + '/administrator/components/com_banners/views/client/tmpl', + '/administrator/components/com_banners/views/client', + '/administrator/components/com_banners/views/banners/tmpl', + '/administrator/components/com_banners/views/banners', + '/administrator/components/com_banners/views/banner/tmpl', + '/administrator/components/com_banners/views/banner', + '/administrator/components/com_banners/views', + '/administrator/components/com_banners/tables', + '/administrator/components/com_banners/models/forms', + '/administrator/components/com_banners/models/fields', + '/administrator/components/com_banners/models', + '/administrator/components/com_banners/helpers/html', + '/administrator/components/com_banners/controllers', + '/administrator/components/com_associations/views/associations/tmpl', + '/administrator/components/com_associations/views/associations', + '/administrator/components/com_associations/views/association/tmpl', + '/administrator/components/com_associations/views/association', + '/administrator/components/com_associations/views', + '/administrator/components/com_associations/models/forms', + '/administrator/components/com_associations/models/fields', + '/administrator/components/com_associations/models', + '/administrator/components/com_associations/layouts/joomla/searchtools/default', + '/administrator/components/com_associations/helpers', + '/administrator/components/com_associations/controllers', + '/administrator/components/com_admin/views/sysinfo/tmpl', + '/administrator/components/com_admin/views/sysinfo', + '/administrator/components/com_admin/views/profile/tmpl', + '/administrator/components/com_admin/views/profile', + '/administrator/components/com_admin/views/help/tmpl', + '/administrator/components/com_admin/views/help', + '/administrator/components/com_admin/views', + '/administrator/components/com_admin/sql/updates/sqlazure', + '/administrator/components/com_admin/models/forms', + '/administrator/components/com_admin/models', + '/administrator/components/com_admin/helpers/html', + '/administrator/components/com_admin/helpers', + '/administrator/components/com_admin/controllers', + '/administrator/components/com_actionlogs/views/actionlogs/tmpl', + '/administrator/components/com_actionlogs/views/actionlogs', + '/administrator/components/com_actionlogs/views', + '/administrator/components/com_actionlogs/models/forms', + '/administrator/components/com_actionlogs/models/fields', + '/administrator/components/com_actionlogs/models', + '/administrator/components/com_actionlogs/libraries', + '/administrator/components/com_actionlogs/layouts', + '/administrator/components/com_actionlogs/helpers', + '/administrator/components/com_actionlogs/controllers', + // 4.0 from Beta 1 to Beta 2 + '/libraries/vendor/joomla/controller/src', + '/libraries/vendor/joomla/controller', + '/api/components/com_installer/src/View/Languages', + '/administrator/components/com_finder/src/Indexer/Driver', + // 4.0 from Beta 4 to Beta 5 + '/plugins/content/imagelazyload', + // 4.0 from Beta 5 to Beta 6 + '/media/system/js/core.es6', + '/administrator/modules/mod_multilangstatus/src/Helper', + '/administrator/modules/mod_multilangstatus/src', + // 4.0 from Beta 6 to Beta 7 + '/media/vendor/skipto/css', + // 4.0 from Beta 7 to RC 1 + '/templates/system/js', + '/templates/cassiopeia/scss/tools/mixins', + '/plugins/fields/subfields/tmpl', + '/plugins/fields/subfields/params', + '/plugins/fields/subfields', + '/media/vendor/punycode/js', + '/media/templates/atum/js', + '/media/templates/atum', + '/libraries/vendor/paragonie/random_compat/dist', + '/libraries/vendor/paragonie/random_compat', + '/libraries/vendor/ozdemirburak/iris/src/Traits', + '/libraries/vendor/ozdemirburak/iris/src/Helpers', + '/libraries/vendor/ozdemirburak/iris/src/Exceptions', + '/libraries/vendor/ozdemirburak/iris/src/Color', + '/libraries/vendor/ozdemirburak/iris/src', + '/libraries/vendor/ozdemirburak/iris', + '/libraries/vendor/ozdemirburak', + '/libraries/vendor/bin', + '/components/com_menus/src/Controller', + '/components/com_csp/src/Controller', + '/components/com_csp/src', + '/components/com_csp', + '/administrator/templates/atum/Service/HTML', + '/administrator/templates/atum/Service', + '/administrator/components/com_joomlaupdate/src/Helper', + '/administrator/components/com_csp/tmpl/reports', + '/administrator/components/com_csp/tmpl', + '/administrator/components/com_csp/src/View/Reports', + '/administrator/components/com_csp/src/View', + '/administrator/components/com_csp/src/Table', + '/administrator/components/com_csp/src/Model', + '/administrator/components/com_csp/src/Helper', + '/administrator/components/com_csp/src/Controller', + '/administrator/components/com_csp/src', + '/administrator/components/com_csp/services', + '/administrator/components/com_csp/forms', + '/administrator/components/com_csp', + '/administrator/components/com_admin/tmpl/profile', + '/administrator/components/com_admin/src/View/Profile', + '/administrator/components/com_admin/forms', + // 4.0 from RC 5 to RC 6 + '/templates/cassiopeia/scss/vendor/fontawesome-free', + '/templates/cassiopeia/css/vendor/fontawesome-free', + '/media/templates/cassiopeia/js/mod_menu', + '/media/templates/cassiopeia/js', + '/media/templates/cassiopeia', + // 4.0 from RC 6 to 4.0.0 (stable) + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation', + '/libraries/vendor/willdurand/negotiation/tests', + '/libraries/vendor/jakeasmith/http_build_url/tests', + '/libraries/vendor/doctrine/inflector/docs/en', + '/libraries/vendor/doctrine/inflector/docs', + '/libraries/vendor/algo26-matthias/idna-convert/tests/unit', + '/libraries/vendor/algo26-matthias/idna-convert/tests/integration', + '/libraries/vendor/algo26-matthias/idna-convert/tests', + // From 4.0.3 to 4.0.4 + '/templates/cassiopeia/images/system', + // From 4.0.x to 4.1.0-beta1 + '/templates/system/scss', + '/templates/system/css', + '/templates/cassiopeia/scss/vendor/metismenu', + '/templates/cassiopeia/scss/vendor/joomla-custom-elements', + '/templates/cassiopeia/scss/vendor/choicesjs', + '/templates/cassiopeia/scss/vendor/bootstrap', + '/templates/cassiopeia/scss/vendor', + '/templates/cassiopeia/scss/tools/variables', + '/templates/cassiopeia/scss/tools/functions', + '/templates/cassiopeia/scss/tools', + '/templates/cassiopeia/scss/system/searchtools', + '/templates/cassiopeia/scss/system', + '/templates/cassiopeia/scss/global', + '/templates/cassiopeia/scss/blocks', + '/templates/cassiopeia/scss', + '/templates/cassiopeia/js', + '/templates/cassiopeia/images', + '/templates/cassiopeia/css/vendor/joomla-custom-elements', + '/templates/cassiopeia/css/vendor/choicesjs', + '/templates/cassiopeia/css/vendor', + '/templates/cassiopeia/css/system/searchtools', + '/templates/cassiopeia/css/system', + '/templates/cassiopeia/css/global', + '/templates/cassiopeia/css', + '/administrator/templates/system/scss', + '/administrator/templates/system/images', + '/administrator/templates/system/css', + '/administrator/templates/atum/scss/vendor/minicolors', + '/administrator/templates/atum/scss/vendor/joomla-custom-elements', + '/administrator/templates/atum/scss/vendor/fontawesome-free', + '/administrator/templates/atum/scss/vendor/choicesjs', + '/administrator/templates/atum/scss/vendor/bootstrap', + '/administrator/templates/atum/scss/vendor/awesomplete', + '/administrator/templates/atum/scss/vendor', + '/administrator/templates/atum/scss/system/searchtools', + '/administrator/templates/atum/scss/system', + '/administrator/templates/atum/scss/pages', + '/administrator/templates/atum/scss/blocks', + '/administrator/templates/atum/scss', + '/administrator/templates/atum/images/logos', + '/administrator/templates/atum/images', + '/administrator/templates/atum/css/vendor/minicolors', + '/administrator/templates/atum/css/vendor/joomla-custom-elements', + '/administrator/templates/atum/css/vendor/fontawesome-free', + '/administrator/templates/atum/css/vendor/choicesjs', + '/administrator/templates/atum/css/vendor/awesomplete', + '/administrator/templates/atum/css/vendor', + '/administrator/templates/atum/css/system/searchtools', + '/administrator/templates/atum/css/system', + '/administrator/templates/atum/css', + // From 4.1.0-beta3 to 4.1.0-rc1 + '/api/components/com_media/src/Helper', + // From 4.1.0 to 4.1.1 + '/libraries/vendor/tobscure/json-api/tests/Exception/Handler', + '/libraries/vendor/tobscure/json-api/tests/Exception', + '/libraries/vendor/tobscure/json-api/tests', + '/libraries/vendor/tobscure/json-api/.git/refs/tags', + '/libraries/vendor/tobscure/json-api/.git/refs/remotes/origin', + '/libraries/vendor/tobscure/json-api/.git/refs/remotes', + '/libraries/vendor/tobscure/json-api/.git/refs/heads', + '/libraries/vendor/tobscure/json-api/.git/refs', + '/libraries/vendor/tobscure/json-api/.git/objects/pack', + '/libraries/vendor/tobscure/json-api/.git/objects/info', + '/libraries/vendor/tobscure/json-api/.git/objects', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes/origin', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/heads', + '/libraries/vendor/tobscure/json-api/.git/logs/refs', + '/libraries/vendor/tobscure/json-api/.git/logs', + '/libraries/vendor/tobscure/json-api/.git/info', + '/libraries/vendor/tobscure/json-api/.git/hooks', + '/libraries/vendor/tobscure/json-api/.git/branches', + '/libraries/vendor/tobscure/json-api/.git', + // From 4.1.3 to 4.1.4 + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar', + '/libraries/vendor/maximebf/debugbar/tests', + '/libraries/vendor/maximebf/debugbar/docs', + '/libraries/vendor/maximebf/debugbar/demo/bridge/twig', + '/libraries/vendor/maximebf/debugbar/demo/bridge/swiftmailer', + '/libraries/vendor/maximebf/debugbar/demo/bridge/slim', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel', + '/libraries/vendor/maximebf/debugbar/demo/bridge/monolog', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src/Demo', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine', + '/libraries/vendor/maximebf/debugbar/demo/bridge/cachecache', + '/libraries/vendor/maximebf/debugbar/demo/bridge', + '/libraries/vendor/maximebf/debugbar/demo', + '/libraries/vendor/maximebf/debugbar/build', + // From 4.1 to 4.2.0-beta1 + '/plugins/twofactorauth/yubikey/tmpl', + '/plugins/twofactorauth/yubikey', + '/plugins/twofactorauth/totp/tmpl', + '/plugins/twofactorauth/totp/postinstall', + '/plugins/twofactorauth/totp', + '/plugins/twofactorauth', + '/libraries/vendor/nyholm/psr7/doc', + // From 4.2.0-beta1 to 4.2.0-beta2 + '/layouts/plugins/user/profile/fields', + '/layouts/plugins/user/profile', + // From 4.2.0-beta2 to 4.2.0-beta3 + '/plugins/system/webauthn/src/Helper', + '/plugins/system/webauthn/src/Exception', + // From 4.2.0-rc1 to 4.2.0 + '/plugins/fields/menuitem/tmpl', + '/plugins/fields/menuitem', + // From 4.2.0 to 4.2.1 + '/media/vendor/hotkeys.js/js', + '/media/vendor/hotkeys.js', + '/libraries/vendor/symfony/string/Resources/bin', + ); + + $status['files_checked'] = $files; + $status['folders_checked'] = $folders; + + foreach ($files as $file) { + if ($fileExists = File::exists(JPATH_ROOT . $file)) { + $status['files_exist'][] = $file; + + if ($dryRun === false) { + if (File::delete(JPATH_ROOT . $file)) { + $status['files_deleted'][] = $file; + } else { + $status['files_errors'][] = Text::sprintf('FILES_JOOMLA_ERROR_FILE_FOLDER', $file); + } + } + } + } + + $this->moveRemainingTemplateFiles(); + + foreach ($folders as $folder) { + if ($folderExists = Folder::exists(JPATH_ROOT . $folder)) { + $status['folders_exist'][] = $folder; + + if ($dryRun === false) { + if (Folder::delete(JPATH_ROOT . $folder)) { + $status['folders_deleted'][] = $folder; + } else { + $status['folders_errors'][] = Text::sprintf('FILES_JOOMLA_ERROR_FILE_FOLDER', $folder); + } + } + } + } + + $this->fixFilenameCasing(); + + /* + * Needed for updates from 3.10 + * If com_search doesn't exist then assume we can delete the search package manifest (included in the update packages) + * We deliberately check for the presence of the files in case people have previously uninstalled their search extension + * but an update has put the files back. In that case it exists even if they don't believe in it! + */ + if ( + !File::exists(JPATH_ROOT . '/administrator/components/com_search/search.php') + && File::exists(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml') + ) { + File::delete(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml'); + } + + if ($suppressOutput === false && count($status['folders_errors'])) { + echo implode('
', $status['folders_errors']); + } + + if ($suppressOutput === false && count($status['files_errors'])) { + echo implode('
', $status['files_errors']); + } + + return $status; + } + + /** + * Method to create assets for newly installed components + * + * @param Installer $installer The class calling this method + * + * @return boolean + * + * @since 3.2 + */ + public function updateAssets($installer) + { + // List all components added since 4.0 + $newComponents = array( + // Components to be added here + ); + + foreach ($newComponents as $component) { + /** @var \Joomla\CMS\Table\Asset $asset */ + $asset = Table::getInstance('Asset'); + + if ($asset->loadByName($component)) { + continue; + } + + $asset->name = $component; + $asset->parent_id = 1; + $asset->rules = '{}'; + $asset->title = $component; + $asset->setLocation(1, 'last-child'); + + if (!$asset->store()) { + // Install failed, roll back changes + $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_COMP_INSTALL_ROLLBACK', $asset->getError(true))); + + return false; + } + } + + return true; + } + + /** + * Converts the site's database tables to support UTF-8 Multibyte. + * + * @param boolean $doDbFixMsg Flag if message to be shown to check db fix + * + * @return void + * + * @since 3.5 + */ + public function convertTablesToUtf8mb4($doDbFixMsg = false) + { + $db = Factory::getDbo(); + + if ($db->getServerType() !== 'mysql') { + return; + } + + // Check if the #__utf8_conversion table exists + $db->setQuery('SHOW TABLES LIKE ' . $db->quote($db->getPrefix() . 'utf8_conversion')); + + try { + $rows = $db->loadRowList(0); + } catch (Exception $e) { + // Render the error message from the Exception object + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + if ($doDbFixMsg) { + // Show an error message telling to check database problems + Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); + } + + return; + } + + // Nothing to do if the table doesn't exist because the CMS has never been updated from a pre-4.0 version + if (count($rows) === 0) { + return; + } + + // Set required conversion status + $converted = 5; + + // Check conversion status in database + $db->setQuery( + 'SELECT ' . $db->quoteName('converted') + . ' FROM ' . $db->quoteName('#__utf8_conversion') + ); + + try { + $convertedDB = $db->loadResult(); + } catch (Exception $e) { + // Render the error message from the Exception object + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + if ($doDbFixMsg) { + // Show an error message telling to check database problems + Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); + } + + return; + } + + // If conversion status from DB is equal to required final status, try to drop the #__utf8_conversion table + if ($convertedDB === $converted) { + $this->dropUtf8ConversionTable(); + + return; + } + + // Perform the required conversions of core tables if not done already in a previous step + if ($convertedDB !== 99) { + $fileName1 = JPATH_ROOT . '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion.sql'; + + if (is_file($fileName1)) { + $fileContents1 = @file_get_contents($fileName1); + $queries1 = $db->splitSql($fileContents1); + + if (!empty($queries1)) { + foreach ($queries1 as $query1) { + try { + $db->setQuery($query1)->execute(); + } catch (Exception $e) { + $converted = $convertedDB; + + // Still render the error message from the Exception object + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + } + } + } + + // If no error before, perform the optional conversions of tables which might or might not exist + if ($converted === 5) { + $fileName2 = JPATH_ROOT . '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion_optional.sql'; + + if (is_file($fileName2)) { + $fileContents2 = @file_get_contents($fileName2); + $queries2 = $db->splitSql($fileContents2); + + if (!empty($queries2)) { + foreach ($queries2 as $query2) { + // Get table name from query + if (preg_match('/^ALTER\s+TABLE\s+([^\s]+)\s+/i', $query2, $matches) === 1) { + $tableName = str_replace('`', '', $matches[1]); + $tableName = str_replace('#__', $db->getPrefix(), $tableName); + + // Check if the table exists and if yes, run the query + try { + $db->setQuery('SHOW TABLES LIKE ' . $db->quote($tableName)); + + $rows = $db->loadRowList(0); + + if (count($rows) > 0) { + $db->setQuery($query2)->execute(); + } + } catch (Exception $e) { + $converted = 99; + + // Still render the error message from the Exception object + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + } + } + } + } + + if ($doDbFixMsg && $converted !== 5) { + // Show an error message telling to check database problems + Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); + } + + // If the conversion was successful try to drop the #__utf8_conversion table + if ($converted === 5 && $this->dropUtf8ConversionTable()) { + // Table successfully dropped + return; + } + + // Set flag in database if the conversion status has changed. + if ($converted !== $convertedDB) { + $db->setQuery('UPDATE ' . $db->quoteName('#__utf8_conversion') + . ' SET ' . $db->quoteName('converted') . ' = ' . $converted . ';')->execute(); + } + } + + /** + * This method clean the Joomla Cache using the method `clean` from the com_cache model + * + * @return void + * + * @since 3.5.1 + */ + private function cleanJoomlaCache() + { + /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ + $model = Factory::getApplication()->bootComponent('com_cache')->getMVCFactory() + ->createModel('Cache', 'Administrator', ['ignore_request' => true]); + + // Clean frontend cache + $model->clean(); + + // Clean admin cache + $model->setState('client_id', 1); + $model->clean(); + } + + /** + * This method drops the #__utf8_conversion table + * + * @return boolean True on success + * + * @since 4.0.0 + */ + private function dropUtf8ConversionTable() + { + $db = Factory::getDbo(); + + try { + $db->setQuery('DROP TABLE ' . $db->quoteName('#__utf8_conversion') . ';')->execute(); + } catch (Exception $e) { + return false; + } + + return true; + } + + /** + * Called after any type of action + * + * @param string $action Which action is happening (install|uninstall|discover_install|update) + * @param Installer $installer The class calling this method + * + * @return boolean True on success + * + * @since 4.0.0 + */ + public function postflight($action, $installer) + { + if ($action !== 'update') { + return true; + } + + if (empty($this->fromVersion) || version_compare($this->fromVersion, '4.0.0', 'ge')) { + return true; + } + + // Update UCM content types. + $this->updateContentTypes(); + + $db = Factory::getDbo(); + Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/Table/'); + + $tableItem = new \Joomla\Component\Menus\Administrator\Table\MenuTable($db); + + $contactItems = $this->contactItems($tableItem); + $finderItems = $this->finderItems($tableItem); + + $menuItems = array_merge($contactItems, $finderItems); + + foreach ($menuItems as $menuItem) { + // Check an existing record + $keys = [ + 'menutype' => $menuItem['menutype'], + 'type' => $menuItem['type'], + 'title' => $menuItem['title'], + 'parent_id' => $menuItem['parent_id'], + 'client_id' => $menuItem['client_id'], + ]; + + if ($tableItem->load($keys)) { + continue; + } + + $newTableItem = new \Joomla\Component\Menus\Administrator\Table\MenuTable($db); + + // Bind the data. + if (!$newTableItem->bind($menuItem)) { + return false; + } + + $newTableItem->setLocation($menuItem['parent_id'], 'last-child'); + + // Check the data. + if (!$newTableItem->check()) { + return false; + } + + // Store the data. + if (!$newTableItem->store()) { + return false; + } + + // Rebuild the tree path. + if (!$newTableItem->rebuildPath($newTableItem->id)) { + return false; + } + } + + return true; + } + + /** + * Prepare the contact menu items + * + * @return array Menu items + * + * @since 4.0.0 + */ + private function contactItems(Table $tableItem): array + { + // Check for the Contact parent Id Menu Item + $keys = [ + 'menutype' => 'main', + 'type' => 'component', + 'title' => 'com_contact', + 'parent_id' => 1, + 'client_id' => 1, + ]; + + $contactMenuitem = $tableItem->load($keys); + + if (!$contactMenuitem) { + return []; + } + + $parentId = $tableItem->id; + $componentId = ExtensionHelper::getExtensionRecord('com_fields', 'component')->extension_id; + + // Add Contact Fields Menu Items. + $menuItems = [ + [ + 'menutype' => 'main', + 'title' => '-', + 'alias' => microtime(true), + 'note' => '', + 'path' => '', + 'link' => '#', + 'type' => 'separator', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'mod_menu_fields', + 'alias' => 'Contact Custom Fields', + 'note' => '', + 'path' => 'contact/Custom Fields', + 'link' => 'index.php?option=com_fields&context=com_contact.contact', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'mod_menu_fields_group', + 'alias' => 'Contact Custom Fields Group', + 'note' => '', + 'path' => 'contact/Custom Fields Group', + 'link' => 'index.php?option=com_fields&view=groups&context=com_contact.contact', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ] + ]; + + return $menuItems; + } + + /** + * Prepare the finder menu items + * + * @return array Menu items + * + * @since 4.0.0 + */ + private function finderItems(Table $tableItem): array + { + // Check for the Finder parent Id Menu Item + $keys = [ + 'menutype' => 'main', + 'type' => 'component', + 'title' => 'com_finder', + 'parent_id' => 1, + 'client_id' => 1, + ]; + + $finderMenuitem = $tableItem->load($keys); + + if (!$finderMenuitem) { + return []; + } + + $parentId = $tableItem->id; + $componentId = ExtensionHelper::getExtensionRecord('com_finder', 'component')->extension_id; + + // Add Finder Fields Menu Items. + $menuItems = [ + [ + 'menutype' => 'main', + 'title' => '-', + 'alias' => microtime(true), + 'note' => '', + 'path' => '', + 'link' => '#', + 'type' => 'separator', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'com_finder_index', + 'alias' => 'Smart-Search-Index', + 'note' => '', + 'path' => 'Smart Search/Index', + 'link' => 'index.php?option=com_finder&view=index', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'com_finder_maps', + 'alias' => 'Smart-Search-Maps', + 'note' => '', + 'path' => 'Smart Search/Maps', + 'link' => 'index.php?option=com_finder&view=maps', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'com_finder_filters', + 'alias' => 'Smart-Search-Filters', + 'note' => '', + 'path' => 'Smart Search/Filters', + 'link' => 'index.php?option=com_finder&view=filters', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'com_finder_searches', + 'alias' => 'Smart-Search-Searches', + 'note' => '', + 'path' => 'Smart Search/Searches', + 'link' => 'index.php?option=com_finder&view=searches', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ] + ]; + + return $menuItems; + } + + /** + * Updates content type table classes. + * + * @return void + * + * @since 4.0.0 + */ + private function updateContentTypes(): void + { + // Content types to update. + $contentTypes = [ + 'com_content.article', + 'com_contact.contact', + 'com_newsfeeds.newsfeed', + 'com_tags.tag', + 'com_banners.banner', + 'com_banners.client', + 'com_users.note', + 'com_content.category', + 'com_contact.category', + 'com_newsfeeds.category', + 'com_banners.category', + 'com_users.category', + 'com_users.user', + ]; + + // Get table definitions. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('type_alias'), + $db->quoteName('table'), + ] + ) + ->from($db->quoteName('#__content_types')) + ->whereIn($db->quoteName('type_alias'), $contentTypes, ParameterType::STRING); + + $db->setQuery($query); + $contentTypes = $db->loadObjectList(); + + // Prepare the update query. + $query = $db->getQuery(true) + ->update($db->quoteName('#__content_types')) + ->set($db->quoteName('table') . ' = :table') + ->where($db->quoteName('type_alias') . ' = :typeAlias') + ->bind(':table', $table) + ->bind(':typeAlias', $typeAlias); + + $db->setQuery($query); + + foreach ($contentTypes as $contentType) { + list($component, $tableType) = explode('.', $contentType->type_alias); + + // Special case for core table classes. + if ($contentType->type_alias === 'com_users.users' || $tableType === 'category') { + $tablePrefix = 'Joomla\\CMS\Table\\'; + $tableType = ucfirst($tableType); + } else { + $tablePrefix = 'Joomla\\Component\\' . ucfirst(substr($component, 4)) . '\\Administrator\\Table\\'; + $tableType = ucfirst($tableType) . 'Table'; + } + + // Bind type alias. + $typeAlias = $contentType->type_alias; + + $table = json_decode($contentType->table); + + // Update table definitions. + $table->special->type = $tableType; + $table->special->prefix = $tablePrefix; + + // Some content types don't have this property. + if (!empty($table->common->prefix)) { + $table->common->prefix = 'Joomla\\CMS\\Table\\'; + } + + $table = json_encode($table); + + // Execute the query. + $db->execute(); + } + } + + /** + * Renames or removes incorrectly cased files. + * + * @return void + * + * @since 3.9.25 + */ + protected function fixFilenameCasing() + { + $files = array( + // 3.10 changes + '/libraries/src/Filesystem/Support/Stringcontroller.php' => '/libraries/src/Filesystem/Support/StringController.php', + '/libraries/src/Form/Rule/SubFormRule.php' => '/libraries/src/Form/Rule/SubformRule.php', + // 4.0.0 + '/media/vendor/skipto/js/skipTo.js' => '/media/vendor/skipto/js/skipto.js', + ); + + foreach ($files as $old => $expected) { + $oldRealpath = realpath(JPATH_ROOT . $old); + + // On Unix without incorrectly cased file. + if ($oldRealpath === false) { + continue; + } + + $oldBasename = basename($oldRealpath); + $newRealpath = realpath(JPATH_ROOT . $expected); + $newBasename = basename($newRealpath); + $expectedBasename = basename($expected); + + // On Windows or Unix with only the incorrectly cased file. + if ($newBasename !== $expectedBasename) { + // Rename the file. + File::move(JPATH_ROOT . $old, JPATH_ROOT . $old . '.tmp'); + File::move(JPATH_ROOT . $old . '.tmp', JPATH_ROOT . $expected); + + continue; + } + + // There might still be an incorrectly cased file on other OS than Windows. + if ($oldBasename === basename($old)) { + // Check if case-insensitive file system, eg on OSX. + if (fileinode($oldRealpath) === fileinode($newRealpath)) { + // Check deeper because even realpath or glob might not return the actual case. + if (!in_array($expectedBasename, scandir(dirname($newRealpath)))) { + // Rename the file. + File::move(JPATH_ROOT . $old, JPATH_ROOT . $old . '.tmp'); + File::move(JPATH_ROOT . $old . '.tmp', JPATH_ROOT . $expected); + } + } else { + // On Unix with both files: Delete the incorrectly cased file. + File::delete(JPATH_ROOT . $old); + } + } + } + } + + /** + * Move core template (s)css or js or image files which are left after deleting + * obsolete core files to the right place in media folder. + * + * @return void + * + * @since 4.1.0 + */ + protected function moveRemainingTemplateFiles() + { + $folders = [ + '/administrator/templates/atum/css' => '/media/templates/administrator/atum/css', + '/administrator/templates/atum/images' => '/media/templates/administrator/atum/images', + '/administrator/templates/atum/js' => '/media/templates/administrator/atum/js', + '/administrator/templates/atum/scss' => '/media/templates/administrator/atum/scss', + '/templates/cassiopeia/css' => '/media/templates/site/cassiopeia/css', + '/templates/cassiopeia/images' => '/media/templates/site/cassiopeia/images', + '/templates/cassiopeia/js' => '/media/templates/site/cassiopeia/js', + '/templates/cassiopeia/scss' => '/media/templates/site/cassiopeia/scss', + ]; + + foreach ($folders as $oldFolder => $newFolder) { + if (Folder::exists(JPATH_ROOT . $oldFolder)) { + $oldPath = realpath(JPATH_ROOT . $oldFolder); + $newPath = realpath(JPATH_ROOT . $newFolder); + $directory = new \RecursiveDirectoryIterator($oldPath); + $directory->setFlags(RecursiveDirectoryIterator::SKIP_DOTS); + $iterator = new \RecursiveIteratorIterator($directory); + + // Handle all files in this folder and all sub-folders + foreach ($iterator as $oldFile) { + if ($oldFile->isDir()) { + continue; + } + + $newFile = $newPath . substr($oldFile, strlen($oldPath)); + + // Create target folder and parent folders if they don't exist yet + if (is_dir(dirname($newFile)) || @mkdir(dirname($newFile), 0755, true)) { + File::move($oldFile, $newFile); + } + } + } + } + } + + /** + * Ensure the core templates are correctly moved to the new mode. + * + * @return void + * + * @since 4.1.0 + */ + protected function fixTemplateMode(): void + { + $db = Factory::getContainer()->get('DatabaseDriver'); + + array_map( + function ($template) use ($db) { + $clientId = $template === 'atum' ? 1 : 0; + $query = $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('inheritable') . ' = 1') + ->where($db->quoteName('template') . ' = ' . $db->quote($template)) + ->where($db->quoteName('client_id') . ' = ' . $clientId); + + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + }, + ['atum', 'cassiopeia'] + ); + } + + /** + * Add the user Auth Provider Column as it could be present from 3.10 already + * + * @return void + * + * @since 4.1.1 + */ + protected function addUserAuthProviderColumn(): void + { + $db = Factory::getContainer()->get('DatabaseDriver'); + + // Check if the column already exists + $fields = $db->getTableColumns('#__users'); + + // Column exists, skip + if (isset($fields['authProvider'])) { + return; + } + + $query = 'ALTER TABLE ' . $db->quoteName('#__users') + . ' ADD COLUMN ' . $db->quoteName('authProvider') . ' varchar(100) DEFAULT ' . $db->quote('') . ' NOT NULL'; + + // Add column + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + } } diff --git a/code/administrator/components/com_admin/services/provider.php b/code/administrator/components/com_admin/services/provider.php index 9973ba6a..d17a3d0c 100644 --- a/code/administrator/components/com_admin/services/provider.php +++ b/code/administrator/components/com_admin/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Admin')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Admin')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Admin')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Admin')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new AdminComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new AdminComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-03-05.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-03-05.sql index c025f928..39c703e6 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-03-05.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-03-05.sql @@ -1,7 +1,13 @@ +-- The following statement was moved from below to here and modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__extensions` DROP COLUMN `system_data` /** CAN FAIL **/; + -- From 4.0.0-2016-07-03.sql -INSERT INTO `#__extensions` (`name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `custom_data`, `system_data`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES -('plg_behaviour_taggable', 'plugin', 'taggable', 'behaviour', 0, 1, 1, 0, '', '{}', '', '', 0, '0000-00-00 00:00:00', 0, 0), -('plg_behaviour_versionable', 'plugin', 'versionable', 'behaviour', 0, 1, 1, 0, '', '{}', '', '', 0, '0000-00-00 00:00:00', 0, 0); +-- The following statement was modified for 4.1.1 by removing the `system_data` column. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT INTO `#__extensions` (`name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `custom_data`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES +('plg_behaviour_taggable', 'plugin', 'taggable', 'behaviour', 0, 1, 1, 0, '', '{}', '', 0, '0000-00-00 00:00:00', 0, 0), +('plg_behaviour_versionable', 'plugin', 'versionable', 'behaviour', 0, 1, 1, 0, '', '{}', '', 0, '0000-00-00 00:00:00', 0, 0); -- From 4.0.0-2016-09-22.sql DELETE FROM `#__extensions` WHERE `type` = 'library' AND `element` = 'phputf8'; @@ -10,17 +16,22 @@ DELETE FROM `#__extensions` WHERE `type` = 'library' AND `element` = 'phputf8'; DELETE FROM `#__extensions` WHERE `type` = 'plugin' AND `element` = 'p3p' AND `folder` = 'system'; -- From 4.0.0-2016-10-02.sql -ALTER TABLE `#__user_keys` DROP COLUMN `invalid`; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__user_keys` DROP COLUMN `invalid` /** CAN FAIL **/; -- -- Insert the new templates into the database. Set as home if the old template is the active one -- -INSERT INTO `#__extensions` (`name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `custom_data`, `system_data`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES -('atum', 'template', 'atum', '', 1, 1, 1, 0, '{}', '{}', '', '', 0, '0000-00-00 00:00:00', 0, 0), -('cassiopeia', 'template', 'cassiopeia', '', 0, 1, 1, 0, '{}', '{}', '', '', 0, '0000-00-00 00:00:00', 0, 0); + +-- The following statement was modified for 4.1.1 by removing the `system_data` column. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT INTO `#__extensions` (`name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `custom_data`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES +('atum', 'template', 'atum', '', 1, 1, 1, 0, '{}', '{}', '', 0, '0000-00-00 00:00:00', 0, 0), +('cassiopeia', 'template', 'cassiopeia', '', 0, 1, 1, 0, '{}', '{}', '', 0, '0000-00-00 00:00:00', 0, 0); -- The following statement had to be modified for 4.1 by adding the `inheritable` and `parent` columns. --- See https://github.com/joomla/joomla-cms/pull/36585 . +-- See https://github.com/joomla/joomla-cms/pull/36585 INSERT INTO `#__template_styles` (`template`, `client_id`, `home`, `title`, `inheritable`, `parent`, `params`) VALUES ('atum', 1, (CASE WHEN (SELECT b.`count` FROM (SELECT count(a.`id`) AS `count` FROM `#__template_styles` a WHERE a.`home` = '1' AND a.`client_id` = 1 AND a.`template` IN ('isis', 'hathor')) AS b) = 0 THEN '0' ELSE '1' END), 'atum - Default', 1, '', '{}'), ('cassiopeia', 0, (CASE WHEN (SELECT d.`count` FROM (SELECT count(c.`id`) AS `count` FROM `#__template_styles` c WHERE c.`home` = '1' AND c.`client_id` = 0 AND c.`template` IN ('protostar', 'beez3')) AS d) = 0 THEN '0' ELSE '1' END), 'cassiopeia - Default', 1, '', '{}'); @@ -49,7 +60,9 @@ DELETE FROM `#__template_styles` WHERE `template` = 'beez3' AND `client_id` = 0; DELETE FROM `#__extensions` WHERE `name` = 'mod_submenu'; -- From 4.0.0-2017-03-18.sql -ALTER TABLE `#__extensions` DROP COLUMN `system_data`; +-- The following statement was moved to the top for 4.1.1. +-- See https://github.com/joomla/joomla-cms/pull/37156 +-- ALTER TABLE `#__extensions` DROP COLUMN `system_data`; -- From 4.0.0-2017-04-25.sql INSERT INTO `#__extensions` (`name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `custom_data`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES @@ -63,8 +76,10 @@ UPDATE `#__menu` SET `link` = 'index.php?option=com_config&view=config' WHERE `l UPDATE `#__menu` SET `link` = 'index.php?option=com_config&view=templates' WHERE `link` = 'index.php?option=com_config&view=templates&controller=config.display.templates'; -- From 4.0.0-2017-06-03.sql -ALTER TABLE `#__extensions` ADD COLUMN `changelogurl` text AFTER `element`; -ALTER TABLE `#__updates` ADD COLUMN `changelogurl` text AFTER `infourl`; +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__extensions` ADD COLUMN `changelogurl` text AFTER `element` /** CAN FAIL **/; +ALTER TABLE `#__updates` ADD COLUMN `changelogurl` text AFTER `infourl` /** CAN FAIL **/; -- From 4.0.0-2017-10-10.sql INSERT INTO `#__extensions` (`name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `custom_data`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-05-15.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-05-15.sql index 7c0a7e2f..12bdd47c 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-05-15.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-05-15.sql @@ -33,7 +33,9 @@ CREATE TABLE IF NOT EXISTS `#__workflows` ( -- Dumping data for table `#__workflows` -- -INSERT INTO `#__workflows` (`id`, `asset_id`, `published`, `title`, `description`, `extension`, `default`, `ordering`, `created`, `created_by`, `modified`, `modified_by`, `checked_out_time`, `checked_out`) VALUES +-- The following statement was modified for 4.1.1 by adding the "IGNORE" keyword. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT IGNORE INTO `#__workflows` (`id`, `asset_id`, `published`, `title`, `description`, `extension`, `default`, `ordering`, `created`, `created_by`, `modified`, `modified_by`, `checked_out_time`, `checked_out`) VALUES (1, 0, 1, 'COM_WORKFLOW_BASIC_WORKFLOW', '', 'com_content.article', 1, 1, CURRENT_TIMESTAMP(), 0, CURRENT_TIMESTAMP(), 0, NULL, 0); -- @@ -78,7 +80,9 @@ CREATE TABLE IF NOT EXISTS `#__workflow_stages` ( -- Dumping data for table `#__workflow_stages` -- -INSERT INTO `#__workflow_stages` (`id`, `asset_id`, `ordering`, `workflow_id`, `published`, `title`, `description`, `default`, `checked_out_time`, `checked_out`) VALUES +-- The following statement was modified for 4.1.1 by adding the "IGNORE" keyword. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT IGNORE INTO `#__workflow_stages` (`id`, `asset_id`, `ordering`, `workflow_id`, `published`, `title`, `description`, `default`, `checked_out_time`, `checked_out`) VALUES (1, 0, 1, 1, 1, 'COM_WORKFLOW_BASIC_STAGE', '', 1, NULL, 0); -- @@ -111,7 +115,9 @@ CREATE TABLE IF NOT EXISTS `#__workflow_transitions` ( -- Dumping data for table `#__workflow_transitions` -- -INSERT INTO `#__workflow_transitions` (`id`, `asset_id`, `published`, `ordering`, `workflow_id`, `title`, `description`, `from_stage_id`, `to_stage_id`, `options`, `checked_out_time`, `checked_out`) VALUES +-- The following statement was modified for 4.1.1 by adding the "IGNORE" keyword. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT IGNORE INTO `#__workflow_transitions` (`id`, `asset_id`, `published`, `ordering`, `workflow_id`, `title`, `description`, `from_stage_id`, `to_stage_id`, `options`, `checked_out_time`, `checked_out`) VALUES (1, 0, 1, 1, 1, 'Unpublish', '', -1, 1, '{"publishing":"0"}', NULL, 0), (2, 0, 1, 2, 1, 'Publish', '', -1, 1, '{"publishing":"1"}', NULL, 0), (3, 0, 1, 3, 1, 'Trash', '', -1, 1, '{"publishing":"-2"}', NULL, 0), @@ -137,5 +143,8 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, -- -- Creating Associations for existing content -- -INSERT INTO `#__workflow_associations` (`item_id`, `stage_id`, `extension`) + +-- The following statement was modified for 4.1.1 by adding the "IGNORE" keyword. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT IGNORE INTO `#__workflow_associations` (`item_id`, `stage_id`, `extension`) SELECT `id`, 1, 'com_content.article' FROM `#__content`; diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-07-29.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-07-29.sql index 426ae323..fe33fa3f 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-07-29.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-07-29.sql @@ -20,11 +20,15 @@ ALTER TABLE `#__finder_links` MODIFY `publish_start_date` datetime NULL DEFAULT ALTER TABLE `#__finder_links` MODIFY `publish_end_date` datetime NULL DEFAULT NULL; ALTER TABLE `#__finder_links` MODIFY `start_date` datetime NULL DEFAULT NULL; ALTER TABLE `#__finder_links` MODIFY `end_date` datetime NULL DEFAULT NULL; -ALTER TABLE `#__finder_links` ADD INDEX `idx_language` (`language`); +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__finder_links` ADD INDEX `idx_language` (`language`) /** CAN FAIL **/; ALTER TABLE `#__finder_links` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE `#__finder_links` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE TABLE `#__finder_links_terms` ( +-- The following statement was modified for 4.1.1 by adding the "IF NOT EXISTS" keywords. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE TABLE IF NOT EXISTS `#__finder_links_terms` ( `link_id` INT UNSIGNED NOT NULL, `term_id` INT UNSIGNED NOT NULL, `weight` FLOAT UNSIGNED NOT NULL DEFAULT 0, @@ -33,22 +37,24 @@ CREATE TABLE `#__finder_links_terms` ( INDEX `idx_link_term_weight` (`link_id`, `term_id`, `weight`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; -DROP TABLE `#__finder_links_terms0`; -DROP TABLE `#__finder_links_terms1`; -DROP TABLE `#__finder_links_terms2`; -DROP TABLE `#__finder_links_terms3`; -DROP TABLE `#__finder_links_terms4`; -DROP TABLE `#__finder_links_terms5`; -DROP TABLE `#__finder_links_terms6`; -DROP TABLE `#__finder_links_terms7`; -DROP TABLE `#__finder_links_terms8`; -DROP TABLE `#__finder_links_terms9`; -DROP TABLE `#__finder_links_termsa`; -DROP TABLE `#__finder_links_termsb`; -DROP TABLE `#__finder_links_termsc`; -DROP TABLE `#__finder_links_termsd`; -DROP TABLE `#__finder_links_termse`; -DROP TABLE `#__finder_links_termsf`; +-- The following 16 statements were modified for 4.1.1 by adding the "IF EXISTS" keywords. +-- See https://github.com/joomla/joomla-cms/pull/37156 +DROP TABLE IF EXISTS `#__finder_links_terms0`; +DROP TABLE IF EXISTS `#__finder_links_terms1`; +DROP TABLE IF EXISTS `#__finder_links_terms2`; +DROP TABLE IF EXISTS `#__finder_links_terms3`; +DROP TABLE IF EXISTS `#__finder_links_terms4`; +DROP TABLE IF EXISTS `#__finder_links_terms5`; +DROP TABLE IF EXISTS `#__finder_links_terms6`; +DROP TABLE IF EXISTS `#__finder_links_terms7`; +DROP TABLE IF EXISTS `#__finder_links_terms8`; +DROP TABLE IF EXISTS `#__finder_links_terms9`; +DROP TABLE IF EXISTS `#__finder_links_termsa`; +DROP TABLE IF EXISTS `#__finder_links_termsb`; +DROP TABLE IF EXISTS `#__finder_links_termsc`; +DROP TABLE IF EXISTS `#__finder_links_termsd`; +DROP TABLE IF EXISTS `#__finder_links_termse`; +DROP TABLE IF EXISTS `#__finder_links_termsf`; CREATE TABLE IF NOT EXISTS `#__finder_logging` ( `searchterm` VARCHAR(255) NOT NULL DEFAULT '', @@ -62,7 +68,9 @@ CREATE TABLE IF NOT EXISTS `#__finder_logging` ( ALTER TABLE `#__finder_logging` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE `#__finder_logging` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -DROP TABLE `#__finder_taxonomy`; +-- The following statement was modified for 4.1.1 by adding the "IF EXISTS" keywords. +-- See https://github.com/joomla/joomla-cms/pull/37156 +DROP TABLE IF EXISTS `#__finder_taxonomy`; CREATE TABLE IF NOT EXISTS `#__finder_taxonomy` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `parent_id` INT UNSIGNED NOT NULL DEFAULT '0', @@ -97,14 +105,18 @@ ALTER TABLE `#__finder_terms` CHANGE `language` `language` CHAR(7) NOT NULL DEFA ALTER TABLE `#__finder_terms` MODIFY `term` varchar(75) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL; ALTER TABLE `#__finder_terms` MODIFY `stem` varchar(75) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT ''; ALTER TABLE `#__finder_terms` MODIFY `soundex` varchar(75) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT ''; -ALTER TABLE `#__finder_terms` DROP INDEX `idx_term`; -ALTER TABLE `#__finder_terms` ADD INDEX `idx_stem` (`stem`); -ALTER TABLE `#__finder_terms` ADD INDEX `idx_language` (`language`); -ALTER TABLE `#__finder_terms` ADD UNIQUE INDEX `idx_term_language` (`term`, `language`); +-- The following 4 statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__finder_terms` DROP INDEX `idx_term` /** CAN FAIL **/; +ALTER TABLE `#__finder_terms` ADD INDEX `idx_stem` (`stem`) /** CAN FAIL **/; +ALTER TABLE `#__finder_terms` ADD INDEX `idx_language` (`language`) /** CAN FAIL **/; +ALTER TABLE `#__finder_terms` ADD UNIQUE INDEX `idx_term_language` (`term`, `language`) /** CAN FAIL **/; ALTER TABLE `#__finder_terms` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; DROP TABLE IF EXISTS `#__finder_terms_common`; -CREATE TABLE `#__finder_terms_common` ( +-- The following statement was modified for 4.1.1 by adding the "IF NOT EXISTS" keywords. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE TABLE IF NOT EXISTS `#__finder_terms_common` ( `term` varchar(75) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '', `language` char(7) NOT NULL DEFAULT '', `custom` int NOT NULL DEFAULT '0', @@ -292,13 +304,17 @@ ALTER TABLE `#__finder_tokens` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_ ALTER TABLE `#__finder_tokens` CHANGE `language` `language` CHAR(7) NOT NULL DEFAULT '' AFTER `context`; ALTER TABLE `#__finder_tokens` MODIFY `term` varchar(75) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL; ALTER TABLE `#__finder_tokens` MODIFY `stem` varchar(75) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT ''; -ALTER TABLE `#__finder_tokens` ADD INDEX `idx_stem` (`stem`); -ALTER TABLE `#__finder_tokens` ADD INDEX `idx_language` (`language`); +-- The following 2 statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__finder_tokens` ADD INDEX `idx_stem` (`stem`) /** CAN FAIL **/; +ALTER TABLE `#__finder_tokens` ADD INDEX `idx_language` (`language`) /** CAN FAIL **/; ALTER TABLE `#__finder_tokens` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; TRUNCATE TABLE `#__finder_tokens_aggregate`; ALTER TABLE `#__finder_tokens_aggregate` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE `#__finder_tokens_aggregate` DROP COLUMN `map_suffix`; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__finder_tokens_aggregate` DROP COLUMN `map_suffix` /** CAN FAIL **/; ALTER TABLE `#__finder_tokens_aggregate` CHANGE `language` `language` CHAR(7) NOT NULL DEFAULT '' AFTER `total_weight`; ALTER TABLE `#__finder_tokens_aggregate` MODIFY `term` varchar(75) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL; ALTER TABLE `#__finder_tokens_aggregate` MODIFY `stem` varchar(75) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT ''; diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-08-29.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-08-29.sql index 320dd3bc..f4f75a8d 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-08-29.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-08-29.sql @@ -6,7 +6,9 @@ ALTER TABLE `#__ucm_content` MODIFY `core_publish_up` datetime NULL DEFAULT NULL ALTER TABLE `#__ucm_content` MODIFY `core_publish_down` datetime NULL DEFAULT NULL; -- Only on MySQL: Update empty strings to null date before converting the column from varchar to datetime -UPDATE `#__ucm_content` SET `core_checked_out_time` = '0000-00-00 00:00:00' WHERE `core_checked_out_time` = ''; +-- The following statement was modified for 4.1.1 by adding a check for NULL and a type cast to CHAR. +-- See https://github.com/joomla/joomla-cms/pull/37156 +UPDATE `#__ucm_content` SET `core_checked_out_time` = '0000-00-00 00:00:00' WHERE `core_checked_out_time` IS NOT NULL AND CAST(`core_checked_out_time` AS char) = ''; ALTER TABLE `#__ucm_content` MODIFY `core_checked_out_time` datetime NULL DEFAULT NULL; diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-09.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-09.sql index 7c0e34b6..9f44d2d9 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-09.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-09.sql @@ -25,7 +25,10 @@ CREATE TABLE IF NOT EXISTS `#__mail_templates` ( PRIMARY KEY (`template_id`, `language`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; -INSERT INTO `#__mail_templates` (`template_id`, `language`, `subject`, `body`, `htmlbody`, `attachments`, `params`) VALUES ('com_config.test_mail', '', 'COM_CONFIG_SENDMAIL_SUBJECT', 'COM_CONFIG_SENDMAIL_BODY', '', '', '{"tags":["sitename","method"]}'); +-- The following statement was modified for 4.1.1 by adding the "IGNORE" keyword. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT IGNORE INTO `#__mail_templates` (`template_id`, `language`, `subject`, `body`, `htmlbody`, `attachments`, `params`) VALUES +('com_config.test_mail', '', 'COM_CONFIG_SENDMAIL_SUBJECT', 'COM_CONFIG_SENDMAIL_BODY', '', '', '{"tags":["sitename","method"]}'); -- From 4.0.0-2019-02-03.sql DELETE FROM `#__menu` WHERE `link` = 'index.php?option=com_postinstall' AND `menutype` = 'main'; diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-30.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-30.sql index 61c15423..5286991e 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-30.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-30.sql @@ -1,5 +1,7 @@ -ALTER TABLE `#__menu` ADD COLUMN `publish_up` datetime; -ALTER TABLE `#__menu` ADD COLUMN `publish_down` datetime; +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__menu` ADD COLUMN `publish_up` datetime /** CAN FAIL **/; +ALTER TABLE `#__menu` ADD COLUMN `publish_down` datetime /** CAN FAIL **/; ALTER TABLE `#__menu` MODIFY `checked_out_time` datetime NULL DEFAULT NULL; diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-05-20.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-05-20.sql index 7c030af4..877147a6 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-05-20.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-05-20.sql @@ -12,4 +12,6 @@ UPDATE `#__menu` SET `link`='index.php?option=com_tags&view=tags' WHERE `menutyp UPDATE `#__menu` SET `link`='index.php?option=com_associations&view=associations' WHERE `menutype`='main' AND `path`='Multilingual Associations'; -- From 4.0.0-2019-05-20.sql -ALTER TABLE `#__extensions` ADD COLUMN `note` varchar(255) AFTER `state`; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__extensions` ADD COLUMN `note` varchar(255) AFTER `state` /** CAN FAIL **/; diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-13.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-13.sql index 4dc1c383..bb35e1ef 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-13.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-13.sql @@ -1,18 +1,24 @@ -- From 4.0.0-2019-07-14.sql -ALTER TABLE `#__contact_details` DROP COLUMN `xreference`; -ALTER TABLE `#__content` DROP COLUMN `xreference`; -ALTER TABLE `#__newsfeeds` DROP COLUMN `xreference`; +-- The following 3 statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__contact_details` DROP COLUMN `xreference` /** CAN FAIL **/; +ALTER TABLE `#__content` DROP COLUMN `xreference` /** CAN FAIL **/; +ALTER TABLE `#__newsfeeds` DROP COLUMN `xreference` /** CAN FAIL **/; -- From 4.0.0-2019-07-16.sql -- This has been removed as com_csp has been removed from the final build -- From 4.0.0-2019-08-03.sql -ALTER TABLE `#__update_sites` ADD COLUMN `checked_out` int unsigned NOT NULL DEFAULT 0; -ALTER TABLE `#__update_sites` ADD COLUMN `checked_out_time` datetime NULL DEFAULT NULL; +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__update_sites` ADD COLUMN `checked_out` int unsigned NOT NULL DEFAULT 0 /** CAN FAIL **/; +ALTER TABLE `#__update_sites` ADD COLUMN `checked_out_time` datetime NULL DEFAULT NULL /** CAN FAIL **/; -- From 4.0.0-2019-08-20.sql -ALTER TABLE `#__content_frontpage` ADD COLUMN `featured_up` datetime; -ALTER TABLE `#__content_frontpage` ADD COLUMN `featured_down` datetime; +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__content_frontpage` ADD COLUMN `featured_up` datetime /** CAN FAIL **/; +ALTER TABLE `#__content_frontpage` ADD COLUMN `featured_down` datetime /** CAN FAIL **/; -- From 4.0.0-2019-08-21.sql INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `custom_data`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES @@ -32,5 +38,7 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, (0, 'plg_webservices_users', 'plugin', 'users', 'webservices', 0, 1, 1, 0, '', '{}', '', 0, '0000-00-00 00:00:00', 0, 0); -- From 4.0.0-2019-09-13.sql -INSERT INTO `#__menu` (`menutype`, `title`, `alias`, `note`, `path`, `link`, `type`, `published`, `parent_id`, `level`, `component_id`, `checked_out`, `checked_out_time`, `browserNav`, `access`, `img`, `template_style_id`, `params`, `lft`, `rgt`, `home`, `language`, `client_id`, `publish_up`, `publish_down`) +-- The following statement was modified for 4.1.1 by adding the "IGNORE" keyword. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT IGNORE INTO `#__menu` (`menutype`, `title`, `alias`, `note`, `path`, `link`, `type`, `published`, `parent_id`, `level`, `component_id`, `checked_out`, `checked_out_time`, `browserNav`, `access`, `img`, `template_style_id`, `params`, `lft`, `rgt`, `home`, `language`, `client_id`, `publish_up`, `publish_down`) SELECT 'main', 'com_messages_manager', 'Private Messages', '', 'Messaging/Private Messages', 'index.php?option=com_messages&view=messages', 'component', 1, 10, 2, `extension_id`, 0, NULL, 0, 0, 'class:messages-add', 0, '', 18, 19, 0, '*', 1, NULL, NULL FROM `#__extensions` WHERE `name` = 'com_messages'; diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-03-25.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-03-25.sql index 5f72531a..a11fffdf 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-03-25.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-03-25.sql @@ -1,6 +1,8 @@ -- Add locked field to extensions table. ALTER TABLE `#__extensions` MODIFY `protected` tinyint NOT NULL DEFAULT 0 COMMENT 'Flag to indicate if the extension is protected. Protected extensions cannot be disabled.'; -ALTER TABLE `#__extensions` ADD COLUMN `locked` tinyint NOT NULL DEFAULT 0 COMMENT 'Flag to indicate if the extension is locked. Locked extensions cannot be uninstalled.'; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__extensions` ADD COLUMN `locked` tinyint NOT NULL DEFAULT 0 COMMENT 'Flag to indicate if the extension is locked. Locked extensions cannot be uninstalled.' /** CAN FAIL **/; -- Set all core extensions as locked extensions and unprotected them. UPDATE `#__extensions` diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-05-29.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-05-29.sql index f1b4f6e0..8157ec80 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-05-29.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-05-29.sql @@ -3,7 +3,9 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, (0, 'plg_quickicon_downloadkey', 'plugin', 'downloadkey', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 0, NULL, 0, 0); -- From 4.0.0-2020-04-16.sql -INSERT INTO `#__mail_templates` (`template_id`, `language`, `subject`, `body`, `htmlbody`, `attachments`, `params`) VALUES +-- The following statement was modified for 4.1.1 by adding the "IGNORE" keyword. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT IGNORE INTO `#__mail_templates` (`template_id`, `language`, `subject`, `body`, `htmlbody`, `attachments`, `params`) VALUES ('com_contact.mail', '', 'COM_CONTACT_ENQUIRY_SUBJECT', 'COM_CONTACT_ENQUIRY_TEXT', '', '', '{"tags":["sitename","name","email","subject","body","url","customfields"]}'), ('com_contact.mail.copy', '', 'COM_CONTACT_COPYSUBJECT_OF', 'COM_CONTACT_COPYTEXT_OF', '', '', '{"tags":["sitename","name","email","subject","body","url","customfields"]}'), ('com_users.massmail.mail', '', 'COM_USERS_MASSMAIL_MAIL_SUBJECT', 'COM_USERS_MASSMAIL_MAIL_BODY', '', '', '{"tags":["subject","body","subjectprefix","bodysuffix"]}'), @@ -13,14 +15,16 @@ INSERT INTO `#__mail_templates` (`template_id`, `language`, `subject`, `body`, ` ('plg_user_joomla.mail', '', 'PLG_USER_JOOMLA_NEW_USER_EMAIL_SUBJECT', 'PLG_USER_JOOMLA_NEW_USER_EMAIL_BODY', '', '', '{"tags":["name","sitename","url","username","password","email"]}'); -- From 4.0.0-2020-05-21.sql +-- The following 4 statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 -- Renaming table -RENAME TABLE `#__ucm_history` TO `#__history`; +RENAME TABLE `#__ucm_history` TO `#__history` /** CAN FAIL **/; -- Rename ucm_item_id to item_id as the new primary identifier for the original content item -ALTER TABLE `#__history` CHANGE `ucm_item_id` `item_id` VARCHAR(50) NOT NULL AFTER `version_id`; +ALTER TABLE `#__history` CHANGE `ucm_item_id` `item_id` VARCHAR(50) NOT NULL AFTER `version_id` /** CAN FAIL **/; -- Extend the original field content with the alias of the content type -UPDATE #__history AS h INNER JOIN #__content_types AS c ON h.ucm_type_id = c.type_id SET h.item_id = CONCAT(c.type_alias, '.', h.item_id); +UPDATE #__history AS h INNER JOIN #__content_types AS c ON h.ucm_type_id = c.type_id SET h.item_id = CONCAT(c.type_alias, '.', h.item_id) /** CAN FAIL **/; -- Now we don't need the ucm_type_id anymore and drop it. -ALTER TABLE `#__history` DROP COLUMN `ucm_type_id`; +ALTER TABLE `#__history` DROP COLUMN `ucm_type_id` /** CAN FAIL **/; ALTER TABLE `#__history` MODIFY `save_date` datetime NOT NULL; -- From 4.0.0-2020-05-29.sql diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-20.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-20.sql index b7e64d06..7b0022e9 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-20.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-20.sql @@ -1,5 +1,7 @@ -- From 4.0.0-2020-12-08.sql -INSERT INTO `#__mail_templates` (`template_id`, `language`, `subject`, `body`, `htmlbody`, `attachments`, `params`) VALUES +-- The following statement was modified for 4.1.1 by adding the "IGNORE" keyword. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT IGNORE INTO `#__mail_templates` (`template_id`, `language`, `subject`, `body`, `htmlbody`, `attachments`, `params`) VALUES ('com_actionlogs.notification', '', 'COM_ACTIONLOGS_EMAIL_SUBJECT', 'COM_ACTIONLOGS_EMAIL_BODY', 'COM_ACTIONLOGS_EMAIL_HTMLBODY', '', '{"tags":["message","date","extension"]}'), ('com_privacy.userdataexport', '', 'COM_PRIVACY_EMAIL_DATA_EXPORT_COMPLETED_BODY', 'COM_PRIVACY_EMAIL_DATA_EXPORT_COMPLETED_SUBJECT', '', '', '{"tags":["sitename","url"]}'), ('com_privacy.notification.export', '', 'COM_PRIVACY_EMAIL_REQUEST_SUBJECT_EXPORT_REQUEST', 'COM_PRIVACY_EMAIL_REQUEST_BODY_EXPORT_REQUEST', '', '', '{"tags":["sitename","url","tokenurl","formurl","token"]}'), diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-22.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-22.sql index 8f8a69da..ddf5d6b3 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-22.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-22.sql @@ -9,12 +9,16 @@ DELETE FROM `#__postinstall_messages` 'TPL_HATHOR_MESSAGE_POSTINSTALL_TITLE'); -- From 4.0.0-2021-04-11.sql -ALTER TABLE `#__fields` ADD COLUMN `only_use_in_subform` tinyint NOT NULL DEFAULT 0; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__fields` ADD COLUMN `only_use_in_subform` tinyint NOT NULL DEFAULT 0 /** CAN FAIL **/; -- From 4.0.0-2021-04-20.sql UPDATE `#__extensions` SET `name` = 'plg_fields_subform', `element` = 'subform' WHERE `name` = 'plg_fields_subfields' AND `type` = 'plugin' AND `element` = 'subfields' AND `folder` = 'fields' AND `client_id` = 0; UPDATE `#__fields` SET `type` = 'subform' WHERE `type` = 'subfields'; -- From 4.0.0-2021-04-22.sql -ALTER TABLE `#__mail_templates` ADD COLUMN `extension` varchar(127) NOT NULL DEFAULT '' AFTER `template_id`; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__mail_templates` ADD COLUMN `extension` varchar(127) NOT NULL DEFAULT '' AFTER `template_id` /** CAN FAIL **/; UPDATE `#__mail_templates` SET `extension` = SUBSTRING(`template_id`, 1, POSITION('.' IN `template_id`) - 1); diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-30.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-30.sql index 44f1ed73..8d5c4f31 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-30.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-30.sql @@ -15,7 +15,9 @@ UPDATE `#__mail_templates` WHERE `template_id` = 'com_privacy.userdataexport'; -- From 4.0.0-2021-05-10.sql -ALTER TABLE `#__finder_taxonomy` ADD INDEX `idx_level` (`level`); +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__finder_taxonomy` ADD INDEX `idx_level` (`level`) /** CAN FAIL **/; -- From 4.0.0-2021-05-21.sql UPDATE `#__modules` diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.1.0-2021-11-20.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.1.0-2021-11-20.sql index a1ae75ba..39972198 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.1.0-2021-11-20.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.1.0-2021-11-20.sql @@ -56,7 +56,9 @@ INSERT INTO `#__action_log_config` (`type_title`, `type_alias`, `id_holder`, `ti ('task', 'com_scheduler.task', 'id', 'title', '#__scheduler_tasks', 'PLG_ACTIONLOG_JOOMLA'); -- Add mail templates -INSERT INTO `#__mail_templates` (`template_id`, `extension`, `language`, `subject`, `body`, `htmlbody`, `attachments`, `params`) VALUES +-- The following statement was modified for 4.1.1 by adding the "IGNORE" keyword. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT IGNORE INTO `#__mail_templates` (`template_id`, `extension`, `language`, `subject`, `body`, `htmlbody`, `attachments`, `params`) VALUES ('plg_system_tasknotification.failure_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_FAILURE_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_FAILURE_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title", "exit_code", "exec_data_time", "task_output"]}'), ('plg_system_tasknotification.fatal_recovery_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title"]}'), ('plg_system_tasknotification.orphan_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title"]}'), diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.1.0-2022-01-24.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.1.0-2022-01-24.sql index 767ef9c3..6ab11496 100644 --- a/code/administrator/components/com_admin/sql/updates/mysql/4.1.0-2022-01-24.sql +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.1.0-2022-01-24.sql @@ -1,2 +1,4 @@ -ALTER TABLE `#__redirect_links` DROP INDEX `idx_link_modifed`; -ALTER TABLE `#__redirect_links` ADD INDEX `idx_link_modified` (`modified_date`); +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE `#__redirect_links` DROP INDEX `idx_link_modifed` /** CAN FAIL **/; +ALTER TABLE `#__redirect_links` ADD INDEX `idx_link_modified` (`modified_date`) /** CAN FAIL **/; diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-05-15.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-05-15.sql new file mode 100644 index 00000000..256b940a --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-05-15.sql @@ -0,0 +1,57 @@ +-- +-- Create the new table for MFA +-- +CREATE TABLE IF NOT EXISTS `#__user_mfa` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `title` varchar(255) NOT NULL DEFAULT '', + `method` varchar(100) NOT NULL, + `default` tinyint NOT NULL DEFAULT 0, + `options` mediumtext NOT NULL, + `created_on` datetime NOT NULL, + `last_used` datetime, + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci COMMENT='Multi-factor Authentication settings'; + +-- +-- Remove obsolete postinstallation message +-- +DELETE FROM `#__postinstall_messages` WHERE `condition_file` = 'site://plugins/twofactorauth/totp/postinstall/actions.php'; + +-- +-- Add new MFA plugins +-- +INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `locked`, `manifest_cache`, `params`, `custom_data`, `ordering`, `state`) VALUES +(0, 'plg_multifactorauth_totp', 'plugin', 'totp', 'multifactorauth', 0, 0, 1, 0, 1, '', '', '', 1, 0), +(0, 'plg_multifactorauth_yubikey', 'plugin', 'yubikey', 'multifactorauth', 0, 0, 1, 0, 1, '', '', '', 2, 0), +(0, 'plg_multifactorauth_webauthn', 'plugin', 'webauthn', 'multifactorauth', 0, 0, 1, 0, 1, '', '', '', 3, 0), +(0, 'plg_multifactorauth_email', 'plugin', 'email', 'multifactorauth', 0, 0, 1, 0, 1, '', '', '', 4, 0), +(0, 'plg_multifactorauth_fixed', 'plugin', 'fixed', 'multifactorauth', 0, 0, 1, 0, 1, '', '', '', 5, 0); + +-- +-- Update MFA plugins' publish status +-- +UPDATE `#__extensions` AS `a` + INNER JOIN `#__extensions` AS `b` on `a`.`element` = `b`.`element` +SET `a`.enabled = `b`.enabled +WHERE `a`.folder = 'multifactorauth' + AND `b`.folder = 'twofactorauth'; + +-- +-- Remove legacy TFA plugins +-- +DELETE FROM `#__extensions` +WHERE `type` = 'plugin' AND `folder` = 'twofactorauth' AND `element` IN ('totp', 'yubikey'); + +-- +-- Add post-installation message +-- +INSERT IGNORE INTO `#__postinstall_messages` (`extension_id`, `title_key`, `description_key`, `action_key`, `language_extension`, `language_client_id`, `type`, `action_file`, `action`, `condition_file`, `condition_method`, `version_introduced`, `enabled`) +SELECT `extension_id`, 'COM_USERS_POSTINSTALL_MULTIFACTORAUTH_TITLE', 'COM_USERS_POSTINSTALL_MULTIFACTORAUTH_BODY', 'COM_USERS_POSTINSTALL_MULTIFACTORAUTH_ACTION', 'com_users', 1, 'action', 'admin://components/com_users/postinstall/multifactorauth.php', 'com_users_postinstall_mfa_action', 'admin://components/com_users/postinstall/multifactorauth.php', 'com_users_postinstall_mfa_condition', '4.2.0', 1 FROM `#__extensions` WHERE `name` = 'files_joomla'; + +-- +-- Create a mail template for plg_multifactorauth_email +-- +INSERT IGNORE INTO `#__mail_templates` (`template_id`, `extension`, `language`, `subject`, `body`, `htmlbody`, `attachments`, `params`) VALUES +('plg_multifactorauth_email.mail', 'plg_multifactorauth_email', '', 'PLG_MULTIFACTORAUTH_EMAIL_EMAIL_SUBJECT', 'PLG_MULTIFACTORAUTH_EMAIL_EMAIL_BODY', '', '', '{"tags":["code","sitename","siteurl","username","email","fullname"]}'); diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-06-15.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-06-15.sql new file mode 100644 index 00000000..d60e1f05 --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-06-15.sql @@ -0,0 +1,4 @@ +-- +-- Increase the size of the htmlbody field in the #__mail_templates table +-- +ALTER TABLE `#__mail_templates` MODIFY `htmlbody` mediumtext NOT NULL COLLATE 'utf8mb4_unicode_ci'; diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-06-19.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-06-19.sql new file mode 100644 index 00000000..55aa01c3 --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-06-19.sql @@ -0,0 +1,3 @@ +-- See https://github.com/joomla/joomla-cms/pull/38092 +INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `locked`, `manifest_cache`, `params`, `custom_data`, `ordering`, `state`) VALUES +(0, 'plg_system_shortcut', 'plugin', 'shortcut', 'system', 0, 1, 1, 0, 1, '', '', '', 0, 0); diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-06-22.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-06-22.sql new file mode 100644 index 00000000..369a8d6c --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-06-22.sql @@ -0,0 +1,9 @@ +-- Set core extensions as locked extensions. +UPDATE `#__extensions` +SET `locked` = 1 +WHERE (`type` = 'plugin' AND + ( + (`folder` = 'system' AND `element` = 'schedulerunner') + OR (`folder` = 'task' AND `element` IN ('checkfiles', 'demotasks', 'requests', 'sitestatus')) + ) +); diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-07-07.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-07-07.sql new file mode 100644 index 00000000..9833260f --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.2.0-2022-07-07.sql @@ -0,0 +1,4 @@ +-- The following statement added with Joonmla version 4.2.0 RC 1 had to be removed with version 4.2.0 (stable). +-- See https://github.com/joomla/joomla-cms/pull/38244 +-- INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `locked`, `manifest_cache`, `params`, `custom_data`, `ordering`, `state`) VALUES +-- (0, 'plg_fields_menuitem', 'plugin', 'menuitem', 'fields', 0, 1, 1, 0, 1, '', '', '', 0, 0); diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.2.1-2022-08-23.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.2.1-2022-08-23.sql new file mode 100644 index 00000000..a86d5f95 --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.2.1-2022-08-23.sql @@ -0,0 +1,3 @@ +-- Revert https://github.com/joomla/joomla-cms/pull/38244 +-- See also file 4.2.0-2022-07-07.sql +DELETE FROM `#__extensions` WHERE `name` = 'plg_fields_menuitem' AND `type` = 'plugin' AND `element` = 'menuitem' AND `folder` = 'fields' AND `client_id` = 0; diff --git a/code/administrator/components/com_admin/sql/updates/mysql/4.2.3-2022-09-07.sql b/code/administrator/components/com_admin/sql/updates/mysql/4.2.3-2022-09-07.sql new file mode 100644 index 00000000..dd92e898 --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/mysql/4.2.3-2022-09-07.sql @@ -0,0 +1,2 @@ +-- Remove the record of any template overrides where the template has already been uninstalled +DELETE FROM `#__template_overrides` WHERE `template` NOT IN (SELECT `name` FROM `#__extensions` WHERE `type`='template'); diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-03-05.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-03-05.sql index ae03fbfc..783105de 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-03-05.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-03-05.sql @@ -1,7 +1,13 @@ +-- The following statement was moved from below to here and modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__extensions" DROP COLUMN "system_data" /** CAN FAIL **/; + -- From 4.0.0-2016-07-03.sql -INSERT INTO "#__extensions" ("name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "system_data", "checked_out", "checked_out_time", "ordering", "state") VALUES -('plg_behaviour_taggable', 'plugin', 'taggable', 'behaviour', 0, 1, 1, 0, '', '{}', '', '', 0, '1970-01-01 00:00:00', 0, 0), -('plg_behaviour_versionable', 'plugin', 'versionable', 'behaviour', 0, 1, 1, 0, '', '{}', '', '', 0, '1970-01-01 00:00:00', 0, 0); +-- The following statement was modified for 4.1.1 by removing the "system_data" column. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT INTO "#__extensions" ("name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "checked_out", "checked_out_time", "ordering", "state") VALUES +('plg_behaviour_taggable', 'plugin', 'taggable', 'behaviour', 0, 1, 1, 0, '', '{}', '', 0, '1970-01-01 00:00:00', 0, 0), +('plg_behaviour_versionable', 'plugin', 'versionable', 'behaviour', 0, 1, 1, 0, '', '{}', '', 0, '1970-01-01 00:00:00', 0, 0); -- From 4.0.0-2016-09-22.sql DELETE FROM "#__extensions" WHERE "type" = 'library' AND "element" = 'phputf8'; @@ -10,17 +16,22 @@ DELETE FROM "#__extensions" WHERE "type" = 'library' AND "element" = 'phputf8'; DELETE FROM "#__extensions" WHERE "type" = 'plugin' AND "element" = 'p3p' AND "folder" = 'system'; -- From 4.0.0-2016-10-02.sql -ALTER TABLE "#__user_keys" DROP COLUMN "invalid"; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__user_keys" DROP COLUMN "invalid" /** CAN FAIL **/; -- -- Insert the new templates into the database. Set as home if the old template is the active one -- -INSERT INTO "#__extensions" ("name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "system_data", "checked_out", "checked_out_time", "ordering", "state") VALUES -('atum', 'template', 'atum', '', 1, 1, 1, 0, '{}', '{}', '', '', 0, '1970-01-01 00:00:00', 0, 0), -('cassiopeia', 'template', 'cassiopeia', '', 0, 1, 1, 0, '{}', '{}', '', '', 0, '1970-01-01 00:00:00', 0, 0); + +-- The following statement was modified for 4.1.1 by removing the "system_data" column. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT INTO "#__extensions" ("name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "checked_out", "checked_out_time", "ordering", "state") VALUES +('atum', 'template', 'atum', '', 1, 1, 1, 0, '{}', '{}', '', 0, '1970-01-01 00:00:00', 0, 0), +('cassiopeia', 'template', 'cassiopeia', '', 0, 1, 1, 0, '{}', '{}', '', 0, '1970-01-01 00:00:00', 0, 0); -- The following statement had to be modified for 4.1 by adding the "inheritable" and "parent" columns. --- See https://github.com/joomla/joomla-cms/pull/36585 . +-- See https://github.com/joomla/joomla-cms/pull/36585 INSERT INTO "#__template_styles" ("template", "client_id", "home", "title", "inheritable", "parent", "params") VALUES ('atum', 1, (CASE WHEN (SELECT b."count" FROM (SELECT count(a."id") AS "count" FROM "#__template_styles" a WHERE a."home" = '1' AND a."client_id" = 1 AND a."template" IN ('isis', 'hathor')) AS b) = 0 THEN '0' ELSE '1' END), 'atum - Default', 1, '', '{}'), ('cassiopeia', 0, (CASE WHEN (SELECT d."count" FROM (SELECT count(c."id") AS "count" FROM "#__template_styles" c WHERE c."home" = '1' AND c."client_id" = 0 AND c."template" IN ('protostar', 'beez3')) AS d) = 0 THEN '0' ELSE '1' END), 'cassiopeia - Default', 1, '', '{}'); @@ -49,7 +60,9 @@ DELETE FROM "#__extensions" WHERE "type" = 'template' AND "element" = 'beez3' AN DELETE FROM "#__extensions" WHERE "name" = 'mod_submenu'; -- From 4.0.0-2017-03-18.sql -ALTER TABLE "#__extensions" DROP COLUMN "system_data"; +-- The following statement was moved to the top for 4.1.1. +-- See https://github.com/joomla/joomla-cms/pull/37156 +-- ALTER TABLE "#__extensions" DROP COLUMN "system_data"; -- From 4.0.0-2017-04-25.sql INSERT INTO "#__extensions" ("name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "checked_out", "checked_out_time", "ordering", "state") VALUES @@ -63,8 +76,10 @@ UPDATE "#__menu" SET "link" = 'index.php?option=com_config&view=config' WHERE "l UPDATE "#__menu" SET "link" = 'index.php?option=com_config&view=templates' WHERE "link" = 'index.php?option=com_config&view=templates&controller=config.display.templates'; -- From 4.0.0-2017-06-03.sql -ALTER TABLE "#__extensions" ADD COLUMN "changelogurl" text; -ALTER TABLE "#__updates" ADD COLUMN "changelogurl" text; +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__extensions" ADD COLUMN "changelogurl" text /** CAN FAIL **/; +ALTER TABLE "#__updates" ADD COLUMN "changelogurl" text /** CAN FAIL **/; -- From 4.0.0-2017-10-10.sql INSERT INTO "#__extensions" ("name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "checked_out", "checked_out_time", "ordering", "state") VALUES diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-05-15.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-05-15.sql index c2fe2794..abf7949d 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-05-15.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-05-15.sql @@ -20,18 +20,23 @@ CREATE TABLE IF NOT EXISTS "#__workflows" ( PRIMARY KEY ("id") ); -CREATE INDEX "#__workflows_idx_asset_id" ON "#__workflows" ("asset_id"); -CREATE INDEX "#__workflows_idx_title" ON "#__workflows" ("title"); -CREATE INDEX "#__workflows_idx_extension" ON "#__workflows" ("extension"); -CREATE INDEX "#__workflows_idx_default" ON "#__workflows" ("default"); -CREATE INDEX "#__workflows_idx_created" ON "#__workflows" ("created"); -CREATE INDEX "#__workflows_idx_created_by" ON "#__workflows" ("created_by"); -CREATE INDEX "#__workflows_idx_modified" ON "#__workflows" ("modified"); -CREATE INDEX "#__workflows_idx_modified_by" ON "#__workflows" ("modified_by"); -CREATE INDEX "#__workflows_idx_checked_out" ON "#__workflows" ("checked_out"); - +-- The following 9 statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__workflows_idx_asset_id" ON "#__workflows" ("asset_id") /** CAN FAIL **/; +CREATE INDEX "#__workflows_idx_title" ON "#__workflows" ("title") /** CAN FAIL **/; +CREATE INDEX "#__workflows_idx_extension" ON "#__workflows" ("extension") /** CAN FAIL **/; +CREATE INDEX "#__workflows_idx_default" ON "#__workflows" ("default") /** CAN FAIL **/; +CREATE INDEX "#__workflows_idx_created" ON "#__workflows" ("created") /** CAN FAIL **/; +CREATE INDEX "#__workflows_idx_created_by" ON "#__workflows" ("created_by") /** CAN FAIL **/; +CREATE INDEX "#__workflows_idx_modified" ON "#__workflows" ("modified") /** CAN FAIL **/; +CREATE INDEX "#__workflows_idx_modified_by" ON "#__workflows" ("modified_by") /** CAN FAIL **/; +CREATE INDEX "#__workflows_idx_checked_out" ON "#__workflows" ("checked_out") /** CAN FAIL **/; + +-- The following statement was modified for 4.1.1 by adding the "ON CONFLICT" clause. +-- See https://github.com/joomla/joomla-cms/pull/37156 INSERT INTO "#__workflows" ("id", "asset_id", "published", "title", "description", "extension", "default", "ordering", "created", "created_by", "modified", "modified_by", "checked_out_time", "checked_out") VALUES -(1, 0, 1, 'COM_WORKFLOW_BASIC_WORKFLOW', '', 'com_content.article', 1, 1, CURRENT_TIMESTAMP, 0, CURRENT_TIMESTAMP, 0, NULL, 0); +(1, 0, 1, 'COM_WORKFLOW_BASIC_WORKFLOW', '', 'com_content.article', 1, 1, CURRENT_TIMESTAMP, 0, CURRENT_TIMESTAMP, 0, NULL, 0) +ON CONFLICT DO NOTHING; SELECT setval('#__workflows_id_seq', 2, false); @@ -45,10 +50,12 @@ CREATE TABLE IF NOT EXISTS "#__workflow_associations" ( "extension" varchar(50) NOT NULL, PRIMARY KEY ("item_id", "extension") ); -CREATE INDEX "#__workflow_associations_idx_item_stage_extension" ON "#__workflow_associations" ("item_id", "stage_id", "extension"); -CREATE INDEX "#__workflow_associations_idx_item_id" ON "#__workflow_associations" ("item_id"); -CREATE INDEX "#__workflow_associations_idx_stage_id" ON "#__workflow_associations" ("stage_id"); -CREATE INDEX "#__workflow_associations_idx_extension" ON "#__workflow_associations" ("extension"); +-- The following 4 statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__workflow_associations_idx_item_stage_extension" ON "#__workflow_associations" ("item_id", "stage_id", "extension") /** CAN FAIL **/; +CREATE INDEX "#__workflow_associations_idx_item_id" ON "#__workflow_associations" ("item_id") /** CAN FAIL **/; +CREATE INDEX "#__workflow_associations_idx_stage_id" ON "#__workflow_associations" ("stage_id") /** CAN FAIL **/; +CREATE INDEX "#__workflow_associations_idx_extension" ON "#__workflow_associations" ("extension") /** CAN FAIL **/; COMMENT ON COLUMN "#__workflow_associations"."item_id" IS 'Extension table id value'; COMMENT ON COLUMN "#__workflow_associations"."stage_id" IS 'Foreign Key to #__workflow_stages.id'; @@ -70,18 +77,23 @@ CREATE TABLE IF NOT EXISTS "#__workflow_stages" ( "checked_out" bigint DEFAULT 0 NOT NULL, PRIMARY KEY ("id") ); -CREATE INDEX "#__workflow_stages_idx_workflow_id" ON "#__workflow_stages" ("workflow_id"); -CREATE INDEX "#__workflow_stages_idx_title" ON "#__workflow_stages" ("title"); -CREATE INDEX "#__workflow_stages_idx_asset_id" ON "#__workflow_stages" ("asset_id"); -CREATE INDEX "#__workflow_stages_idx_default" ON "#__workflow_stages" ("default"); -CREATE INDEX "#__workflow_stages_idx_checked_out" ON "#__workflow_stages" ("checked_out"); +-- The following 5 statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__workflow_stages_idx_workflow_id" ON "#__workflow_stages" ("workflow_id") /** CAN FAIL **/; +CREATE INDEX "#__workflow_stages_idx_title" ON "#__workflow_stages" ("title") /** CAN FAIL **/; +CREATE INDEX "#__workflow_stages_idx_asset_id" ON "#__workflow_stages" ("asset_id") /** CAN FAIL **/; +CREATE INDEX "#__workflow_stages_idx_default" ON "#__workflow_stages" ("default") /** CAN FAIL **/; +CREATE INDEX "#__workflow_stages_idx_checked_out" ON "#__workflow_stages" ("checked_out") /** CAN FAIL **/; -- -- Dumping data for table "#__workflow_stages" -- +-- The following statement was modified for 4.1.1 by adding the "ON CONFLICT" clause. +-- See https://github.com/joomla/joomla-cms/pull/37156 INSERT INTO "#__workflow_stages" ("id", "asset_id", "ordering", "workflow_id", "published", "title", "description", "default", "checked_out_time", "checked_out") VALUES -(1, 0, 1, 1, 1, 'COM_WORKFLOW_BASIC_STAGE', '', 1, NULL, 0); +(1, 0, 1, 1, 1, 'COM_WORKFLOW_BASIC_STAGE', '', 1, NULL, 0) +ON CONFLICT DO NOTHING; SELECT setval('#__workflow_stages_id_seq', 2, false); @@ -104,13 +116,17 @@ CREATE TABLE IF NOT EXISTS "#__workflow_transitions" ( "checked_out" bigint DEFAULT 0 NOT NULL, PRIMARY KEY ("id") ); -CREATE INDEX "#__workflow_transitions_idx_title" ON "#__workflow_transitions" ("title"); -CREATE INDEX "#__workflow_transitions_idx_asset_id" ON "#__workflow_transitions" ("asset_id"); -CREATE INDEX "#__workflow_transitions_idx_from_stage_id" ON "#__workflow_transitions" ("from_stage_id"); -CREATE INDEX "#__workflow_transitions_idx_to_stage_id" ON "#__workflow_transitions" ("to_stage_id"); -CREATE INDEX "#__workflow_transitions_idx_workflow_id" ON "#__workflow_transitions" ("workflow_id"); -CREATE INDEX "#__workflow_transitions_idx_checked_out" ON "#__workflow_transitions" ("checked_out"); - +-- The following 6 statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__workflow_transitions_idx_title" ON "#__workflow_transitions" ("title") /** CAN FAIL **/; +CREATE INDEX "#__workflow_transitions_idx_asset_id" ON "#__workflow_transitions" ("asset_id") /** CAN FAIL **/; +CREATE INDEX "#__workflow_transitions_idx_from_stage_id" ON "#__workflow_transitions" ("from_stage_id") /** CAN FAIL **/; +CREATE INDEX "#__workflow_transitions_idx_to_stage_id" ON "#__workflow_transitions" ("to_stage_id") /** CAN FAIL **/; +CREATE INDEX "#__workflow_transitions_idx_workflow_id" ON "#__workflow_transitions" ("workflow_id") /** CAN FAIL **/; +CREATE INDEX "#__workflow_transitions_idx_checked_out" ON "#__workflow_transitions" ("checked_out") /** CAN FAIL **/; + +-- The following statement was modified for 4.1.1 by adding the "ON CONFLICT" clause. +-- See https://github.com/joomla/joomla-cms/pull/37156 INSERT INTO "#__workflow_transitions" ("id", "asset_id", "published", "ordering", "workflow_id", "title", "description", "from_stage_id", "to_stage_id", "options", "checked_out_time", "checked_out") VALUES (1, 0, 1, 1, 1, 'Unpublish', '', -1, 1, '{"publishing":"0"}', NULL, 0), (2, 0, 1, 2, 1, 'Publish', '', -1, 1, '{"publishing":"1"}', NULL, 0), @@ -118,7 +134,8 @@ INSERT INTO "#__workflow_transitions" ("id", "asset_id", "published", "ordering" (4, 0, 1, 4, 1, 'Archive', '', -1, 1, '{"publishing":"2"}', NULL, 0), (5, 0, 1, 5, 1, 'Feature', '', -1, 1, '{"featuring":"1"}', NULL, 0), (6, 0, 1, 6, 1, 'Unfeature', '', -1, 1, '{"featuring":"0"}', NULL, 0), -(7, 0, 1, 7, 1, 'Publish & Feature', '', -1, 1, '{"publishing":"1","featuring":"1"}', NULL, 0); +(7, 0, 1, 7, 1, 'Publish & Feature', '', -1, 1, '{"publishing":"1","featuring":"1"}', NULL, 0) +ON CONFLICT DO NOTHING; SELECT setval('#__workflow_transitions_id_seq', 8, false); @@ -139,5 +156,9 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", -- -- Creating Associations for existing content -- + +-- The following statement was modified for 4.1.1 by adding the "ON CONFLICT" clause. +-- See https://github.com/joomla/joomla-cms/pull/37156 INSERT INTO "#__workflow_associations" ("item_id", "stage_id", "extension") -SELECT "id", 1, 'com_content.article' FROM "#__content"; +SELECT "id", 1, 'com_content.article' FROM "#__content" +ON CONFLICT DO NOTHING; diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-19.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-19.sql index 57d106ca..1c9f5d31 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-19.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-19.sql @@ -21,8 +21,10 @@ CREATE TABLE IF NOT EXISTS "#__template_overrides" ( "modified_date" timestamp without time zone, PRIMARY KEY ("id") ); -CREATE INDEX "#__template_overrides_idx_template" ON "#__template_overrides" ("template"); -CREATE INDEX "#__template_overrides_idx_extension_id" ON "#__template_overrides" ("extension_id"); +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__template_overrides_idx_template" ON "#__template_overrides" ("template") /** CAN FAIL **/; +CREATE INDEX "#__template_overrides_idx_extension_id" ON "#__template_overrides" ("extension_id") /** CAN FAIL **/; INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "checked_out", "checked_out_time", "ordering", "state") VALUES (0, 'plg_installer_override', 'plugin', 'override', 'installer', 0, 1, 1, 1, '', '', '', 0, '1970-01-01 00:00:00', 4, 0), diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-29.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-29.sql index 1af74c2b..fab5f9b9 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-29.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-29.sql @@ -24,33 +24,41 @@ ALTER TABLE "#__finder_links" ALTER COLUMN "start_date" DROP NOT NULL; ALTER TABLE "#__finder_links" ALTER COLUMN "start_date" DROP DEFAULT; ALTER TABLE "#__finder_links" ALTER COLUMN "end_date" DROP NOT NULL; ALTER TABLE "#__finder_links" ALTER COLUMN "end_date" DROP DEFAULT; -CREATE INDEX "#__finder_links_idx_language" on "#__finder_links" ("language"); +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__finder_links_idx_language" on "#__finder_links" ("language") /** CAN FAIL **/; -CREATE TABLE "#__finder_links_terms" ( +-- The following statement was modified for 4.1.1 by adding the "IF NOT EXISTS" keywords. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE TABLE IF NOT EXISTS "#__finder_links_terms" ( "link_id" bigint NOT NULL, "term_id" bigint NOT NULL, "weight" REAL NOT NULL DEFAULT 0, PRIMARY KEY ("link_id", "term_id") ); -CREATE INDEX "#__finder_links_terms_idx_term_weight" on "#__finder_links_terms" ("term_id", "weight"); -CREATE INDEX "#__finder_links_terms_idx_link_term_weight" on "#__finder_links_terms" ("link_id", "term_id", "weight"); +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__finder_links_terms_idx_term_weight" on "#__finder_links_terms" ("term_id", "weight") /** CAN FAIL **/; +CREATE INDEX "#__finder_links_terms_idx_link_term_weight" on "#__finder_links_terms" ("link_id", "term_id", "weight") /** CAN FAIL **/; -DROP TABLE "#__finder_links_terms0" CASCADE; -DROP TABLE "#__finder_links_terms1" CASCADE; -DROP TABLE "#__finder_links_terms2" CASCADE; -DROP TABLE "#__finder_links_terms3" CASCADE; -DROP TABLE "#__finder_links_terms4" CASCADE; -DROP TABLE "#__finder_links_terms5" CASCADE; -DROP TABLE "#__finder_links_terms6" CASCADE; -DROP TABLE "#__finder_links_terms7" CASCADE; -DROP TABLE "#__finder_links_terms8" CASCADE; -DROP TABLE "#__finder_links_terms9" CASCADE; -DROP TABLE "#__finder_links_termsa" CASCADE; -DROP TABLE "#__finder_links_termsb" CASCADE; -DROP TABLE "#__finder_links_termsc" CASCADE; -DROP TABLE "#__finder_links_termsd" CASCADE; -DROP TABLE "#__finder_links_termse" CASCADE; -DROP TABLE "#__finder_links_termsf" CASCADE; +-- The following 16 statements were modified for 4.1.1 by adding the "IF EXISTS" keywords. +-- See https://github.com/joomla/joomla-cms/pull/37156 +DROP TABLE IF EXISTS "#__finder_links_terms0" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_terms1" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_terms2" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_terms3" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_terms4" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_terms5" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_terms6" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_terms7" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_terms8" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_terms9" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_termsa" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_termsb" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_termsc" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_termsd" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_termse" CASCADE; +DROP TABLE IF EXISTS "#__finder_links_termsf" CASCADE; CREATE TABLE IF NOT EXISTS "#__finder_logging" ( "searchterm" character varying(255) NOT NULL DEFAULT '', @@ -60,10 +68,14 @@ CREATE TABLE IF NOT EXISTS "#__finder_logging" ( "results" integer NOT NULL DEFAULT 0, PRIMARY KEY ("md5sum") ); -CREATE INDEX "#__finder_logging_idx_md5sum" on "#__finder_logging" ("md5sum"); -CREATE INDEX "#__finder_logging_idx_searchterm" on "#__finder_logging" ("searchterm"); +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__finder_logging_idx_md5sum" on "#__finder_logging" ("md5sum") /** CAN FAIL **/; +CREATE INDEX "#__finder_logging_idx_searchterm" on "#__finder_logging" ("searchterm") /** CAN FAIL **/; -DROP TABLE "#__finder_taxonomy"; +-- The following statement was modified for 4.1.1 by adding the "IF EXISTS" keywords. +-- See https://github.com/joomla/joomla-cms/pull/37156 +DROP TABLE IF EXISTS "#__finder_taxonomy"; CREATE TABLE IF NOT EXISTS "#__finder_taxonomy" ( "id" serial NOT NULL, "parent_id" integer DEFAULT 0 NOT NULL, @@ -78,13 +90,15 @@ CREATE TABLE IF NOT EXISTS "#__finder_taxonomy" ( "language" varchar(7) DEFAULT '' NOT NULL, PRIMARY KEY ("id") ); -CREATE INDEX "#__finder_taxonomy_state" on "#__finder_taxonomy" ("state"); -CREATE INDEX "#__finder_taxonomy_access" on "#__finder_taxonomy" ("access"); -CREATE INDEX "#__finder_taxonomy_path" on "#__finder_taxonomy" ("path"); -CREATE INDEX "#__finder_taxonomy_lft_rgt" on "#__finder_taxonomy" ("lft", "rgt"); -CREATE INDEX "#__finder_taxonomy_alias" on "#__finder_taxonomy" ("alias"); -CREATE INDEX "#__finder_taxonomy_language" on "#__finder_taxonomy" ("language"); -CREATE INDEX "#__finder_taxonomy_idx_parent_published" on "#__finder_taxonomy" ("parent_id", "state", "access"); +-- The following 7 statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__finder_taxonomy_state" on "#__finder_taxonomy" ("state") /** CAN FAIL **/; +CREATE INDEX "#__finder_taxonomy_access" on "#__finder_taxonomy" ("access") /** CAN FAIL **/; +CREATE INDEX "#__finder_taxonomy_path" on "#__finder_taxonomy" ("path") /** CAN FAIL **/; +CREATE INDEX "#__finder_taxonomy_lft_rgt" on "#__finder_taxonomy" ("lft", "rgt") /** CAN FAIL **/; +CREATE INDEX "#__finder_taxonomy_alias" on "#__finder_taxonomy" ("alias") /** CAN FAIL **/; +CREATE INDEX "#__finder_taxonomy_language" on "#__finder_taxonomy" ("language") /** CAN FAIL **/; +CREATE INDEX "#__finder_taxonomy_idx_parent_published" on "#__finder_taxonomy" ("parent_id", "state", "access") /** CAN FAIL **/; INSERT INTO "#__finder_taxonomy" ("id", "parent_id", "lft", "rgt", "level", "path", "title", "alias", "state", "access", "language") VALUES (1, 0, 0, 1, 0, '', 'ROOT', 'root', 1, 1, '*'); SELECT setval('#__finder_taxonomy_id_seq', 2, false); @@ -96,19 +110,25 @@ ALTER TABLE "#__finder_terms" ALTER COLUMN "language" TYPE character varying(7); ALTER TABLE "#__finder_terms" ALTER COLUMN "language" SET DEFAULT ''; ALTER TABLE "#__finder_terms" ALTER COLUMN "stem" SET DEFAULT ''; ALTER TABLE "#__finder_terms" ALTER COLUMN "soundex" SET DEFAULT ''; -CREATE INDEX "#__finder_terms_idx_stem" on "#__finder_terms" ("stem"); -CREATE INDEX "#__finder_terms_idx_language" on "#__finder_terms" ("language"); -ALTER TABLE "#__finder_terms" DROP CONSTRAINT "#__finder_terms_idx_term"; -ALTER TABLE "#__finder_terms" ADD CONSTRAINT "#__finder_terms_idx_term_language" UNIQUE ("term", "language"); +-- The following 4 statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__finder_terms_idx_stem" on "#__finder_terms" ("stem" /** CAN FAIL **/); +CREATE INDEX "#__finder_terms_idx_language" on "#__finder_terms" ("language") /** CAN FAIL **/; +ALTER TABLE "#__finder_terms" DROP CONSTRAINT "#__finder_terms_idx_term" /** CAN FAIL **/; +ALTER TABLE "#__finder_terms" ADD CONSTRAINT "#__finder_terms_idx_term_language" UNIQUE ("term", "language") /** CAN FAIL **/; DROP TABLE IF EXISTS "#__finder_terms_common"; -CREATE TABLE "#__finder_terms_common" ( +-- The following statement was modified for 4.1.1 by adding the "IF NOT EXISTS" keywords. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE TABLE IF NOT EXISTS "#__finder_terms_common" ( "term" varchar(75) NOT NULL, "language" varchar(7) DEFAULT '' NOT NULL, "custom" integer DEFAULT 0 NOT NULL, CONSTRAINT "#__finder_terms_common_idx_term_language" UNIQUE ("term", "language") ); -CREATE INDEX "#__finder_terms_common_idx_lang" on "#__finder_terms_common" ("language"); +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__finder_terms_common_idx_lang" on "#__finder_terms_common" ("language") /** CAN FAIL **/; INSERT INTO "#__finder_terms_common" ("term", "language", "custom") VALUES ('i', 'en', 0), ('me', 'en', 0), @@ -289,13 +309,17 @@ TRUNCATE TABLE "#__finder_tokens"; ALTER TABLE "#__finder_tokens" ALTER COLUMN "language" TYPE character varying(7); ALTER TABLE "#__finder_tokens" ALTER COLUMN "language" SET DEFAULT ''; ALTER TABLE "#__finder_tokens" ALTER COLUMN "stem" SET DEFAULT ''; -CREATE INDEX "#__finder_tokens_idx_stem" on "#__finder_tokens" ("stem"); -CREATE INDEX "#__finder_tokens_idx_language" on "#__finder_tokens" ("language"); +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__finder_tokens_idx_stem" on "#__finder_tokens" ("stem") /** CAN FAIL **/; +CREATE INDEX "#__finder_tokens_idx_language" on "#__finder_tokens" ("language") /** CAN FAIL **/; TRUNCATE TABLE "#__finder_tokens_aggregate"; ALTER TABLE "#__finder_tokens_aggregate" ALTER COLUMN "language" TYPE character varying(7); ALTER TABLE "#__finder_tokens_aggregate" ALTER COLUMN "language" SET DEFAULT ''; -ALTER TABLE "#__finder_tokens_aggregate" DROP COLUMN "map_suffix"; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__finder_tokens_aggregate" DROP COLUMN "map_suffix" /** CAN FAIL **/; ALTER TABLE "#__finder_tokens_aggregate" ALTER COLUMN "stem" SET DEFAULT ''; ALTER TABLE "#__finder_tokens_aggregate" ALTER COLUMN "term_weight" SET DEFAULT 0; ALTER TABLE "#__finder_tokens_aggregate" ALTER COLUMN "context_weight" SET DEFAULT 0; diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-09.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-09.sql index dff3c770..4a1c6ac8 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-09.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-09.sql @@ -24,10 +24,16 @@ CREATE TABLE IF NOT EXISTS "#__mail_templates" ( "params" TEXT NOT NULL, CONSTRAINT "#__mail_templates_idx_template_id_language" UNIQUE ("template_id", "language") ); -CREATE INDEX "#__mail_templates_idx_template_id" ON "#__mail_templates" ("template_id"); -CREATE INDEX "#__mail_templates_idx_language" ON "#__mail_templates" ("language"); +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__mail_templates_idx_template_id" ON "#__mail_templates" ("template_id") /** CAN FAIL **/; +CREATE INDEX "#__mail_templates_idx_language" ON "#__mail_templates" ("language") /** CAN FAIL **/; -INSERT INTO "#__mail_templates" ("template_id", "language", "subject", "body", "htmlbody", "attachments", "params") VALUES ('com_config.test_mail', '', 'COM_CONFIG_SENDMAIL_SUBJECT', 'COM_CONFIG_SENDMAIL_BODY', '', '', '{"tags":["sitename","method"]}'); +-- The following statement was modified for 4.1.1 by adding the "ON CONFLICT" clause. +-- See https://github.com/joomla/joomla-cms/pull/37156 +INSERT INTO "#__mail_templates" ("template_id", "language", "subject", "body", "htmlbody", "attachments", "params") VALUES +('com_config.test_mail', '', 'COM_CONFIG_SENDMAIL_SUBJECT', 'COM_CONFIG_SENDMAIL_BODY', '', '', '{"tags":["sitename","method"]}') +ON CONFLICT DO NOTHING; -- From 4.0.0-2019-02-03.sql DELETE FROM "#__menu" WHERE "link" = 'index.php?option=com_postinstall' AND "menutype" = 'main'; diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-30.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-30.sql index 9601b6b1..441588c4 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-30.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-30.sql @@ -1,5 +1,7 @@ -ALTER TABLE "#__menu" ADD COLUMN "publish_up" timestamp without time zone; -ALTER TABLE "#__menu" ADD COLUMN "publish_down" timestamp without time zone; +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__menu" ADD COLUMN "publish_up" timestamp without time zone /** CAN FAIL **/; +ALTER TABLE "#__menu" ADD COLUMN "publish_down" timestamp without time zone /** CAN FAIL **/; ALTER TABLE "#__menu" ALTER COLUMN "checked_out_time" DROP NOT NULL; ALTER TABLE "#__menu" ALTER COLUMN "checked_out_time" DROP DEFAULT; diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-05-20.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-05-20.sql index cf5f4ea8..1c24f465 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-05-20.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-05-20.sql @@ -12,4 +12,6 @@ UPDATE "#__menu" SET "link"='index.php?option=com_tags&view=tags' WHERE "menutyp UPDATE "#__menu" SET "link"='index.php?option=com_associations&view=associations' WHERE "menutype"='main' AND "path"='Multilingual Associations'; -- From 4.0.0-2019-05-20.sql -ALTER TABLE "#__extensions" ADD COLUMN "note" character varying(255); +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__extensions" ADD COLUMN "note" character varying(255) /** CAN FAIL **/; diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-13.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-13.sql index 53195188..1c64f1b8 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-13.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-13.sql @@ -7,7 +7,9 @@ CREATE TABLE IF NOT EXISTS "#__webauthn_credentials" ( PRIMARY KEY ("id") ); -CREATE INDEX "#__webauthn_credentials_user_id" ON "#__webauthn_credentials" ("user_id"); +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__webauthn_credentials_user_id" ON "#__webauthn_credentials" ("user_id") /** CAN FAIL **/; INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "checked_out", "checked_out_time", "ordering", "state") VALUES (0, 'plg_system_webauthn', 'plugin', 'webauthn', 'system', 0, 1, 1, 0, '', '{}', '', 0, '1970-01-01 00:00:00', 8, 0); diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-13.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-13.sql index a37435a8..d258928b 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-13.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-13.sql @@ -1,17 +1,23 @@ -- From 4.0.0-2019-07-14.sql -ALTER TABLE "#__contact_details" DROP COLUMN "xreference"; -ALTER TABLE "#__content" DROP COLUMN "xreference"; -ALTER TABLE "#__newsfeeds" DROP COLUMN "xreference"; +-- The following 3 statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__contact_details" DROP COLUMN "xreference" /** CAN FAIL **/; +ALTER TABLE "#__content" DROP COLUMN "xreference" /** CAN FAIL **/; +ALTER TABLE "#__newsfeeds" DROP COLUMN "xreference" /** CAN FAIL **/; -- From 4.0.0-2019-07-16.sql -- This has been removed as com_csp has been removed from the final build -- From 4.0.0-2019-08-03.sql -ALTER TABLE "#__update_sites" ADD COLUMN "checked_out" bigint DEFAULT 0 NOT NULL; -ALTER TABLE "#__update_sites" ADD COLUMN "checked_out_time" timestamp without time zone DEFAULT NULL; +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__update_sites" ADD COLUMN "checked_out" bigint DEFAULT 0 NOT NULL /** CAN FAIL **/; +ALTER TABLE "#__update_sites" ADD COLUMN "checked_out_time" timestamp without time zone DEFAULT NULL /** CAN FAIL **/; -- From 4.0.0-2019-08-20.sql -ALTER TABLE "#__content_frontpage" ADD COLUMN "featured_up" timestamp without time zone; -ALTER TABLE "#__content_frontpage" ADD COLUMN "featured_down" timestamp without time zone; +-- The following two statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__content_frontpage" ADD COLUMN "featured_up" timestamp without time zone /** CAN FAIL **/; +ALTER TABLE "#__content_frontpage" ADD COLUMN "featured_down" timestamp without time zone /** CAN FAIL **/; -- From 4.0.0-2019-08-21.sql INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "checked_out", "checked_out_time", "ordering", "state") VALUES @@ -31,5 +37,8 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", (0, 'plg_webservices_users', 'plugin', 'users', 'webservices', 0, 1, 1, 0, '', '{}', '', 0, '1970-01-01 00:00:00', 0, 0); -- From 4.0.0-2019-09-13.sql +-- The following statement was modified for 4.1.1 by adding the "ON CONFLICT" clause. +-- See https://github.com/joomla/joomla-cms/pull/37156 INSERT INTO "#__menu" ("menutype", "title", "alias", "note", "path", "link", "type", "published", "parent_id", "level", "component_id", "checked_out", "checked_out_time", "browserNav", "access", "img", "template_style_id", "params", "lft", "rgt", "home", "language", "client_id", "publish_up", "publish_down") -SELECT 'main', 'com_messages_manager', 'Private Messages', '', 'Messaging/Private Messages', 'index.php?option=com_messages&view=messages', 'component', 1, 10, 2, "extension_id", 0, NULL, 0, 0, 'class:messages-add', 0, '', 18, 19, 0, '*', 1, NULL, NULL FROM "#__extensions" WHERE "name" = 'com_messages'; +SELECT 'main', 'com_messages_manager', 'Private Messages', '', 'Messaging/Private Messages', 'index.php?option=com_messages&view=messages', 'component', 1, 10, 2, "extension_id", 0, NULL, 0, 0, 'class:messages-add', 0, '', 18, 19, 0, '*', 1, NULL, NULL FROM "#__extensions" WHERE "name" = 'com_messages' +ON CONFLICT DO NOTHING; diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-03-25.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-03-25.sql index 0d45c215..04ecfaa3 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-03-25.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-03-25.sql @@ -1,5 +1,7 @@ -- Add locked field to extensions table. -ALTER TABLE "#__extensions" ADD COLUMN "locked" smallint DEFAULT 0 NOT NULL; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__extensions" ADD COLUMN "locked" smallint DEFAULT 0 NOT NULL /** CAN FAIL **/; COMMENT ON COLUMN "#__extensions"."protected" IS 'Flag to indicate if the extension is protected. Protected extensions cannot be disabled.'; COMMENT ON COLUMN "#__extensions"."locked" IS 'Flag to indicate if the extension is locked. Locked extensions cannot be uninstalled.'; diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-05-29.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-05-29.sql index 6c33b5c3..de130b24 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-05-29.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-05-29.sql @@ -3,6 +3,8 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", (0, 'plg_quickicon_downloadkey', 'plugin', 'downloadkey', 'quickicon', 0, 1, 1, 0, 1, '', '', '', 0, NULL, 0, 0); -- From 4.0.0-2020-04-16.sql +-- The following statement was modified for 4.1.1 by adding the "ON CONFLICT" clause. +-- See https://github.com/joomla/joomla-cms/pull/37156 INSERT INTO "#__mail_templates" ("template_id", "language", "subject", "body", "htmlbody", "attachments", "params") VALUES ('com_contact.mail', '', 'COM_CONTACT_ENQUIRY_SUBJECT', 'COM_CONTACT_ENQUIRY_TEXT', '', '', '{"tags":["sitename","name","email","subject","body","url","customfields"]}'), ('com_contact.mail.copy', '', 'COM_CONTACT_COPYSUBJECT_OF', 'COM_CONTACT_COPYTEXT_OF', '', '', '{"tags":["sitename","name","email","subject","body","url","customfields"]}'), @@ -10,22 +12,31 @@ INSERT INTO "#__mail_templates" ("template_id", "language", "subject", "body", " ('com_users.password_reset', '', 'COM_USERS_EMAIL_PASSWORD_RESET_SUBJECT', 'COM_USERS_EMAIL_PASSWORD_RESET_BODY', '', '', '{"tags":["name","email","sitename","link_text","link_html","token"]}'), ('com_users.reminder', '', 'COM_USERS_EMAIL_USERNAME_REMINDER_SUBJECT', 'COM_USERS_EMAIL_USERNAME_REMINDER_BODY', '', '', '{"tags":["name","username","sitename","email","link_text","link_html"]}'), ('plg_system_updatenotification.mail', '', 'PLG_SYSTEM_UPDATENOTIFICATION_EMAIL_SUBJECT', 'PLG_SYSTEM_UPDATENOTIFICATION_EMAIL_BODY', '', '', '{"tags":["newversion","curversion","sitename","url","link","releasenews"]}'), -('plg_user_joomla.mail', '', 'PLG_USER_JOOMLA_NEW_USER_EMAIL_SUBJECT', 'PLG_USER_JOOMLA_NEW_USER_EMAIL_BODY', '', '', '{"tags":["name","sitename","url","username","password","email"]}'); +('plg_user_joomla.mail', '', 'PLG_USER_JOOMLA_NEW_USER_EMAIL_SUBJECT', 'PLG_USER_JOOMLA_NEW_USER_EMAIL_BODY', '', '', '{"tags":["name","sitename","url","username","password","email"]}') +ON CONFLICT DO NOTHING; -- From 4.0.0-2020-05-21.sql -- Renaming table -ALTER TABLE "#__ucm_history" RENAME TO "#__history"; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__ucm_history" RENAME TO "#__history" /** CAN FAIL **/; -- Rename ucm_item_id to item_id as the new primary identifier for the original content item -ALTER TABLE "#__history" RENAME "ucm_item_id" TO "item_id"; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__history" RENAME "ucm_item_id" TO "item_id" /** CAN FAIL **/; ALTER TABLE "#__history" ALTER COLUMN "item_id" TYPE character varying(50); ALTER TABLE "#__history" ALTER COLUMN "item_id" SET NOT NULL; ALTER TABLE "#__history" ALTER COLUMN "item_id" DROP DEFAULT; -- Extend the original field content with the alias of the content type -UPDATE "#__history" AS h SET "item_id" = CONCAT(c."type_alias", '.', "item_id") FROM "#__content_types" AS c WHERE h."ucm_type_id" = c."type_id"; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +UPDATE "#__history" AS h SET "item_id" = CONCAT(c."type_alias", '.', "item_id") FROM "#__content_types" AS c WHERE h."ucm_type_id" = c."type_id" /** CAN FAIL **/; -- Now we don't need the ucm_type_id anymore and drop it. -ALTER TABLE "#__history" DROP COLUMN "ucm_type_id"; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__history" DROP COLUMN "ucm_type_id" /** CAN FAIL **/; ALTER TABLE "#__history" ALTER COLUMN "save_date" DROP DEFAULT; -- From 4.0.0-2020-05-29.sql diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-20.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-20.sql index 2fe434b7..3955d942 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-20.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-20.sql @@ -1,4 +1,6 @@ -- From 4.0.0-2020-12-08.sql +-- The following statement was modified for 4.1.1 by adding the "ON CONFLICT" clause. +-- See https://github.com/joomla/joomla-cms/pull/37156 INSERT INTO "#__mail_templates" ("template_id", "language", "subject", "body", "htmlbody", "attachments", "params") VALUES ('com_actionlogs.notification', '', 'COM_ACTIONLOGS_EMAIL_SUBJECT', 'COM_ACTIONLOGS_EMAIL_BODY', 'COM_ACTIONLOGS_EMAIL_HTMLBODY', '', '{"tags":["message","date","extension"]}'), ('com_privacy.userdataexport', '', 'COM_PRIVACY_EMAIL_DATA_EXPORT_COMPLETED_BODY', 'COM_PRIVACY_EMAIL_DATA_EXPORT_COMPLETED_SUBJECT', '', '', '{"tags":["sitename","url"]}'), @@ -17,7 +19,7 @@ INSERT INTO "#__mail_templates" ("template_id", "language", "subject", "body", " ('com_users.registration.admin.verification_request', '', 'COM_USERS_EMAIL_ACTIVATE_WITH_ADMIN_ACTIVATION_SUBJECT', 'COM_USERS_EMAIL_ACTIVATE_WITH_ADMIN_ACTIVATION_BODY', '', '', '{"tags":["name","sitename","email","username","activate"]}'), ('plg_system_privacyconsent.request.reminder', '', 'PLG_SYSTEM_PRIVACYCONSENT_EMAIL_REMIND_SUBJECT', 'PLG_SYSTEM_PRIVACYCONSENT_EMAIL_REMIND_BODY', '', '', '{"tags":["sitename","url","tokenurl","formurl","token"]}'), ('com_messages.new_message', '', 'COM_MESSAGES_NEW_MESSAGE', 'COM_MESSAGES_NEW_MESSAGE_BODY', '', '', '{"tags":["subject","message","fromname","sitename","siteurl","fromemail","toname","toemail"]}') -; +ON CONFLICT DO NOTHING; -- From 4.0.0-2020-12-19.sql UPDATE "#__banners" SET "created" = '1980-01-01 00:00:00' WHERE "created" = '1970-01-01 00:00:00'; diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-22.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-22.sql index 7c84bf0f..d72d4b8e 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-22.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-22.sql @@ -9,12 +9,16 @@ DELETE FROM "#__postinstall_messages" 'TPL_HATHOR_MESSAGE_POSTINSTALL_TITLE'); -- From 4.0.0-2021-04-11.sql -ALTER TABLE "#__fields" ADD COLUMN "only_use_in_subform" smallint DEFAULT 0 NOT NULL; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__fields" ADD COLUMN "only_use_in_subform" smallint DEFAULT 0 NOT NULL /** CAN FAIL **/; -- From 4.0.0-2021-04-20.sql UPDATE "#__extensions" SET "name" = 'plg_fields_subform', "element" = 'subform' WHERE "name" = 'plg_fields_subfields' AND "type" = 'plugin' AND "element" = 'subfields' AND "folder" = 'fields' AND "client_id" = 0; UPDATE "#__fields" SET "type" = 'subform' WHERE "type" = 'subfields'; -- From 4.0.0-2021-04-22.sql -ALTER TABLE "#__mail_templates" ADD COLUMN "extension" VARCHAR(127) NOT NULL DEFAULT ''; +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +ALTER TABLE "#__mail_templates" ADD COLUMN "extension" VARCHAR(127) NOT NULL DEFAULT '' /** CAN FAIL **/; UPDATE "#__mail_templates" SET "extension" = SUBSTRING("template_id", 1, POSITION('.' IN "template_id") - 1); diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-30.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-30.sql index 05029036..e3770d3e 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-30.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-30.sql @@ -15,7 +15,9 @@ UPDATE "#__mail_templates" WHERE "template_id" = 'com_privacy.userdataexport'; -- From 4.0.0-2021-05-10.sql -CREATE INDEX "#__finder_taxonomy_level" ON "#__finder_taxonomy" ("level"); +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__finder_taxonomy_level" ON "#__finder_taxonomy" ("level") /** CAN FAIL **/; -- From 4.0.0-2021-05-21.sql UPDATE "#__modules" diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2021-11-20.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2021-11-20.sql index 02ab2e70..eef427c9 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2021-11-20.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2021-11-20.sql @@ -28,14 +28,16 @@ CREATE TABLE IF NOT EXISTS "#__scheduler_tasks" ( PRIMARY KEY ("id") ); -CREATE INDEX "#__scheduler_tasks_idx_type" ON "#__scheduler_tasks" ("type"); -CREATE INDEX "#__scheduler_tasks_idx_state" ON "#__scheduler_tasks" ("state"); -CREATE INDEX "#__scheduler_tasks_idx_last_exit" ON "#__scheduler_tasks" ("last_exit_code"); -CREATE INDEX "#__scheduler_tasks_idx_next_exec" ON "#__scheduler_tasks" ("next_execution"); -CREATE INDEX "#__scheduler_tasks_idx_locked" ON "#__scheduler_tasks" ("locked"); -CREATE INDEX "#__scheduler_tasks_idx_priority" ON "#__scheduler_tasks" ("priority"); -CREATE INDEX "#__scheduler_tasks_idx_cli_exclusive" ON "#__scheduler_tasks" ("cli_exclusive"); -CREATE INDEX "#__scheduler_tasks_idx_checked_out" ON "#__scheduler_tasks" ("checked_out"); +-- The following 8 statements were modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__scheduler_tasks_idx_type" ON "#__scheduler_tasks" ("type") /** CAN FAIL **/; +CREATE INDEX "#__scheduler_tasks_idx_state" ON "#__scheduler_tasks" ("state") /** CAN FAIL **/; +CREATE INDEX "#__scheduler_tasks_idx_last_exit" ON "#__scheduler_tasks" ("last_exit_code") /** CAN FAIL **/; +CREATE INDEX "#__scheduler_tasks_idx_next_exec" ON "#__scheduler_tasks" ("next_execution") /** CAN FAIL **/; +CREATE INDEX "#__scheduler_tasks_idx_locked" ON "#__scheduler_tasks" ("locked") /** CAN FAIL **/; +CREATE INDEX "#__scheduler_tasks_idx_priority" ON "#__scheduler_tasks" ("priority") /** CAN FAIL **/; +CREATE INDEX "#__scheduler_tasks_idx_cli_exclusive" ON "#__scheduler_tasks" ("cli_exclusive") /** CAN FAIL **/; +CREATE INDEX "#__scheduler_tasks_idx_checked_out" ON "#__scheduler_tasks" ("checked_out") /** CAN FAIL **/; -- Add "com_scheduler" to "#__extensions" INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "locked", "manifest_cache", "params", "custom_data", "ordering", "state") VALUES @@ -57,8 +59,11 @@ INSERT INTO "#__action_log_config" ("type_title", "type_alias", "id_holder", "ti ('task', 'com_scheduler.task', 'id', 'title', '#__scheduler_tasks', 'PLG_ACTIONLOG_JOOMLA'); -- Add mail templates +-- The following statement was modified for 4.1.1 by adding the "ON CONFLICT" clause. +-- See https://github.com/joomla/joomla-cms/pull/37156 INSERT INTO "#__mail_templates" ("template_id", "extension", "language", "subject", "body", "htmlbody", "attachments", "params") VALUES ('plg_system_tasknotification.failure_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_FAILURE_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_FAILURE_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title", "exit_code", "exec_data_time", "task_output"]}'), ('plg_system_tasknotification.fatal_recovery_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_FATAL_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title"]}'), ('plg_system_tasknotification.orphan_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_ORPHAN_MAIL_BODY', '', '', '{"tags": ["task_id", "task_title"]}'), -('plg_system_tasknotification.success_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_BODY', '', '', '{"tags":["task_id", "task_title", "exec_data_time", "task_output"]}'); +('plg_system_tasknotification.success_mail', 'plg_system_tasknotification', '', 'PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_SUBJECT', 'PLG_SYSTEM_TASK_NOTIFICATION_SUCCESS_MAIL_BODY', '', '', '{"tags":["task_id", "task_title", "exec_data_time", "task_output"]}') +ON CONFLICT DO NOTHING; diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2022-01-24.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2022-01-24.sql index 4da6473f..804c4f17 100644 --- a/code/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2022-01-24.sql +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2022-01-24.sql @@ -1,2 +1,4 @@ DROP INDEX IF EXISTS "#__redirect_links_idx_link_modifed"; -CREATE INDEX "#__redirect_links_idx_link_modified" ON "#__redirect_links" ("modified_date"); +-- The following statement was modified for 4.1.1 by adding the "/** CAN FAIL **/" installer hint. +-- See https://github.com/joomla/joomla-cms/pull/37156 +CREATE INDEX "#__redirect_links_idx_link_modified" ON "#__redirect_links" ("modified_date") /** CAN FAIL **/; diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-05-15.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-05-15.sql new file mode 100644 index 00000000..0c06ffb9 --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-05-15.sql @@ -0,0 +1,63 @@ +-- +-- Create the new table for MFA +-- +CREATE TABLE IF NOT EXISTS "#__user_mfa" ( + "id" serial NOT NULL, + "user_id" bigint NOT NULL, + "title" varchar(255) DEFAULT '' NOT NULL, + "method" varchar(100) NOT NULL, + "default" smallint DEFAULT 0 NOT NULL, + "options" text NOT NULL, + "created_on" timestamp without time zone NOT NULL, + "last_used" timestamp without time zone, + PRIMARY KEY ("id") +); + +CREATE INDEX "#__user_mfa_idx_user_id" ON "#__user_mfa" ("user_id") /** CAN FAIL **/; + +COMMENT ON TABLE "#__user_mfa" IS 'Multi-factor Authentication settings'; + +-- +-- Remove obsolete postinstallation message +-- +DELETE FROM "#__postinstall_messages" WHERE "condition_file" = 'site://plugins/twofactorauth/totp/postinstall/actions.php'; + +-- +-- Add new MFA plugins +-- +INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "locked", "manifest_cache", "params", "custom_data", "ordering", "state") VALUES +(0, 'plg_multifactorauth_totp', 'plugin', 'totp', 'multifactorauth', 0, 0, 1, 0, 1, '', '', '', 1, 0), +(0, 'plg_multifactorauth_yubikey', 'plugin', 'yubikey', 'multifactorauth', 0, 0, 1, 0, 1, '', '', '', 2, 0), +(0, 'plg_multifactorauth_webauthn', 'plugin', 'webauthn', 'multifactorauth', 0, 0, 1, 0, 1, '', '', '', 3, 0), +(0, 'plg_multifactorauth_email', 'plugin', 'email', 'multifactorauth', 0, 0, 1, 0, 1, '', '', '', 4, 0), +(0, 'plg_multifactorauth_fixed', 'plugin', 'fixed', 'multifactorauth', 0, 0, 1, 0, 1, '', '', '', 5, 0); + +-- +-- Update MFA plugins' publish status +-- +UPDATE "#__extensions" AS "a" +SET "enabled" = "b"."enabled" +FROM "#__extensions" AS "b" +WHERE "a"."element" = "b"."element" + AND "a"."folder" = 'multifactorauth' + AND "b"."folder" = 'twofactorauth'; + +-- +-- Remove legacy TFA plugins +-- +DELETE FROM "#__extensions" +WHERE "type" = 'plugin' AND "folder" = 'twofactorauth' AND "element" IN ('totp', 'yubikey'); + +-- +-- Add post-installation message +-- +INSERT INTO "#__postinstall_messages" ("extension_id", "title_key", "description_key", "action_key", "language_extension", "language_client_id", "type", "action_file", "action", "condition_file", "condition_method", "version_introduced", "enabled") +SELECT "extension_id", 'COM_USERS_POSTINSTALL_MULTIFACTORAUTH_TITLE', 'COM_USERS_POSTINSTALL_MULTIFACTORAUTH_BODY', 'COM_USERS_POSTINSTALL_MULTIFACTORAUTH_ACTION', 'com_users', 1, 'action', 'admin://components/com_users/postinstall/multifactorauth.php', 'com_users_postinstall_mfa_action', 'admin://components/com_users/postinstall/multifactorauth.php', 'com_users_postinstall_mfa_condition', '4.2.0', 1 FROM "#__extensions" WHERE "name" = 'files_joomla' +ON CONFLICT DO NOTHING; + +-- +-- Create a mail template for plg_multifactorauth_email +-- +INSERT INTO "#__mail_templates" ("template_id", "extension", "language", "subject", "body", "htmlbody", "attachments", "params") VALUES +('plg_multifactorauth_email.mail', 'plg_multifactorauth_email', '', 'PLG_MULTIFACTORAUTH_EMAIL_EMAIL_SUBJECT', 'PLG_MULTIFACTORAUTH_EMAIL_EMAIL_BODY', '', '', '{"tags":["code","sitename","siteurl","username","email","fullname"]}') +ON CONFLICT DO NOTHING; diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-06-19.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-06-19.sql new file mode 100644 index 00000000..6a2a3b20 --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-06-19.sql @@ -0,0 +1,3 @@ +-- See https://github.com/joomla/joomla-cms/pull/38092 +INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "locked", "manifest_cache", "params", "custom_data", "ordering", "state") VALUES +(0, 'plg_system_shortcut', 'plugin', 'shortcut', 'system', 0, 1, 1, 0, 1, '', '', '', 0, 0); diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-06-22.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-06-22.sql new file mode 100644 index 00000000..e9b41f7f --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-06-22.sql @@ -0,0 +1,9 @@ +-- Set core extensions as locked extensions. +UPDATE "#__extensions" +SET "locked" = 1 +WHERE ("type" = 'plugin' AND + ( + ("folder" = 'system' AND "element" = 'schedulerunner') + OR ("folder" = 'task' AND "element" IN ('checkfiles', 'demotasks', 'requests', 'sitestatus')) + ) +); diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-07-07.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-07-07.sql new file mode 100644 index 00000000..118f7f6a --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.2.0-2022-07-07.sql @@ -0,0 +1,4 @@ +-- The following statement added with Joonmla version 4.2.0 RC 1 had to be removed with version 4.2.0 (stable). +-- See https://github.com/joomla/joomla-cms/pull/38244 +-- INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "locked", "manifest_cache", "params", "custom_data", "ordering", "state") VALUES +--(0, 'plg_fields_menuitem', 'plugin', 'menuitem', 'fields', 0, 1, 1, 0, 1, '', '', '', 0, 0); diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.2.1-2022-08-23.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.2.1-2022-08-23.sql new file mode 100644 index 00000000..eca5b000 --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.2.1-2022-08-23.sql @@ -0,0 +1,3 @@ +-- Revert https://github.com/joomla/joomla-cms/pull/38244 +-- See also file 4.2.0-2022-07-07.sql +DELETE FROM "#__extensions" WHERE "name" = 'plg_fields_menuitem' AND "type" = 'plugin' AND "element" = 'menuitem' AND "folder" = 'fields' AND "client_id" = 0; diff --git a/code/administrator/components/com_admin/sql/updates/postgresql/4.2.3-2022-09-07.sql b/code/administrator/components/com_admin/sql/updates/postgresql/4.2.3-2022-09-07.sql new file mode 100644 index 00000000..355f68e1 --- /dev/null +++ b/code/administrator/components/com_admin/sql/updates/postgresql/4.2.3-2022-09-07.sql @@ -0,0 +1,2 @@ +-- Remove the record of any template overrides where the template has already been uninstalled +DELETE FROM "#__template_overrides" WHERE "template" NOT IN (SELECT "name" FROM "#__extensions" WHERE "type"='template'); diff --git a/code/administrator/components/com_admin/src/Controller/DisplayController.php b/code/administrator/components/com_admin/src/Controller/DisplayController.php index 3857024e..293adf07 100644 --- a/code/administrator/components/com_admin/src/Controller/DisplayController.php +++ b/code/administrator/components/com_admin/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', $this->default_view); - $format = $this->input->get('format', 'html'); + /** + * View method + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static Supports chaining. + * + * @since 3.9 + */ + public function display($cachable = false, $urlparams = array()) + { + $viewName = $this->input->get('view', $this->default_view); + $format = $this->input->get('format', 'html'); - // Check CSRF token for sysinfo export views - if ($viewName === 'sysinfo' && ($format === 'text' || $format === 'json')) - { - // Check for request forgeries. - $this->checkToken('GET'); - } + // Check CSRF token for sysinfo export views + if ($viewName === 'sysinfo' && ($format === 'text' || $format === 'json')) { + // Check for request forgeries. + $this->checkToken('GET'); + } - return parent::display($cachable, $urlparams); - } + return parent::display($cachable, $urlparams); + } } diff --git a/code/administrator/components/com_admin/src/Dispatcher/Dispatcher.php b/code/administrator/components/com_admin/src/Dispatcher/Dispatcher.php index 082a22e9..b627b8cd 100644 --- a/code/administrator/components/com_admin/src/Dispatcher/Dispatcher.php +++ b/code/administrator/components/com_admin/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ getRegistry()->register('system', new System); - $this->getRegistry()->register('phpsetting', new PhpSetting); - $this->getRegistry()->register('directory', new Directory); - $this->getRegistry()->register('configuration', new Configuration); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('system', new System()); + $this->getRegistry()->register('phpsetting', new PhpSetting()); + $this->getRegistry()->register('directory', new Directory()); + $this->getRegistry()->register('configuration', new Configuration()); + } } diff --git a/code/administrator/components/com_admin/src/Model/HelpModel.php b/code/administrator/components/com_admin/src/Model/HelpModel.php index cec67d30..f3b8b7d8 100644 --- a/code/administrator/components/com_admin/src/Model/HelpModel.php +++ b/code/administrator/components/com_admin/src/Model/HelpModel.php @@ -1,4 +1,5 @@ help_search)) - { - $this->help_search = Factory::getApplication()->input->getString('helpsearch'); - } - - return $this->help_search; - } - - /** - * Method to get the page - * - * @return string The page - * - * @since 1.6 - */ - public function &getPage() - { - if (\is_null($this->page)) - { - $this->page = Help::createUrl(Factory::getApplication()->input->get('page', 'Start_Here')); - } - - return $this->page; - } - - /** - * Method to get the lang tag - * - * @return string lang iso tag - * - * @since 1.6 - */ - public function getLangTag() - { - if (\is_null($this->lang_tag)) - { - $this->lang_tag = Factory::getLanguage()->getTag(); - - if (!is_dir(JPATH_BASE . '/help/' . $this->lang_tag)) - { - // Use English as fallback - $this->lang_tag = 'en-GB'; - } - } - - return $this->lang_tag; - } - - /** - * Method to get the table of contents - * - * @return array Table of contents - */ - public function &getToc() - { - if (!\is_null($this->toc)) - { - return $this->toc; - } - - // Get vars - $lang_tag = $this->getLangTag(); - $help_search = $this->getHelpSearch(); - - // New style - Check for a TOC \JSON file - if (file_exists(JPATH_BASE . '/help/' . $lang_tag . '/toc.json')) - { - $data = json_decode(file_get_contents(JPATH_BASE . '/help/' . $lang_tag . '/toc.json')); - - // Loop through the data array - foreach ($data as $key => $value) - { - $this->toc[$key] = Text::_('COM_ADMIN_HELP_' . $value); - } - - // Sort the Table of Contents - asort($this->toc); - - return $this->toc; - } - - // Get Help files - $files = Folder::files(JPATH_BASE . '/help/' . $lang_tag, '\.xml$|\.html$'); - $this->toc = array(); - - foreach ($files as $file) - { - $buffer = file_get_contents(JPATH_BASE . '/help/' . $lang_tag . '/' . $file); - - if (!preg_match('#(.*?)#', $buffer, $m)) - { - continue; - } - - $title = trim($m[1]); - - if (!$title) - { - continue; - } - - // Translate the page title - $title = Text::_($title); - - // Strip the extension - $file = preg_replace('#\.xml$|\.html$#', '', $file); - - if ($help_search && StringHelper::strpos(StringHelper::strtolower(strip_tags($buffer)), StringHelper::strtolower($help_search)) === false) - { - continue; - } - - // Add an item in the Table of Contents - $this->toc[$file] = $title; - } - - // Sort the Table of Contents - asort($this->toc); - - return $this->toc; - } + /** + * The search string + * + * @var string + * @since 1.6 + */ + protected $help_search = null; + + /** + * The page to be viewed + * + * @var string + * @since 1.6 + */ + protected $page = null; + + /** + * The ISO language tag + * + * @var string + * @since 1.6 + */ + protected $lang_tag = null; + + /** + * Table of contents + * + * @var array + * @since 1.6 + */ + protected $toc = null; + + /** + * URL for the latest version check + * + * @var string + * @since 1.6 + */ + protected $latest_version_check = null; + + /** + * Method to get the help search string + * + * @return string Help search string + * + * @since 1.6 + */ + public function &getHelpSearch() + { + if (\is_null($this->help_search)) { + $this->help_search = Factory::getApplication()->input->getString('helpsearch'); + } + + return $this->help_search; + } + + /** + * Method to get the page + * + * @return string The page + * + * @since 1.6 + */ + public function &getPage() + { + if (\is_null($this->page)) { + $this->page = Help::createUrl(Factory::getApplication()->input->get('page', 'Start_Here')); + } + + return $this->page; + } + + /** + * Method to get the lang tag + * + * @return string lang iso tag + * + * @since 1.6 + */ + public function getLangTag() + { + if (\is_null($this->lang_tag)) { + $this->lang_tag = Factory::getLanguage()->getTag(); + + if (!is_dir(JPATH_BASE . '/help/' . $this->lang_tag)) { + // Use English as fallback + $this->lang_tag = 'en-GB'; + } + } + + return $this->lang_tag; + } + + /** + * Method to get the table of contents + * + * @return array Table of contents + */ + public function &getToc() + { + if (!\is_null($this->toc)) { + return $this->toc; + } + + // Get vars + $lang_tag = $this->getLangTag(); + $help_search = $this->getHelpSearch(); + + // New style - Check for a TOC \JSON file + if (file_exists(JPATH_BASE . '/help/' . $lang_tag . '/toc.json')) { + $data = json_decode(file_get_contents(JPATH_BASE . '/help/' . $lang_tag . '/toc.json')); + + // Loop through the data array + foreach ($data as $key => $value) { + $this->toc[$key] = Text::_('COM_ADMIN_HELP_' . $value); + } + + // Sort the Table of Contents + asort($this->toc); + + return $this->toc; + } + + // Get Help files + $files = Folder::files(JPATH_BASE . '/help/' . $lang_tag, '\.xml$|\.html$'); + $this->toc = array(); + + foreach ($files as $file) { + $buffer = file_get_contents(JPATH_BASE . '/help/' . $lang_tag . '/' . $file); + + if (!preg_match('#(.*?)#', $buffer, $m)) { + continue; + } + + $title = trim($m[1]); + + if (!$title) { + continue; + } + + // Translate the page title + $title = Text::_($title); + + // Strip the extension + $file = preg_replace('#\.xml$|\.html$#', '', $file); + + if ($help_search && StringHelper::strpos(StringHelper::strtolower(strip_tags($buffer)), StringHelper::strtolower($help_search)) === false) { + continue; + } + + // Add an item in the Table of Contents + $this->toc[$file] = $title; + } + + // Sort the Table of Contents + asort($this->toc); + + return $this->toc; + } } diff --git a/code/administrator/components/com_admin/src/Model/SysinfoModel.php b/code/administrator/components/com_admin/src/Model/SysinfoModel.php index 357db4eb..30f92368 100644 --- a/code/administrator/components/com_admin/src/Model/SysinfoModel.php +++ b/code/administrator/components/com_admin/src/Model/SysinfoModel.php @@ -1,4 +1,5 @@ [ - 'CONTEXT_DOCUMENT_ROOT', - 'Cookie', - 'DOCUMENT_ROOT', - 'extension_dir', - 'error_log', - 'Host', - 'HTTP_COOKIE', - 'HTTP_HOST', - 'HTTP_ORIGIN', - 'HTTP_REFERER', - 'HTTP Request', - 'include_path', - 'mysql.default_socket', - 'MYSQL_SOCKET', - 'MYSQL_INCLUDE', - 'MYSQL_LIBS', - 'mysqli.default_socket', - 'MYSQLI_SOCKET', - 'PATH', - 'Path to sendmail', - 'pdo_mysql.default_socket', - 'Referer', - 'REMOTE_ADDR', - 'SCRIPT_FILENAME', - 'sendmail_path', - 'SERVER_ADDR', - 'SERVER_ADMIN', - 'Server Administrator', - 'SERVER_NAME', - 'Server Root', - 'session.name', - 'session.save_path', - 'upload_tmp_dir', - 'User/Group', - 'open_basedir', - ], - 'other' => [ - 'db', - 'dbprefix', - 'fromname', - 'live_site', - 'log_path', - 'mailfrom', - 'memcached_server_host', - 'open_basedir', - 'Origin', - 'proxy_host', - 'proxy_user', - 'proxy_pass', - 'redis_server_host', - 'redis_server_auth', - 'secret', - 'sendmail', - 'session.save_path', - 'session_memcached_server_host', - 'session_redis_server_host', - 'session_redis_server_auth', - 'sitename', - 'smtphost', - 'tmp_path', - 'open_basedir', - ] - ]; - - /** - * System values that can be "safely" shared - * - * @var array - * - * @since 3.5 - */ - protected $safeData; - - /** - * Information about writable state of directories - * - * @var array - * @since 1.6 - */ - protected $directories = []; - - /** - * The current editor. - * - * @var string - * @since 1.6 - */ - protected $editor = null; - - /** - * Remove sections of data marked as private in the privateSettings - * - * @param array $dataArray Array with data that may contain private information - * @param string $dataType Type of data to search for a specific section in the privateSettings array - * - * @return array - * - * @since 3.5 - */ - protected function cleanPrivateData(array $dataArray, string $dataType = 'other'): array - { - $dataType = isset($this->privateSettings[$dataType]) ? $dataType : 'other'; - - $privateSettings = $this->privateSettings[$dataType]; - - if (!$privateSettings) - { - return $dataArray; - } - - foreach ($dataArray as $section => $values) - { - if (\is_array($values)) - { - $dataArray[$section] = $this->cleanPrivateData($values, $dataType); - } - - if (\in_array($section, $privateSettings, true)) - { - $dataArray[$section] = $this->cleanSectionPrivateData($values); - } - } - - return $dataArray; - } - - /** - * Obfuscate section values - * - * @param mixed $sectionValues Section data - * - * @return string|array - * - * @since 3.5 - */ - protected function cleanSectionPrivateData($sectionValues) - { - if (!\is_array($sectionValues)) - { - if (strstr($sectionValues, JPATH_ROOT)) - { - $sectionValues = 'xxxxxx'; - } - - return \strlen($sectionValues) ? 'xxxxxx' : ''; - } - - foreach ($sectionValues as $setting => $value) - { - $sectionValues[$setting] = \strlen($value) ? 'xxxxxx' : ''; - } - - return $sectionValues; - } - - /** - * Method to get the PHP settings - * - * @return array Some PHP settings - * - * @since 1.6 - */ - public function &getPhpSettings(): array - { - if (!empty($this->php_settings)) - { - return $this->php_settings; - } - - $this->php_settings = [ - 'memory_limit' => ini_get('memory_limit'), - 'upload_max_filesize' => ini_get('upload_max_filesize'), - 'post_max_size' => ini_get('post_max_size'), - 'display_errors' => ini_get('display_errors') == '1', - 'short_open_tag' => ini_get('short_open_tag') == '1', - 'file_uploads' => ini_get('file_uploads') == '1', - 'output_buffering' => (int) ini_get('output_buffering') !== 0, - 'open_basedir' => ini_get('open_basedir'), - 'session.save_path' => ini_get('session.save_path'), - 'session.auto_start' => ini_get('session.auto_start'), - 'disable_functions' => ini_get('disable_functions'), - 'xml' => \extension_loaded('xml'), - 'zlib' => \extension_loaded('zlib'), - 'zip' => \function_exists('zip_open') && \function_exists('zip_read'), - 'mbstring' => \extension_loaded('mbstring'), - 'fileinfo' => \extension_loaded('fileinfo'), - 'gd' => \extension_loaded('gd'), - 'iconv' => \function_exists('iconv'), - 'intl' => \function_exists('transliterator_transliterate'), - 'max_input_vars' => ini_get('max_input_vars'), - ]; - - return $this->php_settings; - } - - /** - * Method to get the config - * - * @return array config values - * - * @since 1.6 - */ - public function &getConfig(): array - { - if (!empty($this->config)) - { - return $this->config; - } - - $registry = new Registry(new \JConfig); - $this->config = $registry->toArray(); - $hidden = [ - 'host', 'user', 'password', 'ftp_user', 'ftp_pass', - 'smtpuser', 'smtppass', 'redis_server_auth', 'session_redis_server_auth', - 'proxy_user', 'proxy_pass', 'secret' - ]; - - foreach ($hidden as $key) - { - $this->config[$key] = 'xxxxxx'; - } - - return $this->config; - } - - /** - * Method to get the system information - * - * @return array System information values - * - * @since 1.6 - */ - public function &getInfo(): array - { - if (!empty($this->info)) - { - return $this->info; - } - - $db = $this->getDbo(); - - $this->info = [ - 'php' => php_uname(), - 'dbserver' => $db->getServerType(), - 'dbversion' => $db->getVersion(), - 'dbcollation' => $db->getCollation(), - 'dbconnectioncollation' => $db->getConnectionCollation(), - 'dbconnectionencryption' => $db->getConnectionEncryption(), - 'dbconnencryptsupported' => $db->isConnectionEncryptionSupported(), - 'phpversion' => PHP_VERSION, - 'server' => $_SERVER['SERVER_SOFTWARE'] ?? getenv('SERVER_SOFTWARE'), - 'sapi_name' => PHP_SAPI, - 'version' => (new Version)->getLongVersion(), - 'useragent' => $_SERVER['HTTP_USER_AGENT'] ?? '', - ]; - - return $this->info; - } - - /** - * Check if the phpinfo function is enabled - * - * @return boolean True if enabled - * - * @since 3.4.1 - */ - public function phpinfoEnabled(): bool - { - return !\in_array('phpinfo', explode(',', ini_get('disable_functions'))); - } - - /** - * Method to get filter data from the model - * - * @param string $dataType Type of data to get safely - * @param bool $public If true no sensitive information will be removed - * - * @return array - * - * @since 3.5 - */ - public function getSafeData(string $dataType, bool $public = true): array - { - if (isset($this->safeData[$dataType])) - { - return $this->safeData[$dataType]; - } - - $methodName = 'get' . ucfirst($dataType); - - if (!method_exists($this, $methodName)) - { - return []; - } - - $data = $this->$methodName($public); - - $this->safeData[$dataType] = $this->cleanPrivateData($data, $dataType); - - return $this->safeData[$dataType]; - } - - /** - * Method to get the PHP info - * - * @return string PHP info - * - * @since 1.6 - */ - public function &getPHPInfo(): string - { - if (!$this->phpinfoEnabled()) - { - $this->php_info = Text::_('COM_ADMIN_PHPINFO_DISABLED'); - - return $this->php_info; - } - - if (!\is_null($this->php_info)) - { - return $this->php_info; - } - - ob_start(); - date_default_timezone_set('UTC'); - phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES); - $phpInfo = ob_get_contents(); - ob_end_clean(); - preg_match_all('#]*>(.*)#siU', $phpInfo, $output); - $output = preg_replace('#]*>#', '', $output[1][0]); - $output = preg_replace('#(\w),(\w)#', '\1, \2', $output); - $output = preg_replace('#
#', '', $output); - $output = str_replace('
', '', $output); - $output = preg_replace('#
(.*)#', '$1', $output); - $output = str_replace('
', '', $output); - $output = str_replace('', '', $output); - $this->php_info = $output; - - return $this->php_info; - } - - /** - * Get phpinfo() output as array - * - * @return array - * - * @since 3.5 - */ - public function getPhpInfoArray(): array - { - // Already cached - if (null !== $this->phpInfoArray) - { - return $this->phpInfoArray; - } - - $phpInfo = $this->getPHPInfo(); - - $this->phpInfoArray = $this->parsePhpInfo($phpInfo); - - return $this->phpInfoArray; - } - - /** - * Method to get a list of installed extensions - * - * @return array installed extensions - * - * @since 3.5 - */ - public function getExtensions(): array - { - $installed = []; - $db = Factory::getContainer()->get('DatabaseDriver'); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__extensions')); - $db->setQuery($query); - - try - { - $extensions = $db->loadObjectList(); - } - catch (\Exception $e) - { - try - { - Log::add(Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()), - 'warning' - ); - } - - return $installed; - } - - if (empty($extensions)) - { - return $installed; - } - - foreach ($extensions as $extension) - { - if (\strlen($extension->name) == 0) - { - continue; - } - - $installed[$extension->name] = [ - 'name' => $extension->name, - 'type' => $extension->type, - 'state' => $extension->enabled ? Text::_('JENABLED') : Text::_('JDISABLED'), - 'author' => 'unknown', - 'version' => 'unknown', - 'creationDate' => 'unknown', - 'authorUrl' => 'unknown', - ]; - - $manifest = new Registry($extension->manifest_cache); - - $extraData = [ - 'author' => $manifest->get('author', ''), - 'version' => $manifest->get('version', ''), - 'creationDate' => $manifest->get('creationDate', ''), - 'authorUrl' => $manifest->get('authorUrl', '') - ]; - - $installed[$extension->name] = array_merge($installed[$extension->name], $extraData); - } - - return $installed; - } - - /** - * Method to get the directory states - * - * @param bool $public If true no information is going to be removed - * - * @return array States of directories - * - * @throws \Exception - * @since 1.6 - */ - public function getDirectory(bool $public = false): array - { - if (!empty($this->directories)) - { - return $this->directories; - } - - $this->directories = []; - - $registry = Factory::getApplication()->getConfig(); - $cparams = ComponentHelper::getParams('com_media'); - - $this->addDirectory('administrator/components', JPATH_ADMINISTRATOR . '/components'); - $this->addDirectory('administrator/components/com_joomlaupdate', JPATH_ADMINISTRATOR . '/components/com_joomlaupdate'); - $this->addDirectory('administrator/language', JPATH_ADMINISTRATOR . '/language'); - - // List all admin languages - $admin_langs = new \DirectoryIterator(JPATH_ADMINISTRATOR . '/language'); - - foreach ($admin_langs as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory( - 'administrator/language/' . $folder->getFilename(), - JPATH_ADMINISTRATOR . '/language/' . $folder->getFilename() - ); - } - - // List all manifests folders - $manifests = new \DirectoryIterator(JPATH_ADMINISTRATOR . '/manifests'); - - foreach ($manifests as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory( - 'administrator/manifests/' . $folder->getFilename(), - JPATH_ADMINISTRATOR . '/manifests/' . $folder->getFilename() - ); - } - - $this->addDirectory('administrator/modules', JPATH_ADMINISTRATOR . '/modules'); - $this->addDirectory('administrator/templates', JPATH_THEMES); - - $this->addDirectory('components', JPATH_SITE . '/components'); - - $this->addDirectory($cparams->get('image_path'), JPATH_SITE . '/' . $cparams->get('image_path')); - - // List all images folders - $image_folders = new \DirectoryIterator(JPATH_SITE . '/' . $cparams->get('image_path')); - - foreach ($image_folders as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory( - 'images/' . $folder->getFilename(), - JPATH_SITE . '/' . $cparams->get('image_path') . '/' . $folder->getFilename() - ); - } - - $this->addDirectory('language', JPATH_SITE . '/language'); - - // List all site languages - $site_langs = new \DirectoryIterator(JPATH_SITE . '/language'); - - foreach ($site_langs as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory('language/' . $folder->getFilename(), JPATH_SITE . '/language/' . $folder->getFilename()); - } - - $this->addDirectory('libraries', JPATH_LIBRARIES); - - $this->addDirectory('media', JPATH_SITE . '/media'); - $this->addDirectory('modules', JPATH_SITE . '/modules'); - $this->addDirectory('plugins', JPATH_PLUGINS); - - $plugin_groups = new \DirectoryIterator(JPATH_SITE . '/plugins'); - - foreach ($plugin_groups as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory('plugins/' . $folder->getFilename(), JPATH_PLUGINS . '/' . $folder->getFilename()); - } - - $this->addDirectory('templates', JPATH_SITE . '/templates'); - $this->addDirectory('configuration.php', JPATH_CONFIGURATION . '/configuration.php'); - - // Is there a cache path in configuration.php? - if ($cache_path = trim($registry->get('cache_path', ''))) - { - // Frontend and backend use same directory for caching. - $this->addDirectory($cache_path, $cache_path, 'COM_ADMIN_CACHE_DIRECTORY'); - } - else - { - $this->addDirectory('administrator/cache', JPATH_CACHE, 'COM_ADMIN_CACHE_DIRECTORY'); - } - - $this->addDirectory('media/cache', JPATH_ROOT . '/media/cache', 'COM_ADMIN_MEDIA_CACHE_DIRECTORY'); - - if ($public) - { - $this->addDirectory( - 'log', - $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), - 'COM_ADMIN_LOG_DIRECTORY' - ); - $this->addDirectory( - 'tmp', - $registry->get('tmp_path', JPATH_ROOT . '/tmp'), - 'COM_ADMIN_TEMP_DIRECTORY' - ); - } - else - { - $this->addDirectory( - $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), - $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), - 'COM_ADMIN_LOG_DIRECTORY' - ); - $this->addDirectory( - $registry->get('tmp_path', JPATH_ROOT . '/tmp'), - $registry->get('tmp_path', JPATH_ROOT . '/tmp'), - 'COM_ADMIN_TEMP_DIRECTORY' - ); - } - - return $this->directories; - } - - /** - * Method to add a directory - * - * @param string $name Directory Name - * @param string $path Directory path - * @param string $message Message - * - * @return void - * - * @since 1.6 - */ - private function addDirectory(string $name, string $path, string $message = ''): void - { - $this->directories[$name] = ['writable' => is_writable($path), 'message' => $message,]; - } - - /** - * Method to get the editor - * - * @return string The default editor - * - * @note Has to be removed (it is present in the config...) - * @since 1.6 - */ - public function &getEditor(): string - { - if (!is_null($this->editor)) - { - return $this->editor; - } - - $this->editor = Factory::getApplication()->get('editor'); - - return $this->editor; - } - - /** - * Parse phpinfo output into an array - * Source https://gist.github.com/sbmzhcn/6255314 - * - * @param string $html Output of phpinfo() - * - * @return array - * - * @since 3.5 - */ - protected function parsePhpInfo(string $html): array - { - $html = strip_tags($html, '

'); - $html = preg_replace('/]*>([^<]+)<\/th>/', '\1', $html); - $html = preg_replace('/]*>([^<]+)<\/td>/', '\1', $html); - $t = preg_split('/(]*>[^<]+<\/h2>)/', $html, -1, PREG_SPLIT_DELIM_CAPTURE); - $r = []; - $count = \count($t); - $p1 = '([^<]+)<\/info>'; - $p2 = '/' . $p1 . '\s*' . $p1 . '\s*' . $p1 . '/'; - $p3 = '/' . $p1 . '\s*' . $p1 . '/'; - - for ($i = 1; $i < $count; $i++) - { - if (preg_match('/]*>([^<]+)<\/h2>/', $t[$i], $matches)) - { - $name = trim($matches[1]); - $vals = explode("\n", $t[$i + 1]); - - foreach ($vals AS $val) - { - // 3cols - if (preg_match($p2, $val, $matches)) - { - $r[$name][trim($matches[1])] = [trim($matches[2]), trim($matches[3]),]; - } - // 2cols - elseif (preg_match($p3, $val, $matches)) - { - $r[$name][trim($matches[1])] = trim($matches[2]); - } - } - } - } - - return $r; - } + /** + * Some PHP settings + * + * @var array + * @since 1.6 + */ + protected $php_settings = []; + + /** + * Config values + * + * @var array + * @since 1.6 + */ + protected $config = []; + + /** + * Some system values + * + * @var array + * @since 1.6 + */ + protected $info = []; + + /** + * PHP info + * + * @var string + * @since 1.6 + */ + protected $php_info = null; + + /** + * Array containing the phpinfo() data. + * + * @var array + * + * @since 3.5 + */ + protected $phpInfoArray; + + /** + * Private/critical data that we don't want to share + * + * @var array + * + * @since 3.5 + */ + protected $privateSettings = [ + 'phpInfoArray' => [ + 'CONTEXT_DOCUMENT_ROOT', + 'Cookie', + 'DOCUMENT_ROOT', + 'extension_dir', + 'error_log', + 'Host', + 'HTTP_COOKIE', + 'HTTP_HOST', + 'HTTP_ORIGIN', + 'HTTP_REFERER', + 'HTTP Request', + 'include_path', + 'mysql.default_socket', + 'MYSQL_SOCKET', + 'MYSQL_INCLUDE', + 'MYSQL_LIBS', + 'mysqli.default_socket', + 'MYSQLI_SOCKET', + 'PATH', + 'Path to sendmail', + 'pdo_mysql.default_socket', + 'Referer', + 'REMOTE_ADDR', + 'SCRIPT_FILENAME', + 'sendmail_path', + 'SERVER_ADDR', + 'SERVER_ADMIN', + 'Server Administrator', + 'SERVER_NAME', + 'Server Root', + 'session.name', + 'session.save_path', + 'upload_tmp_dir', + 'User/Group', + 'open_basedir', + ], + 'other' => [ + 'db', + 'dbprefix', + 'fromname', + 'live_site', + 'log_path', + 'mailfrom', + 'memcached_server_host', + 'open_basedir', + 'Origin', + 'proxy_host', + 'proxy_user', + 'proxy_pass', + 'redis_server_host', + 'redis_server_auth', + 'secret', + 'sendmail', + 'session.save_path', + 'session_memcached_server_host', + 'session_redis_server_host', + 'session_redis_server_auth', + 'sitename', + 'smtphost', + 'tmp_path', + 'open_basedir', + ] + ]; + + /** + * System values that can be "safely" shared + * + * @var array + * + * @since 3.5 + */ + protected $safeData; + + /** + * Information about writable state of directories + * + * @var array + * @since 1.6 + */ + protected $directories = []; + + /** + * The current editor. + * + * @var string + * @since 1.6 + */ + protected $editor = null; + + /** + * Remove sections of data marked as private in the privateSettings + * + * @param array $dataArray Array with data that may contain private information + * @param string $dataType Type of data to search for a specific section in the privateSettings array + * + * @return array + * + * @since 3.5 + */ + protected function cleanPrivateData(array $dataArray, string $dataType = 'other'): array + { + $dataType = isset($this->privateSettings[$dataType]) ? $dataType : 'other'; + + $privateSettings = $this->privateSettings[$dataType]; + + if (!$privateSettings) { + return $dataArray; + } + + foreach ($dataArray as $section => $values) { + if (\is_array($values)) { + $dataArray[$section] = $this->cleanPrivateData($values, $dataType); + } + + if (\in_array($section, $privateSettings, true)) { + $dataArray[$section] = $this->cleanSectionPrivateData($values); + } + } + + return $dataArray; + } + + /** + * Obfuscate section values + * + * @param mixed $sectionValues Section data + * + * @return string|array + * + * @since 3.5 + */ + protected function cleanSectionPrivateData($sectionValues) + { + if (!\is_array($sectionValues)) { + if (strstr($sectionValues, JPATH_ROOT)) { + $sectionValues = 'xxxxxx'; + } + + return \strlen($sectionValues) ? 'xxxxxx' : ''; + } + + foreach ($sectionValues as $setting => $value) { + $sectionValues[$setting] = \strlen($value) ? 'xxxxxx' : ''; + } + + return $sectionValues; + } + + /** + * Method to get the PHP settings + * + * @return array Some PHP settings + * + * @since 1.6 + */ + public function &getPhpSettings(): array + { + if (!empty($this->php_settings)) { + return $this->php_settings; + } + + $this->php_settings = [ + 'memory_limit' => ini_get('memory_limit'), + 'upload_max_filesize' => ini_get('upload_max_filesize'), + 'post_max_size' => ini_get('post_max_size'), + 'display_errors' => ini_get('display_errors') == '1', + 'short_open_tag' => ini_get('short_open_tag') == '1', + 'file_uploads' => ini_get('file_uploads') == '1', + 'output_buffering' => (int) ini_get('output_buffering') !== 0, + 'open_basedir' => ini_get('open_basedir'), + 'session.save_path' => ini_get('session.save_path'), + 'session.auto_start' => ini_get('session.auto_start'), + 'disable_functions' => ini_get('disable_functions'), + 'xml' => \extension_loaded('xml'), + 'zlib' => \extension_loaded('zlib'), + 'zip' => \function_exists('zip_open') && \function_exists('zip_read'), + 'mbstring' => \extension_loaded('mbstring'), + 'fileinfo' => \extension_loaded('fileinfo'), + 'gd' => \extension_loaded('gd'), + 'iconv' => \function_exists('iconv'), + 'intl' => \function_exists('transliterator_transliterate'), + 'max_input_vars' => ini_get('max_input_vars'), + ]; + + return $this->php_settings; + } + + /** + * Method to get the config + * + * @return array config values + * + * @since 1.6 + */ + public function &getConfig(): array + { + if (!empty($this->config)) { + return $this->config; + } + + $registry = new Registry(new \JConfig()); + $this->config = $registry->toArray(); + $hidden = [ + 'host', 'user', 'password', 'ftp_user', 'ftp_pass', + 'smtpuser', 'smtppass', 'redis_server_auth', 'session_redis_server_auth', + 'proxy_user', 'proxy_pass', 'secret' + ]; + + foreach ($hidden as $key) { + $this->config[$key] = 'xxxxxx'; + } + + return $this->config; + } + + /** + * Method to get the system information + * + * @return array System information values + * + * @since 1.6 + */ + public function &getInfo(): array + { + if (!empty($this->info)) { + return $this->info; + } + + $db = $this->getDatabase(); + + $this->info = [ + 'php' => php_uname(), + 'dbserver' => $db->getServerType(), + 'dbversion' => $db->getVersion(), + 'dbcollation' => $db->getCollation(), + 'dbconnectioncollation' => $db->getConnectionCollation(), + 'dbconnectionencryption' => $db->getConnectionEncryption(), + 'dbconnencryptsupported' => $db->isConnectionEncryptionSupported(), + 'phpversion' => PHP_VERSION, + 'server' => $_SERVER['SERVER_SOFTWARE'] ?? getenv('SERVER_SOFTWARE'), + 'sapi_name' => PHP_SAPI, + 'version' => (new Version())->getLongVersion(), + 'useragent' => $_SERVER['HTTP_USER_AGENT'] ?? '', + ]; + + return $this->info; + } + + /** + * Check if the phpinfo function is enabled + * + * @return boolean True if enabled + * + * @since 3.4.1 + */ + public function phpinfoEnabled(): bool + { + return !\in_array('phpinfo', explode(',', ini_get('disable_functions'))); + } + + /** + * Method to get filter data from the model + * + * @param string $dataType Type of data to get safely + * @param bool $public If true no sensitive information will be removed + * + * @return array + * + * @since 3.5 + */ + public function getSafeData(string $dataType, bool $public = true): array + { + if (isset($this->safeData[$dataType])) { + return $this->safeData[$dataType]; + } + + $methodName = 'get' . ucfirst($dataType); + + if (!method_exists($this, $methodName)) { + return []; + } + + $data = $this->$methodName($public); + + $this->safeData[$dataType] = $this->cleanPrivateData($data, $dataType); + + return $this->safeData[$dataType]; + } + + /** + * Method to get the PHP info + * + * @return string PHP info + * + * @since 1.6 + */ + public function &getPHPInfo(): string + { + if (!$this->phpinfoEnabled()) { + $this->php_info = Text::_('COM_ADMIN_PHPINFO_DISABLED'); + + return $this->php_info; + } + + if (!\is_null($this->php_info)) { + return $this->php_info; + } + + ob_start(); + date_default_timezone_set('UTC'); + phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES); + $phpInfo = ob_get_contents(); + ob_end_clean(); + preg_match_all('#]*>(.*)#siU', $phpInfo, $output); + $output = preg_replace('#]*>#', '', $output[1][0]); + $output = preg_replace('#(\w),(\w)#', '\1, \2', $output); + $output = preg_replace('#
#', '', $output); + $output = str_replace('
', '', $output); + $output = preg_replace('#
(.*)#', '$1', $output); + $output = str_replace('
', '', $output); + $output = str_replace('', '', $output); + $this->php_info = $output; + + return $this->php_info; + } + + /** + * Get phpinfo() output as array + * + * @return array + * + * @since 3.5 + */ + public function getPhpInfoArray(): array + { + // Already cached + if (null !== $this->phpInfoArray) { + return $this->phpInfoArray; + } + + $phpInfo = $this->getPHPInfo(); + + $this->phpInfoArray = $this->parsePhpInfo($phpInfo); + + return $this->phpInfoArray; + } + + /** + * Method to get a list of installed extensions + * + * @return array installed extensions + * + * @since 3.5 + */ + public function getExtensions(): array + { + $installed = []; + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__extensions')); + $db->setQuery($query); + + try { + $extensions = $db->loadObjectList(); + } catch (\Exception $e) { + try { + Log::add(Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage( + Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()), + 'warning' + ); + } + + return $installed; + } + + if (empty($extensions)) { + return $installed; + } + + foreach ($extensions as $extension) { + if (\strlen($extension->name) == 0) { + continue; + } + + $installed[$extension->name] = [ + 'name' => $extension->name, + 'type' => $extension->type, + 'state' => $extension->enabled ? Text::_('JENABLED') : Text::_('JDISABLED'), + 'author' => 'unknown', + 'version' => 'unknown', + 'creationDate' => 'unknown', + 'authorUrl' => 'unknown', + ]; + + $manifest = new Registry($extension->manifest_cache); + + $extraData = [ + 'author' => $manifest->get('author', ''), + 'version' => $manifest->get('version', ''), + 'creationDate' => $manifest->get('creationDate', ''), + 'authorUrl' => $manifest->get('authorUrl', '') + ]; + + $installed[$extension->name] = array_merge($installed[$extension->name], $extraData); + } + + return $installed; + } + + /** + * Method to get the directory states + * + * @param bool $public If true no information is going to be removed + * + * @return array States of directories + * + * @throws \Exception + * @since 1.6 + */ + public function getDirectory(bool $public = false): array + { + if (!empty($this->directories)) { + return $this->directories; + } + + $this->directories = []; + + $registry = Factory::getApplication()->getConfig(); + $cparams = ComponentHelper::getParams('com_media'); + + $this->addDirectory('administrator/components', JPATH_ADMINISTRATOR . '/components'); + $this->addDirectory('administrator/components/com_joomlaupdate', JPATH_ADMINISTRATOR . '/components/com_joomlaupdate'); + $this->addDirectory('administrator/language', JPATH_ADMINISTRATOR . '/language'); + + // List all admin languages + $admin_langs = new \DirectoryIterator(JPATH_ADMINISTRATOR . '/language'); + + foreach ($admin_langs as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory( + 'administrator/language/' . $folder->getFilename(), + JPATH_ADMINISTRATOR . '/language/' . $folder->getFilename() + ); + } + + // List all manifests folders + $manifests = new \DirectoryIterator(JPATH_ADMINISTRATOR . '/manifests'); + + foreach ($manifests as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory( + 'administrator/manifests/' . $folder->getFilename(), + JPATH_ADMINISTRATOR . '/manifests/' . $folder->getFilename() + ); + } + + $this->addDirectory('administrator/modules', JPATH_ADMINISTRATOR . '/modules'); + $this->addDirectory('administrator/templates', JPATH_THEMES); + + $this->addDirectory('components', JPATH_SITE . '/components'); + + $this->addDirectory($cparams->get('image_path'), JPATH_SITE . '/' . $cparams->get('image_path')); + + // List all images folders + $image_folders = new \DirectoryIterator(JPATH_SITE . '/' . $cparams->get('image_path')); + + foreach ($image_folders as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory( + 'images/' . $folder->getFilename(), + JPATH_SITE . '/' . $cparams->get('image_path') . '/' . $folder->getFilename() + ); + } + + $this->addDirectory('language', JPATH_SITE . '/language'); + + // List all site languages + $site_langs = new \DirectoryIterator(JPATH_SITE . '/language'); + + foreach ($site_langs as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory('language/' . $folder->getFilename(), JPATH_SITE . '/language/' . $folder->getFilename()); + } + + $this->addDirectory('libraries', JPATH_LIBRARIES); + + $this->addDirectory('media', JPATH_SITE . '/media'); + $this->addDirectory('modules', JPATH_SITE . '/modules'); + $this->addDirectory('plugins', JPATH_PLUGINS); + + $plugin_groups = new \DirectoryIterator(JPATH_SITE . '/plugins'); + + foreach ($plugin_groups as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory('plugins/' . $folder->getFilename(), JPATH_PLUGINS . '/' . $folder->getFilename()); + } + + $this->addDirectory('templates', JPATH_SITE . '/templates'); + $this->addDirectory('configuration.php', JPATH_CONFIGURATION . '/configuration.php'); + + // Is there a cache path in configuration.php? + if ($cache_path = trim($registry->get('cache_path', ''))) { + // Frontend and backend use same directory for caching. + $this->addDirectory($cache_path, $cache_path, 'COM_ADMIN_CACHE_DIRECTORY'); + } else { + $this->addDirectory('administrator/cache', JPATH_CACHE, 'COM_ADMIN_CACHE_DIRECTORY'); + } + + $this->addDirectory('media/cache', JPATH_ROOT . '/media/cache', 'COM_ADMIN_MEDIA_CACHE_DIRECTORY'); + + if ($public) { + $this->addDirectory( + 'log', + $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), + 'COM_ADMIN_LOG_DIRECTORY' + ); + $this->addDirectory( + 'tmp', + $registry->get('tmp_path', JPATH_ROOT . '/tmp'), + 'COM_ADMIN_TEMP_DIRECTORY' + ); + } else { + $this->addDirectory( + $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), + $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), + 'COM_ADMIN_LOG_DIRECTORY' + ); + $this->addDirectory( + $registry->get('tmp_path', JPATH_ROOT . '/tmp'), + $registry->get('tmp_path', JPATH_ROOT . '/tmp'), + 'COM_ADMIN_TEMP_DIRECTORY' + ); + } + + return $this->directories; + } + + /** + * Method to add a directory + * + * @param string $name Directory Name + * @param string $path Directory path + * @param string $message Message + * + * @return void + * + * @since 1.6 + */ + private function addDirectory(string $name, string $path, string $message = ''): void + { + $this->directories[$name] = ['writable' => is_writable($path), 'message' => $message,]; + } + + /** + * Method to get the editor + * + * @return string The default editor + * + * @note Has to be removed (it is present in the config...) + * @since 1.6 + */ + public function &getEditor(): string + { + if (!is_null($this->editor)) { + return $this->editor; + } + + $this->editor = Factory::getApplication()->get('editor'); + + return $this->editor; + } + + /** + * Parse phpinfo output into an array + * Source https://gist.github.com/sbmzhcn/6255314 + * + * @param string $html Output of phpinfo() + * + * @return array + * + * @since 3.5 + */ + protected function parsePhpInfo(string $html): array + { + $html = strip_tags($html, '

'); + $html = preg_replace('/]*>([^<]+)<\/th>/', '\1', $html); + $html = preg_replace('/]*>([^<]+)<\/td>/', '\1', $html); + $t = preg_split('/(]*>[^<]+<\/h2>)/', $html, -1, PREG_SPLIT_DELIM_CAPTURE); + $r = []; + $count = \count($t); + $p1 = '([^<]+)<\/info>'; + $p2 = '/' . $p1 . '\s*' . $p1 . '\s*' . $p1 . '/'; + $p3 = '/' . $p1 . '\s*' . $p1 . '/'; + + for ($i = 1; $i < $count; $i++) { + if (preg_match('/]*>([^<]+)<\/h2>/', $t[$i], $matches)) { + $name = trim($matches[1]); + $vals = explode("\n", $t[$i + 1]); + + foreach ($vals as $val) { + // 3cols + if (preg_match($p2, $val, $matches)) { + $r[$name][trim($matches[1])] = [trim($matches[2]), trim($matches[3]),]; + } elseif (preg_match($p3, $val, $matches)) { + // 2cols + $r[$name][trim($matches[1])] = trim($matches[2]); + } + } + } + } + + return $r; + } } diff --git a/code/administrator/components/com_admin/src/Service/HTML/Directory.php b/code/administrator/components/com_admin/src/Service/HTML/Directory.php index f5de97a6..46137792 100644 --- a/code/administrator/components/com_admin/src/Service/HTML/Directory.php +++ b/code/administrator/components/com_admin/src/Service/HTML/Directory.php @@ -1,4 +1,5 @@ ' . Text::_('COM_ADMIN_WRITABLE') . ''; - } - - return '' . Text::_('COM_ADMIN_UNWRITABLE') . ''; - } - - /** - * Method to generate a message for a directory - * - * @param string $dir the directory - * @param boolean $message the message - * @param boolean $visible is the $dir visible? - * - * @return string html code - */ - public function message($dir, $message, $visible = true) - { - $output = $visible ? $dir : ''; - - if (empty($message)) - { - return $output; - } - - return $output . ' ' . Text::_($message) . ''; - } + /** + * Method to generate a (un)writable message for directory + * + * @param boolean $writable is the directory writable? + * + * @return string html code + */ + public function writable($writable) + { + if ($writable) { + return '' . Text::_('COM_ADMIN_WRITABLE') . ''; + } + + return '' . Text::_('COM_ADMIN_UNWRITABLE') . ''; + } + + /** + * Method to generate a message for a directory + * + * @param string $dir the directory + * @param boolean $message the message + * @param boolean $visible is the $dir visible? + * + * @return string html code + */ + public function message($dir, $message, $visible = true) + { + $output = $visible ? $dir : ''; + + if (empty($message)) { + return $output; + } + + return $output . ' ' . Text::_($message) . ''; + } } diff --git a/code/administrator/components/com_admin/src/Service/HTML/PhpSetting.php b/code/administrator/components/com_admin/src/Service/HTML/PhpSetting.php index 8d4a1cd6..c1f18f44 100644 --- a/code/administrator/components/com_admin/src/Service/HTML/PhpSetting.php +++ b/code/administrator/components/com_admin/src/Service/HTML/PhpSetting.php @@ -1,4 +1,5 @@ getModel(); - $this->helpSearch = $model->getHelpSearch(); - $this->page = $model->getPage(); - $this->toc = $model->getToc(); - $this->languageTag = $model->getLangTag(); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var HelpModel $model */ + $model = $this->getModel(); + $this->helpSearch = $model->getHelpSearch(); + $this->page = $model->getPage(); + $this->toc = $model->getToc(); + $this->languageTag = $model->getLangTag(); - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Setup the Toolbar - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - ToolbarHelper::title(Text::_('COM_ADMIN_HELP'), 'support help_header'); - } + /** + * Setup the Toolbar + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_ADMIN_HELP'), 'support help_header'); + } } diff --git a/code/administrator/components/com_admin/src/View/Sysinfo/HtmlView.php b/code/administrator/components/com_admin/src/View/Sysinfo/HtmlView.php index 664bb154..741cc013 100644 --- a/code/administrator/components/com_admin/src/View/Sysinfo/HtmlView.php +++ b/code/administrator/components/com_admin/src/View/Sysinfo/HtmlView.php @@ -1,4 +1,5 @@ authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + // Access check. + if (!$this->getCurrentUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - /** @var SysinfoModel $model */ - $model = $this->getModel(); - $this->phpSettings = $model->getPhpSettings(); - $this->config = $model->getConfig(); - $this->info = $model->getInfo(); - $this->phpInfo = $model->getPHPInfo(); - $this->directory = $model->getDirectory(); + /** @var SysinfoModel $model */ + $model = $this->getModel(); + $this->phpSettings = $model->getPhpSettings(); + $this->config = $model->getConfig(); + $this->info = $model->getInfo(); + $this->phpInfo = $model->getPHPInfo(); + $this->directory = $model->getDirectory(); - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Setup the Toolbar - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - ToolbarHelper::title(Text::_('COM_ADMIN_SYSTEM_INFORMATION'), 'info-circle systeminfo'); - ToolbarHelper::link( - Route::_('index.php?option=com_admin&view=sysinfo&format=text&' . Session::getFormToken() . '=1'), - 'COM_ADMIN_DOWNLOAD_SYSTEM_INFORMATION_TEXT', - 'download' - ); - ToolbarHelper::link( - Route::_('index.php?option=com_admin&view=sysinfo&format=json&' . Session::getFormToken() . '=1'), - 'COM_ADMIN_DOWNLOAD_SYSTEM_INFORMATION_JSON', - 'download' - ); - ToolbarHelper::help('Site_System_Information'); - } + /** + * Setup the Toolbar + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_ADMIN_SYSTEM_INFORMATION'), 'info-circle systeminfo'); + ToolbarHelper::link( + Route::_('index.php?option=com_admin&view=sysinfo&format=text&' . Session::getFormToken() . '=1'), + 'COM_ADMIN_DOWNLOAD_SYSTEM_INFORMATION_TEXT', + 'download' + ); + ToolbarHelper::link( + Route::_('index.php?option=com_admin&view=sysinfo&format=json&' . Session::getFormToken() . '=1'), + 'COM_ADMIN_DOWNLOAD_SYSTEM_INFORMATION_JSON', + 'download' + ); + ToolbarHelper::help('Site_System_Information'); + } } diff --git a/code/administrator/components/com_admin/src/View/Sysinfo/JsonView.php b/code/administrator/components/com_admin/src/View/Sysinfo/JsonView.php index 116a0875..c11a1cdf 100644 --- a/code/administrator/components/com_admin/src/View/Sysinfo/JsonView.php +++ b/code/administrator/components/com_admin/src/View/Sysinfo/JsonView.php @@ -1,4 +1,5 @@ authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.5 + * + * @throws Exception + */ + public function display($tpl = null): void + { + // Access check. + if (!Factory::getUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - header('MIME-Version: 1.0'); - header('Content-Disposition: attachment; filename="systeminfo-' . date('c') . '.json"'); - header('Content-Transfer-Encoding: binary'); + header('MIME-Version: 1.0'); + header('Content-Disposition: attachment; filename="systeminfo-' . date('c') . '.json"'); + header('Content-Transfer-Encoding: binary'); - $data = $this->getLayoutData(); + $data = $this->getLayoutData(); - echo json_encode($data, JSON_PRETTY_PRINT); + echo json_encode($data, JSON_PRETTY_PRINT); - Factory::getApplication()->close(); - } + Factory::getApplication()->close(); + } - /** - * Get the data for the view - * - * @return array - * - * @since 3.5 - */ - protected function getLayoutData(): array - { - /** @var SysinfoModel $model */ - $model = $this->getModel(); + /** + * Get the data for the view + * + * @return array + * + * @since 3.5 + */ + protected function getLayoutData(): array + { + /** @var SysinfoModel $model */ + $model = $this->getModel(); - return [ - 'info' => $model->getSafeData('info'), - 'phpSettings' => $model->getSafeData('phpSettings'), - 'config' => $model->getSafeData('config'), - 'directories' => $model->getSafeData('directory', true), - 'phpInfo' => $model->getSafeData('phpInfoArray'), - 'extensions' => $model->getSafeData('extensions') - ]; - } + return [ + 'info' => $model->getSafeData('info'), + 'phpSettings' => $model->getSafeData('phpSettings'), + 'config' => $model->getSafeData('config'), + 'directories' => $model->getSafeData('directory', true), + 'phpInfo' => $model->getSafeData('phpInfoArray'), + 'extensions' => $model->getSafeData('extensions') + ]; + } } diff --git a/code/administrator/components/com_admin/src/View/Sysinfo/TextView.php b/code/administrator/components/com_admin/src/View/Sysinfo/TextView.php index 6da1078e..7c1ef1b4 100644 --- a/code/administrator/components/com_admin/src/View/Sysinfo/TextView.php +++ b/code/administrator/components/com_admin/src/View/Sysinfo/TextView.php @@ -1,4 +1,5 @@ authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - header('Content-Type: text/plain; charset=utf-8'); - header('Content-Description: File Transfer'); - header('Content-Disposition: attachment; filename="systeminfo-' . date('c') . '.txt"'); - header('Cache-Control: must-revalidate'); - - $data = $this->getLayoutData(); - - $lines = []; - - foreach ($data as $sectionName => $section) - { - $customRenderingMethod = 'render' . ucfirst($sectionName); - - if (method_exists($this, $customRenderingMethod)) - { - $lines[] = $this->$customRenderingMethod($section['title'], $section['data']); - } - else - { - $lines[] = $this->renderSection($section['title'], $section['data']); - } - } - - echo str_replace(JPATH_ROOT, 'xxxxxx', implode("\n\n", $lines)); - - Factory::getApplication()->close(); - } - - /** - * Get the data for the view - * - * @return array - * - * @since 3.5 - */ - protected function getLayoutData(): array - { - /** @var SysinfoModel $model */ - $model = $this->getModel(); - - return [ - 'info' => [ - 'title' => Text::_('COM_ADMIN_SYSTEM_INFORMATION', true), - 'data' => $model->getSafeData('info') - ], - 'phpSettings' => [ - 'title' => Text::_('COM_ADMIN_PHP_SETTINGS', true), - 'data' => $model->getSafeData('phpSettings') - ], - 'config' => [ - 'title' => Text::_('COM_ADMIN_CONFIGURATION_FILE', true), - 'data' => $model->getSafeData('config') - ], - 'directories' => [ - 'title' => Text::_('COM_ADMIN_DIRECTORY_PERMISSIONS', true), - 'data' => $model->getSafeData('directory', true) - ], - 'phpInfo' => [ - 'title' => Text::_('COM_ADMIN_PHP_INFORMATION', true), - 'data' => $model->getSafeData('phpInfoArray') - ], - 'extensions' => [ - 'title' => Text::_('COM_ADMIN_EXTENSIONS', true), - 'data' => $model->getSafeData('extensions') - ] - ]; - } - - /** - * Render a section - * - * @param string $sectionName Name of the section to render - * @param array $sectionData Data of the section to render - * @param integer $level Depth level for indentation - * - * @return string - * - * @since 3.5 - */ - protected function renderSection(string $sectionName, array $sectionData, int $level = 0): string - { - $lines = []; - - $margin = ($level > 0) ? str_repeat("\t", $level) : null; - - $lines[] = $margin . '============='; - $lines[] = $margin . $sectionName; - $lines[] = $margin . '============='; - $level++; - - foreach ($sectionData as $name => $value) - { - if (\is_array($value)) - { - if ($name == 'Directive') - { - continue; - } - - $lines[] = ''; - $lines[] = $this->renderSection($name, $value, $level); - } - else - { - if (\is_bool($value)) - { - $value = $value ? 'true' : 'false'; - } - - if (\is_int($name) && ($name == 0 || $name == 1)) - { - $name = ($name == 0 ? 'Local Value' : 'Master Value'); - } - - $lines[] = $margin . $name . ': ' . $value; - } - } - - return implode("\n", $lines); - } - - /** - * Specific rendering for directories - * - * @param string $sectionName Name of the section - * @param array $sectionData Directories information - * @param integer $level Starting level - * - * @return string - * - * @since 3.5 - */ - protected function renderDirectories(string $sectionName, array $sectionData, int $level = -1): string - { - foreach ($sectionData as $directory => $data) - { - $sectionData[$directory] = $data['writable'] ? ' writable' : ' NOT writable'; - } - - return $this->renderSection($sectionName, $sectionData, $level); - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + * + * @since 3.5 + * + * @throws Exception + */ + public function display($tpl = null): void + { + // Access check. + if (!Factory::getUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + header('Content-Type: text/plain; charset=utf-8'); + header('Content-Description: File Transfer'); + header('Content-Disposition: attachment; filename="systeminfo-' . date('c') . '.txt"'); + header('Cache-Control: must-revalidate'); + + $data = $this->getLayoutData(); + + $lines = []; + + foreach ($data as $sectionName => $section) { + $customRenderingMethod = 'render' . ucfirst($sectionName); + + if (method_exists($this, $customRenderingMethod)) { + $lines[] = $this->$customRenderingMethod($section['title'], $section['data']); + } else { + $lines[] = $this->renderSection($section['title'], $section['data']); + } + } + + echo str_replace(JPATH_ROOT, 'xxxxxx', implode("\n\n", $lines)); + + Factory::getApplication()->close(); + } + + /** + * Get the data for the view + * + * @return array + * + * @since 3.5 + */ + protected function getLayoutData(): array + { + /** @var SysinfoModel $model */ + $model = $this->getModel(); + + return [ + 'info' => [ + 'title' => Text::_('COM_ADMIN_SYSTEM_INFORMATION', true), + 'data' => $model->getSafeData('info') + ], + 'phpSettings' => [ + 'title' => Text::_('COM_ADMIN_PHP_SETTINGS', true), + 'data' => $model->getSafeData('phpSettings') + ], + 'config' => [ + 'title' => Text::_('COM_ADMIN_CONFIGURATION_FILE', true), + 'data' => $model->getSafeData('config') + ], + 'directories' => [ + 'title' => Text::_('COM_ADMIN_DIRECTORY_PERMISSIONS', true), + 'data' => $model->getSafeData('directory', true) + ], + 'phpInfo' => [ + 'title' => Text::_('COM_ADMIN_PHP_INFORMATION', true), + 'data' => $model->getSafeData('phpInfoArray') + ], + 'extensions' => [ + 'title' => Text::_('COM_ADMIN_EXTENSIONS', true), + 'data' => $model->getSafeData('extensions') + ] + ]; + } + + /** + * Render a section + * + * @param string $sectionName Name of the section to render + * @param array $sectionData Data of the section to render + * @param integer $level Depth level for indentation + * + * @return string + * + * @since 3.5 + */ + protected function renderSection(string $sectionName, array $sectionData, int $level = 0): string + { + $lines = []; + + $margin = ($level > 0) ? str_repeat("\t", $level) : null; + + $lines[] = $margin . '============='; + $lines[] = $margin . $sectionName; + $lines[] = $margin . '============='; + $level++; + + foreach ($sectionData as $name => $value) { + if (\is_array($value)) { + if ($name == 'Directive') { + continue; + } + + $lines[] = ''; + $lines[] = $this->renderSection($name, $value, $level); + } else { + if (\is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + + if (\is_int($name) && ($name == 0 || $name == 1)) { + $name = ($name == 0 ? 'Local Value' : 'Master Value'); + } + + $lines[] = $margin . $name . ': ' . $value; + } + } + + return implode("\n", $lines); + } + + /** + * Specific rendering for directories + * + * @param string $sectionName Name of the section + * @param array $sectionData Directories information + * @param integer $level Starting level + * + * @return string + * + * @since 3.5 + */ + protected function renderDirectories(string $sectionName, array $sectionData, int $level = -1): string + { + foreach ($sectionData as $directory => $data) { + $sectionData[$directory] = $data['writable'] ? ' writable' : ' NOT writable'; + } + + return $this->renderSection($sectionName, $sectionData, $level); + } } diff --git a/code/administrator/components/com_admin/tmpl/help/default.php b/code/administrator/components/com_admin/tmpl/help/default.php index 1cdac797..69480f7d 100644 --- a/code/administrator/components/com_admin/tmpl/help/default.php +++ b/code/administrator/components/com_admin/tmpl/help/default.php @@ -1,4 +1,5 @@
-
- -
- -
-
- +
+ +
+ +
+
+
diff --git a/code/administrator/components/com_admin/tmpl/help/langforum.php b/code/administrator/components/com_admin/tmpl/help/langforum.php index 3e4b0782..979c7265 100644 --- a/code/administrator/components/com_admin/tmpl/help/langforum.php +++ b/code/administrator/components/com_admin/tmpl/help/langforum.php @@ -1,4 +1,5 @@
- 'site', 'recall' => true, 'breakpoint' => 768]); ?> + 'site', 'recall' => true, 'breakpoint' => 768]); ?> - - loadTemplate('system'); ?> - + + loadTemplate('system'); ?> + - - loadTemplate('phpsettings'); ?> - + + loadTemplate('phpsettings'); ?> + - - loadTemplate('config'); ?> - + + loadTemplate('config'); ?> + - - loadTemplate('directory'); ?> - + + loadTemplate('directory'); ?> + - - loadTemplate('phpinfo'); ?> - + + loadTemplate('phpinfo'); ?> + - +
diff --git a/code/administrator/components/com_admin/tmpl/sysinfo/default_config.php b/code/administrator/components/com_admin/tmpl/sysinfo/default_config.php index 49e3f14b..9cc7f0d5 100644 --- a/code/administrator/components/com_admin/tmpl/sysinfo/default_config.php +++ b/code/administrator/components/com_admin/tmpl/sysinfo/default_config.php @@ -1,4 +1,5 @@
- - - - - - - - - - config as $key => $value) : ?> - - - - - - -
- -
- - - -
- - - -
+ + + + + + + + + + config as $key => $value) : ?> + + + + + + +
+ +
+ + + +
+ + + +
diff --git a/code/administrator/components/com_admin/tmpl/sysinfo/default_directory.php b/code/administrator/components/com_admin/tmpl/sysinfo/default_directory.php index a18fd59e..135c16ea 100644 --- a/code/administrator/components/com_admin/tmpl/sysinfo/default_directory.php +++ b/code/administrator/components/com_admin/tmpl/sysinfo/default_directory.php @@ -1,4 +1,5 @@
- - - - - - - - - - directory as $dir => $info) : ?> - - - - - - -
- -
- - - -
- - - -
+ + + + + + + + + + directory as $dir => $info) : ?> + + + + + + +
+ +
+ + + +
+ + + +
diff --git a/code/administrator/components/com_admin/tmpl/sysinfo/default_phpinfo.php b/code/administrator/components/com_admin/tmpl/sysinfo/default_phpinfo.php index b4c8a68f..18e0fb1f 100644 --- a/code/administrator/components/com_admin/tmpl/sysinfo/default_phpinfo.php +++ b/code/administrator/components/com_admin/tmpl/sysinfo/default_phpinfo.php @@ -1,4 +1,5 @@
- phpInfo; ?> + phpInfo; ?>
diff --git a/code/administrator/components/com_admin/tmpl/sysinfo/default_phpsettings.php b/code/administrator/components/com_admin/tmpl/sysinfo/default_phpsettings.php index 3ea85c31..078555d2 100644 --- a/code/administrator/components/com_admin/tmpl/sysinfo/default_phpsettings.php +++ b/code/administrator/components/com_admin/tmpl/sysinfo/default_phpsettings.php @@ -1,4 +1,5 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - -
- - - phpSettings['upload_max_filesize']); ?> -
- - - phpSettings['post_max_size']); ?> -
- - - phpSettings['memory_limit']); ?> -
- - - phpSettings['open_basedir']); ?> -
- - - phpSettings['display_errors']); ?> -
- - - phpSettings['short_open_tag']); ?> -
- - - phpSettings['file_uploads']); ?> -
- - - phpSettings['output_buffering']); ?> -
- - - phpSettings['session.save_path']); ?> -
- - - phpSettings['session.auto_start']; ?> -
- - - phpSettings['xml']); ?> -
- - - phpSettings['zlib']); ?> -
- - - phpSettings['zip']); ?> -
- - - phpSettings['disable_functions']); ?> -
- - - phpSettings['fileinfo']); ?> -
- - - phpSettings['mbstring']); ?> -
- - - phpSettings['gd']); ?> -
- - - phpSettings['iconv']); ?> -
- - - phpSettings['intl']); ?> -
- - - phpSettings['max_input_vars']; ?> -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + phpSettings['upload_max_filesize']); ?> +
+ + + phpSettings['post_max_size']); ?> +
+ + + phpSettings['memory_limit']); ?> +
+ + + phpSettings['open_basedir']); ?> +
+ + + phpSettings['display_errors']); ?> +
+ + + phpSettings['short_open_tag']); ?> +
+ + + phpSettings['file_uploads']); ?> +
+ + + phpSettings['output_buffering']); ?> +
+ + + phpSettings['session.save_path']); ?> +
+ + + phpSettings['session.auto_start']; ?> +
+ + + phpSettings['xml']); ?> +
+ + + phpSettings['zlib']); ?> +
+ + + phpSettings['zip']); ?> +
+ + + phpSettings['disable_functions']); ?> +
+ + + phpSettings['fileinfo']); ?> +
+ + + phpSettings['mbstring']); ?> +
+ + + phpSettings['gd']); ?> +
+ + + phpSettings['iconv']); ?> +
+ + + phpSettings['intl']); ?> +
+ + + phpSettings['max_input_vars']; ?> +
diff --git a/code/administrator/components/com_admin/tmpl/sysinfo/default_system.php b/code/administrator/components/com_admin/tmpl/sysinfo/default_system.php index 97ddce3a..0891b5c2 100644 --- a/code/administrator/components/com_admin/tmpl/sysinfo/default_system.php +++ b/code/administrator/components/com_admin/tmpl/sysinfo/default_system.php @@ -1,4 +1,5 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - -
- - - info['php']; ?> -
- - - info['dbserver']; ?> -
- - - info['dbversion']; ?> -
- - - info['dbcollation']; ?> -
- - - info['dbconnectioncollation']; ?> -
- - - info['dbconnectionencryption'] ?: Text::_('JNONE'); ?> -
- - - info['dbconnencryptsupported'] ? Text::_('JYES') : Text::_('JNO'); ?> -
- - - info['phpversion']; ?> -
- - - info['server']); ?> -
- - - info['sapi_name']; ?> -
- - - info['version']; ?> -
- - - info['useragent'], ENT_COMPAT, 'UTF-8'); ?> -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + info['php']; ?> +
+ + + info['dbserver']; ?> +
+ + + info['dbversion']; ?> +
+ + + info['dbcollation']; ?> +
+ + + info['dbconnectioncollation']; ?> +
+ + + info['dbconnectionencryption'] ?: Text::_('JNONE'); ?> +
+ + + info['dbconnencryptsupported'] ? Text::_('JYES') : Text::_('JNO'); ?> +
+ + + info['phpversion']; ?> +
+ + + info['server']); ?> +
+ + + info['sapi_name']; ?> +
+ + + info['version']; ?> +
+ + + info['useragent'], ENT_COMPAT, 'UTF-8'); ?> +
diff --git a/code/administrator/components/com_ajax/ajax.php b/code/administrator/components/com_ajax/ajax.php index 40e35cd1..37e61ad9 100644 --- a/code/administrator/components/com_ajax/ajax.php +++ b/code/administrator/components/com_ajax/ajax.php @@ -1,4 +1,5 @@ com_ajax Joomla! Project - August 2013 + 2013-08 (C) 2013 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_associations/associations.xml b/code/administrator/components/com_associations/associations.xml index 89fe2905..40e48f3e 100644 --- a/code/administrator/components/com_associations/associations.xml +++ b/code/administrator/components/com_associations/associations.xml @@ -2,7 +2,7 @@ com_associations Joomla! Project - January 2017 + 2017-01 (C) 2017 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_associations/layouts/joomla/searchtools/default.php b/code/administrator/components/com_associations/layouts/joomla/searchtools/default.php index c360f56e..91ee3448 100644 --- a/code/administrator/components/com_associations/layouts/joomla/searchtools/default.php +++ b/code/administrator/components/com_associations/layouts/joomla/searchtools/default.php @@ -1,4 +1,5 @@ filterForm) && !empty($data['view']->filterForm)) -{ - // Checks if a selector (e.g. client_id) exists. - if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) - { - $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; - - // Checks if a selector should be shown in the current layout. - if (isset($data['view']->layout)) - { - $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; - } - - // Unset the selector field from active filters group. - unset($data['view']->activeFilters[$selectorFieldName]); - } - - // Checks if the filters button should exist. - $filters = $data['view']->filterForm->getGroup('filter'); - $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; - - // Checks if it should show the be hidden. - $hideActiveFilters = empty($data['view']->activeFilters); - - // Check if the no results message should appear. - if (isset($data['view']->total) && (int) $data['view']->total === 0) - { - $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); - if (!empty($noResults)) - { - $noResultsText = Text::_($noResults); - } - } +if (isset($data['view']->filterForm) && !empty($data['view']->filterForm)) { + // Checks if a selector (e.g. client_id) exists. + if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) { + $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; + + // Checks if a selector should be shown in the current layout. + if (isset($data['view']->layout)) { + $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; + } + + // Unset the selector field from active filters group. + unset($data['view']->activeFilters[$selectorFieldName]); + } + + // Checks if the filters button should exist. + $filters = $data['view']->filterForm->getGroup('filter'); + $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; + + // Checks if it should show the be hidden. + $hideActiveFilters = empty($data['view']->activeFilters); + + // Check if the no results message should appear. + if (isset($data['view']->total) && (int) $data['view']->total === 0) { + $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); + if (!empty($noResults)) { + $noResultsText = Text::_($noResults); + } + } } // Set some basic options. $customOptions = array( - 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, - 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, - 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), - 'searchFieldSelector' => '#filter_search', - 'selectorFieldName' => $selectorFieldName, - 'showSelector' => $showSelector, - 'orderFieldSelector' => '#list_fullordering', - 'showNoResults' => !empty($noResultsText), - 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', - 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', + 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, + 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, + 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), + 'searchFieldSelector' => '#filter_search', + 'selectorFieldName' => $selectorFieldName, + 'showSelector' => $showSelector, + 'orderFieldSelector' => '#list_fullordering', + 'showNoResults' => !empty($noResultsText), + 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', + 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', ); // Merge custom options in the options array. @@ -85,44 +81,44 @@ HTMLHelper::_('searchtools.form', $data['options']['formSelector'], $data['options']); ?> - sublayout('noitems', $data); ?> + sublayout('noitems', $data); ?> diff --git a/code/administrator/components/com_associations/services/provider.php b/code/administrator/components/com_associations/services/provider.php index 4ad47fec..071fec00 100644 --- a/code/administrator/components/com_associations/services/provider.php +++ b/code/administrator/components/com_associations/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Associations')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Associations')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Associations')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Associations')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_associations/src/Controller/AssociationController.php b/code/administrator/components/com_associations/src/Controller/AssociationController.php index e3e7f128..5bf36d27 100644 --- a/code/administrator/components/com_associations/src/Controller/AssociationController.php +++ b/code/administrator/components/com_associations/src/Controller/AssociationController.php @@ -1,4 +1,5 @@ input->get('itemtype', '', 'string'), 2); - - $id = $this->input->get('id', 0, 'int'); - - // Check if reference item can be edited. - if (!AssociationsHelper::allowEdit($extensionName, $typeName, $id)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); - $this->setRedirect(Route::_('index.php?option=com_associations&view=associations', false)); - - return false; - } - - return parent::display(); - } - - /** - * Method for canceling the edit action - * - * @param string $key The name of the primary key of the URL variable. - * - * @return void - * - * @since 3.7.0 - */ - public function cancel($key = null) - { - $this->checkToken(); - - list($extensionName, $typeName) = explode('.', $this->input->get('itemtype', '', 'string'), 2); - - // Only check in, if component item type allows to check out. - if (AssociationsHelper::typeSupportsCheckout($extensionName, $typeName)) - { - $ids = array(); - $targetId = $this->input->get('target-id', '', 'string'); - - if ($targetId !== '') - { - $ids = array_unique(explode(',', $targetId)); - } - - $ids[] = $this->input->get('id', 0, 'int'); - - foreach ($ids as $key => $id) - { - AssociationsHelper::getItem($extensionName, $typeName, $id)->checkIn(); - } - } - - $this->setRedirect(Route::_('index.php?option=com_associations&view=associations', false)); - } + /** + * Method to edit an existing record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * (sometimes required to avoid router collisions). + * + * @return FormController|boolean True if access level check and checkout passes, false otherwise. + * + * @since 3.7.0 + */ + public function edit($key = null, $urlVar = null) + { + list($extensionName, $typeName) = explode('.', $this->input->get('itemtype', '', 'string'), 2); + + $id = $this->input->get('id', 0, 'int'); + + // Check if reference item can be edited. + if (!AssociationsHelper::allowEdit($extensionName, $typeName, $id)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_associations&view=associations', false)); + + return false; + } + + return parent::display(); + } + + /** + * Method for canceling the edit action + * + * @param string $key The name of the primary key of the URL variable. + * + * @return void + * + * @since 3.7.0 + */ + public function cancel($key = null) + { + $this->checkToken(); + + list($extensionName, $typeName) = explode('.', $this->input->get('itemtype', '', 'string'), 2); + + // Only check in, if component item type allows to check out. + if (AssociationsHelper::typeSupportsCheckout($extensionName, $typeName)) { + $ids = array(); + $targetId = $this->input->get('target-id', '', 'string'); + + if ($targetId !== '') { + $ids = array_unique(explode(',', $targetId)); + } + + $ids[] = $this->input->get('id', 0, 'int'); + + foreach ($ids as $key => $id) { + AssociationsHelper::getItem($extensionName, $typeName, $id)->checkIn(); + } + } + + $this->setRedirect(Route::_('index.php?option=com_associations&view=associations', false)); + } } diff --git a/code/administrator/components/com_associations/src/Controller/AssociationsController.php b/code/administrator/components/com_associations/src/Controller/AssociationsController.php index 22bf8c7f..948adcfb 100644 --- a/code/administrator/components/com_associations/src/Controller/AssociationsController.php +++ b/code/administrator/components/com_associations/src/Controller/AssociationsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to purge the associations table. - * - * @return void - * - * @since 3.7.0 - */ - public function purge() - { - $this->checkToken(); - - $this->getModel('associations')->purge(); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); - } - - /** - * Method to delete the orphans from the associations table. - * - * @return void - * - * @since 3.7.0 - */ - public function clean() - { - $this->checkToken(); - - $this->getModel('associations')->clean(); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); - } - - /** - * Method to check in an item from the association item overview. - * - * @return void - * - * @since 3.7.1 - */ - public function checkin() - { - // Set the redirect so we can just stop processing when we find a condition we can't process - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); - - // Figure out if the item supports checking and check it in - list($extensionName, $typeName) = explode('.', $this->input->get('itemtype')); - - $extension = AssociationsHelper::getSupportedExtension($extensionName); - $types = $extension->get('types'); - - if (!\array_key_exists($typeName, $types)) - { - return; - } - - if (AssociationsHelper::typeSupportsCheckout($extensionName, $typeName) === false) - { - // How on earth we came to that point, eject internet - return; - } - - $cid = (array) $this->input->get('cid', array(), 'int'); - - if (empty($cid)) - { - // Seems we don't have an id to work with. - return; - } - - // We know the first element is the one we need because we don't allow multi selection of rows - $id = $cid[0]; - - if ($id === 0) - { - // Seems we don't have an id to work with. - return; - } - - if (AssociationsHelper::canCheckinItem($extensionName, $typeName, $id) === true) - { - $item = AssociationsHelper::getItem($extensionName, $typeName, $id); - - $item->checkIn($id); - - return; - } - - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list), - Text::_('COM_ASSOCIATIONS_YOU_ARE_NOT_ALLOWED_TO_CHECKIN_THIS_ITEM') - ); - } + /** + * The URL view list variable. + * + * @var string + * + * @since 3.7.0 + */ + protected $view_list = 'associations'; + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel|boolean + * + * @since 3.7.0 + */ + public function getModel($name = 'Associations', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to purge the associations table. + * + * @return void + * + * @since 3.7.0 + */ + public function purge() + { + $this->checkToken(); + + $this->getModel('associations')->purge(); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); + } + + /** + * Method to delete the orphans from the associations table. + * + * @return void + * + * @since 3.7.0 + */ + public function clean() + { + $this->checkToken(); + + $this->getModel('associations')->clean(); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); + } + + /** + * Method to check in an item from the association item overview. + * + * @return void + * + * @since 3.7.1 + */ + public function checkin() + { + // Set the redirect so we can just stop processing when we find a condition we can't process + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); + + // Figure out if the item supports checking and check it in + list($extensionName, $typeName) = explode('.', $this->input->get('itemtype')); + + $extension = AssociationsHelper::getSupportedExtension($extensionName); + $types = $extension->get('types'); + + if (!\array_key_exists($typeName, $types)) { + return; + } + + if (AssociationsHelper::typeSupportsCheckout($extensionName, $typeName) === false) { + // How on earth we came to that point, eject internet + return; + } + + $cid = (array) $this->input->get('cid', array(), 'int'); + + if (empty($cid)) { + // Seems we don't have an id to work with. + return; + } + + // We know the first element is the one we need because we don't allow multi selection of rows + $id = $cid[0]; + + if ($id === 0) { + // Seems we don't have an id to work with. + return; + } + + if (AssociationsHelper::canCheckinItem($extensionName, $typeName, $id) === true) { + $item = AssociationsHelper::getItem($extensionName, $typeName, $id); + + $item->checkIn($id); + + return; + } + + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list), + Text::_('COM_ASSOCIATIONS_YOU_ARE_NOT_ALLOWED_TO_CHECKIN_THIS_ITEM') + ); + } } diff --git a/code/administrator/components/com_associations/src/Controller/DisplayController.php b/code/administrator/components/com_associations/src/Controller/DisplayController.php index e2a48320..25ddad9c 100644 --- a/code/administrator/components/com_associations/src/Controller/DisplayController.php +++ b/code/administrator/components/com_associations/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('itemtype', '', 'string'); - - if ($itemType !== '') - { - list($extensionName, $typeName) = explode('.', $itemType); - - if (!AssociationsHelper::hasSupport($extensionName)) - { - throw new \Exception( - Text::sprintf('COM_ASSOCIATIONS_COMPONENT_NOT_SUPPORTED', $this->app->getLanguage()->_($extensionName)), - 404 - ); - } - - if (!$this->app->getIdentity()->authorise('core.manage', $extensionName)) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } - } + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + * + * @throws \Exception|NotAllowed + */ + protected function checkAccess() + { + parent::checkAccess(); + + // Check if user has permission to access the component item type. + $itemType = $this->input->get('itemtype', '', 'string'); + + if ($itemType !== '') { + list($extensionName, $typeName) = explode('.', $itemType); + + if (!AssociationsHelper::hasSupport($extensionName)) { + throw new \Exception( + Text::sprintf('COM_ASSOCIATIONS_COMPONENT_NOT_SUPPORTED', $this->app->getLanguage()->_($extensionName)), + 404 + ); + } + + if (!$this->app->getIdentity()->authorise('core.manage', $extensionName)) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } + } } diff --git a/code/administrator/components/com_associations/src/Field/ItemlanguageField.php b/code/administrator/components/com_associations/src/Field/ItemlanguageField.php index 0d63fe59..79230d58 100644 --- a/code/administrator/components/com_associations/src/Field/ItemlanguageField.php +++ b/code/administrator/components/com_associations/src/Field/ItemlanguageField.php @@ -1,4 +1,5 @@ input; - - list($extensionName, $typeName) = explode('.', $input->get('itemtype', '', 'string'), 2); - - // Get the extension specific helper method - $helper = AssociationsHelper::getExtensionHelper($extensionName); - - $languageField = $helper->getTypeFieldName($typeName, 'language'); - $referenceId = $input->get('id', 0, 'int'); - $reference = ArrayHelper::fromObject(AssociationsHelper::getItem($extensionName, $typeName, $referenceId)); - $referenceLang = $reference[$languageField]; - - // Get item associations given ID and item type - $associations = AssociationsHelper::getAssociationList($extensionName, $typeName, $referenceId); - - // Check if user can create items in this component item type. - $canCreate = AssociationsHelper::allowAdd($extensionName, $typeName); - - // Gets existing languages. - $existingLanguages = LanguageHelper::getContentLanguages(array(0, 1), false); - - $options = array(); - - // Each option has the format "|", example: "en-GB|1" - foreach ($existingLanguages as $langCode => $language) - { - // If language code is equal to reference language we don't need it. - if ($language->lang_code == $referenceLang) - { - continue; - } - - $options[$langCode] = new \stdClass; - $options[$langCode]->text = $language->title; - - // If association exists in this language. - if (isset($associations[$language->lang_code])) - { - $itemId = (int) $associations[$language->lang_code]['id']; - $options[$langCode]->value = $language->lang_code . ':' . $itemId . ':edit'; - - // Check if user does have permission to edit the associated item. - $canEdit = AssociationsHelper::allowEdit($extensionName, $typeName, $itemId); - - // Check if item can be checked out - $canCheckout = AssociationsHelper::canCheckinItem($extensionName, $typeName, $itemId); - - // Disable language if user is not allowed to edit the item associated to it. - $options[$langCode]->disable = !($canEdit && $canCheckout); - } - else - { - // New item, id = 0 and disabled if user is not allowed to create new items. - $options[$langCode]->value = $language->lang_code . ':0:add'; - - // Disable language if user is not allowed to create items. - $options[$langCode]->disable = !$canCreate; - } - } - - return array_merge(parent::getOptions(), $options); - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'Itemlanguage'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $input = Factory::getApplication()->input; + + list($extensionName, $typeName) = explode('.', $input->get('itemtype', '', 'string'), 2); + + // Get the extension specific helper method + $helper = AssociationsHelper::getExtensionHelper($extensionName); + + $languageField = $helper->getTypeFieldName($typeName, 'language'); + $referenceId = $input->get('id', 0, 'int'); + $reference = ArrayHelper::fromObject(AssociationsHelper::getItem($extensionName, $typeName, $referenceId)); + $referenceLang = $reference[$languageField]; + + // Get item associations given ID and item type + $associations = AssociationsHelper::getAssociationList($extensionName, $typeName, $referenceId); + + // Check if user can create items in this component item type. + $canCreate = AssociationsHelper::allowAdd($extensionName, $typeName); + + // Gets existing languages. + $existingLanguages = LanguageHelper::getContentLanguages(array(0, 1), false); + + $options = array(); + + // Each option has the format "|", example: "en-GB|1" + foreach ($existingLanguages as $langCode => $language) { + // If language code is equal to reference language we don't need it. + if ($language->lang_code == $referenceLang) { + continue; + } + + $options[$langCode] = new \stdClass(); + $options[$langCode]->text = $language->title; + + // If association exists in this language. + if (isset($associations[$language->lang_code])) { + $itemId = (int) $associations[$language->lang_code]['id']; + $options[$langCode]->value = $language->lang_code . ':' . $itemId . ':edit'; + + // Check if user does have permission to edit the associated item. + $canEdit = AssociationsHelper::allowEdit($extensionName, $typeName, $itemId); + + // Check if item can be checked out + $canCheckout = AssociationsHelper::canCheckinItem($extensionName, $typeName, $itemId); + + // Disable language if user is not allowed to edit the item associated to it. + $options[$langCode]->disable = !($canEdit && $canCheckout); + } else { + // New item, id = 0 and disabled if user is not allowed to create new items. + $options[$langCode]->value = $language->lang_code . ':0:add'; + + // Disable language if user is not allowed to create items. + $options[$langCode]->disable = !$canCreate; + } + } + + return array_merge(parent::getOptions(), $options); + } } diff --git a/code/administrator/components/com_associations/src/Field/ItemtypeField.php b/code/administrator/components/com_associations/src/Field/ItemtypeField.php index c31c081e..601e0fdd 100644 --- a/code/administrator/components/com_associations/src/Field/ItemtypeField.php +++ b/code/administrator/components/com_associations/src/Field/ItemtypeField.php @@ -1,4 +1,5 @@ get('associationssupport') === true) - { - foreach ($extension->get('types') as $type) - { - $context = $extension->get('component') . '.' . $type->get('name'); - $options[$extension->get('title')][] = HTMLHelper::_('select.option', $context, $type->get('title')); - } - } - } + foreach ($extensions as $extension) { + if ($extension->get('associationssupport') === true) { + foreach ($extension->get('types') as $type) { + $context = $extension->get('component') . '.' . $type->get('name'); + $options[$extension->get('title')][] = HTMLHelper::_('select.option', $context, $type->get('title')); + } + } + } - // Sort by alpha order. - uksort($options, 'strnatcmp'); + // Sort by alpha order. + uksort($options, 'strnatcmp'); - // Add options to parent array. - return array_merge(parent::getGroups(), $options); - } + // Add options to parent array. + return array_merge(parent::getGroups(), $options); + } } diff --git a/code/administrator/components/com_associations/src/Field/Modal/AssociationField.php b/code/administrator/components/com_associations/src/Field/Modal/AssociationField.php index f05d8904..5238418d 100644 --- a/code/administrator/components/com_associations/src/Field/Modal/AssociationField.php +++ b/code/administrator/components/com_associations/src/Field/Modal/AssociationField.php @@ -1,4 +1,5 @@ value ?: ''; - - $doc = Factory::getApplication()->getDocument(); - $wa = $doc->getWebAssetManager(); - - $doc->addScriptOptions('admin_associations_modal', ['itemId' => $value]); - $wa->useScript('com_associations.admin-associations-modal'); - - // Setup variables for display. - $html = array(); - - $linkAssociations = 'index.php?option=com_associations&view=associations&layout=modal&tmpl=component' - . '&forcedItemType=' . Factory::getApplication()->input->get('itemtype', '', 'string') . '&function=jSelectAssociation_' . $this->id; - - $linkAssociations .= "&forcedLanguage=' + document.getElementById('target-association').getAttribute('data-language') + '"; - - $urlSelect = $linkAssociations . '&' . Session::getFormToken() . '=1'; - - // Select custom association button - $html[] = '' - . ' ' - . '' - . ''; - - // Clear association button - $html[] = '' - . ' ' . Text::_('JCLEAR') - . ''; - - $html[] = ''; - - // Select custom association modal - $html[] = HTMLHelper::_( - 'bootstrap.renderModal', - 'associationSelect' . $this->id . 'Modal', - array( - 'title' => Text::_('COM_ASSOCIATIONS_SELECT_TARGET'), - 'backdrop' => 'static', - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - - return implode("\n", $html); - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'Modal_Association'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 3.7.0 + */ + protected function getInput() + { + // @todo USE Layouts here!!! + // The active item id field. + $value = (int) $this->value ?: ''; + + $doc = Factory::getApplication()->getDocument(); + $wa = $doc->getWebAssetManager(); + + $doc->addScriptOptions('admin_associations_modal', ['itemId' => $value]); + $wa->useScript('com_associations.admin-associations-modal'); + + // Setup variables for display. + $html = array(); + + $linkAssociations = 'index.php?option=com_associations&view=associations&layout=modal&tmpl=component' + . '&forcedItemType=' . Factory::getApplication()->input->get('itemtype', '', 'string') . '&function=jSelectAssociation_' . $this->id; + + $linkAssociations .= "&forcedLanguage=' + document.getElementById('target-association').getAttribute('data-language') + '"; + + $urlSelect = $linkAssociations . '&' . Session::getFormToken() . '=1'; + + // Select custom association button + $html[] = '' + . ' ' + . '' + . ''; + + // Clear association button + $html[] = '' + . ' ' . Text::_('JCLEAR') + . ''; + + $html[] = ''; + + // Select custom association modal + $html[] = HTMLHelper::_( + 'bootstrap.renderModal', + 'associationSelect' . $this->id . 'Modal', + array( + 'title' => Text::_('COM_ASSOCIATIONS_SELECT_TARGET'), + 'backdrop' => 'static', + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + + return implode("\n", $html); + } } diff --git a/code/administrator/components/com_associations/src/Helper/AssociationsHelper.php b/code/administrator/components/com_associations/src/Helper/AssociationsHelper.php index 5bf5b1df..2b5d7074 100644 --- a/code/administrator/components/com_associations/src/Helper/AssociationsHelper.php +++ b/code/administrator/components/com_associations/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getAssociationList($typeName, $itemId); - - } - - /** - * Get the the instance of the extension helper class - * - * @param string $extensionName The extension name with com_ - * - * @return \Joomla\CMS\Association\AssociationExtensionHelper|null - * - * @since 3.7.0 - */ - public static function getExtensionHelper($extensionName) - { - if (!self::hasSupport($extensionName)) - { - return null; - } - - $support = self::$extensionsSupport[$extensionName]; - - return $support->get('helper'); - } - - /** - * Get item information - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return \Joomla\CMS\Table\Table|null - * - * @since 3.7.0 - */ - public static function getItem($extensionName, $typeName, $itemId) - { - if (!self::hasSupport($extensionName)) - { - return null; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - return $helper->getItem($typeName, $itemId); - } - - /** - * Check if extension supports associations - * - * @param string $extensionName The extension name with com_ - * - * @return boolean - * - * @since 3.7.0 - */ - public static function hasSupport($extensionName) - { - if (\is_null(self::$extensionsSupport)) - { - self::getSupportedExtensions(); - } - - return \in_array($extensionName, self::$supportedExtensionsList); - } - - /** - * Loads the helper for the given class. - * - * @param string $extensionName The extension name with com_ - * - * @return AssociationExtensionInterface|null - * - * @since 4.0.0 - */ - private static function loadHelper($extensionName) - { - $component = Factory::getApplication()->bootComponent($extensionName); - - if ($component instanceof AssociationServiceInterface) - { - return $component->getAssociationsExtension(); - } - - // Check if associations helper exists - if (!file_exists(JPATH_ADMINISTRATOR . '/components/' . $extensionName . '/helpers/associations.php')) - { - return null; - } - - require_once JPATH_ADMINISTRATOR . '/components/' . $extensionName . '/helpers/associations.php'; - - $componentAssociationsHelperClassName = self::getExtensionHelperClassName($extensionName); - - if (!class_exists($componentAssociationsHelperClassName, false)) - { - return null; - } - - // Create an instance of the helper class - return new $componentAssociationsHelperClassName; - } - - /** - * Get the extension specific helper class name - * - * @param string $extensionName The extension name with com_ - * - * @return string - * - * @since 3.7.0 - */ - private static function getExtensionHelperClassName($extensionName) - { - $realName = self::getExtensionRealName($extensionName); - - return ucfirst($realName) . 'AssociationsHelper'; - } - - /** - * Get the real extension name. This means without com_ - * - * @param string $extensionName The extension name with com_ - * - * @return string - * - * @since 3.7.0 - */ - private static function getExtensionRealName($extensionName) - { - return strpos($extensionName, 'com_') === false ? $extensionName : substr($extensionName, 4); - } - - /** - * Get the associated language edit links Html. - * - * @param string $extensionName Extension Name - * @param string $typeName ItemType - * @param integer $itemId Item id. - * @param string $itemLanguage Item language code. - * @param boolean $addLink True for adding edit links. False for just text. - * @param boolean $assocLanguages True for showing non associated content languages. False only languages with associations. - * - * @return string The language HTML - * - * @since 3.7.0 - */ - public static function getAssociationHtmlList($extensionName, $typeName, $itemId, $itemLanguage, $addLink = true, $assocLanguages = true) - { - // Get the associations list for this item. - $items = self::getAssociationList($extensionName, $typeName, $itemId); - - $titleFieldName = self::getTypeFieldName($extensionName, $typeName, 'title'); - - // Get all content languages. - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - $content_languages = array_column($languages, 'lang_code'); - - // Display warning if Content Language is trashed or deleted - foreach ($items as $item) - { - if (!\in_array($item['language'], $content_languages)) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item['language']), 'warning'); - } - } - - $canEditReference = self::allowEdit($extensionName, $typeName, $itemId); - $canCreate = self::allowAdd($extensionName, $typeName); - - // Create associated items list. - foreach ($languages as $langCode => $language) - { - // Don't do for the reference language. - if ($langCode == $itemLanguage) - { - continue; - } - - // Don't show languages with associations, if we don't want to show them. - if ($assocLanguages && isset($items[$langCode])) - { - unset($items[$langCode]); - continue; - } - - // Don't show languages without associations, if we don't want to show them. - if (!$assocLanguages && !isset($items[$langCode])) - { - continue; - } - - // Get html parameters. - if (isset($items[$langCode])) - { - $title = $items[$langCode][$titleFieldName]; - $additional = ''; - - if (isset($items[$langCode]['catid'])) - { - $db = Factory::getDbo(); - - // Get the category name - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $items[$langCode]['catid'], ParameterType::INTEGER); - - $db->setQuery($query); - $categoryTitle = $db->loadResult(); - - $additional = '' . Text::sprintf('JCATEGORY_SPRINTF', $categoryTitle) . '
'; - } - elseif (isset($items[$langCode]['menutype'])) - { - $db = Factory::getDbo(); - - // Get the menutype name - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('menutype') . ' = :menutype') - ->bind(':menutype', $items[$langCode]['menutype']); - - $db->setQuery($query); - $menutypeTitle = $db->loadResult(); - - $additional = '' . Text::sprintf('COM_MENUS_MENU_SPRINTF', $menutypeTitle) . '
'; - } - - $labelClass = 'bg-secondary'; - $target = $langCode . ':' . $items[$langCode]['id'] . ':edit'; - $allow = $canEditReference - && self::allowEdit($extensionName, $typeName, $items[$langCode]['id']) - && self::canCheckinItem($extensionName, $typeName, $items[$langCode]['id']); - - $additional .= $addLink && $allow ? Text::_('COM_ASSOCIATIONS_EDIT_ASSOCIATION') : ''; - } - else - { - $items[$langCode] = array(); - - $title = Text::_('COM_ASSOCIATIONS_NO_ASSOCIATION'); - $additional = $addLink ? Text::_('COM_ASSOCIATIONS_ADD_NEW_ASSOCIATION') : ''; - $labelClass = 'bg-warning text-dark'; - $target = $langCode . ':0:add'; - $allow = $canCreate; - } - - // Generate item Html. - $options = array( - 'option' => 'com_associations', - 'view' => 'association', - 'layout' => 'edit', - 'itemtype' => $extensionName . '.' . $typeName, - 'task' => 'association.edit', - 'id' => $itemId, - 'target' => $target, - ); - - $url = Route::_('index.php?' . http_build_query($options)); - $url = $allow && $addLink ? $url : ''; - $text = $language->lang_code; - - $tooltip = '' . htmlspecialchars($language->title, ENT_QUOTES, 'UTF-8') . '
' - . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '

' . $additional; - $classes = 'badge ' . $labelClass; - - $items[$langCode]['link'] = '' . $text . '' - . '
' . $tooltip . '
'; - } - - return LayoutHelper::render('joomla.content.associations', $items); - } - - /** - * Get all extensions with associations support. - * - * @return array The extensions. - * - * @since 3.7.0 - */ - public static function getSupportedExtensions() - { - if (!\is_null(self::$extensionsSupport)) - { - return self::$extensionsSupport; - } - - self::$extensionsSupport = array(); - - $extensions = self::getEnabledExtensions(); - - foreach ($extensions as $extension) - { - $support = self::getSupportedExtension($extension->element); - - if ($support->get('associationssupport') === true) - { - self::$supportedExtensionsList[] = $extension->element; - } - - self::$extensionsSupport[$extension->element] = $support; - } - - return self::$extensionsSupport; - } - - /** - * Get item context based on the item key. - * - * @param string $extensionName The extension identifier. - * - * @return \Joomla\Registry\Registry The item properties. - * - * @since 3.7.0 - */ - public static function getSupportedExtension($extensionName) - { - $result = new Registry; - - $result->def('component', $extensionName); - $result->def('associationssupport', false); - $result->def('helper', null); - - $helper = self::loadHelper($extensionName); - - if (!$helper) - { - return $result; - } - - $result->set('helper', $helper); - - if ($helper->hasAssociationsSupport() === false) - { - return $result; - } - - $result->set('associationssupport', true); - - // Get the translated titles. - $languagePath = JPATH_ADMINISTRATOR . '/components/' . $extensionName; - $lang = Factory::getLanguage(); - - $lang->load($extensionName . '.sys', JPATH_ADMINISTRATOR); - $lang->load($extensionName . '.sys', $languagePath); - $lang->load($extensionName, JPATH_ADMINISTRATOR); - $lang->load($extensionName, $languagePath); - - $result->def('title', Text::_(strtoupper($extensionName))); - - // Get the supported types - $types = $helper->getItemTypes(); - $rTypes = array(); - - foreach ($types as $typeName) - { - $details = $helper->getType($typeName); - $context = 'component'; - $title = $helper->getTypeTitle($typeName); - $languageKey = $typeName; - - $typeNameExploded = explode('.', $typeName); - - if (array_pop($typeNameExploded) === 'category') - { - $languageKey = strtoupper($extensionName) . '_CATEGORIES'; - $context = 'category'; - } - - if ($lang->hasKey(strtoupper($extensionName . '_' . $title . 'S'))) - { - $languageKey = strtoupper($extensionName . '_' . $title . 'S'); - } - - $title = $lang->hasKey($languageKey) ? Text::_($languageKey) : Text::_('COM_ASSOCIATIONS_ITEMS'); - - $rType = new Registry; - - $rType->def('name', $typeName); - $rType->def('details', $details); - $rType->def('title', $title); - $rType->def('context', $context); - - $rTypes[$typeName] = $rType; - } - - $result->def('types', $rTypes); - - return $result; - } - - /** - * Get all installed and enabled extensions - * - * @return mixed - * - * @since 3.7.0 - */ - private static function getEnabledExtensions() - { - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component')) - ->where($db->quoteName('enabled') . ' = 1'); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Get all the content languages. - * - * @return array Array of objects all content languages by language code. - * - * @since 3.7.0 - */ - public static function getContentLanguages() - { - return LanguageHelper::getContentLanguages(array(0, 1)); - } - - /** - * Get the associated items for an item - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return boolean - * - * @since 3.7.0 - */ - public static function allowEdit($extensionName, $typeName, $itemId) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - if (method_exists($helper, 'allowEdit')) - { - return $helper->allowEdit($typeName, $itemId); - } - - return Factory::getUser()->authorise('core.edit', $extensionName); - } - - /** - * Check if user is allowed to create items. - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * - * @return boolean True on allowed. - * - * @since 3.7.0 - */ - public static function allowAdd($extensionName, $typeName) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - if (method_exists($helper, 'allowAdd')) - { - return $helper->allowAdd($typeName); - } - - return Factory::getUser()->authorise('core.create', $extensionName); - } - - /** - * Check if an item is checked out - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return boolean True if item is checked out. - * - * @since 3.7.0 - */ - public static function isCheckoutItem($extensionName, $typeName, $itemId) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - if (!self::typeSupportsCheckout($extensionName, $typeName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - if (method_exists($helper, 'isCheckoutItem')) - { - return $helper->isCheckoutItem($typeName, $itemId); - } - - $item = self::getItem($extensionName, $typeName, $itemId); - - $checkedOutFieldName = $helper->getTypeFieldName($typeName, 'checked_out'); - - return $item->{$checkedOutFieldName} != 0; - } - - /** - * Check if user can checkin an item. - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return boolean True on allowed. - * - * @since 3.7.0 - */ - public static function canCheckinItem($extensionName, $typeName, $itemId) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - if (!self::typeSupportsCheckout($extensionName, $typeName)) - { - return true; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - if (method_exists($helper, 'canCheckinItem')) - { - return $helper->canCheckinItem($typeName, $itemId); - } - - $item = self::getItem($extensionName, $typeName, $itemId); - - $checkedOutFieldName = $helper->getTypeFieldName($typeName, 'checked_out'); - - $userId = Factory::getUser()->id; - - return ($item->{$checkedOutFieldName} == $userId || $item->{$checkedOutFieldName} == 0); - } - - /** - * Check if the type supports checkout - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * - * @return boolean True on allowed. - * - * @since 3.7.0 - */ - public static function typeSupportsCheckout($extensionName, $typeName) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - $support = $helper->getTypeSupport($typeName); - - return !empty($support['checkout']); - } - - /** - * Get a table field name for a type - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param string $fieldName The item type - * - * @return boolean True on allowed. - * - * @since 3.7.0 - */ - public static function getTypeFieldName($extensionName, $typeName, $fieldName) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - return $helper->getTypeFieldName($typeName, $fieldName); - } - - /** - * Gets the language filter system plugin extension id. - * - * @return integer The language filter system plugin extension id. - * - * @since 3.7.2 - */ - public static function getLanguagefilterPluginId() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('languagefilter')); - $db->setQuery($query); - - try - { - $result = (int) $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - return $result; - } + /** + * Array of Registry objects of extensions + * + * @var array + * @since 3.7.0 + */ + public static $extensionsSupport = null; + + /** + * List of extensions name with support + * + * @var array + * @since 3.7.0 + */ + public static $supportedExtensionsList = array(); + + /** + * Get the associated items for an item + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public static function getAssociationList($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return array(); + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + return $helper->getAssociationList($typeName, $itemId); + } + + /** + * Get the the instance of the extension helper class + * + * @param string $extensionName The extension name with com_ + * + * @return \Joomla\CMS\Association\AssociationExtensionHelper|null + * + * @since 3.7.0 + */ + public static function getExtensionHelper($extensionName) + { + if (!self::hasSupport($extensionName)) { + return null; + } + + $support = self::$extensionsSupport[$extensionName]; + + return $support->get('helper'); + } + + /** + * Get item information + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return \Joomla\CMS\Table\Table|null + * + * @since 3.7.0 + */ + public static function getItem($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return null; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + return $helper->getItem($typeName, $itemId); + } + + /** + * Check if extension supports associations + * + * @param string $extensionName The extension name with com_ + * + * @return boolean + * + * @since 3.7.0 + */ + public static function hasSupport($extensionName) + { + if (\is_null(self::$extensionsSupport)) { + self::getSupportedExtensions(); + } + + return \in_array($extensionName, self::$supportedExtensionsList); + } + + /** + * Loads the helper for the given class. + * + * @param string $extensionName The extension name with com_ + * + * @return AssociationExtensionInterface|null + * + * @since 4.0.0 + */ + private static function loadHelper($extensionName) + { + $component = Factory::getApplication()->bootComponent($extensionName); + + if ($component instanceof AssociationServiceInterface) { + return $component->getAssociationsExtension(); + } + + // Check if associations helper exists + if (!file_exists(JPATH_ADMINISTRATOR . '/components/' . $extensionName . '/helpers/associations.php')) { + return null; + } + + require_once JPATH_ADMINISTRATOR . '/components/' . $extensionName . '/helpers/associations.php'; + + $componentAssociationsHelperClassName = self::getExtensionHelperClassName($extensionName); + + if (!class_exists($componentAssociationsHelperClassName, false)) { + return null; + } + + // Create an instance of the helper class + return new $componentAssociationsHelperClassName(); + } + + /** + * Get the extension specific helper class name + * + * @param string $extensionName The extension name with com_ + * + * @return string + * + * @since 3.7.0 + */ + private static function getExtensionHelperClassName($extensionName) + { + $realName = self::getExtensionRealName($extensionName); + + return ucfirst($realName) . 'AssociationsHelper'; + } + + /** + * Get the real extension name. This means without com_ + * + * @param string $extensionName The extension name with com_ + * + * @return string + * + * @since 3.7.0 + */ + private static function getExtensionRealName($extensionName) + { + return strpos($extensionName, 'com_') === false ? $extensionName : substr($extensionName, 4); + } + + /** + * Get the associated language edit links Html. + * + * @param string $extensionName Extension Name + * @param string $typeName ItemType + * @param integer $itemId Item id. + * @param string $itemLanguage Item language code. + * @param boolean $addLink True for adding edit links. False for just text. + * @param boolean $assocLanguages True for showing non associated content languages. False only languages with associations. + * + * @return string The language HTML + * + * @since 3.7.0 + */ + public static function getAssociationHtmlList($extensionName, $typeName, $itemId, $itemLanguage, $addLink = true, $assocLanguages = true) + { + // Get the associations list for this item. + $items = self::getAssociationList($extensionName, $typeName, $itemId); + + $titleFieldName = self::getTypeFieldName($extensionName, $typeName, 'title'); + + // Get all content languages. + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + $content_languages = array_column($languages, 'lang_code'); + + // Display warning if Content Language is trashed or deleted + foreach ($items as $item) { + if (!\in_array($item['language'], $content_languages)) { + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item['language']), 'warning'); + } + } + + $canEditReference = self::allowEdit($extensionName, $typeName, $itemId); + $canCreate = self::allowAdd($extensionName, $typeName); + + // Create associated items list. + foreach ($languages as $langCode => $language) { + // Don't do for the reference language. + if ($langCode == $itemLanguage) { + continue; + } + + // Don't show languages with associations, if we don't want to show them. + if ($assocLanguages && isset($items[$langCode])) { + unset($items[$langCode]); + continue; + } + + // Don't show languages without associations, if we don't want to show them. + if (!$assocLanguages && !isset($items[$langCode])) { + continue; + } + + // Get html parameters. + if (isset($items[$langCode])) { + $title = $items[$langCode][$titleFieldName]; + $additional = ''; + + if (isset($items[$langCode]['catid'])) { + $db = Factory::getDbo(); + + // Get the category name + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $items[$langCode]['catid'], ParameterType::INTEGER); + + $db->setQuery($query); + $categoryTitle = $db->loadResult(); + + $additional = '' . Text::sprintf('JCATEGORY_SPRINTF', $categoryTitle) . '
'; + } elseif (isset($items[$langCode]['menutype'])) { + $db = Factory::getDbo(); + + // Get the menutype name + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('menutype') . ' = :menutype') + ->bind(':menutype', $items[$langCode]['menutype']); + + $db->setQuery($query); + $menutypeTitle = $db->loadResult(); + + $additional = '' . Text::sprintf('COM_MENUS_MENU_SPRINTF', $menutypeTitle) . '
'; + } + + $labelClass = 'bg-secondary'; + $target = $langCode . ':' . $items[$langCode]['id'] . ':edit'; + $allow = $canEditReference + && self::allowEdit($extensionName, $typeName, $items[$langCode]['id']) + && self::canCheckinItem($extensionName, $typeName, $items[$langCode]['id']); + + $additional .= $addLink && $allow ? Text::_('COM_ASSOCIATIONS_EDIT_ASSOCIATION') : ''; + } else { + $items[$langCode] = array(); + + $title = Text::_('COM_ASSOCIATIONS_NO_ASSOCIATION'); + $additional = $addLink ? Text::_('COM_ASSOCIATIONS_ADD_NEW_ASSOCIATION') : ''; + $labelClass = 'bg-warning text-dark'; + $target = $langCode . ':0:add'; + $allow = $canCreate; + } + + // Generate item Html. + $options = array( + 'option' => 'com_associations', + 'view' => 'association', + 'layout' => 'edit', + 'itemtype' => $extensionName . '.' . $typeName, + 'task' => 'association.edit', + 'id' => $itemId, + 'target' => $target, + ); + + $url = Route::_('index.php?' . http_build_query($options)); + $url = $allow && $addLink ? $url : ''; + $text = $language->lang_code; + + $tooltip = '' . htmlspecialchars($language->title, ENT_QUOTES, 'UTF-8') . '
' + . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '

' . $additional; + $classes = 'badge ' . $labelClass; + + $items[$langCode]['link'] = '' . $text . '' + . '
' . $tooltip . '
'; + } + + return LayoutHelper::render('joomla.content.associations', $items); + } + + /** + * Get all extensions with associations support. + * + * @return array The extensions. + * + * @since 3.7.0 + */ + public static function getSupportedExtensions() + { + if (!\is_null(self::$extensionsSupport)) { + return self::$extensionsSupport; + } + + self::$extensionsSupport = array(); + + $extensions = self::getEnabledExtensions(); + + foreach ($extensions as $extension) { + $support = self::getSupportedExtension($extension->element); + + if ($support->get('associationssupport') === true) { + self::$supportedExtensionsList[] = $extension->element; + } + + self::$extensionsSupport[$extension->element] = $support; + } + + return self::$extensionsSupport; + } + + /** + * Get item context based on the item key. + * + * @param string $extensionName The extension identifier. + * + * @return \Joomla\Registry\Registry The item properties. + * + * @since 3.7.0 + */ + public static function getSupportedExtension($extensionName) + { + $result = new Registry(); + + $result->def('component', $extensionName); + $result->def('associationssupport', false); + $result->def('helper', null); + + $helper = self::loadHelper($extensionName); + + if (!$helper) { + return $result; + } + + $result->set('helper', $helper); + + if ($helper->hasAssociationsSupport() === false) { + return $result; + } + + $result->set('associationssupport', true); + + // Get the translated titles. + $languagePath = JPATH_ADMINISTRATOR . '/components/' . $extensionName; + $lang = Factory::getLanguage(); + + $lang->load($extensionName . '.sys', JPATH_ADMINISTRATOR); + $lang->load($extensionName . '.sys', $languagePath); + $lang->load($extensionName, JPATH_ADMINISTRATOR); + $lang->load($extensionName, $languagePath); + + $result->def('title', Text::_(strtoupper($extensionName))); + + // Get the supported types + $types = $helper->getItemTypes(); + $rTypes = array(); + + foreach ($types as $typeName) { + $details = $helper->getType($typeName); + $context = 'component'; + $title = $helper->getTypeTitle($typeName); + $languageKey = $typeName; + + $typeNameExploded = explode('.', $typeName); + + if (array_pop($typeNameExploded) === 'category') { + $languageKey = strtoupper($extensionName) . '_CATEGORIES'; + $context = 'category'; + } + + if ($lang->hasKey(strtoupper($extensionName . '_' . $title . 'S'))) { + $languageKey = strtoupper($extensionName . '_' . $title . 'S'); + } + + $title = $lang->hasKey($languageKey) ? Text::_($languageKey) : Text::_('COM_ASSOCIATIONS_ITEMS'); + + $rType = new Registry(); + + $rType->def('name', $typeName); + $rType->def('details', $details); + $rType->def('title', $title); + $rType->def('context', $context); + + $rTypes[$typeName] = $rType; + } + + $result->def('types', $rTypes); + + return $result; + } + + /** + * Get all installed and enabled extensions + * + * @return mixed + * + * @since 3.7.0 + */ + private static function getEnabledExtensions() + { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ->where($db->quoteName('enabled') . ' = 1'); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Get all the content languages. + * + * @return array Array of objects all content languages by language code. + * + * @since 3.7.0 + */ + public static function getContentLanguages() + { + return LanguageHelper::getContentLanguages(array(0, 1)); + } + + /** + * Get the associated items for an item + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return boolean + * + * @since 3.7.0 + */ + public static function allowEdit($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + if (method_exists($helper, 'allowEdit')) { + return $helper->allowEdit($typeName, $itemId); + } + + return Factory::getUser()->authorise('core.edit', $extensionName); + } + + /** + * Check if user is allowed to create items. + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * + * @return boolean True on allowed. + * + * @since 3.7.0 + */ + public static function allowAdd($extensionName, $typeName) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + if (method_exists($helper, 'allowAdd')) { + return $helper->allowAdd($typeName); + } + + return Factory::getUser()->authorise('core.create', $extensionName); + } + + /** + * Check if an item is checked out + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return boolean True if item is checked out. + * + * @since 3.7.0 + */ + public static function isCheckoutItem($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + if (!self::typeSupportsCheckout($extensionName, $typeName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + if (method_exists($helper, 'isCheckoutItem')) { + return $helper->isCheckoutItem($typeName, $itemId); + } + + $item = self::getItem($extensionName, $typeName, $itemId); + + $checkedOutFieldName = $helper->getTypeFieldName($typeName, 'checked_out'); + + return $item->{$checkedOutFieldName} != 0; + } + + /** + * Check if user can checkin an item. + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return boolean True on allowed. + * + * @since 3.7.0 + */ + public static function canCheckinItem($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + if (!self::typeSupportsCheckout($extensionName, $typeName)) { + return true; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + if (method_exists($helper, 'canCheckinItem')) { + return $helper->canCheckinItem($typeName, $itemId); + } + + $item = self::getItem($extensionName, $typeName, $itemId); + + $checkedOutFieldName = $helper->getTypeFieldName($typeName, 'checked_out'); + + $userId = Factory::getUser()->id; + + return ($item->{$checkedOutFieldName} == $userId || $item->{$checkedOutFieldName} == 0); + } + + /** + * Check if the type supports checkout + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * + * @return boolean True on allowed. + * + * @since 3.7.0 + */ + public static function typeSupportsCheckout($extensionName, $typeName) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + $support = $helper->getTypeSupport($typeName); + + return !empty($support['checkout']); + } + + /** + * Get a table field name for a type + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param string $fieldName The item type + * + * @return boolean True on allowed. + * + * @since 3.7.0 + */ + public static function getTypeFieldName($extensionName, $typeName, $fieldName) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + return $helper->getTypeFieldName($typeName, $fieldName); + } + + /** + * Gets the language filter system plugin extension id. + * + * @return integer The language filter system plugin extension id. + * + * @since 3.7.2 + */ + public static function getLanguagefilterPluginId() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('languagefilter')); + $db->setQuery($query); + + try { + $result = (int) $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + return $result; + } } diff --git a/code/administrator/components/com_associations/src/Model/AssociationModel.php b/code/administrator/components/com_associations/src/Model/AssociationModel.php index 54796fde..1e66ed24 100644 --- a/code/administrator/components/com_associations/src/Model/AssociationModel.php +++ b/code/administrator/components/com_associations/src/Model/AssociationModel.php @@ -1,4 +1,5 @@ loadForm('com_associations.association', 'association', array('control' => 'jform', 'load_data' => $loadData)); + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 3.7.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_associations.association', 'association', array('control' => 'jform', 'load_data' => $loadData)); - return !empty($form) ? $form : false; - } + return !empty($form) ? $form : false; + } } diff --git a/code/administrator/components/com_associations/src/Model/AssociationsModel.php b/code/administrator/components/com_associations/src/Model/AssociationsModel.php index 9b491e51..37de1add 100644 --- a/code/administrator/components/com_associations/src/Model/AssociationsModel.php +++ b/code/administrator/components/com_associations/src/Model/AssociationsModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - $forcedItemType = $app->input->get('forcedItemType', '', 'string'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - // Adjust the context to support forced component item types. - if ($forcedItemType) - { - $this->context .= '.' . $forcedItemType; - } - - $this->setState('itemtype', $this->getUserStateFromRequest($this->context . '.itemtype', 'itemtype', '', 'string')); - $this->setState('language', $this->getUserStateFromRequest($this->context . '.language', 'language', '', 'string')); - - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); - $this->setState('filter.category_id', $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id', '', 'cmd')); - $this->setState('filter.menutype', $this->getUserStateFromRequest($this->context . '.filter.menutype', 'filter_menutype', '', 'string')); - $this->setState('filter.access', $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '', 'string')); - $this->setState('filter.level', $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level', '', 'cmd')); - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('language', $forcedLanguage); - } - - // Force a component item type. - if (!empty($forcedItemType)) - { - $this->setState('itemtype', $forcedItemType); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 3.7.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('itemtype'); - $id .= ':' . $this->getState('language'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.category_id'); - $id .= ':' . $this->getState('filter.menutype'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.level'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery|boolean - * - * @since 3.7.0 - */ - protected function getListQuery() - { - $type = null; - - list($extensionName, $typeName) = explode('.', $this->state->get('itemtype'), 2); - - $extension = AssociationsHelper::getSupportedExtension($extensionName); - $types = $extension->get('types'); - - if (\array_key_exists($typeName, $types)) - { - $type = $types[$typeName]; - } - - if (\is_null($type)) - { - return false; - } - - // Create a new query object. - $user = Factory::getUser(); - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $details = $type->get('details'); - - if (!\array_key_exists('support', $details)) - { - return false; - } - - $support = $details['support']; - - if (!\array_key_exists('fields', $details)) - { - return false; - } - - $fields = $details['fields']; - - // Main query. - $query->select($db->quoteName($fields['id'], 'id')) - ->select($db->quoteName($fields['title'], 'title')) - ->select($db->quoteName($fields['alias'], 'alias')); - - if (!\array_key_exists('tables', $details)) - { - return false; - } - - $tables = $details['tables']; - - foreach ($tables as $key => $table) - { - $query->from($db->quoteName($table, $key)); - } - - if (!\array_key_exists('joins', $details)) - { - return false; - } - - $joins = $details['joins']; - - foreach ($joins as $join) - { - $query->join($join['type'], $db->quoteName($join['condition'])); - } - - // Join over the language. - $query->select($db->quoteName($fields['language'], 'language')) - ->select($db->quoteName('l.title', 'language_title')) - ->select($db->quoteName('l.image', 'language_image')) - ->join( - 'LEFT', - $db->quoteName('#__languages', 'l'), - $db->quoteName('l.lang_code') . ' = ' . $db->quoteName($fields['language']) - ); - $extensionNameItem = $extensionName . '.item'; - - // Join over the associations. - $query->select('COUNT(' . $db->quoteName('asso2.id') . ') > 1 AS ' . $db->quoteName('association')) - ->join( - 'LEFT', - $db->quoteName('#__associations', 'asso'), - $db->quoteName('asso.id') . ' = ' . $db->quoteName($fields['id']) - . ' AND ' . $db->quoteName('asso.context') . ' = :context' - ) - ->join( - 'LEFT', - $db->quoteName('#__associations', 'asso2'), - $db->quoteName('asso2.key') . ' = ' . $db->quoteName('asso.key') - ) - ->bind(':context', $extensionNameItem); - - // Prepare the group by clause. - $groupby = array( - $fields['id'], - $fields['title'], - $fields['alias'], - $fields['language'], - 'l.title', - 'l.image', - ); - - // Select author for ACL checks. - if (!empty($fields['created_user_id'])) - { - $query->select($db->quoteName($fields['created_user_id'], 'created_user_id')); - - $groupby[] = $fields['created_user_id']; - } - - // Select checked out data for check in checkins. - if (!empty($fields['checked_out']) && !empty($fields['checked_out_time'])) - { - $query->select($db->quoteName($fields['checked_out'], 'checked_out')) - ->select($db->quoteName($fields['checked_out_time'], 'checked_out_time')); - - // Join over the users. - $query->select($db->quoteName('u.name', 'editor')) - ->join( - 'LEFT', - $db->quoteName('#__users', 'u'), - $db->quoteName('u.id') . ' = ' . $db->quoteName($fields['checked_out']) - ); - - $groupby[] = 'u.name'; - $groupby[] = $fields['checked_out']; - $groupby[] = $fields['checked_out_time']; - } - - // If component item type supports ordering, select the ordering also. - if (!empty($fields['ordering'])) - { - $query->select($db->quoteName($fields['ordering'], 'ordering')); - - $groupby[] = $fields['ordering']; - } - - // If component item type supports state, select the item state also. - if (!empty($fields['state'])) - { - $query->select($db->quoteName($fields['state'], 'state')); - - $groupby[] = $fields['state']; - } - - // If component item type supports level, select the level also. - if (!empty($fields['level'])) - { - $query->select($db->quoteName($fields['level'], 'level')); - - $groupby[] = $fields['level']; - } - - // If component item type supports categories, select the category also. - if (!empty($fields['catid'])) - { - $query->select($db->quoteName($fields['catid'], 'catid')); - - // Join over the categories. - $query->select($db->quoteName('c.title', 'category_title')) - ->join( - 'LEFT', - $db->quoteName('#__categories', 'c'), - $db->quoteName('c.id') . ' = ' . $db->quoteName($fields['catid']) - ); - - $groupby[] = 'c.title'; - $groupby[] = $fields['catid']; - } - - // If component item type supports menu type, select the menu type also. - if (!empty($fields['menutype'])) - { - $query->select($db->quoteName($fields['menutype'], 'menutype')); - - // Join over the menu types. - $query->select($db->quoteName('mt.title', 'menutype_title')) - ->select($db->quoteName('mt.id', 'menutypeid')) - ->join( - 'LEFT', - $db->quoteName('#__menu_types', 'mt'), - $db->quoteName('mt.menutype') . ' = ' . $db->quoteName($fields['menutype']) - ); - - $groupby[] = 'mt.title'; - $groupby[] = 'mt.id'; - $groupby[] = $fields['menutype']; - } - - // If component item type supports access level, select the access level also. - if (\array_key_exists('acl', $support) && $support['acl'] == true && !empty($fields['access'])) - { - $query->select($db->quoteName($fields['access'], 'access')); - - // Join over the access levels. - $query->select($db->quoteName('ag.title', 'access_level')) - ->join( - 'LEFT', - $db->quoteName('#__viewlevels', 'ag'), - $db->quoteName('ag.id') . ' = ' . $db->quoteName($fields['access']) - ); - - $groupby[] = 'ag.title'; - $groupby[] = $fields['access']; - - // Implement View Level Access. - if (!$user->authorise('core.admin', $extensionName)) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName($fields['access']), $groups); - } - } - - // If component item type is menus we need to remove the root item and the administrator menu. - if ($extensionName === 'com_menus') - { - $query->where($db->quoteName($fields['id']) . ' > 1') - ->where($db->quoteName('a.client_id') . ' = 0'); - } - - // If component item type is category we need to remove all other component categories. - if ($typeName === 'category') - { - $query->where($db->quoteName('a.extension') . ' = :extensionname') - ->bind(':extensionname', $extensionName); - } - elseif ($typeNameExploded = explode('.', $typeName)) - { - if (\count($typeNameExploded) > 1 && array_pop($typeNameExploded) === 'category') - { - $section = implode('.', $typeNameExploded); - $extensionNameSection = $extensionName . '.' . $section; - $query->where($db->quoteName('a.extension') . ' = :extensionsection') - ->bind(':extensionsection', $extensionNameSection); - } - } - - // Filter on the language. - if ($language = $this->getState('language')) - { - $query->where($db->quoteName($fields['language']) . ' = :language') - ->bind(':language', $language); - } - - // Filter by item state. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName($fields['state']) . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif ($state === '') - { - $query->whereIn($db->quoteName($fields['state']), [0, 1]); - } - - // Filter on the category. - $baselevel = 1; - - if ($categoryId = $this->getState('filter.category_id')) - { - $categoryTable = Table::getInstance('Category', 'JTable'); - $categoryTable->load($categoryId); - $baselevel = (int) $categoryTable->level; - - $lft = (int) $categoryTable->lft; - $rgt = (int) $categoryTable->rgt; - $query->where($db->quoteName('c.lft') . ' >= :lft') - ->where($db->quoteName('c.rgt') . ' <= :rgt') - ->bind(':lft', $lft, ParameterType::INTEGER) - ->bind(':rgt', $rgt, ParameterType::INTEGER); - } - - // Filter on the level. - if ($level = $this->getState('filter.level')) - { - $queryLevel = ((int) $level + (int) $baselevel - 1); - $query->where($db->quoteName('a.level') . ' <= :alevel') - ->bind(':alevel', $queryLevel, ParameterType::INTEGER); - } - - // Filter by menu type. - if ($menutype = $this->getState('filter.menutype')) - { - $query->where($db->quoteName($fields['menutype']) . ' = :menutype2') - ->bind(':menutype2', $menutype); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - $access = (int) $access; - $query->where($db->quoteName($fields['access']) . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Filter by search in name. - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName($fields['id']) . ' = :searchid') - ->bind(':searchid', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName($fields['title']) . ' LIKE :title' - . ' OR ' . $db->quoteName($fields['alias']) . ' LIKE :alias)' - ) - ->bind(':title', $search) - ->bind(':alias', $search); - } - } - - // Add the group by clause - $query->group($db->quoteName($groupby)); - - // Add the list ordering clause - $listOrdering = $this->state->get('list.ordering', 'id'); - $orderDirn = $this->state->get('list.direction', 'ASC'); - - $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); - - return $query; - } - - /** - * Delete associations from #__associations table. - * - * @param string $context The associations context. Empty for all. - * @param string $key The associations key. Empty for all. - * - * @return boolean True on success. - * - * @since 3.7.0 - */ - public function purge($context = '', $key = '') - { - $app = Factory::getApplication(); - $db = $this->getDbo(); - $query = $db->getQuery(true)->delete($db->quoteName('#__associations')); - - // Filter by associations context. - if ($context) - { - $query->where($db->quoteName('context') . ' = :context') - ->bind(':context', $context); - } - - // Filter by key. - if ($key) - { - $query->where($db->quoteName('key') . ' = :key') - ->bind(':key', $key); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $app->enqueueMessage(Text::_('COM_ASSOCIATIONS_PURGE_FAILED'), 'error'); - - return false; - } - - $app->enqueueMessage( - Text::_((int) $db->getAffectedRows() > 0 ? 'COM_ASSOCIATIONS_PURGE_SUCCESS' : 'COM_ASSOCIATIONS_PURGE_NONE'), - 'message' - ); - - return true; - } - - /** - * Delete orphans from the #__associations table. - * - * @param string $context The associations context. Empty for all. - * @param string $key The associations key. Empty for all. - * - * @return boolean True on success - * - * @since 3.7.0 - */ - public function clean($context = '', $key = '') - { - $app = Factory::getApplication(); - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('key') . ', COUNT(*)') - ->from($db->quoteName('#__associations')) - ->group($db->quoteName('key')) - ->having('COUNT(*) = 1'); - - // Filter by associations context. - if ($context) - { - $query->where($db->quoteName('context') . ' = :context') - ->bind(':context', $context); - } - - // Filter by key. - if ($key) - { - $query->where($db->quoteName('key') . ' = :key') - ->bind(':key', $key); - } - - $db->setQuery($query); - - $assocKeys = $db->loadObjectList(); - - $count = 0; - - // We have orphans. Let's delete them. - foreach ($assocKeys as $value) - { - $query->clear() - ->delete($db->quoteName('#__associations')) - ->where($db->quoteName('key') . ' = :valuekey') - ->bind(':valuekey', $value->key); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $app->enqueueMessage(Text::_('COM_ASSOCIATIONS_DELETE_ORPHANS_FAILED'), 'error'); - - return false; - } - - $count += (int) $db->getAffectedRows(); - } - - $app->enqueueMessage( - Text::_($count > 0 ? 'COM_ASSOCIATIONS_DELETE_ORPHANS_SUCCESS' : 'COM_ASSOCIATIONS_DELETE_ORPHANS_NONE'), - 'message' - ); - - return true; - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.7 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', + 'title', + 'ordering', + 'itemtype', + 'language', + 'association', + 'menutype', + 'menutype_title', + 'level', + 'state', + 'category_id', + 'category_title', + 'access', + 'access_level', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState($ordering = 'ordering', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + $forcedItemType = $app->input->get('forcedItemType', '', 'string'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + // Adjust the context to support forced component item types. + if ($forcedItemType) { + $this->context .= '.' . $forcedItemType; + } + + $this->setState('itemtype', $this->getUserStateFromRequest($this->context . '.itemtype', 'itemtype', '', 'string')); + $this->setState('language', $this->getUserStateFromRequest($this->context . '.language', 'language', '', 'string')); + + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); + $this->setState('filter.category_id', $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id', '', 'cmd')); + $this->setState('filter.menutype', $this->getUserStateFromRequest($this->context . '.filter.menutype', 'filter_menutype', '', 'string')); + $this->setState('filter.access', $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '', 'string')); + $this->setState('filter.level', $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level', '', 'cmd')); + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('language', $forcedLanguage); + } + + // Force a component item type. + if (!empty($forcedItemType)) { + $this->setState('itemtype', $forcedItemType); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 3.7.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('itemtype'); + $id .= ':' . $this->getState('language'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.category_id'); + $id .= ':' . $this->getState('filter.menutype'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.level'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery|boolean + * + * @since 3.7.0 + */ + protected function getListQuery() + { + $type = null; + + list($extensionName, $typeName) = explode('.', $this->state->get('itemtype'), 2); + + $extension = AssociationsHelper::getSupportedExtension($extensionName); + $types = $extension->get('types'); + + if (\array_key_exists($typeName, $types)) { + $type = $types[$typeName]; + } + + if (\is_null($type)) { + return false; + } + + // Create a new query object. + $user = Factory::getUser(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $details = $type->get('details'); + + if (!\array_key_exists('support', $details)) { + return false; + } + + $support = $details['support']; + + if (!\array_key_exists('fields', $details)) { + return false; + } + + $fields = $details['fields']; + + // Main query. + $query->select($db->quoteName($fields['id'], 'id')) + ->select($db->quoteName($fields['title'], 'title')) + ->select($db->quoteName($fields['alias'], 'alias')); + + if (!\array_key_exists('tables', $details)) { + return false; + } + + $tables = $details['tables']; + + foreach ($tables as $key => $table) { + $query->from($db->quoteName($table, $key)); + } + + if (!\array_key_exists('joins', $details)) { + return false; + } + + $joins = $details['joins']; + + foreach ($joins as $join) { + $query->join($join['type'], $db->quoteName($join['condition'])); + } + + // Join over the language. + $query->select($db->quoteName($fields['language'], 'language')) + ->select($db->quoteName('l.title', 'language_title')) + ->select($db->quoteName('l.image', 'language_image')) + ->join( + 'LEFT', + $db->quoteName('#__languages', 'l'), + $db->quoteName('l.lang_code') . ' = ' . $db->quoteName($fields['language']) + ); + $extensionNameItem = $extensionName . '.item'; + + // Join over the associations. + $query->select('COUNT(' . $db->quoteName('asso2.id') . ') > 1 AS ' . $db->quoteName('association')) + ->join( + 'LEFT', + $db->quoteName('#__associations', 'asso'), + $db->quoteName('asso.id') . ' = ' . $db->quoteName($fields['id']) + . ' AND ' . $db->quoteName('asso.context') . ' = :context' + ) + ->join( + 'LEFT', + $db->quoteName('#__associations', 'asso2'), + $db->quoteName('asso2.key') . ' = ' . $db->quoteName('asso.key') + ) + ->bind(':context', $extensionNameItem); + + // Prepare the group by clause. + $groupby = array( + $fields['id'], + $fields['title'], + $fields['alias'], + $fields['language'], + 'l.title', + 'l.image', + ); + + // Select author for ACL checks. + if (!empty($fields['created_user_id'])) { + $query->select($db->quoteName($fields['created_user_id'], 'created_user_id')); + + $groupby[] = $fields['created_user_id']; + } + + // Select checked out data for check in checkins. + if (!empty($fields['checked_out']) && !empty($fields['checked_out_time'])) { + $query->select($db->quoteName($fields['checked_out'], 'checked_out')) + ->select($db->quoteName($fields['checked_out_time'], 'checked_out_time')); + + // Join over the users. + $query->select($db->quoteName('u.name', 'editor')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'u'), + $db->quoteName('u.id') . ' = ' . $db->quoteName($fields['checked_out']) + ); + + $groupby[] = 'u.name'; + $groupby[] = $fields['checked_out']; + $groupby[] = $fields['checked_out_time']; + } + + // If component item type supports ordering, select the ordering also. + if (!empty($fields['ordering'])) { + $query->select($db->quoteName($fields['ordering'], 'ordering')); + + $groupby[] = $fields['ordering']; + } + + // If component item type supports state, select the item state also. + if (!empty($fields['state'])) { + $query->select($db->quoteName($fields['state'], 'state')); + + $groupby[] = $fields['state']; + } + + // If component item type supports level, select the level also. + if (!empty($fields['level'])) { + $query->select($db->quoteName($fields['level'], 'level')); + + $groupby[] = $fields['level']; + } + + // If component item type supports categories, select the category also. + if (!empty($fields['catid'])) { + $query->select($db->quoteName($fields['catid'], 'catid')); + + // Join over the categories. + $query->select($db->quoteName('c.title', 'category_title')) + ->join( + 'LEFT', + $db->quoteName('#__categories', 'c'), + $db->quoteName('c.id') . ' = ' . $db->quoteName($fields['catid']) + ); + + $groupby[] = 'c.title'; + $groupby[] = $fields['catid']; + } + + // If component item type supports menu type, select the menu type also. + if (!empty($fields['menutype'])) { + $query->select($db->quoteName($fields['menutype'], 'menutype')); + + // Join over the menu types. + $query->select($db->quoteName('mt.title', 'menutype_title')) + ->select($db->quoteName('mt.id', 'menutypeid')) + ->join( + 'LEFT', + $db->quoteName('#__menu_types', 'mt'), + $db->quoteName('mt.menutype') . ' = ' . $db->quoteName($fields['menutype']) + ); + + $groupby[] = 'mt.title'; + $groupby[] = 'mt.id'; + $groupby[] = $fields['menutype']; + } + + // If component item type supports access level, select the access level also. + if (\array_key_exists('acl', $support) && $support['acl'] == true && !empty($fields['access'])) { + $query->select($db->quoteName($fields['access'], 'access')); + + // Join over the access levels. + $query->select($db->quoteName('ag.title', 'access_level')) + ->join( + 'LEFT', + $db->quoteName('#__viewlevels', 'ag'), + $db->quoteName('ag.id') . ' = ' . $db->quoteName($fields['access']) + ); + + $groupby[] = 'ag.title'; + $groupby[] = $fields['access']; + + // Implement View Level Access. + if (!$user->authorise('core.admin', $extensionName)) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName($fields['access']), $groups); + } + } + + // If component item type is menus we need to remove the root item and the administrator menu. + if ($extensionName === 'com_menus') { + $query->where($db->quoteName($fields['id']) . ' > 1') + ->where($db->quoteName('a.client_id') . ' = 0'); + } + + // If component item type is category we need to remove all other component categories. + if ($typeName === 'category') { + $query->where($db->quoteName('a.extension') . ' = :extensionname') + ->bind(':extensionname', $extensionName); + } elseif ($typeNameExploded = explode('.', $typeName)) { + if (\count($typeNameExploded) > 1 && array_pop($typeNameExploded) === 'category') { + $section = implode('.', $typeNameExploded); + $extensionNameSection = $extensionName . '.' . $section; + $query->where($db->quoteName('a.extension') . ' = :extensionsection') + ->bind(':extensionsection', $extensionNameSection); + } + } + + // Filter on the language. + if ($language = $this->getState('language')) { + $query->where($db->quoteName($fields['language']) . ' = :language') + ->bind(':language', $language); + } + + // Filter by item state. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName($fields['state']) . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif ($state === '') { + $query->whereIn($db->quoteName($fields['state']), [0, 1]); + } + + // Filter on the category. + $baselevel = 1; + + if ($categoryId = $this->getState('filter.category_id')) { + $categoryTable = Table::getInstance('Category', 'JTable'); + $categoryTable->load($categoryId); + $baselevel = (int) $categoryTable->level; + + $lft = (int) $categoryTable->lft; + $rgt = (int) $categoryTable->rgt; + $query->where($db->quoteName('c.lft') . ' >= :lft') + ->where($db->quoteName('c.rgt') . ' <= :rgt') + ->bind(':lft', $lft, ParameterType::INTEGER) + ->bind(':rgt', $rgt, ParameterType::INTEGER); + } + + // Filter on the level. + if ($level = $this->getState('filter.level')) { + $queryLevel = ((int) $level + (int) $baselevel - 1); + $query->where($db->quoteName('a.level') . ' <= :alevel') + ->bind(':alevel', $queryLevel, ParameterType::INTEGER); + } + + // Filter by menu type. + if ($menutype = $this->getState('filter.menutype')) { + $query->where($db->quoteName($fields['menutype']) . ' = :menutype2') + ->bind(':menutype2', $menutype); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + $access = (int) $access; + $query->where($db->quoteName($fields['access']) . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Filter by search in name. + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName($fields['id']) . ' = :searchid') + ->bind(':searchid', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName($fields['title']) . ' LIKE :title' + . ' OR ' . $db->quoteName($fields['alias']) . ' LIKE :alias)') + ->bind(':title', $search) + ->bind(':alias', $search); + } + } + + // Add the group by clause + $query->group($db->quoteName($groupby)); + + // Add the list ordering clause + $listOrdering = $this->state->get('list.ordering', 'id'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + + $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); + + return $query; + } + + /** + * Delete associations from #__associations table. + * + * @param string $context The associations context. Empty for all. + * @param string $key The associations key. Empty for all. + * + * @return boolean True on success. + * + * @since 3.7.0 + */ + public function purge($context = '', $key = '') + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true)->delete($db->quoteName('#__associations')); + + // Filter by associations context. + if ($context) { + $query->where($db->quoteName('context') . ' = :context') + ->bind(':context', $context); + } + + // Filter by key. + if ($key) { + $query->where($db->quoteName('key') . ' = :key') + ->bind(':key', $key); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + $app->enqueueMessage(Text::_('COM_ASSOCIATIONS_PURGE_FAILED'), 'error'); + + return false; + } + + $app->enqueueMessage( + Text::_((int) $db->getAffectedRows() > 0 ? 'COM_ASSOCIATIONS_PURGE_SUCCESS' : 'COM_ASSOCIATIONS_PURGE_NONE'), + 'message' + ); + + return true; + } + + /** + * Delete orphans from the #__associations table. + * + * @param string $context The associations context. Empty for all. + * @param string $key The associations key. Empty for all. + * + * @return boolean True on success + * + * @since 3.7.0 + */ + public function clean($context = '', $key = '') + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('key') . ', COUNT(*)') + ->from($db->quoteName('#__associations')) + ->group($db->quoteName('key')) + ->having('COUNT(*) = 1'); + + // Filter by associations context. + if ($context) { + $query->where($db->quoteName('context') . ' = :context') + ->bind(':context', $context); + } + + // Filter by key. + if ($key) { + $query->where($db->quoteName('key') . ' = :key') + ->bind(':key', $key); + } + + $db->setQuery($query); + + $assocKeys = $db->loadObjectList(); + + $count = 0; + + // We have orphans. Let's delete them. + foreach ($assocKeys as $value) { + $query->clear() + ->delete($db->quoteName('#__associations')) + ->where($db->quoteName('key') . ' = :valuekey') + ->bind(':valuekey', $value->key); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + $app->enqueueMessage(Text::_('COM_ASSOCIATIONS_DELETE_ORPHANS_FAILED'), 'error'); + + return false; + } + + $count += (int) $db->getAffectedRows(); + } + + $app->enqueueMessage( + Text::_($count > 0 ? 'COM_ASSOCIATIONS_DELETE_ORPHANS_SUCCESS' : 'COM_ASSOCIATIONS_DELETE_ORPHANS_NONE'), + 'message' + ); + + return true; + } } diff --git a/code/administrator/components/com_associations/src/View/Association/HtmlView.php b/code/administrator/components/com_associations/src/View/Association/HtmlView.php index c54f8b80..d0479d3e 100644 --- a/code/administrator/components/com_associations/src/View/Association/HtmlView.php +++ b/code/administrator/components/com_associations/src/View/Association/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - - // Check for errors. - if (\count($errors = $model->getErrors())) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->app = Factory::getApplication(); - $this->form = $model->getForm(); - /** @var Input $input */ - $input = $this->app->input; - $this->referenceId = $input->get('id', 0, 'int'); - - [$extensionName, $typeName] = explode('.', $input->get('itemtype', '', 'string'), 2); - - /** @var Registry $extension */ - $extension = AssociationsHelper::getSupportedExtension($extensionName); - $types = $extension->get('types'); - - if (\array_key_exists($typeName, $types)) - { - $this->type = $types[$typeName]; - $this->typeSupports = []; - $details = $this->type->get('details'); - $this->save2copy = false; - - if (\array_key_exists('support', $details)) - { - $support = $details['support']; - $this->typeSupports = $support; - } - - if (!empty($this->typeSupports['save2copy'])) - { - $this->save2copy = true; - } - } - - $this->extensionName = $extensionName; - $this->typeName = $typeName; - $this->itemType = $extensionName . '.' . $typeName; - - $languageField = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'language'); - $referenceId = $input->get('id', 0, 'int'); - $reference = ArrayHelper::fromObject(AssociationsHelper::getItem($extensionName, $typeName, $referenceId)); - - $this->referenceLanguage = $reference[$languageField]; - $this->referenceTitle = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'title'); - $this->referenceTitleValue = $reference[$this->referenceTitle]; - - // Check for special case category - $typeNameExploded = explode('.', $typeName); - - if (array_pop($typeNameExploded) === 'category') - { - $this->typeName = 'category'; - - if ($typeNameExploded) - { - $extensionName .= '.' . implode('.', $typeNameExploded); - } - - $options = [ - 'option' => 'com_categories', - 'view' => 'category', - 'extension' => $extensionName, - 'tmpl' => 'component', - ]; - } - else - { - $options = [ - 'option' => $extensionName, - 'view' => $typeName, - 'extension' => $extensionName, - 'tmpl' => 'component', - ]; - } - - // Reference and target edit links. - $this->editUri = 'index.php?' . http_build_query($options); - - // Get target language. - $this->targetId = '0'; - $this->targetLanguage = ''; - $this->defaultTargetSrc = ''; - $this->targetAction = ''; - $this->targetTitle = ''; - - if ($target = $input->get('target', '', 'string')) - { - $matches = preg_split("#[\:]+#", $target); - $this->targetAction = $matches[2]; - $this->targetId = $matches[1]; - $this->targetLanguage = $matches[0]; - $this->targetTitle = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'title'); - $task = $typeName . '.' . $this->targetAction; - - /** - * Let's put the target src into a variable to use in the javascript code - * to avoid race conditions when the reference iframe loads. - */ - $this->document->addScriptOptions('targetSrc', Route::_($this->editUri . '&task=' . $task . '&id=' . (int) $this->targetId)); - $this->form->setValue('itemlanguage', '', $this->targetLanguage . ':' . $this->targetId . ':' . $this->targetAction); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.7.0 - * - * @throws \Exception - */ - protected function addToolbar(): void - { - // Hide main menu. - $this->app->input->set('hidemainmenu', 1); - - $helper = AssociationsHelper::getExtensionHelper($this->extensionName); - $title = $helper->getTypeTitle($this->typeName); - - $languageKey = strtoupper($this->extensionName . '_' . $title . 'S'); - - if ($this->typeName === 'category') - { - $languageKey = strtoupper($this->extensionName) . '_CATEGORIES'; - } - - ToolbarHelper::title( - Text::sprintf( - 'COM_ASSOCIATIONS_TITLE_EDIT', - Text::_($this->extensionName), - Text::_($languageKey) - ), - 'language assoc' - ); - - $bar = Toolbar::getInstance(); - - $bar->appendButton( - 'Custom', '', 'reference' - ); - - $bar->appendButton( - 'Custom', '', 'target' - ); - - if ($this->typeName === 'category' || $this->extensionName === 'com_menus' || $this->save2copy === true) - { - ToolbarHelper::custom('copy', 'copy.png', '', 'COM_ASSOCIATIONS_COPY_REFERENCE', false); - } - - ToolbarHelper::cancel('association.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::help('Multilingual_Associations:_Edit'); - } + /** + * An array of items + * + * @var array + * + * @since 3.7.0 + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * + * @since 3.7.0 + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * Selected item type properties. + * + * @var Registry + * + * @since 3.7.0 + */ + protected $itemType; + + /** + * The application + * + * @var AdministratorApplication + * @since 3.7.0 + */ + protected $app; + + /** + * The ID of the reference language + * + * @var integer + * @since 3.7.0 + */ + protected $referenceId = 0; + + /** + * The type name + * + * @var string + * @since 3.7.0 + */ + protected $typeName = ''; + + /** + * The reference language + * + * @var string + * @since 3.7.0 + */ + protected $referenceLanguage = ''; + + /** + * The title of the reference language + * + * @var string + * @since 3.7.0 + */ + protected $referenceTitle = ''; + + /** + * The value of the reference title + * + * @var string + * @since 3.7.0 + */ + protected $referenceTitleValue = ''; + + /** + * The URL to the edit screen + * + * @var string + * @since 3.7.0 + */ + protected $editUri = ''; + + /** + * The ID of the target field + * + * @var string + * @since 3.7.0 + */ + protected $targetId = ''; + + /** + * The target language + * + * @var string + * @since 3.7.0 + */ + protected $targetLanguage = ''; + + /** + * The source of the target field + * + * @var string + * @since 3.7.0 + */ + protected $defaultTargetSrc = ''; + + /** + * The action to perform for the target field + * + * @var string + * @since 3.7.0 + */ + protected $targetAction = ''; + + /** + * The title of the target field + * + * @var string + * @since 3.7.0 + */ + protected $targetTitle = ''; + + /** + * The edit form + * + * @var Form + * @since 3.7.0 + */ + protected $form; + + /** + * Set if the option is set to save as copy + * + * @var boolean + * @since 3.7.0 + */ + private $save2copy = false; + + /** + * The type of language + * + * @var Registry + * @since 3.7.0 + */ + private $type; + + /** + * The supported types + * + * @var array + * @since 3.7.0 + */ + private $typeSupports = []; + + /** + * The extension name + * + * @var string + * @since 3.7.0 + */ + private $extensionName = ''; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.7.0 + * + * @throws \Exception + */ + public function display($tpl = null): void + { + /** @var AssociationModel $model */ + $model = $this->getModel(); + + // Check for errors. + if (\count($errors = $model->getErrors())) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->app = Factory::getApplication(); + $this->form = $model->getForm(); + /** @var Input $input */ + $input = $this->app->input; + $this->referenceId = $input->get('id', 0, 'int'); + + [$extensionName, $typeName] = explode('.', $input->get('itemtype', '', 'string'), 2); + + /** @var Registry $extension */ + $extension = AssociationsHelper::getSupportedExtension($extensionName); + $types = $extension->get('types'); + + if (\array_key_exists($typeName, $types)) { + $this->type = $types[$typeName]; + $this->typeSupports = []; + $details = $this->type->get('details'); + $this->save2copy = false; + + if (\array_key_exists('support', $details)) { + $support = $details['support']; + $this->typeSupports = $support; + } + + if (!empty($this->typeSupports['save2copy'])) { + $this->save2copy = true; + } + } + + $this->extensionName = $extensionName; + $this->typeName = $typeName; + $this->itemType = $extensionName . '.' . $typeName; + + $languageField = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'language'); + $referenceId = $input->get('id', 0, 'int'); + $reference = ArrayHelper::fromObject(AssociationsHelper::getItem($extensionName, $typeName, $referenceId)); + + $this->referenceLanguage = $reference[$languageField]; + $this->referenceTitle = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'title'); + $this->referenceTitleValue = $reference[$this->referenceTitle]; + + // Check for special case category + $typeNameExploded = explode('.', $typeName); + + if (array_pop($typeNameExploded) === 'category') { + $this->typeName = 'category'; + + if ($typeNameExploded) { + $extensionName .= '.' . implode('.', $typeNameExploded); + } + + $options = [ + 'option' => 'com_categories', + 'view' => 'category', + 'extension' => $extensionName, + 'tmpl' => 'component', + ]; + } else { + $options = [ + 'option' => $extensionName, + 'view' => $typeName, + 'extension' => $extensionName, + 'tmpl' => 'component', + ]; + } + + // Reference and target edit links. + $this->editUri = 'index.php?' . http_build_query($options); + + // Get target language. + $this->targetId = '0'; + $this->targetLanguage = ''; + $this->defaultTargetSrc = ''; + $this->targetAction = ''; + $this->targetTitle = ''; + + if ($target = $input->get('target', '', 'string')) { + $matches = preg_split("#[\:]+#", $target); + $this->targetAction = $matches[2]; + $this->targetId = $matches[1]; + $this->targetLanguage = $matches[0]; + $this->targetTitle = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'title'); + $task = $typeName . '.' . $this->targetAction; + + /** + * Let's put the target src into a variable to use in the javascript code + * to avoid race conditions when the reference iframe loads. + */ + $this->document->addScriptOptions('targetSrc', Route::_($this->editUri . '&task=' . $task . '&id=' . (int) $this->targetId)); + $this->form->setValue('itemlanguage', '', $this->targetLanguage . ':' . $this->targetId . ':' . $this->targetAction); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.7.0 + * + * @throws \Exception + */ + protected function addToolbar(): void + { + // Hide main menu. + $this->app->input->set('hidemainmenu', 1); + + $helper = AssociationsHelper::getExtensionHelper($this->extensionName); + $title = $helper->getTypeTitle($this->typeName); + + $languageKey = strtoupper($this->extensionName . '_' . $title . 'S'); + + if ($this->typeName === 'category') { + $languageKey = strtoupper($this->extensionName) . '_CATEGORIES'; + } + + ToolbarHelper::title( + Text::sprintf( + 'COM_ASSOCIATIONS_TITLE_EDIT', + Text::_($this->extensionName), + Text::_($languageKey) + ), + 'language assoc' + ); + + $bar = Toolbar::getInstance(); + + $bar->appendButton( + 'Custom', + '', + 'reference' + ); + + $bar->appendButton( + 'Custom', + '', + 'target' + ); + + if ($this->typeName === 'category' || $this->extensionName === 'com_menus' || $this->save2copy === true) { + ToolbarHelper::custom('copy', 'copy.png', '', 'COM_ASSOCIATIONS_COPY_REFERENCE', false); + } + + ToolbarHelper::cancel('association.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::help('Multilingual_Associations:_Edit'); + } } diff --git a/code/administrator/components/com_associations/src/View/Associations/HtmlView.php b/code/administrator/components/com_associations/src/View/Associations/HtmlView.php index 3fe4c66e..54c7c230 100644 --- a/code/administrator/components/com_associations/src/View/Associations/HtmlView.php +++ b/code/administrator/components/com_associations/src/View/Associations/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!Associations::isEnabled()) - { - $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . AssociationsHelper::getLanguagefilterPluginId()); - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_ASSOCIATIONS_ERROR_NO_ASSOC', $link), 'warning'); - } - elseif ($this->state->get('itemtype') != '' && $this->state->get('language') != '') - { - $type = null; - - list($extensionName, $typeName) = explode('.', $this->state->get('itemtype'), 2); - - $extension = AssociationsHelper::getSupportedExtension($extensionName); - - $types = $extension->get('types'); - - if (\array_key_exists($typeName, $types)) - { - $type = $types[$typeName]; - } - - $this->itemType = $type; - - if (\is_null($type)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_ASSOCIATIONS_ERROR_NO_TYPE'), 'warning'); - } - else - { - $this->extensionName = $extensionName; - $this->typeName = $typeName; - $this->typeSupports = array(); - $this->typeFields = array(); - - $details = $type->get('details'); - - if (\array_key_exists('support', $details)) - { - $support = $details['support']; - $this->typeSupports = $support; - } - - if (\array_key_exists('fields', $details)) - { - $fields = $details['fields']; - $this->typeFields = $fields; - } - - // Dynamic filter form. - // This selectors doesn't have to activate the filter bar. - unset($this->activeFilters['itemtype']); - unset($this->activeFilters['language']); - - // Remove filters options depending on selected type. - if (empty($support['state'])) - { - unset($this->activeFilters['state']); - $this->filterForm->removeField('state', 'filter'); - } - - if (empty($support['category'])) - { - unset($this->activeFilters['category_id']); - $this->filterForm->removeField('category_id', 'filter'); - } - - if ($extensionName !== 'com_menus') - { - unset($this->activeFilters['menutype']); - $this->filterForm->removeField('menutype', 'filter'); - } - - if (empty($support['level'])) - { - unset($this->activeFilters['level']); - $this->filterForm->removeField('level', 'filter'); - } - - if (empty($support['acl'])) - { - unset($this->activeFilters['access']); - $this->filterForm->removeField('access', 'filter'); - } - - // Add extension attribute to category filter. - if (empty($support['catid'])) - { - $this->filterForm->setFieldAttribute('category_id', 'extension', $extensionName, 'filter'); - - if ($this->getLayout() == 'modal') - { - // We need to change the category filter to only show categories tagged to All or to the forced language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); - } - } - } - - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - - $linkParameters = array( - 'layout' => 'edit', - 'itemtype' => $extensionName . '.' . $typeName, - 'task' => 'association.edit', - ); - - $this->editUri = 'index.php?option=com_associations&view=association&' . http_build_query($linkParameters); - } - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new \Exception(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $user = Factory::getUser(); - - if (isset($this->typeName) && isset($this->extensionName)) - { - $helper = AssociationsHelper::getExtensionHelper($this->extensionName); - $title = $helper->getTypeTitle($this->typeName); - - $languageKey = strtoupper($this->extensionName . '_' . $title . 'S'); - - if ($this->typeName === 'category') - { - $languageKey = strtoupper($this->extensionName) . '_CATEGORIES'; - } - - ToolbarHelper::title( - Text::sprintf( - 'COM_ASSOCIATIONS_TITLE_LIST', Text::_($this->extensionName), Text::_($languageKey) - ), 'language assoc' - ); - } - else - { - ToolbarHelper::title(Text::_('COM_ASSOCIATIONS_TITLE_LIST_SELECT'), 'language assoc'); - } - - if ($user->authorise('core.admin', 'com_associations') || $user->authorise('core.options', 'com_associations')) - { - if (!isset($this->typeName)) - { - ToolbarHelper::custom('associations.purge', 'purge', '', 'COM_ASSOCIATIONS_PURGE', false, false); - ToolbarHelper::custom('associations.clean', 'refresh', '', 'COM_ASSOCIATIONS_DELETE_ORPHANS', false, false); - } - - ToolbarHelper::preferences('com_associations'); - } - - ToolbarHelper::help('Multilingual_Associations'); - } + /** + * An array of items + * + * @var array + * + * @since 3.7.0 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.7.0 + */ + protected $pagination; + + /** + * The model state + * + * @var object + * + * @since 3.7.0 + */ + protected $state; + + /** + * Selected item type properties. + * + * @var \Joomla\Registry\Registry + * + * @since 3.7.0 + */ + public $itemType = null; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!Associations::isEnabled()) { + $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . AssociationsHelper::getLanguagefilterPluginId()); + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_ASSOCIATIONS_ERROR_NO_ASSOC', $link), 'warning'); + } elseif ($this->state->get('itemtype') != '' && $this->state->get('language') != '') { + $type = null; + + list($extensionName, $typeName) = explode('.', $this->state->get('itemtype'), 2); + + $extension = AssociationsHelper::getSupportedExtension($extensionName); + + $types = $extension->get('types'); + + if (\array_key_exists($typeName, $types)) { + $type = $types[$typeName]; + } + + $this->itemType = $type; + + if (\is_null($type)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_ASSOCIATIONS_ERROR_NO_TYPE'), 'warning'); + } else { + $this->extensionName = $extensionName; + $this->typeName = $typeName; + $this->typeSupports = array(); + $this->typeFields = array(); + + $details = $type->get('details'); + + if (\array_key_exists('support', $details)) { + $support = $details['support']; + $this->typeSupports = $support; + } + + if (\array_key_exists('fields', $details)) { + $fields = $details['fields']; + $this->typeFields = $fields; + } + + // Dynamic filter form. + // This selectors doesn't have to activate the filter bar. + unset($this->activeFilters['itemtype']); + unset($this->activeFilters['language']); + + // Remove filters options depending on selected type. + if (empty($support['state'])) { + unset($this->activeFilters['state']); + $this->filterForm->removeField('state', 'filter'); + } + + if (empty($support['category'])) { + unset($this->activeFilters['category_id']); + $this->filterForm->removeField('category_id', 'filter'); + } + + if ($extensionName !== 'com_menus') { + unset($this->activeFilters['menutype']); + $this->filterForm->removeField('menutype', 'filter'); + } + + if (empty($support['level'])) { + unset($this->activeFilters['level']); + $this->filterForm->removeField('level', 'filter'); + } + + if (empty($support['acl'])) { + unset($this->activeFilters['access']); + $this->filterForm->removeField('access', 'filter'); + } + + // Add extension attribute to category filter. + if (empty($support['catid'])) { + $this->filterForm->setFieldAttribute('category_id', 'extension', $extensionName, 'filter'); + + if ($this->getLayout() == 'modal') { + // We need to change the category filter to only show categories tagged to All or to the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + } + + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + + $linkParameters = array( + 'layout' => 'edit', + 'itemtype' => $extensionName . '.' . $typeName, + 'task' => 'association.edit', + ); + + $this->editUri = 'index.php?option=com_associations&view=association&' . http_build_query($linkParameters); + } + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new \Exception(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $user = $this->getCurrentUser(); + + if (isset($this->typeName) && isset($this->extensionName)) { + $helper = AssociationsHelper::getExtensionHelper($this->extensionName); + $title = $helper->getTypeTitle($this->typeName); + + $languageKey = strtoupper($this->extensionName . '_' . $title . 'S'); + + if ($this->typeName === 'category') { + $languageKey = strtoupper($this->extensionName) . '_CATEGORIES'; + } + + ToolbarHelper::title( + Text::sprintf( + 'COM_ASSOCIATIONS_TITLE_LIST', + Text::_($this->extensionName), + Text::_($languageKey) + ), + 'language assoc' + ); + } else { + ToolbarHelper::title(Text::_('COM_ASSOCIATIONS_TITLE_LIST_SELECT'), 'language assoc'); + } + + if ($user->authorise('core.admin', 'com_associations') || $user->authorise('core.options', 'com_associations')) { + if (!isset($this->typeName)) { + ToolbarHelper::custom('associations.purge', 'purge', '', 'COM_ASSOCIATIONS_PURGE', false, false); + ToolbarHelper::custom('associations.clean', 'refresh', '', 'COM_ASSOCIATIONS_DELETE_ORPHANS', false, false); + } + + ToolbarHelper::preferences('com_associations'); + } + + ToolbarHelper::help('Multilingual_Associations'); + } } diff --git a/code/administrator/components/com_associations/tmpl/association/edit.php b/code/administrator/components/com_associations/tmpl/association/edit.php index 422799a4..0f7b5bc1 100644 --- a/code/administrator/components/com_associations/tmpl/association/edit.php +++ b/code/administrator/components/com_associations/tmpl/association/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->usePreset('com_associations.sidebyside') - ->useScript('webcomponent.core-loader'); + ->useScript('form.validate') + ->usePreset('com_associations.sidebyside') + ->useScript('webcomponent.core-loader'); $options = [ - 'layout' => $this->app->input->get('layout', '', 'string'), - 'itemtype' => $this->itemType, - 'id' => $this->referenceId, + 'layout' => $this->app->input->get('layout', '', 'string'), + 'itemtype' => $this->itemType, + 'id' => $this->referenceId, ]; ?>
-
-
-
-

- -
-
-
-
-
-
-

-
-
-
- form->getLabel('itemlanguage'); ?> -
- form->getInput('itemlanguage'); ?> -
-
- form->getInput('modalassociation'); ?> -
-
- -
-
-
+
+
+
+

+ +
+
+
+
+
+
+

+
+
+
+ form->getLabel('itemlanguage'); ?> +
+ form->getInput('itemlanguage'); ?> +
+
+ form->getInput('modalassociation'); ?> +
+
+ +
+
+
- - - + + +
diff --git a/code/administrator/components/com_associations/tmpl/associations/default.php b/code/administrator/components/com_associations/tmpl/associations/default.php index baddfd2a..ec539fed 100644 --- a/code/administrator/components/com_associations/tmpl/associations/default.php +++ b/code/administrator/components/com_associations/tmpl/associations/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect') - ->useScript('com_associations.admin-associations-default'); +$wa->useScript('com_associations.admin-associations-default') + ->useScript('table.columns') + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $canManageCheckin = Factory::getUser()->authorise('core.manage', 'com_checkin'); $iconStates = array( - -2 => 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', + -2 => 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', ); Text::script('COM_ASSOCIATIONS_PURGE_CONFIRM_PROMPT', true); ?>
-
-
-
- $this)); ?> - state->get('itemtype') == '' || $this->state->get('language') == '') : ?> -
- - -
- items)) : ?> -
- - -
- - - - - - typeSupports['state'])) : ?> - - - - - - - typeFields['menutype'])) : ?> - - - typeFields['access'])) : ?> - - - - - - - items as $i => $item) : - $canCheckin = true; - $canEdit = AssociationsHelper::allowEdit($this->extensionName, $this->typeName, $item->id); - $canCheckin = $canManageCheckin || AssociationsHelper::canCheckinItem($this->extensionName, $this->typeName, $item->id); - $isCheckout = AssociationsHelper::isCheckoutItem($this->extensionName, $this->typeName, $item->id); - ?> - - typeSupports['state'])) : ?> - - - - - - - typeFields['menutype'])) : ?> - - - typeFields['access'])) : ?> - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - -
- - -
- level)) : ?> - $item->level)); ?> - - - editor, $item->checked_out_time, 'associations.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - - typeFields['alias'])) : ?> -
- escape($item->alias)); ?> -
- - typeFields['catid'])) : ?> -
- escape($item->category_title); ?> -
- -
-
- - - extensionName, $this->typeName, (int) $item->id, $item->language, !$isCheckout, false); ?> - - extensionName, $this->typeName, (int) $item->id, $item->language, !$isCheckout, true); ?> - - escape($item->menutype_title); ?> - - escape($item->access_level); ?> - - id; ?> -
+
+
+
+ $this)); ?> + state->get('itemtype') == '' || $this->state->get('language') == '') : ?> +
+ + +
+ items)) : ?> +
+ + +
+ + + + + + typeSupports['state'])) : ?> + + + + + + + typeFields['menutype'])) : ?> + + + typeFields['access'])) : ?> + + + + + + + items as $i => $item) : + $canCheckin = true; + $canEdit = AssociationsHelper::allowEdit($this->extensionName, $this->typeName, $item->id); + $canCheckin = $canManageCheckin || AssociationsHelper::canCheckinItem($this->extensionName, $this->typeName, $item->id); + $isCheckout = AssociationsHelper::isCheckoutItem($this->extensionName, $this->typeName, $item->id); + ?> + + typeSupports['state'])) : ?> + + + + + + + typeFields['menutype'])) : ?> + + + typeFields['access'])) : ?> + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + +
+ + +
+ level)) : ?> + $item->level)); ?> + + + editor, $item->checked_out_time, 'associations.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + + typeFields['alias'])) : ?> +
+ escape($item->alias)); ?> +
+ + typeFields['catid'])) : ?> +
+ escape($item->category_title); ?> +
+ +
+
+ + + extensionName, $this->typeName, (int) $item->id, $item->language, !$isCheckout, false); ?> + + extensionName, $this->typeName, (int) $item->id, $item->language, !$isCheckout, true); ?> + + escape($item->menutype_title); ?> + + escape($item->access_level); ?> + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - -
-
-
+ + + +
+
+
diff --git a/code/administrator/components/com_associations/tmpl/associations/modal.php b/code/administrator/components/com_associations/tmpl/associations/modal.php index b46c1659..398e2712 100644 --- a/code/administrator/components/com_associations/tmpl/associations/modal.php +++ b/code/administrator/components/com_associations/tmpl/associations/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('multiselect') - ->useScript('com_associations.admin-associations-modal'); + ->useScript('com_associations.admin-associations-modal'); $function = $app->input->getCmd('function', 'jSelectAssociation'); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -35,136 +35,136 @@ $canManageCheckin = Factory::getUser()->authorise('core.manage', 'com_checkin'); $iconStates = array( - -2 => 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', + -2 => 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', ); $this->document->addScriptOptions('associations-modal', ['func' => $function]); ?>
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - typeSupports['state'])) : ?> - - - - - - typeFields['menutype'])) : ?> - - - typeSupports['acl'])) : ?> - - - - - - - items as $i => $item) : - $canEdit = AssociationsHelper::allowEdit($this->extensionName, $this->typeName, $item->id); - $canCheckin = $canManageCheckin || AssociationsHelper::canCheckinItem($this->extensionName, $this->typeName, $item->id); - $isCheckout = AssociationsHelper::isCheckoutItem($this->extensionName, $this->typeName, $item->id); - ?> - - typeSupports['state'])) : ?> - - - - - - typeFields['menutype'])) : ?> - - - typeSupports['acl'])) : ?> - - - - - - -
- , - , - -
- - - - - - - - - - - - - -
- - - - - level)) : ?> - $item->level)); ?> - - - - escape($item->title); ?> - - editor, $item->checked_out_time, 'associations.'); ?> - - escape($item->title); ?> - - - escape($item->title); ?> - - typeFields['alias'])) : ?> - - escape($item->alias)); ?> - - - typeFields['catid'])) : ?> -
- escape($item->category_title); ?> -
- -
- - - association) : ?> - extensionName, $this->typeName, (int) $item->id, $item->language, false, false); ?> - - - escape($item->menutype_title); ?> - - escape($item->access_level); ?> - - id; ?> -
+ + $this)); ?> + items)) : ?> +
+ + +
+ + + + + + typeSupports['state'])) : ?> + + + + + + typeFields['menutype'])) : ?> + + + typeSupports['acl'])) : ?> + + + + + + + items as $i => $item) : + $canEdit = AssociationsHelper::allowEdit($this->extensionName, $this->typeName, $item->id); + $canCheckin = $canManageCheckin || AssociationsHelper::canCheckinItem($this->extensionName, $this->typeName, $item->id); + $isCheckout = AssociationsHelper::isCheckoutItem($this->extensionName, $this->typeName, $item->id); + ?> + + typeSupports['state'])) : ?> + + + + + + typeFields['menutype'])) : ?> + + + typeSupports['acl'])) : ?> + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + +
+ + + + + level)) : ?> + $item->level)); ?> + + + + escape($item->title); ?> + + editor, $item->checked_out_time, 'associations.'); ?> + + escape($item->title); ?> + + + escape($item->title); ?> + + typeFields['alias'])) : ?> + + escape($item->alias)); ?> + + + typeFields['catid'])) : ?> +
+ escape($item->category_title); ?> +
+ +
+ + + association) : ?> + extensionName, $this->typeName, (int) $item->id, $item->language, false, false); ?> + + + escape($item->menutype_title); ?> + + escape($item->access_level); ?> + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - -
+ + + + +
diff --git a/code/administrator/components/com_banners/banners.xml b/code/administrator/components/com_banners/banners.xml index b907e858..22c89551 100644 --- a/code/administrator/components/com_banners/banners.xml +++ b/code/administrator/components/com_banners/banners.xml @@ -2,7 +2,7 @@ com_banners Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_banners/forms/filter_tracks.xml b/code/administrator/components/com_banners/forms/filter_tracks.xml index 28d33929..50c734aa 100644 --- a/code/administrator/components/com_banners/forms/filter_tracks.xml +++ b/code/administrator/components/com_banners/forms/filter_tracks.xml @@ -63,6 +63,7 @@ hint="COM_BANNERS_BEGIN_HINT" format="%Y-%m-%d" filter="user_utc" + onchange="this.form.submit();" /> diff --git a/code/administrator/components/com_banners/helpers/banners.php b/code/administrator/components/com_banners/helpers/banners.php index 97747c9c..5e63b6e8 100644 --- a/code/administrator/components/com_banners/helpers/banners.php +++ b/code/administrator/components/com_banners/helpers/banners.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Banners component helper. @@ -18,5 +23,4 @@ */ class BannersHelper extends \Joomla\Component\Banners\Administrator\Helper\BannersHelper { - } diff --git a/code/administrator/components/com_banners/services/provider.php b/code/administrator/components/com_banners/services/provider.php index 6f629fed..02bb499b 100644 --- a/code/administrator/components/com_banners/services/provider.php +++ b/code/administrator/components/com_banners/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Banners')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Banners')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Banners')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Banners')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Banners')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Banners')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Banners')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Banners')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new BannersComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new BannersComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_banners/src/Controller/BannerController.php b/code/administrator/components/com_banners/src/Controller/BannerController.php index bd3f642f..195b04fc 100644 --- a/code/administrator/components/com_banners/src/Controller/BannerController.php +++ b/code/administrator/components/com_banners/src/Controller/BannerController.php @@ -1,4 +1,5 @@ input->getInt('filter_category_id'); - $categoryId = ArrayHelper::getValue($data, 'catid', $filter, 'int'); - - if ($categoryId) - { - // If the category has been passed in the URL check it. - return $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); - } - - // In the absence of better information, revert to the component permissions. - return parent::allowAdd($data); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $categoryId = 0; - - if ($recordId) - { - $categoryId = (int) $this->getModel()->getItem($recordId)->catid; - } - - if ($categoryId) - { - // The category has been set. Check the category permissions. - return $this->app->getIdentity()->authorise('core.edit', $this->option . '.category.' . $categoryId); - } - - // Since there is no asset tracking, revert to the component permissions. - return parent::allowEdit($data, $key); - } - - /** - * Method to run batch operations. - * - * @param string $model The model - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Banner', '', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_banners&view=banners' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } + use VersionableControllerTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_BANNERS_BANNER'; + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $filter = $this->input->getInt('filter_category_id'); + $categoryId = ArrayHelper::getValue($data, 'catid', $filter, 'int'); + + if ($categoryId) { + // If the category has been passed in the URL check it. + return $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); + } + + // In the absence of better information, revert to the component permissions. + return parent::allowAdd($data); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $categoryId = 0; + + if ($recordId) { + $categoryId = (int) $this->getModel()->getItem($recordId)->catid; + } + + if ($categoryId) { + // The category has been set. Check the category permissions. + return $this->app->getIdentity()->authorise('core.edit', $this->option . '.category.' . $categoryId); + } + + // Since there is no asset tracking, revert to the component permissions. + return parent::allowEdit($data, $key); + } + + /** + * Method to run batch operations. + * + * @param string $model The model + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Banner', '', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_banners&view=banners' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } } diff --git a/code/administrator/components/com_banners/src/Controller/BannersController.php b/code/administrator/components/com_banners/src/Controller/BannersController.php index 8dcc0df9..363c700d 100644 --- a/code/administrator/components/com_banners/src/Controller/BannersController.php +++ b/code/administrator/components/com_banners/src/Controller/BannersController.php @@ -1,4 +1,5 @@ registerTask('sticky_unpublish', 'sticky_publish'); - } + $this->registerTask('sticky_unpublish', 'sticky_publish'); + } - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 1.6 - */ - public function getModel($name = 'Banner', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Banner', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } - /** - * Stick items - * - * @return void - * - * @since 1.6 - */ - public function sticky_publish() - { - // Check for request forgeries. - $this->checkToken(); + /** + * Stick items + * + * @return void + * + * @since 1.6 + */ + public function sticky_publish() + { + // Check for request forgeries. + $this->checkToken(); - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('sticky_publish' => 1, 'sticky_unpublish' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('sticky_publish' => 1, 'sticky_unpublish' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); - // Remove zero values resulting from input filter - $ids = array_filter($ids); + // Remove zero values resulting from input filter + $ids = array_filter($ids); - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('COM_BANNERS_NO_BANNERS_SELECTED'), 'warning'); - } - else - { - // Get the model. - /** @var \Joomla\Component\Banners\Administrator\Model\BannerModel $model */ - $model = $this->getModel(); + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('COM_BANNERS_NO_BANNERS_SELECTED'), 'warning'); + } else { + // Get the model. + /** @var \Joomla\Component\Banners\Administrator\Model\BannerModel $model */ + $model = $this->getModel(); - // Change the state of the records. - if (!$model->stick($ids, $value)) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - else - { - if ($value == 1) - { - $ntext = 'COM_BANNERS_N_BANNERS_STUCK'; - } - else - { - $ntext = 'COM_BANNERS_N_BANNERS_UNSTUCK'; - } + // Change the state of the records. + if (!$model->stick($ids, $value)) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } else { + if ($value == 1) { + $ntext = 'COM_BANNERS_N_BANNERS_STUCK'; + } else { + $ntext = 'COM_BANNERS_N_BANNERS_UNSTUCK'; + } - $this->setMessage(Text::plural($ntext, \count($ids))); - } - } + $this->setMessage(Text::plural($ntext, \count($ids))); + } + } - $this->setRedirect('index.php?option=com_banners&view=banners'); - } + $this->setRedirect('index.php?option=com_banners&view=banners'); + } } diff --git a/code/administrator/components/com_banners/src/Controller/ClientController.php b/code/administrator/components/com_banners/src/Controller/ClientController.php index 9b28ec21..bb29ccc1 100644 --- a/code/administrator/components/com_banners/src/Controller/ClientController.php +++ b/code/administrator/components/com_banners/src/Controller/ClientController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Client', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_banners/src/Controller/DisplayController.php b/code/administrator/components/com_banners/src/Controller/DisplayController.php index 41468a8b..95cee5ca 100644 --- a/code/administrator/components/com_banners/src/Controller/DisplayController.php +++ b/code/administrator/components/com_banners/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'banners'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + $view = $this->input->get('view', 'banners'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'banner' && $layout == 'edit' && !$this->checkEditId('com_banners.edit.banner', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'banner' && $layout == 'edit' && !$this->checkEditId('com_banners.edit.banner', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_banners&view=banners', false)); + $this->setRedirect(Route::_('index.php?option=com_banners&view=banners', false)); - return false; - } - elseif ($view == 'client' && $layout == 'edit' && !$this->checkEditId('com_banners.edit.client', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + return false; + } elseif ($view == 'client' && $layout == 'edit' && !$this->checkEditId('com_banners.edit.client', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_banners&view=clients', false)); + $this->setRedirect(Route::_('index.php?option=com_banners&view=clients', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/code/administrator/components/com_banners/src/Controller/TracksController.php b/code/administrator/components/com_banners/src/Controller/TracksController.php index fbca3db5..b7059415 100644 --- a/code/administrator/components/com_banners/src/Controller/TracksController.php +++ b/code/administrator/components/com_banners/src/Controller/TracksController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to remove a record. - * - * @return void - * - * @since 1.6 - */ - public function delete() - { - // Check for request forgeries. - $this->checkToken(); - - // Get the model. - /** @var \Joomla\Component\Banners\Administrator\Model\TracksModel $model */ - $model = $this->getModel(); - - // Load the filter state. - $model->setState('filter.type', $this->app->getUserState($this->context . '.filter.type')); - $model->setState('filter.begin', $this->app->getUserState($this->context . '.filter.begin')); - $model->setState('filter.end', $this->app->getUserState($this->context . '.filter.end')); - $model->setState('filter.category_id', $this->app->getUserState($this->context . '.filter.category_id')); - $model->setState('filter.client_id', $this->app->getUserState($this->context . '.filter.client_id')); - $model->setState('list.limit', 0); - $model->setState('list.start', 0); - - $count = $model->getTotal(); - - // Remove the items. - if (!$model->delete()) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - elseif ($count > 0) - { - $this->setMessage(Text::plural('COM_BANNERS_TRACKS_N_ITEMS_DELETED', $count)); - } - else - { - $this->setMessage(Text::_('COM_BANNERS_TRACKS_NO_ITEMS_DELETED')); - } - - $this->setRedirect('index.php?option=com_banners&view=tracks'); - } - - /** - * Display method for the raw track data. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. - * - * @return static This object to support chaining. - * - * @since 1.5 - * @todo This should be done as a view, not here! - */ - public function display($cachable = false, $urlparams = array()) - { - // Get the document object. - $vName = 'tracks'; - - // Get and render the view. - if ($view = $this->getView($vName, 'raw')) - { - // Check for request forgeries. - $this->checkToken('GET'); - - // Get the model for the view. - /** @var \Joomla\Component\Banners\Administrator\Model\TracksModel $model */ - $model = $this->getModel($vName); - - // Load the filter state. - $app = $this->app; - - $model->setState('filter.type', $app->getUserState($this->context . '.filter.type')); - $model->setState('filter.begin', $app->getUserState($this->context . '.filter.begin')); - $model->setState('filter.end', $app->getUserState($this->context . '.filter.end')); - $model->setState('filter.category_id', $app->getUserState($this->context . '.filter.category_id')); - $model->setState('filter.client_id', $app->getUserState($this->context . '.filter.client_id')); - $model->setState('list.limit', 0); - $model->setState('list.start', 0); - - $form = $this->input->get('jform', array(), 'array'); - - $model->setState('basename', $form['basename']); - $model->setState('compressed', $form['compressed']); - - // Create one year cookies. - $cookieLifeTime = time() + 365 * 86400; - $cookieDomain = $app->get('cookie_domain', ''); - $cookiePath = $app->get('cookie_path', '/'); - $isHttpsForced = $app->isHttpsForced(); - - $this->input->cookie->set( - ApplicationHelper::getHash($this->context . '.basename'), - $form['basename'], - $cookieLifeTime, - $cookiePath, - $cookieDomain, - $isHttpsForced, - true - ); - - $this->input->cookie->set( - ApplicationHelper::getHash($this->context . '.compressed'), - $form['compressed'], - $cookieLifeTime, - $cookiePath, - $cookieDomain, - $isHttpsForced, - true - ); - - // Push the model into the view (as default). - $view->setModel($model, true); - - // Push document object into the view. - $view->document = $this->app->getDocument(); - - $view->display(); - } - - return $this; - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $context = 'com_banners.tracks'; + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Tracks', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to remove a record. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + // Check for request forgeries. + $this->checkToken(); + + // Get the model. + /** @var \Joomla\Component\Banners\Administrator\Model\TracksModel $model */ + $model = $this->getModel(); + + // Load the filter state. + $model->setState('filter.type', $this->app->getUserState($this->context . '.filter.type')); + $model->setState('filter.begin', $this->app->getUserState($this->context . '.filter.begin')); + $model->setState('filter.end', $this->app->getUserState($this->context . '.filter.end')); + $model->setState('filter.category_id', $this->app->getUserState($this->context . '.filter.category_id')); + $model->setState('filter.client_id', $this->app->getUserState($this->context . '.filter.client_id')); + $model->setState('list.limit', 0); + $model->setState('list.start', 0); + + $count = $model->getTotal(); + + // Remove the items. + if (!$model->delete()) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } elseif ($count > 0) { + $this->setMessage(Text::plural('COM_BANNERS_TRACKS_N_ITEMS_DELETED', $count)); + } else { + $this->setMessage(Text::_('COM_BANNERS_TRACKS_NO_ITEMS_DELETED')); + } + + $this->setRedirect('index.php?option=com_banners&view=tracks'); + } + + /** + * Display method for the raw track data. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + * @todo This should be done as a view, not here! + */ + public function display($cachable = false, $urlparams = array()) + { + // Get the document object. + $vName = 'tracks'; + + // Get and render the view. + if ($view = $this->getView($vName, 'raw')) { + // Check for request forgeries. + $this->checkToken('GET'); + + // Get the model for the view. + /** @var \Joomla\Component\Banners\Administrator\Model\TracksModel $model */ + $model = $this->getModel($vName); + + // Load the filter state. + $app = $this->app; + + $model->setState('filter.type', $app->getUserState($this->context . '.filter.type')); + $model->setState('filter.begin', $app->getUserState($this->context . '.filter.begin')); + $model->setState('filter.end', $app->getUserState($this->context . '.filter.end')); + $model->setState('filter.category_id', $app->getUserState($this->context . '.filter.category_id')); + $model->setState('filter.client_id', $app->getUserState($this->context . '.filter.client_id')); + $model->setState('list.limit', 0); + $model->setState('list.start', 0); + + $form = $this->input->get('jform', array(), 'array'); + + $model->setState('basename', $form['basename']); + $model->setState('compressed', $form['compressed']); + + // Create one year cookies. + $cookieLifeTime = time() + 365 * 86400; + $cookieDomain = $app->get('cookie_domain', ''); + $cookiePath = $app->get('cookie_path', '/'); + $isHttpsForced = $app->isHttpsForced(); + + $this->input->cookie->set( + ApplicationHelper::getHash($this->context . '.basename'), + $form['basename'], + $cookieLifeTime, + $cookiePath, + $cookieDomain, + $isHttpsForced, + true + ); + + $this->input->cookie->set( + ApplicationHelper::getHash($this->context . '.compressed'), + $form['compressed'], + $cookieLifeTime, + $cookiePath, + $cookieDomain, + $isHttpsForced, + true + ); + + // Push the model into the view (as default). + $view->setModel($model, true); + + // Push document object into the view. + $view->document = $this->app->getDocument(); + + $view->display(); + } + + return $this; + } } diff --git a/code/administrator/components/com_banners/src/Extension/BannersComponent.php b/code/administrator/components/com_banners/src/Extension/BannersComponent.php index 316121c2..75b40c9e 100644 --- a/code/administrator/components/com_banners/src/Extension/BannersComponent.php +++ b/code/administrator/components/com_banners/src/Extension/BannersComponent.php @@ -1,4 +1,5 @@ setDatabase($container->get(DatabaseInterface::class)); - /** - * Booting the extension. This is the function to set up the environment of the extension like - * registering new class loaders, etc. - * - * If required, some initial set up can be done from services of the container, eg. - * registering HTML services. - * - * @param ContainerInterface $container The container - * - * @return void - * - * @since 4.0.0 - */ - public function boot(ContainerInterface $container) - { - $this->getRegistry()->register('banner', new Banner); - } + $this->getRegistry()->register('banner', $banner); + } - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return 'banners'; - } + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return 'banners'; + } } diff --git a/code/administrator/components/com_banners/src/Field/BannerclientField.php b/code/administrator/components/com_banners/src/Field/BannerclientField.php index 5c0f3386..9afb8945 100644 --- a/code/administrator/components/com_banners/src/Field/BannerclientField.php +++ b/code/administrator/components/com_banners/src/Field/BannerclientField.php @@ -1,4 +1,5 @@ id . '\').value=\'0\';"'; + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $onclick = ' onclick="document.getElementById(\'' . $this->id . '\').value=\'0\';"'; - return '
' - . '
'; - } + return '
' + . '
'; + } } diff --git a/code/administrator/components/com_banners/src/Field/ImpmadeField.php b/code/administrator/components/com_banners/src/Field/ImpmadeField.php index f385b853..3cf5a255 100644 --- a/code/administrator/components/com_banners/src/Field/ImpmadeField.php +++ b/code/administrator/components/com_banners/src/Field/ImpmadeField.php @@ -1,4 +1,5 @@ id . '\').value=\'0\';"'; + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $onclick = ' onclick="document.getElementById(\'' . $this->id . '\').value=\'0\';"'; - return '
' - . '
'; - } + return '
' + . '
'; + } } diff --git a/code/administrator/components/com_banners/src/Field/ImptotalField.php b/code/administrator/components/com_banners/src/Field/ImptotalField.php index 54550bba..2fc6b6c5 100644 --- a/code/administrator/components/com_banners/src/Field/ImptotalField.php +++ b/code/administrator/components/com_banners/src/Field/ImptotalField.php @@ -1,4 +1,5 @@ id . '_unlimited\').checked=document.getElementById(\'' . $this->id - . '\').value==\'\';"'; - $onclick = ' onclick="if (document.getElementById(\'' . $this->id . '_unlimited\').checked) document.getElementById(\'' . $this->id - . '\').value=\'\';"'; - $value = empty($this->value) ? '' : $this->value; - $checked = empty($this->value) ? ' checked="checked"' : ''; + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $class = ' class="form-control validate-numeric text_area"'; + $onchange = ' onchange="document.getElementById(\'' . $this->id . '_unlimited\').checked=document.getElementById(\'' . $this->id + . '\').value==\'\';"'; + $onclick = ' onclick="if (document.getElementById(\'' . $this->id . '_unlimited\').checked) document.getElementById(\'' . $this->id + . '\').value=\'\';"'; + $value = empty($this->value) ? '' : $this->value; + $checked = empty($this->value) ? ' checked="checked"' : ''; - return '' - . '
' - . '
'; - } + return '' + . '
' + . '
'; + } } diff --git a/code/administrator/components/com_banners/src/Helper/BannersHelper.php b/code/administrator/components/com_banners/src/Helper/BannersHelper.php index 824318b5..81e9efda 100644 --- a/code/administrator/components/com_banners/src/Helper/BannersHelper.php +++ b/code/administrator/components/com_banners/src/Helper/BannersHelper.php @@ -1,4 +1,5 @@ getIdentity(); - - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__banners')) - ->where( - [ - $db->quoteName('reset') . ' <= :date', - $db->quoteName('reset') . ' IS NOT NULL', - ] - ) - ->bind(':date', $date) - ->extendWhere( - 'AND', - [ - $db->quoteName('checked_out') . ' IS NULL', - $db->quoteName('checked_out') . ' = :userId', - ], - 'OR' - ) - ->bind(':userId', $user->id, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - foreach ($rows as $row) - { - $purchaseType = $row->purchase_type; - - if ($purchaseType < 0 && $row->cid) - { - /** @var \Joomla\Component\Banners\Administrator\Table\ClientTable $client */ - $client = Table::getInstance('ClientTable', '\\Joomla\\Component\\Banners\\Administrator\\Table\\'); - $client->load($row->cid); - $purchaseType = $client->purchase_type; - } - - if ($purchaseType < 0) - { - $params = ComponentHelper::getParams('com_banners'); - $purchaseType = $params->get('purchase_type'); - } - - switch ($purchaseType) - { - case 1: - $reset = null; - break; - case 2: - $date = Factory::getDate('+1 year ' . date('Y-m-d')); - $reset = $date->toSql(); - break; - case 3: - $date = Factory::getDate('+1 month ' . date('Y-m-d')); - $reset = $date->toSql(); - break; - case 4: - $date = Factory::getDate('+7 day ' . date('Y-m-d')); - $reset = $date->toSql(); - break; - case 5: - $date = Factory::getDate('+1 day ' . date('Y-m-d')); - $reset = $date->toSql(); - break; - } - - // Update the row ordering field. - $query = $db->getQuery(true) - ->update($db->quoteName('#__banners')) - ->set( - [ - $db->quoteName('reset') . ' = :reset', - $db->quoteName('impmade') . ' = 0', - $db->quoteName('clicks') . ' = 0', - ] - ) - ->where($db->quoteName('id') . ' = :id') - ->bind(':reset', $reset, $reset === null ? ParameterType::NULL : ParameterType::STRING) - ->bind(':id', $row->id, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - - return true; - } - - /** - * Get client list in text/value format for a select field - * - * @return array - */ - public static function getClientOptions() - { - $options = array(); - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id', 'value'), - $db->quoteName('name', 'text'), - ] - ) - ->from($db->quoteName('#__banner_clients', 'a')) - ->where($db->quoteName('a.state') . ' = 1') - ->order($db->quoteName('a.name')); - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('COM_BANNERS_NO_CLIENT'))); - - return $options; - } + /** + * Update / reset the banners + * + * @return boolean + * + * @since 1.6 + */ + public static function updateReset() + { + $db = Factory::getDbo(); + $date = Factory::getDate(); + $app = Factory::getApplication(); + $user = $app->getIdentity(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__banners')) + ->where( + [ + $db->quoteName('reset') . ' <= :date', + $db->quoteName('reset') . ' IS NOT NULL', + ] + ) + ->bind(':date', $date) + ->extendWhere( + 'AND', + [ + $db->quoteName('checked_out') . ' IS NULL', + $db->quoteName('checked_out') . ' = :userId', + ], + 'OR' + ) + ->bind(':userId', $user->id, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + foreach ($rows as $row) { + $purchaseType = $row->purchase_type; + + if ($purchaseType < 0 && $row->cid) { + /** @var \Joomla\Component\Banners\Administrator\Table\ClientTable $client */ + $client = Table::getInstance('ClientTable', '\\Joomla\\Component\\Banners\\Administrator\\Table\\'); + $client->load($row->cid); + $purchaseType = $client->purchase_type; + } + + if ($purchaseType < 0) { + $params = ComponentHelper::getParams('com_banners'); + $purchaseType = $params->get('purchase_type'); + } + + switch ($purchaseType) { + case 1: + $reset = null; + break; + case 2: + $date = Factory::getDate('+1 year ' . date('Y-m-d')); + $reset = $date->toSql(); + break; + case 3: + $date = Factory::getDate('+1 month ' . date('Y-m-d')); + $reset = $date->toSql(); + break; + case 4: + $date = Factory::getDate('+7 day ' . date('Y-m-d')); + $reset = $date->toSql(); + break; + case 5: + $date = Factory::getDate('+1 day ' . date('Y-m-d')); + $reset = $date->toSql(); + break; + } + + // Update the row ordering field. + $query = $db->getQuery(true) + ->update($db->quoteName('#__banners')) + ->set( + [ + $db->quoteName('reset') . ' = :reset', + $db->quoteName('impmade') . ' = 0', + $db->quoteName('clicks') . ' = 0', + ] + ) + ->where($db->quoteName('id') . ' = :id') + ->bind(':reset', $reset, $reset === null ? ParameterType::NULL : ParameterType::STRING) + ->bind(':id', $row->id, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } + + return true; + } + + /** + * Get client list in text/value format for a select field + * + * @return array + */ + public static function getClientOptions() + { + $options = array(); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id', 'value'), + $db->quoteName('name', 'text'), + ] + ) + ->from($db->quoteName('#__banner_clients', 'a')) + ->where($db->quoteName('a.state') . ' = 1') + ->order($db->quoteName('a.name')); + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('COM_BANNERS_NO_CLIENT'))); + + return $options; + } } diff --git a/code/administrator/components/com_banners/src/Model/BannerModel.php b/code/administrator/components/com_banners/src/Model/BannerModel.php index 15485197..307b8896 100644 --- a/code/administrator/components/com_banners/src/Model/BannerModel.php +++ b/code/administrator/components/com_banners/src/Model/BannerModel.php @@ -1,4 +1,5 @@ 'batchClient', - 'language_id' => 'batchLanguage' - ); - - /** - * Batch client changes for a group of banners. - * - * @param string $value The new value matching a client. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - protected function batchClient($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - - /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $table */ - $table = $this->getTable(); - - foreach ($pks as $pk) - { - if (!$user->authorise('core.edit', $contexts[$pk])) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - - $table->reset(); - $table->load($pk); - $table->cid = (int) $value; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->state != -2) - { - return false; - } - - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.delete', 'com_banners.category.' . (int) $record->catid); - } - - return parent::canDelete($record); - } - - /** - * A method to preprocess generating a new title in order to allow tables with alternative names - * for alias and title to use the batch move and copy methods - * - * @param integer $categoryId The target category id - * @param Table $table The JTable within which move or copy is taking place - * - * @return void - * - * @since 3.8.12 - */ - public function generateTitle($categoryId, $table) - { - // Alter the title & alias - $data = $this->generateNewTitle($categoryId, $table->alias, $table->name); - $table->name = $data['0']; - $table->alias = $data['1']; - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - // Check against the category. - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.edit.state', 'com_banners.category.' . (int) $record->catid); - } - - // Default to component settings if category not known. - return parent::canEditState($record); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. [optional] - * @param boolean $loadData True if the form is to load its own data (default case), false if not. [optional] - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_banners.banner', 'banner', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('publish_up', 'disabled', 'true'); - $form->setFieldAttribute('publish_down', 'disabled', 'true'); - $form->setFieldAttribute('state', 'disabled', 'true'); - $form->setFieldAttribute('sticky', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('publish_up', 'filter', 'unset'); - $form->setFieldAttribute('publish_down', 'filter', 'unset'); - $form->setFieldAttribute('state', 'filter', 'unset'); - $form->setFieldAttribute('sticky', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_banners.edit.banner.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Prime some default values. - if ($this->getState('banner.id') == 0) - { - $filters = (array) $app->getUserState('com_banners.banners.filter'); - $filterCatId = $filters['category_id'] ?? null; - - $data->set('catid', $app->input->getInt('catid', $filterCatId)); - } - } - - $this->preprocessData('com_banners.banner', $data); - - return $data; - } - - /** - * Method to stick records. - * - * @param array $pks The ids of the items to publish. - * @param integer $value The value of the published state - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function stick(&$pks, $value = 1) - { - /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $table */ - $table = $this->getTable(); - $pks = (array) $pks; - - // Access checks. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if (!$this->canEditState($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - } - } - - // Attempt to change the state of the records. - if (!$table->stick($pks, $value, Factory::getUser()->id)) - { - $this->setError($table->getError()); - - return false; - } - - return true; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param Table $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('catid') . ' = ' . (int) $table->catid, - $this->_db->quoteName('state') . ' >= 0', - ]; - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param Table $table A Table object. - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - if (empty($table->id)) - { - // Set the values - $table->created = $date->toSql(); - $table->created_by = $user->id; - - // Set ordering to the last item if not set - if (empty($table->ordering)) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('MAX(' . $db->quoteName('ordering') . ')') - ->from($db->quoteName('#__banners')); - - $db->setQuery($query); - $max = $db->loadResult(); - - $table->ordering = $max + 1; - } - } - else - { - // Set the values - $table->modified = $date->toSql(); - $table->modified_by = $user->id; - } - - // Increment the content version number. - $table->version++; - } - - /** - * Allows preprocessing of the Form object. - * - * @param Form $form The form object - * @param array $data The data to be merged into the form object - * @param string $group The plugin group to be executed - * - * @return void - * - * @since 3.6.1 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - if ($this->canCreateCategory()) - { - $form->setFieldAttribute('catid', 'allowAdd', 'true'); - - // Add a prefix for categories created on the fly. - $form->setFieldAttribute('catid', 'customPrefix', '#new#'); - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $input = Factory::getApplication()->input; - - // Create new category, if needed. - $createCategory = true; - - // If category ID is provided, check if it's valid. - if (is_numeric($data['catid']) && $data['catid']) - { - $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_banners'); - } - - // Save New Category - if ($createCategory && $this->canCreateCategory()) - { - $category = [ - // Remove #new# prefix, if exists. - 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], - 'parent_id' => 1, - 'extension' => 'com_banners', - 'language' => $data['language'], - 'published' => 1, - ]; - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - - // Create new category. - if (!$categoryModel->save($category)) - { - $this->setError($categoryModel->getError()); - - return false; - } - - // Get the new category ID. - $data['catid'] = $categoryModel->getState('category.id'); - } - - // Alter the name for save as copy - if ($input->get('task') == 'save2copy') - { - /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $origTable */ - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['name'] == $origTable->name) - { - list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); - $data['name'] = $name; - $data['alias'] = $alias; - } - else - { - if ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - $data['state'] = 0; - } - - return parent::save($data); - } - - /** - * Is the user allowed to create an on the fly category? - * - * @return boolean - * - * @since 3.6.1 - */ - private function canCreateCategory() - { - return Factory::getUser()->authorise('core.create', 'com_banners'); - } + use VersionableModelTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_BANNERS_BANNER'; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_banners.banner'; + + /** + * Batch copy/move command. If set to false, the batch copy/move command is not supported + * + * @var string + */ + protected $batch_copymove = 'category_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'client_id' => 'batchClient', + 'language_id' => 'batchLanguage' + ); + + /** + * Batch client changes for a group of banners. + * + * @param string $value The new value matching a client. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + protected function batchClient($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + + /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $table */ + $table = $this->getTable(); + + foreach ($pks as $pk) { + if (!$user->authorise('core.edit', $contexts[$pk])) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + + $table->reset(); + $table->load($pk); + $table->cid = (int) $value; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->state != -2) { + return false; + } + + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.delete', 'com_banners.category.' . (int) $record->catid); + } + + return parent::canDelete($record); + } + + /** + * A method to preprocess generating a new title in order to allow tables with alternative names + * for alias and title to use the batch move and copy methods + * + * @param integer $categoryId The target category id + * @param Table $table The JTable within which move or copy is taking place + * + * @return void + * + * @since 3.8.12 + */ + public function generateTitle($categoryId, $table) + { + // Alter the title & alias + $data = $this->generateNewTitle($categoryId, $table->alias, $table->name); + $table->name = $data['0']; + $table->alias = $data['1']; + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + // Check against the category. + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.edit.state', 'com_banners.category.' . (int) $record->catid); + } + + // Default to component settings if category not known. + return parent::canEditState($record); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. [optional] + * @param boolean $loadData True if the form is to load its own data (default case), false if not. [optional] + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_banners.banner', 'banner', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + $form->setFieldAttribute('sticky', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + $form->setFieldAttribute('sticky', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_banners.edit.banner.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Prime some default values. + if ($this->getState('banner.id') == 0) { + $filters = (array) $app->getUserState('com_banners.banners.filter'); + $filterCatId = $filters['category_id'] ?? null; + + $data->set('catid', $app->input->getInt('catid', $filterCatId)); + } + } + + $this->preprocessData('com_banners.banner', $data); + + return $data; + } + + /** + * Method to stick records. + * + * @param array $pks The ids of the items to publish. + * @param integer $value The value of the published state + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function stick(&$pks, $value = 1) + { + /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $table */ + $table = $this->getTable(); + $pks = (array) $pks; + + // Access checks. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if (!$this->canEditState($table)) { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } + } + } + + // Attempt to change the state of the records. + if (!$table->stick($pks, $value, Factory::getUser()->id)) { + $this->setError($table->getError()); + + return false; + } + + return true; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param Table $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('catid') . ' = ' . (int) $table->catid, + $db->quoteName('state') . ' >= 0', + ]; + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param Table $table A Table object. + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + if (empty($table->id)) { + // Set the values + $table->created = $date->toSql(); + $table->created_by = $user->id; + + // Set ordering to the last item if not set + if (empty($table->ordering)) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('MAX(' . $db->quoteName('ordering') . ')') + ->from($db->quoteName('#__banners')); + + $db->setQuery($query); + $max = $db->loadResult(); + + $table->ordering = $max + 1; + } + } else { + // Set the values + $table->modified = $date->toSql(); + $table->modified_by = $user->id; + } + + // Increment the content version number. + $table->version++; + } + + /** + * Allows preprocessing of the Form object. + * + * @param Form $form The form object + * @param array $data The data to be merged into the form object + * @param string $group The plugin group to be executed + * + * @return void + * + * @since 3.6.1 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + if ($this->canCreateCategory()) { + $form->setFieldAttribute('catid', 'allowAdd', 'true'); + + // Add a prefix for categories created on the fly. + $form->setFieldAttribute('catid', 'customPrefix', '#new#'); + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $input = Factory::getApplication()->input; + + // Create new category, if needed. + $createCategory = true; + + // If category ID is provided, check if it's valid. + if (is_numeric($data['catid']) && $data['catid']) { + $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_banners'); + } + + // Save New Category + if ($createCategory && $this->canCreateCategory()) { + $category = [ + // Remove #new# prefix, if exists. + 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], + 'parent_id' => 1, + 'extension' => 'com_banners', + 'language' => $data['language'], + 'published' => 1, + ]; + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + + // Create new category. + if (!$categoryModel->save($category)) { + $this->setError($categoryModel->getError()); + + return false; + } + + // Get the new category ID. + $data['catid'] = $categoryModel->getState('category.id'); + } + + // Alter the name for save as copy + if ($input->get('task') == 'save2copy') { + /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $origTable */ + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['name'] == $origTable->name) { + list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); + $data['name'] = $name; + $data['alias'] = $alias; + } else { + if ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + $data['state'] = 0; + } + + return parent::save($data); + } + + /** + * Is the user allowed to create an on the fly category? + * + * @return boolean + * + * @since 3.6.1 + */ + private function canCreateCategory() + { + return Factory::getUser()->authorise('core.create', 'com_banners'); + } } diff --git a/code/administrator/components/com_banners/src/Model/BannersModel.php b/code/administrator/components/com_banners/src/Model/BannersModel.php index e0838007..16186d86 100644 --- a/code/administrator/components/com_banners/src/Model/BannersModel.php +++ b/code/administrator/components/com_banners/src/Model/BannersModel.php @@ -1,4 +1,5 @@ cache['categoryorders'])) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select( - [ - 'MAX(' . $db->quoteName('ordering') . ') AS ' . $db->quoteName('max'), - $db->quoteName('catid'), - ] - ) - ->from($db->quoteName('#__banners')) - ->group($db->quoteName('catid')); - $db->setQuery($query); - $this->cache['categoryorders'] = $db->loadAssocList('catid', 0); - } + /** + * Method to get the maximum ordering value for each category. + * + * @return array + * + * @since 1.6 + */ + public function &getCategoryOrders() + { + if (!isset($this->cache['categoryorders'])) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + 'MAX(' . $db->quoteName('ordering') . ') AS ' . $db->quoteName('max'), + $db->quoteName('catid'), + ] + ) + ->from($db->quoteName('#__banners')) + ->group($db->quoteName('catid')); + $db->setQuery($query); + $this->cache['categoryorders'] = $db->loadAssocList('catid', 0); + } - return $this->cache['categoryorders']; - } + return $this->cache['categoryorders']; + } - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDbo(); - $query = $db->getQuery(true); + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.name'), - $db->quoteName('a.alias'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.catid'), - $db->quoteName('a.clicks'), - $db->quoteName('a.metakey'), - $db->quoteName('a.sticky'), - $db->quoteName('a.impmade'), - $db->quoteName('a.imptotal'), - $db->quoteName('a.state'), - $db->quoteName('a.ordering'), - $db->quoteName('a.purchase_type'), - $db->quoteName('a.language'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - ] - ) - ) - ->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('uc.name', 'editor'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('cl.name', 'client_name'), - $db->quoteName('cl.purchase_type', 'client_purchase_type'), - ] - ) - ->from($db->quoteName('#__banners', 'a')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')); + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.name'), + $db->quoteName('a.alias'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.catid'), + $db->quoteName('a.clicks'), + $db->quoteName('a.metakey'), + $db->quoteName('a.sticky'), + $db->quoteName('a.impmade'), + $db->quoteName('a.imptotal'), + $db->quoteName('a.state'), + $db->quoteName('a.ordering'), + $db->quoteName('a.purchase_type'), + $db->quoteName('a.language'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + ] + ) + ) + ->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('uc.name', 'editor'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('cl.name', 'client_name'), + $db->quoteName('cl.purchase_type', 'client_purchase_type'), + ] + ) + ->from($db->quoteName('#__banners', 'a')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')); - // Filter by published state - $published = (string) $this->getState('filter.published'); + // Filter by published state + $published = (string) $this->getState('filter.published'); - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.state') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.state') . ' IN (0, 1)'); - } + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.state') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.state') . ' IN (0, 1)'); + } - // Filter by category. - $categoryId = $this->getState('filter.category_id'); + // Filter by category. + $categoryId = $this->getState('filter.category_id'); - if (is_numeric($categoryId)) - { - $categoryId = (int) $categoryId; - $query->where($db->quoteName('a.catid') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } + if (is_numeric($categoryId)) { + $categoryId = (int) $categoryId; + $query->where($db->quoteName('a.catid') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } - // Filter by client. - $clientId = $this->getState('filter.client_id'); + // Filter by client. + $clientId = $this->getState('filter.client_id'); - if (is_numeric($clientId)) - { - $clientId = (int) $clientId; - $query->where($db->quoteName('a.cid') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } + if (is_numeric($clientId)) { + $clientId = (int) $clientId; + $query->where($db->quoteName('a.cid') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } - // Filter by search in title - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('a.name') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - } + // Filter by search in title + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('a.name') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + } - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'a.name'); - $orderDirn = $this->state->get('list.direction', 'ASC'); + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'a.name'); + $orderDirn = $this->state->get('list.direction', 'ASC'); - if ($orderCol === 'a.ordering' || $orderCol === 'category_title') - { - $ordering = [ - $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), - $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), - ]; - } - else - { - if ($orderCol === 'client_name') - { - $orderCol = 'cl.name'; - } + if ($orderCol === 'a.ordering' || $orderCol === 'category_title') { + $ordering = [ + $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), + $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), + ]; + } else { + if ($orderCol === 'client_name') { + $orderCol = 'cl.name'; + } - $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); - } + $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); + } - $query->order($ordering); + $query->order($ordering); - return $query; - } + return $query; + } - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.category_id'); - $id .= ':' . $this->getState('filter.client_id'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . $this->getState('filter.level'); + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.category_id'); + $id .= ':' . $this->getState('filter.client_id'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . $this->getState('filter.level'); - return parent::getStoreId($id); - } + return parent::getStoreId($id); + } - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 1.6 - */ - public function getTable($type = 'Banner', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 1.6 + */ + public function getTable($type = 'Banner', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.name', $direction = 'asc') - { - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_banners')); + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_banners')); - // List state information. - parent::populateState($ordering, $direction); - } + // List state information. + parent::populateState($ordering, $direction); + } } diff --git a/code/administrator/components/com_banners/src/Model/ClientModel.php b/code/administrator/components/com_banners/src/Model/ClientModel.php index bbe474d5..c67db604 100644 --- a/code/administrator/components/com_banners/src/Model/ClientModel.php +++ b/code/administrator/components/com_banners/src/Model/ClientModel.php @@ -1,4 +1,5 @@ id) || $record->state != -2) - { - return false; - } - - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.delete', 'com_banners.category.' . (int) $record->catid); - } - - return parent::canDelete($record); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. - * Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - if (!empty($record->catid)) - { - return $user->authorise('core.edit.state', 'com_banners.category.' . (int) $record->catid); - } - - return $user->authorise('core.edit.state', 'com_banners'); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_banners.client', 'client', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_banners.edit.client.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_banners.client', $data); - - return $data; - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param Table $table A Table object. - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); - } + use VersionableModelTrait; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_banners.client'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->state != -2) { + return false; + } + + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.delete', 'com_banners.category.' . (int) $record->catid); + } + + return parent::canDelete($record); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. + * Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + if (!empty($record->catid)) { + return $user->authorise('core.edit.state', 'com_banners.category.' . (int) $record->catid); + } + + return $user->authorise('core.edit.state', 'com_banners'); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_banners.client', 'client', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_banners.edit.client.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_banners.client', $data); + + return $data; + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param Table $table A Table object. + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); + } } diff --git a/code/administrator/components/com_banners/src/Model/ClientsModel.php b/code/administrator/components/com_banners/src/Model/ClientsModel.php index b0e7b3aa..38bbe308 100644 --- a/code/administrator/components/com_banners/src/Model/ClientsModel.php +++ b/code/administrator/components/com_banners/src/Model/ClientsModel.php @@ -1,4 +1,5 @@ setState('params', ComponentHelper::getParams('com_banners')); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.purchase_type'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $defaultPurchase = (int) ComponentHelper::getParams('com_banners')->get('purchase_type', 3); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.name'), - $db->quoteName('a.contact'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.state'), - $db->quoteName('a.metakey'), - $db->quoteName('a.purchase_type'), - ] - ) - ) - ->select( - [ - 'COUNT(' . $db->quoteName('b.id') . ') AS ' . $db->quoteName('nbanners'), - $db->quoteName('uc.name', 'editor'), - ] - ); - - $query->from($db->quoteName('#__banner_clients', 'a')); - - // Join over the banners for counting - $query->join('LEFT', $db->quoteName('#__banners', 'b'), $db->quoteName('a.id') . ' = ' . $db->quoteName('b.cid')); - - // Join over the users for the checked out user. - $query->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Filter by published state - $published = (string) $this->getState('filter.state'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.state') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.state') . ' IN (0, 1)'); - } - - $query->group( - [ - $db->quoteName('a.id'), - $db->quoteName('a.name'), - $db->quoteName('a.contact'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.state'), - $db->quoteName('a.metakey'), - $db->quoteName('a.purchase_type'), - $db->quoteName('uc.name'), - ] - ); - - // Filter by search in title - if ($search = trim($this->getState('filter.search', ''))) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', $search) . '%'; - $query->where($db->quoteName('a.name') . ' LIKE :search') - ->bind(':search', $search); - } - } - - // Filter by purchase type - if ($purchaseType = (int) $this->getState('filter.purchase_type')) - { - if ($defaultPurchase === $purchaseType) - { - $query->where('(' . $db->quoteName('a.purchase_type') . ' = :type OR ' . $db->quoteName('a.purchase_type') . ' = -1)'); - } - else - { - $query->where($db->quoteName('a.purchase_type') . ' = :type'); - } - - $query->bind(':type', $purchaseType, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $query->order( - $db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Overrides the getItems method to attach additional metrics to the list. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.6 - */ - public function getItems() - { - // Get a storage key. - $store = $this->getStoreId('getItems'); - - // Try to load the data from internal storage. - if (!empty($this->cache[$store])) - { - return $this->cache[$store]; - } - - // Load the list items. - $items = parent::getItems(); - - // If empty or an error, just return. - if (empty($items)) - { - return array(); - } - - // Getting the following metric by joins is WAY TOO SLOW. - // Faster to do three queries for very large banner trees. - - // Get the clients in the list. - $db = $this->getDbo(); - $clientIds = array_column($items, 'id'); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('cid'), - 'COUNT(' . $db->quoteName('cid') . ') AS ' . $db->quoteName('count_published'), - ] - ) - ->from($db->quoteName('#__banners')) - ->where($db->quoteName('state') . ' = :state') - ->whereIn($db->quoteName('cid'), $clientIds) - ->group($db->quoteName('cid')) - ->bind(':state', $state, ParameterType::INTEGER); - - $db->setQuery($query); - - // Get the published banners count. - try - { - $state = 1; - $countPublished = $db->loadAssocList('cid', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the unpublished banners count. - try - { - $state = 0; - $countUnpublished = $db->loadAssocList('cid', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the trashed banners count. - try - { - $state = -2; - $countTrashed = $db->loadAssocList('cid', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the archived banners count. - try - { - $state = 2; - $countArchived = $db->loadAssocList('cid', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Inject the values back into the array. - foreach ($items as $item) - { - $item->count_published = isset($countPublished[$item->id]) ? $countPublished[$item->id] : 0; - $item->count_unpublished = isset($countUnpublished[$item->id]) ? $countUnpublished[$item->id] : 0; - $item->count_trashed = isset($countTrashed[$item->id]) ? $countTrashed[$item->id] : 0; - $item->count_archived = isset($countArchived[$item->id]) ? $countArchived[$item->id] : 0; - } - - // Add the items to the internal cache. - $this->cache[$store] = $items; - - return $this->cache[$store]; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'contact', 'a.contact', + 'state', 'a.state', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'purchase_type', 'a.purchase_type' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_banners')); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.purchase_type'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $defaultPurchase = (int) ComponentHelper::getParams('com_banners')->get('purchase_type', 3); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.name'), + $db->quoteName('a.contact'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.state'), + $db->quoteName('a.metakey'), + $db->quoteName('a.purchase_type'), + ] + ) + ) + ->select( + [ + 'COUNT(' . $db->quoteName('b.id') . ') AS ' . $db->quoteName('nbanners'), + $db->quoteName('uc.name', 'editor'), + ] + ); + + $query->from($db->quoteName('#__banner_clients', 'a')); + + // Join over the banners for counting + $query->join('LEFT', $db->quoteName('#__banners', 'b'), $db->quoteName('a.id') . ' = ' . $db->quoteName('b.cid')); + + // Join over the users for the checked out user. + $query->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Filter by published state + $published = (string) $this->getState('filter.state'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.state') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.state') . ' IN (0, 1)'); + } + + $query->group( + [ + $db->quoteName('a.id'), + $db->quoteName('a.name'), + $db->quoteName('a.contact'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.state'), + $db->quoteName('a.metakey'), + $db->quoteName('a.purchase_type'), + $db->quoteName('uc.name'), + ] + ); + + // Filter by search in title + if ($search = trim($this->getState('filter.search', ''))) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', $search) . '%'; + $query->where($db->quoteName('a.name') . ' LIKE :search') + ->bind(':search', $search); + } + } + + // Filter by purchase type + if ($purchaseType = (int) $this->getState('filter.purchase_type')) { + if ($defaultPurchase === $purchaseType) { + $query->where('(' . $db->quoteName('a.purchase_type') . ' = :type OR ' . $db->quoteName('a.purchase_type') . ' = -1)'); + } else { + $query->where($db->quoteName('a.purchase_type') . ' = :type'); + } + + $query->bind(':type', $purchaseType, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $query->order( + $db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) + ); + + return $query; + } + + /** + * Overrides the getItems method to attach additional metrics to the list. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.6 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId('getItems'); + + // Try to load the data from internal storage. + if (!empty($this->cache[$store])) { + return $this->cache[$store]; + } + + // Load the list items. + $items = parent::getItems(); + + // If empty or an error, just return. + if (empty($items)) { + return array(); + } + + // Getting the following metric by joins is WAY TOO SLOW. + // Faster to do three queries for very large banner trees. + + // Get the clients in the list. + $db = $this->getDatabase(); + $clientIds = array_column($items, 'id'); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('cid'), + 'COUNT(' . $db->quoteName('cid') . ') AS ' . $db->quoteName('count_published'), + ] + ) + ->from($db->quoteName('#__banners')) + ->where($db->quoteName('state') . ' = :state') + ->whereIn($db->quoteName('cid'), $clientIds) + ->group($db->quoteName('cid')) + ->bind(':state', $state, ParameterType::INTEGER); + + $db->setQuery($query); + + // Get the published banners count. + try { + $state = 1; + $countPublished = $db->loadAssocList('cid', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the unpublished banners count. + try { + $state = 0; + $countUnpublished = $db->loadAssocList('cid', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the trashed banners count. + try { + $state = -2; + $countTrashed = $db->loadAssocList('cid', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the archived banners count. + try { + $state = 2; + $countArchived = $db->loadAssocList('cid', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Inject the values back into the array. + foreach ($items as $item) { + $item->count_published = isset($countPublished[$item->id]) ? $countPublished[$item->id] : 0; + $item->count_unpublished = isset($countUnpublished[$item->id]) ? $countUnpublished[$item->id] : 0; + $item->count_trashed = isset($countTrashed[$item->id]) ? $countTrashed[$item->id] : 0; + $item->count_archived = isset($countArchived[$item->id]) ? $countArchived[$item->id] : 0; + } + + // Add the items to the internal cache. + $this->cache[$store] = $items; + + return $this->cache[$store]; + } } diff --git a/code/administrator/components/com_banners/src/Model/DownloadModel.php b/code/administrator/components/com_banners/src/Model/DownloadModel.php index 5cd2d4ff..a93fdeef 100644 --- a/code/administrator/components/com_banners/src/Model/DownloadModel.php +++ b/code/administrator/components/com_banners/src/Model/DownloadModel.php @@ -1,4 +1,5 @@ input; + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $input = Factory::getApplication()->input; - $this->setState('basename', $input->cookie->getString(ApplicationHelper::getHash($this->_context . '.basename'), '__SITE__')); - $this->setState('compressed', $input->cookie->getInt(ApplicationHelper::getHash($this->_context . '.compressed'), 1)); - } + $this->setState('basename', $input->cookie->getString(ApplicationHelper::getHash($this->_context . '.basename'), '__SITE__')); + $this->setState('compressed', $input->cookie->getInt(ApplicationHelper::getHash($this->_context . '.compressed'), 1)); + } - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_banners.download', 'download', array('control' => 'jform', 'load_data' => $loadData)); + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_banners.download', 'download', array('control' => 'jform', 'load_data' => $loadData)); - if (empty($form)) - { - return false; - } + if (empty($form)) { + return false; + } - return $form; - } + return $form; + } - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $data = (object) array( - 'basename' => $this->getState('basename'), - 'compressed' => $this->getState('compressed'), - ); + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $data = (object) array( + 'basename' => $this->getState('basename'), + 'compressed' => $this->getState('compressed'), + ); - $this->preprocessData('com_banners.download', $data); + $this->preprocessData('com_banners.download', $data); - return $data; - } + return $data; + } } diff --git a/code/administrator/components/com_banners/src/Model/TracksModel.php b/code/administrator/components/com_banners/src/Model/TracksModel.php index dd353dfb..bd7c25f4 100644 --- a/code/administrator/components/com_banners/src/Model/TracksModel.php +++ b/code/administrator/components/com_banners/src/Model/TracksModel.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Banners\Administrator\Model; -\defined('_JEXEC') or die; +namespace Joomla\Component\Banners\Administrator\Model; use Joomla\Archive\Archive; use Joomla\CMS\Component\ComponentHelper; @@ -20,6 +20,10 @@ use Joomla\Database\ParameterType; use Joomla\String\StringHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Methods supporting a list of tracks. * @@ -27,524 +31,464 @@ */ class TracksModel extends ListModel { - /** - * The base name - * - * @var string - * @since 1.6 - */ - protected $basename; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * - * @since 1.6 - */ - public function __construct($config = array()) - { - if (empty($config['filter_fields'])) - { - $config['filter_fields'] = array( - 'b.name', 'banner_name', - 'cl.name', 'client_name', 'client_id', - 'c.title', 'category_title', 'category_id', - 'track_type', 'a.track_type', 'type', - 'count', 'a.count', - 'track_date', 'a.track_date', 'end', 'begin', - 'level', 'c.level', - ); - } - - parent::__construct($config); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'b.name', $direction = 'asc') - { - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_banners')); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - [ - $db->quoteName('a.track_date'), - $db->quoteName('a.track_type'), - $db->quoteName('a.count'), - $db->quoteName('b.name', 'banner_name'), - $db->quoteName('cl.name', 'client_name'), - $db->quoteName('c.title', 'category_title'), - ] - ); - - // From tracks table. - $query->from($db->quoteName('#__banner_tracks', 'a')); - - // Join with the banners. - $query->join('LEFT', $db->quoteName('#__banners', 'b'), $db->quoteName('b.id') . ' = ' . $db->quoteName('a.banner_id')); - - // Join with the client. - $query->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('b.cid')); - - // Join with the category. - $query->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('b.catid')); - - // Filter by type. - - if ($type = (int) $this->getState('filter.type')) - { - $query->where($db->quoteName('a.track_type') . ' = :type') - ->bind(':type', $type, ParameterType::INTEGER); - } - - // Filter by client. - $clientId = $this->getState('filter.client_id'); - - if (is_numeric($clientId)) - { - $clientId = (int) $clientId; - $query->where($db->quoteName('b.cid') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - // Filter by category. - $categoryId = $this->getState('filter.category_id'); - - if (is_numeric($categoryId)) - { - $categoryId = (int) $categoryId; - $query->where($db->quoteName('b.catid') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - - // Filter by begin date. - if ($begin = $this->getState('filter.begin')) - { - $query->where($db->quoteName('a.track_date') . ' >= :begin') - ->bind(':begin', $begin); - } - - // Filter by end date. - if ($end = $this->getState('filter.end')) - { - $query->where($db->quoteName('a.track_date') . ' <= :end') - ->bind(':end', $end); - } - - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by search in banner name or client name. - if ($search = $this->getState('filter.search')) - { - $search = '%' . StringHelper::strtolower($search) . '%'; - $query->where('(LOWER(' . $db->quoteName('b.name') . ') LIKE :search1 OR LOWER(' . $db->quoteName('cl.name') . ') LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $query->order( - $db->quoteName($db->escape($this->getState('list.ordering', 'b.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Method to delete rows. - * - * @return boolean Returns true on success, false on failure. - */ - public function delete() - { - $user = Factory::getUser(); - $categoryId = (int) $this->getState('category_id'); - - // Access checks. - if ($categoryId) - { - $allow = $user->authorise('core.delete', 'com_banners.category.' . $categoryId); - } - else - { - $allow = $user->authorise('core.delete', 'com_banners'); - } - - if ($allow) - { - // Delete tracks from this banner - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__banner_tracks')); - - // Filter by type - if ($type = (int) $this->getState('filter.type')) - { - $query->where($db->quoteName('track_type') . ' = :type') - ->bind(':type', $type, ParameterType::INTEGER); - } - - // Filter by begin date - if ($begin = $this->getState('filter.begin')) - { - $query->where($db->quoteName('track_date') . ' >= :begin') - ->bind(':begin', $begin); - } - - // Filter by end date - if ($end = $this->getState('filter.end')) - { - $query->where($db->quoteName('track_date') . ' <= :end') - ->bind(':end', $end); - } - - $subQuery = $db->getQuery(true); - $subQuery->select($db->quoteName('id')) - ->from($db->quoteName('#__banners')); - - // Filter by client - if ($clientId = (int) $this->getState('filter.client_id')) - { - $subQuery->where($db->quoteName('cid') . ' = :clientId'); - $query->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - // Filter by category - if ($categoryId) - { - $subQuery->where($db->quoteName('catid') . ' = :categoryId'); - $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - - $query->where($db->quoteName('banner_id') . ' IN (' . $subQuery . ')'); - - $db->setQuery($query); - $this->setError((string) $query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - } - - return true; - } - - /** - * Get file name - * - * @return string The file name - * - * @since 1.6 - */ - public function getBaseName() - { - if (!isset($this->basename)) - { - $basename = str_replace('__SITE__', Factory::getApplication()->get('sitename'), $this->getState('basename')); - $categoryId = $this->getState('filter.category_id'); - - if (is_numeric($categoryId)) - { - if ($categoryId > 0) - { - $basename = str_replace('__CATID__', $categoryId, $basename); - } - else - { - $basename = str_replace('__CATID__', '', $basename); - } - - $categoryName = $this->getCategoryName(); - $basename = str_replace('__CATNAME__', $categoryName, $basename); - } - else - { - $basename = str_replace(array('__CATID__', '__CATNAME__'), '', $basename); - } - - $clientId = $this->getState('filter.client_id'); - - if (is_numeric($clientId)) - { - if ($clientId > 0) - { - $basename = str_replace('__CLIENTID__', $clientId, $basename); - } - else - { - $basename = str_replace('__CLIENTID__', '', $basename); - } - - $clientName = $this->getClientName(); - $basename = str_replace('__CLIENTNAME__', $clientName, $basename); - } - else - { - $basename = str_replace(array('__CLIENTID__', '__CLIENTNAME__'), '', $basename); - } - - $type = $this->getState('filter.type'); - - if ($type > 0) - { - $basename = str_replace('__TYPE__', $type, $basename); - $typeName = Text::_('COM_BANNERS_TYPE' . $type); - $basename = str_replace('__TYPENAME__', $typeName, $basename); - } - else - { - $basename = str_replace(array('__TYPE__', '__TYPENAME__'), '', $basename); - } - - $begin = $this->getState('filter.begin'); - - if (!empty($begin)) - { - $basename = str_replace('__BEGIN__', $begin, $basename); - } - else - { - $basename = str_replace('__BEGIN__', '', $basename); - } - - $end = $this->getState('filter.end'); - - if (!empty($end)) - { - $basename = str_replace('__END__', $end, $basename); - } - else - { - $basename = str_replace('__END__', '', $basename); - } - - $this->basename = $basename; - } - - return $this->basename; - } - - /** - * Get the category name. - * - * @return string The category name - * - * @since 1.6 - */ - protected function getCategoryName() - { - $categoryId = (int) $this->getState('filter.category_id'); - - if ($categoryId) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('id') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $name = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $name; - } - - return Text::_('COM_BANNERS_NOCATEGORYNAME'); - } - - /** - * Get the client name - * - * @return string The client name. - * - * @since 1.6 - */ - protected function getClientName() - { - $clientId = (int) $this->getState('filter.client_id'); - - if ($clientId) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__banner_clients')) - ->where($db->quoteName('id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $name = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $name; - } - - return Text::_('COM_BANNERS_NOCLIENTNAME'); - } - - /** - * Get the file type. - * - * @return string The file type - * - * @since 1.6 - */ - public function getFileType() - { - return $this->getState('compressed') ? 'zip' : 'csv'; - } - - /** - * Get the mime type. - * - * @return string The mime type. - * - * @since 1.6 - */ - public function getMimeType() - { - return $this->getState('compressed') ? 'application/zip' : 'text/csv'; - } - - /** - * Get the content - * - * @return string The content. - * - * @since 1.6 - */ - public function getContent() - { - if (!isset($this->content)) - { - $this->content = '"' . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_NAME')) . '","' - . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_CLIENT')) . '","' - . str_replace('"', '""', Text::_('JCATEGORY')) . '","' - . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_TYPE')) . '","' - . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_COUNT')) . '","' - . str_replace('"', '""', Text::_('JDATE')) . '"' . "\n"; - - foreach ($this->getItems() as $item) - { - $this->content .= '"' . str_replace('"', '""', $item->banner_name) . '","' - . str_replace('"', '""', $item->client_name) . '","' - . str_replace('"', '""', $item->category_title) . '","' - . str_replace('"', '""', ($item->track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'))) . '","' - . str_replace('"', '""', $item->count) . '","' - . str_replace('"', '""', $item->track_date) . '"' . "\n"; - } - - if ($this->getState('compressed')) - { - $app = Factory::getApplication(); - - $files = array( - 'track' => array( - 'name' => $this->getBaseName() . '.csv', - 'data' => $this->content, - 'time' => time() - ) - ); - $ziproot = $app->get('tmp_path') . '/' . uniqid('banners_tracks_') . '.zip'; - - // Run the packager - $delete = Folder::files($app->get('tmp_path') . '/', uniqid('banners_tracks_'), false, true); - - if (!empty($delete)) - { - if (!File::delete($delete)) - { - // File::delete throws an error - $this->setError(Text::_('COM_BANNERS_ERR_ZIP_DELETE_FAILURE')); - - return false; - } - } - - $archive = new Archive; - - if (!$packager = $archive->getAdapter('zip')) - { - $this->setError(Text::_('COM_BANNERS_ERR_ZIP_ADAPTER_FAILURE')); - - return false; - } - elseif (!$packager->create($ziproot, $files)) - { - $this->setError(Text::_('COM_BANNERS_ERR_ZIP_CREATE_FAILURE')); - - return false; - } - - $this->content = file_get_contents($ziproot); - - // Remove tmp zip file, it's no longer needed. - File::delete($ziproot); - } - } - - return $this->content; - } + /** + * The base name + * + * @var string + * @since 1.6 + */ + protected $basename; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'b.name', 'banner_name', + 'cl.name', 'client_name', 'client_id', + 'c.title', 'category_title', 'category_id', + 'track_type', 'a.track_type', 'type', + 'count', 'a.count', + 'track_date', 'a.track_date', 'end', 'begin', + 'level', 'c.level', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'b.name', $direction = 'asc') + { + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_banners')); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + [ + $db->quoteName('a.track_date'), + $db->quoteName('a.track_type'), + $db->quoteName('a.count'), + $db->quoteName('b.name', 'banner_name'), + $db->quoteName('cl.name', 'client_name'), + $db->quoteName('c.title', 'category_title'), + ] + ); + + // From tracks table. + $query->from($db->quoteName('#__banner_tracks', 'a')); + + // Join with the banners. + $query->join('LEFT', $db->quoteName('#__banners', 'b'), $db->quoteName('b.id') . ' = ' . $db->quoteName('a.banner_id')); + + // Join with the client. + $query->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('b.cid')); + + // Join with the category. + $query->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('b.catid')); + + // Filter by type. + + if ($type = (int) $this->getState('filter.type')) { + $query->where($db->quoteName('a.track_type') . ' = :type') + ->bind(':type', $type, ParameterType::INTEGER); + } + + // Filter by client. + $clientId = $this->getState('filter.client_id'); + + if (is_numeric($clientId)) { + $clientId = (int) $clientId; + $query->where($db->quoteName('b.cid') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + // Filter by category. + $categoryId = $this->getState('filter.category_id'); + + if (is_numeric($categoryId)) { + $categoryId = (int) $categoryId; + $query->where($db->quoteName('b.catid') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + + // Filter by begin date. + if ($begin = $this->getState('filter.begin')) { + $query->where($db->quoteName('a.track_date') . ' >= :begin') + ->bind(':begin', $begin); + } + + // Filter by end date. + if ($end = $this->getState('filter.end')) { + $query->where($db->quoteName('a.track_date') . ' <= :end') + ->bind(':end', $end); + } + + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by search in banner name or client name. + if ($search = $this->getState('filter.search')) { + $search = '%' . StringHelper::strtolower($search) . '%'; + $query->where('(LOWER(' . $db->quoteName('b.name') . ') LIKE :search1 OR LOWER(' . $db->quoteName('cl.name') . ') LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $query->order( + $db->quoteName($db->escape($this->getState('list.ordering', 'b.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) + ); + + return $query; + } + + /** + * Method to delete rows. + * + * @return boolean Returns true on success, false on failure. + */ + public function delete() + { + $user = Factory::getUser(); + $categoryId = (int) $this->getState('category_id'); + + // Access checks. + if ($categoryId) { + $allow = $user->authorise('core.delete', 'com_banners.category.' . $categoryId); + } else { + $allow = $user->authorise('core.delete', 'com_banners'); + } + + if ($allow) { + // Delete tracks from this banner + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__banner_tracks')); + + // Filter by type + if ($type = (int) $this->getState('filter.type')) { + $query->where($db->quoteName('track_type') . ' = :type') + ->bind(':type', $type, ParameterType::INTEGER); + } + + // Filter by begin date + if ($begin = $this->getState('filter.begin')) { + $query->where($db->quoteName('track_date') . ' >= :begin') + ->bind(':begin', $begin); + } + + // Filter by end date + if ($end = $this->getState('filter.end')) { + $query->where($db->quoteName('track_date') . ' <= :end') + ->bind(':end', $end); + } + + $subQuery = $db->getQuery(true); + $subQuery->select($db->quoteName('id')) + ->from($db->quoteName('#__banners')); + + // Filter by client + if ($clientId = (int) $this->getState('filter.client_id')) { + $subQuery->where($db->quoteName('cid') . ' = :clientId'); + $query->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + // Filter by category + if ($categoryId) { + $subQuery->where($db->quoteName('catid') . ' = :categoryId'); + $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + + $query->where($db->quoteName('banner_id') . ' IN (' . $subQuery . ')'); + + $db->setQuery($query); + $this->setError((string) $query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } else { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + } + + return true; + } + + /** + * Get file name + * + * @return string The file name + * + * @since 1.6 + */ + public function getBaseName() + { + if (!isset($this->basename)) { + $basename = str_replace('__SITE__', Factory::getApplication()->get('sitename'), $this->getState('basename')); + $categoryId = $this->getState('filter.category_id'); + + if (is_numeric($categoryId)) { + if ($categoryId > 0) { + $basename = str_replace('__CATID__', $categoryId, $basename); + } else { + $basename = str_replace('__CATID__', '', $basename); + } + + $categoryName = $this->getCategoryName(); + $basename = str_replace('__CATNAME__', $categoryName, $basename); + } else { + $basename = str_replace(array('__CATID__', '__CATNAME__'), '', $basename); + } + + $clientId = $this->getState('filter.client_id'); + + if (is_numeric($clientId)) { + if ($clientId > 0) { + $basename = str_replace('__CLIENTID__', $clientId, $basename); + } else { + $basename = str_replace('__CLIENTID__', '', $basename); + } + + $clientName = $this->getClientName(); + $basename = str_replace('__CLIENTNAME__', $clientName, $basename); + } else { + $basename = str_replace(array('__CLIENTID__', '__CLIENTNAME__'), '', $basename); + } + + $type = $this->getState('filter.type'); + + if ($type > 0) { + $basename = str_replace('__TYPE__', $type, $basename); + $typeName = Text::_('COM_BANNERS_TYPE' . $type); + $basename = str_replace('__TYPENAME__', $typeName, $basename); + } else { + $basename = str_replace(array('__TYPE__', '__TYPENAME__'), '', $basename); + } + + $begin = $this->getState('filter.begin'); + + if (!empty($begin)) { + $basename = str_replace('__BEGIN__', $begin, $basename); + } else { + $basename = str_replace('__BEGIN__', '', $basename); + } + + $end = $this->getState('filter.end'); + + if (!empty($end)) { + $basename = str_replace('__END__', $end, $basename); + } else { + $basename = str_replace('__END__', '', $basename); + } + + $this->basename = $basename; + } + + return $this->basename; + } + + /** + * Get the category name. + * + * @return string The category name + * + * @since 1.6 + */ + protected function getCategoryName() + { + $categoryId = (int) $this->getState('filter.category_id'); + + if ($categoryId) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $name = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $name; + } + + return Text::_('COM_BANNERS_NOCATEGORYNAME'); + } + + /** + * Get the client name + * + * @return string The client name. + * + * @since 1.6 + */ + protected function getClientName() + { + $clientId = (int) $this->getState('filter.client_id'); + + if ($clientId) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__banner_clients')) + ->where($db->quoteName('id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $name = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $name; + } + + return Text::_('COM_BANNERS_NOCLIENTNAME'); + } + + /** + * Get the file type. + * + * @return string The file type + * + * @since 1.6 + */ + public function getFileType() + { + return $this->getState('compressed') ? 'zip' : 'csv'; + } + + /** + * Get the mime type. + * + * @return string The mime type. + * + * @since 1.6 + */ + public function getMimeType() + { + return $this->getState('compressed') ? 'application/zip' : 'text/csv'; + } + + /** + * Get the content + * + * @return string The content. + * + * @since 1.6 + */ + public function getContent() + { + if (!isset($this->content)) { + $this->content = '"' . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_NAME')) . '","' + . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_CLIENT')) . '","' + . str_replace('"', '""', Text::_('JCATEGORY')) . '","' + . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_TYPE')) . '","' + . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_COUNT')) . '","' + . str_replace('"', '""', Text::_('JDATE')) . '"' . "\n"; + + foreach ($this->getItems() as $item) { + $this->content .= '"' . str_replace('"', '""', $item->banner_name) . '","' + . str_replace('"', '""', $item->client_name) . '","' + . str_replace('"', '""', $item->category_title) . '","' + . str_replace('"', '""', ($item->track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'))) . '","' + . str_replace('"', '""', $item->count) . '","' + . str_replace('"', '""', $item->track_date) . '"' . "\n"; + } + + if ($this->getState('compressed')) { + $app = Factory::getApplication(); + + $files = array( + 'track' => array( + 'name' => $this->getBaseName() . '.csv', + 'data' => $this->content, + 'time' => time() + ) + ); + $ziproot = $app->get('tmp_path') . '/' . uniqid('banners_tracks_') . '.zip'; + + // Run the packager + $delete = Folder::files($app->get('tmp_path') . '/', uniqid('banners_tracks_'), false, true); + + if (!empty($delete)) { + if (!File::delete($delete)) { + // File::delete throws an error + $this->setError(Text::_('COM_BANNERS_ERR_ZIP_DELETE_FAILURE')); + + return false; + } + } + + $archive = new Archive(); + + if (!$packager = $archive->getAdapter('zip')) { + $this->setError(Text::_('COM_BANNERS_ERR_ZIP_ADAPTER_FAILURE')); + + return false; + } elseif (!$packager->create($ziproot, $files)) { + $this->setError(Text::_('COM_BANNERS_ERR_ZIP_CREATE_FAILURE')); + + return false; + } + + $this->content = file_get_contents($ziproot); + + // Remove tmp zip file, it's no longer needed. + File::delete($ziproot); + } + } + + return $this->content; + } } diff --git a/code/administrator/components/com_banners/src/Service/Html/Banner.php b/code/administrator/components/com_banners/src/Service/Html/Banner.php index cd0ae5cc..7ca56fa4 100644 --- a/code/administrator/components/com_banners/src/Service/Html/Banner.php +++ b/code/administrator/components/com_banners/src/Service/Html/Banner.php @@ -1,4 +1,5 @@ ', - Text::_('COM_BANNERS_BATCH_CLIENT_LABEL'), - '', - '' - ) - ); - } + use DatabaseAwareTrait; + + /** + * Display a batch widget for the client selector. + * + * @return string The necessary HTML for the widget. + * + * @since 2.5 + */ + public function clients() + { + // Create the batch selector to change the client on a selection list. + return implode( + "\n", + array( + '', + '' + ) + ); + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 1.6 - */ - public function clientlist() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id', 'value'), - $db->quoteName('name', 'text'), - ] - ) - ->from($db->quoteName('#__banner_clients')) - ->order($db->quoteName('name')); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 1.6 + */ + public function clientlist() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id', 'value'), + $db->quoteName('name', 'text'), + ] + ) + ->from($db->quoteName('#__banner_clients')) + ->order($db->quoteName('name')); - // Get the options. - $db->setQuery($query); + // Get the options. + $db->setQuery($query); - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } - return $options; - } + return $options; + } - /** - * Returns a pinned state on a grid - * - * @param integer $value The state value. - * @param integer $i The row index - * @param boolean $enabled An optional setting for access control on the action. - * @param string $checkbox An optional prefix for checkboxes. - * - * @return string The Html code - * - * @see HTMLHelperJGrid::state - * @since 2.5.5 - */ - public function pinned($value, $i, $enabled = true, $checkbox = 'cb') - { - $states = array( - 1 => array( - 'sticky_unpublish', - 'COM_BANNERS_BANNERS_PINNED', - 'COM_BANNERS_BANNERS_HTML_PIN_BANNER', - 'COM_BANNERS_BANNERS_PINNED', - true, - 'publish', - 'publish' - ), - 0 => array( - 'sticky_publish', - 'COM_BANNERS_BANNERS_UNPINNED', - 'COM_BANNERS_BANNERS_HTML_UNPIN_BANNER', - 'COM_BANNERS_BANNERS_UNPINNED', - true, - 'unpublish', - 'unpublish' - ), - ); + /** + * Returns a pinned state on a grid + * + * @param integer $value The state value. + * @param integer $i The row index + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * + * @return string The Html code + * + * @see HTMLHelperJGrid::state + * @since 2.5.5 + */ + public function pinned($value, $i, $enabled = true, $checkbox = 'cb') + { + $states = array( + 1 => array( + 'sticky_unpublish', + 'COM_BANNERS_BANNERS_PINNED', + 'COM_BANNERS_BANNERS_HTML_PIN_BANNER', + 'COM_BANNERS_BANNERS_PINNED', + true, + 'publish', + 'publish' + ), + 0 => array( + 'sticky_publish', + 'COM_BANNERS_BANNERS_UNPINNED', + 'COM_BANNERS_BANNERS_HTML_UNPIN_BANNER', + 'COM_BANNERS_BANNERS_UNPINNED', + true, + 'unpublish', + 'unpublish' + ), + ); - return HTMLHelper::_('jgrid.state', $states, $value, $i, 'banners.', $enabled, true, $checkbox); - } + return HTMLHelper::_('jgrid.state', $states, $value, $i, 'banners.', $enabled, true, $checkbox); + } } diff --git a/code/administrator/components/com_banners/src/Table/BannerTable.php b/code/administrator/components/com_banners/src/Table/BannerTable.php index dd23a615..90d50365 100644 --- a/code/administrator/components/com_banners/src/Table/BannerTable.php +++ b/code/administrator/components/com_banners/src/Table/BannerTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_banners.banner'; - - parent::__construct('#__banners', 'id', $db); - - $this->created = Factory::getDate()->toSql(); - $this->setColumnAlias('published', 'state'); - } - - /** - * Increase click count - * - * @return void - */ - public function clicks() - { - $id = (int) $this->id; - $query = $this->_db->getQuery(true) - ->update($this->_db->quoteName('#__banners')) - ->set($this->_db->quoteName('clicks') . ' = ' . $this->_db->quoteName('clicks') . ' + 1') - ->where($this->_db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $this->_db->setQuery($query); - $this->_db->execute(); - } - - /** - * Overloaded check function - * - * @return boolean - * - * @see Table::check - * @since 1.5 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Set name - $this->name = htmlspecialchars_decode($this->name, ENT_QUOTES); - - // Set alias - if (trim($this->alias) == '') - { - $this->alias = $this->name; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); - - if (trim(str_replace('-', '', $this->alias)) == '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - // Check for a valid category. - if (!$this->catid = (int) $this->catid) - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); - - return false; - } - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = Factory::getDate()->toSql(); - } - - // Set publish_up, publish_down to null if not set - if (!$this->publish_up) - { - $this->publish_up = null; - } - - if (!$this->publish_down) - { - $this->publish_down = null; - } - - // Check the publish down date is not earlier than publish up. - if (!\is_null($this->publish_down) && !\is_null($this->publish_up) && $this->publish_down < $this->publish_up) - { - $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); - - return false; - } - - // Set ordering - if ($this->state < 0) - { - // Set ordering to 0 if state is archived or trashed - $this->ordering = 0; - } - elseif (empty($this->ordering)) - { - // Set ordering to last if ordering was 0 - $this->ordering = self::getNextOrder($this->_db->quoteName('catid') . ' = ' . ((int) $this->catid) . ' AND ' . $this->_db->quoteName('state') . ' >= 0'); - } - - // Set modified to created if not set - if (!$this->modified) - { - $this->modified = $this->created; - } - - // Set modified_by to created_by if not set - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - - return true; - } - - /** - * Overloaded bind function - * - * @param mixed $array An associative array or object to bind to the \JTable instance. - * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success - * - * @since 1.5 - */ - public function bind($array, $ignore = array()) - { - if (isset($array['params']) && \is_array($array['params'])) - { - $registry = new Registry($array['params']); - - if ((int) $registry->get('width', 0) < 0) - { - $this->setError(Text::sprintf('JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED', Text::_('COM_BANNERS_FIELD_WIDTH_LABEL'))); - - return false; - } - - if ((int) $registry->get('height', 0) < 0) - { - $this->setError(Text::sprintf('JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED', Text::_('COM_BANNERS_FIELD_HEIGHT_LABEL'))); - - return false; - } - - // Converts the width and height to an absolute numeric value: - $width = abs((int) $registry->get('width', 0)); - $height = abs((int) $registry->get('height', 0)); - - // Sets the width and height to an empty string if = 0 - $registry->set('width', $width ?: ''); - $registry->set('height', $height ?: ''); - - $array['params'] = (string) $registry; - } - - if (isset($array['imptotal'])) - { - $array['imptotal'] = abs((int) $array['imptotal']); - } - - return parent::bind($array, $ignore); - } - - /** - * Method to store a row - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success, false on failure. - */ - public function store($updateNulls = true) - { - $db = $this->getDbo(); - - if (empty($this->id)) - { - $purchaseType = $this->purchase_type; - - if ($purchaseType < 0 && $this->cid) - { - $client = new ClientTable($db); - $client->load($this->cid); - $purchaseType = $client->purchase_type; - } - - if ($purchaseType < 0) - { - $purchaseType = ComponentHelper::getParams('com_banners')->get('purchase_type'); - } - - switch ($purchaseType) - { - case 1: - $this->reset = null; - break; - case 2: - $date = Factory::getDate('+1 year ' . date('Y-m-d')); - $this->reset = $date->toSql(); - break; - case 3: - $date = Factory::getDate('+1 month ' . date('Y-m-d')); - $this->reset = $date->toSql(); - break; - case 4: - $date = Factory::getDate('+7 day ' . date('Y-m-d')); - $this->reset = $date->toSql(); - break; - case 5: - $date = Factory::getDate('+1 day ' . date('Y-m-d')); - $this->reset = $date->toSql(); - break; - } - - // Store the row - parent::store($updateNulls); - } - else - { - // Get the old row - /** @var BannerTable $oldrow */ - $oldrow = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $db)); - - if (!$oldrow->load($this->id) && $oldrow->getError()) - { - $this->setError($oldrow->getError()); - } - - // Verify that the alias is unique - /** @var BannerTable $table */ - $table = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $db)); - - if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_BANNERS_ERROR_UNIQUE_ALIAS')); - - return false; - } - - // Store the new row - parent::store($updateNulls); - - // Need to reorder ? - if ($oldrow->state >= 0 && ($this->state < 0 || $oldrow->catid != $this->catid)) - { - // Reorder the oldrow - $this->reorder($this->_db->quoteName('catid') . ' = ' . ((int) $oldrow->catid) . ' AND ' . $this->_db->quoteName('state') . ' >= 0'); - } - } - - return \count($this->getErrors()) == 0; - } - - /** - * Method to set the sticky state for a row or list of rows in the database - * table. The method respects checked out rows by other users and will attempt - * to checkin rows that it can after adjustments are made. - * - * @param mixed $pks An optional array of primary key values to update. If not set the instance property value is used. - * @param integer $state The sticky state. eg. [0 = unsticked, 1 = sticked] - * @param integer $userId The user id of the user performing the operation. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function stick($pks = null, $state = 1, $userId = 0) - { - $k = $this->_tbl_key; - - // Sanitize input. - $pks = ArrayHelper::toInteger($pks); - $userId = (int) $userId; - $state = (int) $state; - - // If there are no primary keys set check to see if the instance key is set. - if (empty($pks)) - { - if ($this->$k) - { - $pks = array($this->$k); - } - // Nothing to set publishing state on, return false. - else - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); - - return false; - } - } - - // Get an instance of the table - /** @var BannerTable $table */ - $table = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $this->_db)); - - // For all keys - foreach ($pks as $pk) - { - // Load the banner - if (!$table->load($pk)) - { - $this->setError($table->getError()); - } - - // Verify checkout - if (\is_null($table->checked_out) || $table->checked_out == $userId) - { - // Change the state - $table->sticky = $state; - $table->checked_out = null; - $table->checked_out_time = null; - - // Check the row - $table->check(); - - // Store the row - if (!$table->store()) - { - $this->setError($table->getError()); - } - } - } - - return \count($this->getErrors()) == 0; - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 1.5 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_banners.banner'; + + parent::__construct('#__banners', 'id', $db); + + $this->created = Factory::getDate()->toSql(); + $this->setColumnAlias('published', 'state'); + } + + /** + * Increase click count + * + * @return void + */ + public function clicks() + { + $id = (int) $this->id; + $query = $this->_db->getQuery(true) + ->update($this->_db->quoteName('#__banners')) + ->set($this->_db->quoteName('clicks') . ' = ' . $this->_db->quoteName('clicks') . ' + 1') + ->where($this->_db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $this->_db->setQuery($query); + $this->_db->execute(); + } + + /** + * Overloaded check function + * + * @return boolean + * + * @see Table::check + * @since 1.5 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Set name + $this->name = htmlspecialchars_decode($this->name, ENT_QUOTES); + + // Set alias + if (trim($this->alias) == '') { + $this->alias = $this->name; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); + + if (trim(str_replace('-', '', $this->alias)) == '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + // Check for a valid category. + if (!$this->catid = (int) $this->catid) { + $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); + + return false; + } + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = Factory::getDate()->toSql(); + } + + // Set publish_up, publish_down to null if not set + if (!$this->publish_up) { + $this->publish_up = null; + } + + if (!$this->publish_down) { + $this->publish_down = null; + } + + // Check the publish down date is not earlier than publish up. + if (!\is_null($this->publish_down) && !\is_null($this->publish_up) && $this->publish_down < $this->publish_up) { + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); + + return false; + } + + // Set ordering + if ($this->state < 0) { + // Set ordering to 0 if state is archived or trashed + $this->ordering = 0; + } elseif (empty($this->ordering)) { + // Set ordering to last if ordering was 0 + $this->ordering = self::getNextOrder($this->_db->quoteName('catid') . ' = ' . ((int) $this->catid) . ' AND ' . $this->_db->quoteName('state') . ' >= 0'); + } + + // Set modified to created if not set + if (!$this->modified) { + $this->modified = $this->created; + } + + // Set modified_by to created_by if not set + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + + return true; + } + + /** + * Overloaded bind function + * + * @param mixed $array An associative array or object to bind to the \JTable instance. + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success + * + * @since 1.5 + */ + public function bind($array, $ignore = array()) + { + if (isset($array['params']) && \is_array($array['params'])) { + $registry = new Registry($array['params']); + + if ((int) $registry->get('width', 0) < 0) { + $this->setError(Text::sprintf('JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED', Text::_('COM_BANNERS_FIELD_WIDTH_LABEL'))); + + return false; + } + + if ((int) $registry->get('height', 0) < 0) { + $this->setError(Text::sprintf('JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED', Text::_('COM_BANNERS_FIELD_HEIGHT_LABEL'))); + + return false; + } + + // Converts the width and height to an absolute numeric value: + $width = abs((int) $registry->get('width', 0)); + $height = abs((int) $registry->get('height', 0)); + + // Sets the width and height to an empty string if = 0 + $registry->set('width', $width ?: ''); + $registry->set('height', $height ?: ''); + + $array['params'] = (string) $registry; + } + + if (isset($array['imptotal'])) { + $array['imptotal'] = abs((int) $array['imptotal']); + } + + return parent::bind($array, $ignore); + } + + /** + * Method to store a row + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success, false on failure. + */ + public function store($updateNulls = true) + { + $db = $this->getDbo(); + + if (empty($this->id)) { + $purchaseType = $this->purchase_type; + + if ($purchaseType < 0 && $this->cid) { + $client = new ClientTable($db); + $client->load($this->cid); + $purchaseType = $client->purchase_type; + } + + if ($purchaseType < 0) { + $purchaseType = ComponentHelper::getParams('com_banners')->get('purchase_type'); + } + + switch ($purchaseType) { + case 1: + $this->reset = null; + break; + case 2: + $date = Factory::getDate('+1 year ' . date('Y-m-d')); + $this->reset = $date->toSql(); + break; + case 3: + $date = Factory::getDate('+1 month ' . date('Y-m-d')); + $this->reset = $date->toSql(); + break; + case 4: + $date = Factory::getDate('+7 day ' . date('Y-m-d')); + $this->reset = $date->toSql(); + break; + case 5: + $date = Factory::getDate('+1 day ' . date('Y-m-d')); + $this->reset = $date->toSql(); + break; + } + + // Store the row + parent::store($updateNulls); + } else { + // Get the old row + /** @var BannerTable $oldrow */ + $oldrow = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $db)); + + if (!$oldrow->load($this->id) && $oldrow->getError()) { + $this->setError($oldrow->getError()); + } + + // Verify that the alias is unique + /** @var BannerTable $table */ + $table = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $db)); + + if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_BANNERS_ERROR_UNIQUE_ALIAS')); + + return false; + } + + // Store the new row + parent::store($updateNulls); + + // Need to reorder ? + if ($oldrow->state >= 0 && ($this->state < 0 || $oldrow->catid != $this->catid)) { + // Reorder the oldrow + $this->reorder($this->_db->quoteName('catid') . ' = ' . ((int) $oldrow->catid) . ' AND ' . $this->_db->quoteName('state') . ' >= 0'); + } + } + + return \count($this->getErrors()) == 0; + } + + /** + * Method to set the sticky state for a row or list of rows in the database + * table. The method respects checked out rows by other users and will attempt + * to checkin rows that it can after adjustments are made. + * + * @param mixed $pks An optional array of primary key values to update. If not set the instance property value is used. + * @param integer $state The sticky state. eg. [0 = unsticked, 1 = sticked] + * @param integer $userId The user id of the user performing the operation. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function stick($pks = null, $state = 1, $userId = 0) + { + $k = $this->_tbl_key; + + // Sanitize input. + $pks = ArrayHelper::toInteger($pks); + $userId = (int) $userId; + $state = (int) $state; + + // If there are no primary keys set check to see if the instance key is set. + if (empty($pks)) { + if ($this->$k) { + $pks = array($this->$k); + } else { + // Nothing to set publishing state on, return false. + $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); + + return false; + } + } + + // Get an instance of the table + /** @var BannerTable $table */ + $table = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $this->_db)); + + // For all keys + foreach ($pks as $pk) { + // Load the banner + if (!$table->load($pk)) { + $this->setError($table->getError()); + } + + // Verify checkout + if (\is_null($table->checked_out) || $table->checked_out == $userId) { + // Change the state + $table->sticky = $state; + $table->checked_out = null; + $table->checked_out_time = null; + + // Check the row + $table->check(); + + // Store the row + if (!$table->store()) { + $this->setError($table->getError()); + } + } + } + + return \count($this->getErrors()) == 0; + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/code/administrator/components/com_banners/src/Table/ClientTable.php b/code/administrator/components/com_banners/src/Table/ClientTable.php index 977dc5f6..dab3cece 100644 --- a/code/administrator/components/com_banners/src/Table/ClientTable.php +++ b/code/administrator/components/com_banners/src/Table/ClientTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_banners.client'; - - $this->setColumnAlias('published', 'state'); - - parent::__construct('#__banner_clients', 'id', $db); - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } - - /** - * Overloaded check function - * - * @return boolean True if the object is ok - * - * @see Table::check() - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Check for valid name - if (trim($this->name) === '') - { - $this->setError(Text::_('COM_BANNERS_WARNING_PROVIDE_VALID_NAME')); - - return false; - } - - // Check for valid contact - if (trim($this->contact) === '') - { - $this->setError(Text::_('COM_BANNERS_PROVIDE_VALID_CONTACT')); - - return false; - } - - return true; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 1.5 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_banners.client'; + + $this->setColumnAlias('published', 'state'); + + parent::__construct('#__banner_clients', 'id', $db); + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } + + /** + * Overloaded check function + * + * @return boolean True if the object is ok + * + * @see Table::check() + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Check for valid name + if (trim($this->name) === '') { + $this->setError(Text::_('COM_BANNERS_WARNING_PROVIDE_VALID_NAME')); + + return false; + } + + // Check for valid contact + if (trim($this->contact) === '') { + $this->setError(Text::_('COM_BANNERS_PROVIDE_VALID_CONTACT')); + + return false; + } + + return true; + } } diff --git a/code/administrator/components/com_banners/src/View/Banner/HtmlView.php b/code/administrator/components/com_banners/src/View/Banner/HtmlView.php index 1de2d41b..c5b2ebe5 100644 --- a/code/administrator/components/com_banners/src/View/Banner/HtmlView.php +++ b/code/administrator/components/com_banners/src/View/Banner/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->form = $model->getForm(); - $this->item = $model->getItem(); - $this->state = $model->getState(); - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws Exception - */ - protected function addToolbar(): void - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = Factory::getUser(); - $userId = $user->id; - $isNew = ($this->item->id == 0); - $checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_banners', 'category', $this->item->catid); - - ToolbarHelper::title($isNew ? Text::_('COM_BANNERS_MANAGER_BANNER_NEW') : Text::_('COM_BANNERS_MANAGER_BANNER_EDIT'), 'bookmark banners'); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if (!$checkedOut && ($canDo->get('core.edit') || \count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0)) - { - ToolbarHelper::apply('banner.apply'); - $toolbarButtons[] = ['save', 'banner.save']; - - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'banner.save2new']; - } - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'banner.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('banner.cancel'); - } - else - { - ToolbarHelper::cancel('banner.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) - { - ToolbarHelper::versions('com_banners.banner', $this->item->id); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Banners:_Edit'); - } + /** + * The Form object + * + * @var Form + * @since 1.5 + */ + protected $form; + + /** + * The active item + * + * @var object + * @since 1.5 + */ + protected $item; + + /** + * The model state + * + * @var object + * @since 1.5 + */ + protected $state; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var BannerModel $model */ + $model = $this->getModel(); + $this->form = $model->getForm(); + $this->item = $model->getItem(); + $this->state = $model->getState(); + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws Exception + */ + protected function addToolbar(): void + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = ($this->item->id == 0); + $checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_banners', 'category', $this->item->catid); + + ToolbarHelper::title($isNew ? Text::_('COM_BANNERS_MANAGER_BANNER_NEW') : Text::_('COM_BANNERS_MANAGER_BANNER_EDIT'), 'bookmark banners'); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if (!$checkedOut && ($canDo->get('core.edit') || \count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0)) { + ToolbarHelper::apply('banner.apply'); + $toolbarButtons[] = ['save', 'banner.save']; + + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'banner.save2new']; + } + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'banner.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('banner.cancel'); + } else { + ToolbarHelper::cancel('banner.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) { + ToolbarHelper::versions('com_banners.banner', $this->item->id); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Banners:_Edit'); + } } diff --git a/code/administrator/components/com_banners/src/View/Banners/HtmlView.php b/code/administrator/components/com_banners/src/View/Banners/HtmlView.php index 5019ffd9..461cf0db 100644 --- a/code/administrator/components/com_banners/src/View/Banners/HtmlView.php +++ b/code/administrator/components/com_banners/src/View/Banners/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->categories = $model->getCategoryOrders(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_banners', 'category', $this->state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_BANNERS'), 'bookmark banners'); - - if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0) - { - $toolbar->addNew('banner.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - if ($this->state->get('filter.published') != 2) - { - $childBar->publish('banners.publish')->listCheck(true); - - $childBar->unpublish('banners.unpublish')->listCheck(true); - } - - if ($this->state->get('filter.published') != -1) - { - if ($this->state->get('filter.published') != 2) - { - $childBar->archive('banners.archive')->listCheck(true); - } - elseif ($this->state->get('filter.published') == 2) - { - $childBar->publish('publish')->task('banners.publish')->listCheck(true); - } - } - - $childBar->checkin('banners.checkin')->listCheck(true); - - if ($this->state->get('filter.published') != -2) - { - $childBar->trash('banners.trash')->listCheck(true); - } - } - - if ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('banners.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - // Add a batch button - if ($user->authorise('core.create', 'com_banners') - && $user->authorise('core.edit', 'com_banners') - && $user->authorise('core.edit.state', 'com_banners')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if ($user->authorise('core.admin', 'com_banners') || $user->authorise('core.options', 'com_banners')) - { - $toolbar->preferences('com_banners'); - } - - $toolbar->help('Banners'); - } + /** + * The search tools form + * + * @var Form + * @since 1.6 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 1.6 + */ + public $activeFilters = []; + + /** + * Category data + * + * @var array + * @since 1.6 + */ + protected $categories = []; + + /** + * An array of items + * + * @var array + * @since 1.6 + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var Registry + * @since 1.6 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 1.6 + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var BannersModel $model */ + $model = $this->getModel(); + $this->categories = $model->getCategoryOrders(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_banners', 'category', $this->state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_BANNERS'), 'bookmark banners'); + + if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0) { + $toolbar->addNew('banner.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + if ($this->state->get('filter.published') != 2) { + $childBar->publish('banners.publish')->listCheck(true); + + $childBar->unpublish('banners.unpublish')->listCheck(true); + } + + if ($this->state->get('filter.published') != -1) { + if ($this->state->get('filter.published') != 2) { + $childBar->archive('banners.archive')->listCheck(true); + } elseif ($this->state->get('filter.published') == 2) { + $childBar->publish('publish')->task('banners.publish')->listCheck(true); + } + } + + $childBar->checkin('banners.checkin')->listCheck(true); + + if ($this->state->get('filter.published') != -2) { + $childBar->trash('banners.trash')->listCheck(true); + } + } + + if ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('banners.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_banners') + && $user->authorise('core.edit', 'com_banners') + && $user->authorise('core.edit.state', 'com_banners') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if ($user->authorise('core.admin', 'com_banners') || $user->authorise('core.options', 'com_banners')) { + $toolbar->preferences('com_banners'); + } + + $toolbar->help('Banners'); + } } diff --git a/code/administrator/components/com_banners/src/View/Client/HtmlView.php b/code/administrator/components/com_banners/src/View/Client/HtmlView.php index 06557383..b3bc2049 100644 --- a/code/administrator/components/com_banners/src/View/Client/HtmlView.php +++ b/code/administrator/components/com_banners/src/View/Client/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->form = $model->getForm(); - $this->item = $model->getItem(); - $this->state = $model->getState(); - $this->canDo = ContentHelper::getActions('com_banners'); - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * - * @throws Exception - */ - protected function addToolbar(): void - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = Factory::getUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $user->id); - $canDo = $this->canDo; - - ToolbarHelper::title( - $isNew ? Text::_('COM_BANNERS_MANAGER_CLIENT_NEW') : Text::_('COM_BANNERS_MANAGER_CLIENT_EDIT'), - 'bookmark banners-clients' - ); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if (!$checkedOut && ($canDo->get('core.edit') || $canDo->get('core.create'))) - { - ToolbarHelper::apply('client.apply'); - $toolbarButtons[] = ['save', 'client.save']; - } - - if (!$checkedOut && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'client.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'client.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('client.cancel'); - } - else - { - ToolbarHelper::cancel('client.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) - { - ToolbarHelper::versions('com_banners.client', $this->item->id); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Banners:_New_or_Edit_Client'); - } + /** + * The Form object + * + * @var Form + * @since 1.5 + */ + protected $form; + + /** + * The active item + * + * @var CMSObject + * @since 1.5 + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + * @since 1.5 + */ + protected $state; + + /** + * Object containing permissions for the item + * + * @var CMSObject + * @since 1.5 + */ + protected $canDo; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var ClientModel $model */ + $model = $this->getModel(); + $this->form = $model->getForm(); + $this->item = $model->getItem(); + $this->state = $model->getState(); + $this->canDo = ContentHelper::getActions('com_banners'); + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + protected function addToolbar(): void + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $user->id); + $canDo = $this->canDo; + + ToolbarHelper::title( + $isNew ? Text::_('COM_BANNERS_MANAGER_CLIENT_NEW') : Text::_('COM_BANNERS_MANAGER_CLIENT_EDIT'), + 'bookmark banners-clients' + ); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if (!$checkedOut && ($canDo->get('core.edit') || $canDo->get('core.create'))) { + ToolbarHelper::apply('client.apply'); + $toolbarButtons[] = ['save', 'client.save']; + } + + if (!$checkedOut && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'client.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'client.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('client.cancel'); + } else { + ToolbarHelper::cancel('client.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) { + ToolbarHelper::versions('com_banners.client', $this->item->id); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Banners:_New_or_Edit_Client'); + } } diff --git a/code/administrator/components/com_banners/src/View/Clients/HtmlView.php b/code/administrator/components/com_banners/src/View/Clients/HtmlView.php index b18edbf3..01f283c1 100644 --- a/code/administrator/components/com_banners/src/View/Clients/HtmlView.php +++ b/code/administrator/components/com_banners/src/View/Clients/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_banners'); - - ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_CLIENTS'), 'bookmark banners-clients'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('client.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('clients.publish')->listCheck(true); - $childBar->unpublish('clients.unpublish')->listCheck(true); - $childBar->archive('clients.archive')->listCheck(true); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('clients.checkin')->listCheck(true); - } - - if (!$this->state->get('filter.state') == -2) - { - $childBar->trash('clients.trash')->listCheck(true); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.state') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('clients.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_banners'); - } - - $toolbar->help('Banners:_Clients'); - } + /** + * The search tools form + * + * @var Form + * @since 1.6 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 1.6 + */ + public $activeFilters = []; + + /** + * An array of items + * + * @var array + * @since 1.6 + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var ClientsModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_banners'); + + ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_CLIENTS'), 'bookmark banners-clients'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('client.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('clients.publish')->listCheck(true); + $childBar->unpublish('clients.unpublish')->listCheck(true); + $childBar->archive('clients.archive')->listCheck(true); + + if ($canDo->get('core.admin')) { + $childBar->checkin('clients.checkin')->listCheck(true); + } + + if (!$this->state->get('filter.state') == -2) { + $childBar->trash('clients.trash')->listCheck(true); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.state') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('clients.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_banners'); + } + + $toolbar->help('Banners:_Clients'); + } } diff --git a/code/administrator/components/com_banners/src/View/Download/HtmlView.php b/code/administrator/components/com_banners/src/View/Download/HtmlView.php index 6307ee8d..1f104358 100644 --- a/code/administrator/components/com_banners/src/View/Download/HtmlView.php +++ b/code/administrator/components/com_banners/src/View/Download/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->form = $model->getForm(); - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - } + /** + * The Form object + * + * @var Form + * @since 1.6 + */ + protected $form; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var DownloadModel $model */ + $model = $this->getModel(); + $this->form = $model->getForm(); + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + } } diff --git a/code/administrator/components/com_banners/src/View/Tracks/HtmlView.php b/code/administrator/components/com_banners/src/View/Tracks/HtmlView.php index 5931f49a..dab4bc3f 100644 --- a/code/administrator/components/com_banners/src/View/Tracks/HtmlView.php +++ b/code/administrator/components/com_banners/src/View/Tracks/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_banners', 'category', $this->state->get('filter.category_id')); - - ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_TRACKS'), 'bookmark banners-tracks'); - - $bar = Toolbar::getInstance('toolbar'); - - if (!$this->isEmptyState) - { - $bar->popupButton() - ->url(Route::_('index.php?option=com_banners&view=download&tmpl=component')) - ->text('JTOOLBAR_EXPORT') - ->selector('downloadModal') - ->icon('icon-download') - ->footer('' - . '' - ); - } - - if (!$this->isEmptyState && $canDo->get('core.delete')) - { - $bar->appendButton('Confirm', 'COM_BANNERS_DELETE_MSG', 'delete', 'COM_BANNERS_TRACKS_DELETE', 'tracks.delete', false); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_banners'); - } - - ToolbarHelper::help('Banners:_Tracks'); - } + /** + * The search tools form + * + * @var Form + * @since 1.6 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 1.6 + */ + public $activeFilters = []; + + /** + * An array of items + * + * @var array + * @since 1.6 + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var TracksModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_banners', 'category', $this->state->get('filter.category_id')); + + ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_TRACKS'), 'bookmark banners-tracks'); + + $bar = Toolbar::getInstance('toolbar'); + + if (!$this->isEmptyState) { + $bar->popupButton() + ->url(Route::_('index.php?option=com_banners&view=download&tmpl=component')) + ->text('JTOOLBAR_EXPORT') + ->selector('downloadModal') + ->icon('icon-download') + ->footer('' + . ''); + } + + if (!$this->isEmptyState && $canDo->get('core.delete')) { + $bar->appendButton('Confirm', 'COM_BANNERS_DELETE_MSG', 'delete', 'COM_BANNERS_TRACKS_DELETE', 'tracks.delete', false); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_banners'); + } + + ToolbarHelper::help('Banners:_Tracks'); + } } diff --git a/code/administrator/components/com_banners/src/View/Tracks/RawView.php b/code/administrator/components/com_banners/src/View/Tracks/RawView.php index 17470ab6..dbafaabd 100644 --- a/code/administrator/components/com_banners/src/View/Tracks/RawView.php +++ b/code/administrator/components/com_banners/src/View/Tracks/RawView.php @@ -1,4 +1,5 @@ getModel(); - $basename = $model->getBaseName(); - $fileType = $model->getFileType(); - $mimeType = $model->getMimeType(); - $content = $model->getContent(); + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var TracksModel $model */ + $model = $this->getModel(); + $basename = $model->getBaseName(); + $fileType = $model->getFileType(); + $mimeType = $model->getMimeType(); + $content = $model->getContent(); - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - $this->document->setMimeEncoding($mimeType); + $this->document->setMimeEncoding($mimeType); - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - $app->setHeader( - 'Content-disposition', - 'attachment; filename="' . $basename . '.' . $fileType . '"; creation-date="' . Factory::getDate()->toRFC822() . '"', - true - ); - echo $content; - } + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + $app->setHeader( + 'Content-disposition', + 'attachment; filename="' . $basename . '.' . $fileType . '"; creation-date="' . Factory::getDate()->toRFC822() . '"', + true + ); + echo $content; + } } diff --git a/code/administrator/components/com_banners/tmpl/banner/edit.php b/code/administrator/components/com_banners/tmpl/banner/edit.php index 3320b312..d6fe9c69 100644 --- a/code/administrator/components/com_banners/tmpl/banner/edit.php +++ b/code/administrator/components/com_banners/tmpl/banner/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_banners.admin-banner-edit'); + ->useScript('form.validate') + ->useScript('com_banners.admin-banner-edit'); ?> diff --git a/code/administrator/components/com_banners/tmpl/banners/default.php b/code/administrator/components/com_banners/tmpl/banners/default.php index 83627068..bbbf3962 100644 --- a/code/administrator/components/com_banners/tmpl/banners/default.php +++ b/code/administrator/components/com_banners/tmpl/banners/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $userId = $user->get('id'); @@ -29,173 +31,174 @@ $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'a.ordering'; -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_banners&task=banners.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_banners&task=banners.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
-
-
- $this]); - ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $ordering = ($listOrder == 'ordering'); - $item->cat_link = Route::_('index.php?option=com_categories&extension=com_banners&task=edit&type=other&cid[]=' . $item->catid); - $canCreate = $user->authorise('core.create', 'com_banners.category.' . $item->catid); - $canEdit = $user->authorise('core.edit', 'com_banners.category.' . $item->catid); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_banners.category.' . $item->catid) && $canCheckin; - ?> - - - + + + + + + + + + + + + + +
- , - , - -
- - - - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->name); ?> - - +
+
+ $this]); + ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $ordering = ($listOrder == 'ordering'); + $item->cat_link = Route::_('index.php?option=com_categories&extension=com_banners&task=edit&type=other&cid[]=' . $item->catid); + $canCreate = $user->authorise('core.create', 'com_banners.category.' . $item->catid); + $canEdit = $user->authorise('core.edit', 'com_banners.category.' . $item->catid); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_banners.category.' . $item->catid) && $canCheckin; + ?> + + + - - - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->name); ?> + + - - - - - - - - state, $i, 'banners.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'banners.', $canCheckin); ?> - - - - escape($item->name); ?> - - escape($item->name); ?> - -
- escape($item->alias)); ?> -
-
- escape($item->category_title); ?> -
-
-
- sticky, $i, $canChange); ?> - - client_name; ?> - - impmade, $item->imptotal ?: Text::_('COM_BANNERS_UNLIMITED')); ?> - - clicks; ?> - - impmade ? 100 * $item->clicks / $item->impmade : 0); ?> - - - - id; ?> -
+ if (!$canChange) { + $iconClass = ' inactive'; + } elseif (!$saveOrder) { + $iconClass = ' inactive" title="' . Text::_('JORDERINGDISABLED'); + } + ?> + + + + + + +
+ state, $i, 'banners.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'banners.', $canCheckin); ?> + + + + escape($item->name); ?> + + escape($item->name); ?> + +
+ escape($item->alias)); ?> +
+
+ escape($item->category_title); ?> +
+
+
+ sticky, $i, $canChange); ?> + + client_name; ?> + + impmade, $item->imptotal ?: Text::_('COM_BANNERS_UNLIMITED')); ?> + + clicks; ?> - + impmade ? 100 * $item->clicks / $item->impmade : 0); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_banners') - && $user->authorise('core.edit', 'com_banners') - && $user->authorise('core.edit.state', 'com_banners')) : ?> - Text::_('COM_BANNERS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer') - ], - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_banners') + && $user->authorise('core.edit', 'com_banners') + && $user->authorise('core.edit.state', 'com_banners') + ) : ?> + Text::_('COM_BANNERS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer') + ], + $this->loadTemplate('batch_body') + ); ?> + + - - - -
-
-
+ + + + + +
diff --git a/code/administrator/components/com_banners/tmpl/banners/default_batch_body.php b/code/administrator/components/com_banners/tmpl/banners/default_batch_body.php index f53c0395..fbc990db 100644 --- a/code/administrator/components/com_banners/tmpl/banners/default_batch_body.php +++ b/code/administrator/components/com_banners/tmpl/banners/default_batch_body.php @@ -1,4 +1,5 @@ -
- -
-
- -
-
- -
-
- -
-
-
-
- = 0) : ?> -
-
- 'com_banners']); ?> -
-
- -
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ 'com_banners']); ?> +
+
+ +
diff --git a/code/administrator/components/com_banners/tmpl/banners/default_batch_footer.php b/code/administrator/components/com_banners/tmpl/banners/default_batch_footer.php index 723541c3..d1282ec1 100644 --- a/code/administrator/components/com_banners/tmpl/banners/default_batch_footer.php +++ b/code/administrator/components/com_banners/tmpl/banners/default_batch_footer.php @@ -1,4 +1,5 @@ diff --git a/code/administrator/components/com_banners/tmpl/banners/emptystate.php b/code/administrator/components/com_banners/tmpl/banners/emptystate.php index ec83587c..b4b0db97 100644 --- a/code/administrator/components/com_banners/tmpl/banners/emptystate.php +++ b/code/administrator/components/com_banners/tmpl/banners/emptystate.php @@ -1,4 +1,5 @@ 'COM_BANNERS', - 'formURL' => 'index.php?option=com_banners&view=banners', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners', - 'icon' => 'icon-bookmark banners', + 'textPrefix' => 'COM_BANNERS', + 'formURL' => 'index.php?option=com_banners&view=banners', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners', + 'icon' => 'icon-bookmark banners', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_banners') || count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_banners&task=banner.add'; +if ($user->authorise('core.create', 'com_banners') || count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_banners&task=banner.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_banners/tmpl/client/edit.php b/code/administrator/components/com_banners/tmpl/client/edit.php index 9b8466a5..bd0b60e0 100644 --- a/code/administrator/components/com_banners/tmpl/client/edit.php +++ b/code/administrator/components/com_banners/tmpl/client/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
- + -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> - item->id) ? Text::_('COM_BANNERS_NEW_CLIENT') : Text::_('COM_BANNERS_EDIT_CLIENT')); ?> -
-
- form->renderField('contact'); - echo $this->form->renderField('email'); - echo $this->form->renderField('purchase_type'); - echo $this->form->renderField('track_impressions'); - echo $this->form->renderField('track_clicks'); - echo $this->form->renderFieldset('extra'); - ?> -
-
- -
-
- + item->id) ? Text::_('COM_BANNERS_NEW_CLIENT') : Text::_('COM_BANNERS_EDIT_CLIENT')); ?> +
+
+ form->renderField('contact'); + echo $this->form->renderField('email'); + echo $this->form->renderField('purchase_type'); + echo $this->form->renderField('track_impressions'); + echo $this->form->renderField('track_clicks'); + echo $this->form->renderFieldset('extra'); + ?> +
+
+ +
+
+ - -
-
-
- -
- form->renderFieldset('metadata'); ?> -
-
-
-
- + +
+
+
+ +
+ form->renderFieldset('metadata'); ?> +
+
+
+
+ - -
+ +
- - + +
diff --git a/code/administrator/components/com_banners/tmpl/clients/default.php b/code/administrator/components/com_banners/tmpl/clients/default.php index 4ff67de5..8db12d77 100644 --- a/code/administrator/components/com_banners/tmpl/clients/default.php +++ b/code/administrator/components/com_banners/tmpl/clients/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $purchaseTypes = [ - '1' => 'UNLIMITED', - '2' => 'YEARLY', - '3' => 'MONTHLY', - '4' => 'WEEKLY', - '5' => 'DAILY', + '1' => 'UNLIMITED', + '2' => 'YEARLY', + '3' => 'MONTHLY', + '4' => 'WEEKLY', + '5' => 'DAILY', ]; $user = Factory::getUser(); $userId = $user->get('id'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); -$params = $this->state->params ?? new CMSObject; +$params = $this->state->params ?? new CMSObject(); ?>
-
-
-
- $this]); - ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_banners'); - $canEdit = $user->authorise('core.edit', 'com_banners'); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_banners') && $canCheckin; - ?> - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->name); ?> - - state, $i, 'clients.', $canChange); ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'clients.', $canCheckin); ?> - - - - escape($item->name); ?> - - escape($item->name); ?> - -
-
- contact; ?> - - - count_published; ?> - - - - - count_unpublished; ?> - - - - - count_archived; ?> - - - - - count_trashed; ?> - - - - purchase_type < 0) : ?> - get('purchase_type')])); ?> - - purchase_type]); ?> - - - id; ?> -
+
+
+
+ $this]); + ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_banners'); + $canEdit = $user->authorise('core.edit', 'com_banners'); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_banners') && $canCheckin; + ?> + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->name); ?> + + state, $i, 'clients.', $canChange); ?> + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'clients.', $canCheckin); ?> + + + + escape($item->name); ?> + + escape($item->name); ?> + +
+
+ contact; ?> + + + count_published; ?> + + + + + count_unpublished; ?> + + + + + count_archived; ?> + + + + + count_trashed; ?> + + + + purchase_type < 0) : ?> + get('purchase_type')])); ?> + + purchase_type]); ?> + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
-
-
+ + + +
+
+
diff --git a/code/administrator/components/com_banners/tmpl/clients/emptystate.php b/code/administrator/components/com_banners/tmpl/clients/emptystate.php index 1feedff9..c7e79091 100644 --- a/code/administrator/components/com_banners/tmpl/clients/emptystate.php +++ b/code/administrator/components/com_banners/tmpl/clients/emptystate.php @@ -1,4 +1,5 @@ 'COM_BANNERS_CLIENT', - 'formURL' => 'index.php?option=com_banners&view=clients', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners:_Clients', - 'icon' => 'icon-bookmark banners', + 'textPrefix' => 'COM_BANNERS_CLIENT', + 'formURL' => 'index.php?option=com_banners&view=clients', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners:_Clients', + 'icon' => 'icon-bookmark banners', ]; -if (count(Factory::getApplication()->getIdentity()->getAuthorisedCategories('com_banners', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_banners&task=client.add'; +if (count(Factory::getApplication()->getIdentity()->getAuthorisedCategories('com_banners', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_banners&task=client.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_banners/tmpl/download/default.php b/code/administrator/components/com_banners/tmpl/download/default.php index dc433005..dacd8035 100644 --- a/code/administrator/components/com_banners/tmpl/download/default.php +++ b/code/administrator/components/com_banners/tmpl/download/default.php @@ -1,4 +1,5 @@
-
- - form->getFieldset() as $field) : ?> - form->renderField($field->fieldname); ?> - - - -
+
+ + form->getFieldset() as $field) : ?> + form->renderField($field->fieldname); ?> + + + +
diff --git a/code/administrator/components/com_banners/tmpl/tracks/default.php b/code/administrator/components/com_banners/tmpl/tracks/default.php index 5ec45ffd..04ff5e1f 100644 --- a/code/administrator/components/com_banners/tmpl/tracks/default.php +++ b/code/administrator/components/com_banners/tmpl/tracks/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); +$wa->useScript('table.columns'); + /** @var \Joomla\Component\Banners\Administrator\View\Tracks\HtmlView $this */ $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
-
-
- $this]); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - - - -
- , - , - -
- - - - - - - - - -
- banner_name; ?> -
- escape($item->category_title); ?> -
-
- client_name; ?> - - track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'); ?> - - count; ?> - - track_date, Text::_('DATE_FORMAT_LC5')); ?> -
+
+
+
+ $this]); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ banner_name; ?> +
+ escape($item->category_title); ?> +
+
+ client_name; ?> + + track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'); ?> + + count; ?> + + track_date, Text::_('DATE_FORMAT_LC5')); ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + +
+
+
diff --git a/code/administrator/components/com_banners/tmpl/tracks/emptystate.php b/code/administrator/components/com_banners/tmpl/tracks/emptystate.php index 4ad37d7c..0fe1cd2f 100644 --- a/code/administrator/components/com_banners/tmpl/tracks/emptystate.php +++ b/code/administrator/components/com_banners/tmpl/tracks/emptystate.php @@ -1,4 +1,5 @@ 'COM_BANNERS_TRACKS', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners:_Tracks', - 'icon' => 'icon-bookmark banners', + 'textPrefix' => 'COM_BANNERS_TRACKS', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners:_Tracks', + 'icon' => 'icon-bookmark banners', ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_cache/cache.xml b/code/administrator/components/com_cache/cache.xml index 5689deb5..30107356 100644 --- a/code/administrator/components/com_cache/cache.xml +++ b/code/administrator/components/com_cache/cache.xml @@ -2,7 +2,7 @@ com_cache Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_cache/services/provider.php b/code/administrator/components/com_cache/services/provider.php index 09e3a1e2..01ca10a3 100644 --- a/code/administrator/components/com_cache/services/provider.php +++ b/code/administrator/components/com_cache/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Cache')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Cache')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Cache')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Cache')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_cache/src/Controller/DisplayController.php b/code/administrator/components/com_cache/src/Controller/DisplayController.php index ccbdcc96..67a2290d 100644 --- a/code/administrator/components/com_cache/src/Controller/DisplayController.php +++ b/code/administrator/components/com_cache/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ getModel('Cache'); - - $data = $model->getData(); - - $size = 0; - - if (!empty($data)) - { - foreach ($data as $d) - { - $size += $d->size; - } - } - - // Number bytes are returned in format xxx.xx MB - $bytes = HTMLHelper::_('number.bytes', $size, 'MB', 1); - - if (!empty($bytes)) - { - $result['amount'] = $bytes; - $result['sronly'] = Text::sprintf('COM_CACHE_QUICKICON_SRONLY', $bytes); - } - else - { - $result['amount'] = 0; - $result['sronly'] = Text::sprintf('COM_CACHE_QUICKICON_SRONLY_NOCACHE'); - } - - echo new JsonResponse($result); - } - - /** - * Method to delete a list of cache groups. - * - * @return void - */ - public function delete() - { - // Check for request forgeries - $this->checkToken(); - - $cid = (array) $this->input->post->get('cid', array(), 'string'); - - if (empty($cid)) - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'warning'); - } - else - { - $result = $this->getModel('cache')->cleanlist($cid); - - if ($result !== array()) - { - $this->app->enqueueMessage(Text::sprintf('COM_CACHE_EXPIRED_ITEMS_DELETE_ERROR', implode(', ', $result)), 'error'); - } - else - { - $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_HAVE_BEEN_DELETED'), 'message'); - } - } - - $this->setRedirect('index.php?option=com_cache'); - } - - /** - * Method to delete all cache groups. - * - * @return void - * - * @since 3.6.0 - */ - public function deleteAll() - { - // Check for request forgeries - $this->checkToken(); - - /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ - $model = $this->getModel('cache'); - $allCleared = true; - - $mCache = $model->getCache(); - - foreach ($mCache->getAll() as $cache) - { - if ($mCache->clean($cache->group) === false) - { - $this->app->enqueueMessage( - Text::sprintf( - 'COM_CACHE_EXPIRED_ITEMS_DELETE_ERROR', Text::_('JADMINISTRATOR') . ' > ' . $cache->group - ), 'error' - ); - $allCleared = false; - } - } - - if ($allCleared) - { - $this->app->enqueueMessage(Text::_('COM_CACHE_MSG_ALL_CACHE_GROUPS_CLEARED'), 'message'); - } - else - { - $this->app->enqueueMessage(Text::_('COM_CACHE_MSG_SOME_CACHE_GROUPS_CLEARED'), 'warning'); - } - - $this->app->triggerEvent('onAfterPurge', array()); - $this->setRedirect('index.php?option=com_cache&view=cache'); - } - - /** - * Purge the cache. - * - * @return void - */ - public function purge() - { - // Check for request forgeries - $this->checkToken(); - - if (!$this->getModel('cache')->purge()) - { - $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_PURGING_ERROR'), 'error'); - } - else - { - $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_HAVE_BEEN_PURGED'), 'message'); - } - - $this->setRedirect('index.php?option=com_cache&view=cache'); - } + /** + * The default view for the display method. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'cache'; + + /** + * Method to get The Cache Size + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Cache'); + + $data = $model->getData(); + + $size = 0; + + if (!empty($data)) { + foreach ($data as $d) { + $size += $d->size; + } + } + + // Number bytes are returned in format xxx.xx MB + $bytes = HTMLHelper::_('number.bytes', $size, 'MB', 1); + + if (!empty($bytes)) { + $result['amount'] = $bytes; + $result['sronly'] = Text::sprintf('COM_CACHE_QUICKICON_SRONLY', $bytes); + } else { + $result['amount'] = 0; + $result['sronly'] = Text::sprintf('COM_CACHE_QUICKICON_SRONLY_NOCACHE'); + } + + echo new JsonResponse($result); + } + + /** + * Method to delete a list of cache groups. + * + * @return void + */ + public function delete() + { + // Check for request forgeries + $this->checkToken(); + + $cid = (array) $this->input->post->get('cid', array(), 'string'); + + if (empty($cid)) { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'warning'); + } else { + $result = $this->getModel('cache')->cleanlist($cid); + + if ($result !== array()) { + $this->app->enqueueMessage(Text::sprintf('COM_CACHE_EXPIRED_ITEMS_DELETE_ERROR', implode(', ', $result)), 'error'); + } else { + $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_HAVE_BEEN_DELETED'), 'message'); + } + } + + $this->setRedirect('index.php?option=com_cache'); + } + + /** + * Method to delete all cache groups. + * + * @return void + * + * @since 3.6.0 + */ + public function deleteAll() + { + // Check for request forgeries + $this->checkToken(); + + /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ + $model = $this->getModel('cache'); + $allCleared = true; + + $mCache = $model->getCache(); + + foreach ($mCache->getAll() as $cache) { + if ($mCache->clean($cache->group) === false) { + $this->app->enqueueMessage( + Text::sprintf( + 'COM_CACHE_EXPIRED_ITEMS_DELETE_ERROR', + Text::_('JADMINISTRATOR') . ' > ' . $cache->group + ), + 'error' + ); + $allCleared = false; + } + } + + if ($allCleared) { + $this->app->enqueueMessage(Text::_('COM_CACHE_MSG_ALL_CACHE_GROUPS_CLEARED'), 'message'); + } else { + $this->app->enqueueMessage(Text::_('COM_CACHE_MSG_SOME_CACHE_GROUPS_CLEARED'), 'warning'); + } + + $this->app->triggerEvent('onAfterPurge', array()); + $this->setRedirect('index.php?option=com_cache&view=cache'); + } + + /** + * Purge the cache. + * + * @return void + */ + public function purge() + { + // Check for request forgeries + $this->checkToken(); + + if (!$this->getModel('cache')->purge()) { + $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_PURGING_ERROR'), 'error'); + } else { + $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_HAVE_BEEN_PURGED'), 'message'); + } + + $this->setRedirect('index.php?option=com_cache&view=cache'); + } } diff --git a/code/administrator/components/com_cache/src/Model/CacheModel.php b/code/administrator/components/com_cache/src/Model/CacheModel.php index cdbcebf5..09674652 100644 --- a/code/administrator/components/com_cache/src/Model/CacheModel.php +++ b/code/administrator/components/com_cache/src/Model/CacheModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 3.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to get cache data - * - * @return array - */ - public function getData() - { - if (empty($this->_data)) - { - try - { - $cache = $this->getCache(); - $data = $cache->getAll(); - - if ($data && \count($data) > 0) - { - // Process filter by search term. - if ($search = $this->getState('filter.search')) - { - foreach ($data as $key => $cacheItem) - { - if (stripos($cacheItem->group, $search) === false) - { - unset($data[$key]); - } - } - } - - // Process ordering. - $listOrder = $this->getState('list.ordering', 'group'); - $listDirn = $this->getState('list.direction', 'ASC'); - - $this->_data = ArrayHelper::sortObjects($data, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); - - // Process pagination. - $limit = (int) $this->getState('list.limit', 25); - - if ($limit !== 0) - { - $start = (int) $this->getState('list.start', 0); - - return \array_slice($this->_data, $start, $limit); - } - } - else - { - $this->_data = array(); - } - } - catch (CacheConnectingException $exception) - { - $this->setError(Text::_('COM_CACHE_ERROR_CACHE_CONNECTION_FAILED')); - $this->_data = array(); - } - catch (UnsupportedCacheException $exception) - { - $this->setError(Text::_('COM_CACHE_ERROR_CACHE_DRIVER_UNSUPPORTED')); - $this->_data = array(); - } - } - - return $this->_data; - } - - /** - * Method to get cache instance. - * - * @return CacheController - */ - public function getCache() - { - $app = Factory::getApplication(); - - $options = array( - 'defaultgroup' => '', - 'storage' => $app->get('cache_handler', ''), - 'caching' => true, - 'cachebase' => $app->get('cache_path', JPATH_CACHE) - ); - - return Cache::getInstance('', $options); - } - - /** - * Get the number of current Cache Groups. - * - * @return integer - */ - public function getTotal() - { - if (empty($this->_total)) - { - $this->_total = count($this->getData()); - } - - return $this->_total; - } - - /** - * Method to get a pagination object for the cache. - * - * @return Pagination - */ - public function getPagination() - { - if (empty($this->_pagination)) - { - $this->_pagination = new Pagination($this->getTotal(), $this->getState('list.start'), $this->getState('list.limit')); - } - - return $this->_pagination; - } - - /** - * Clean out a cache group as named by param. - * If no param is passed clean all cache groups. - * - * @param string $group Cache group name. - * - * @return boolean True on success, false otherwise - */ - public function clean($group = '') - { - try - { - $this->getCache()->clean($group); - } - catch (CacheConnectingException $exception) - { - return false; - } - catch (UnsupportedCacheException $exception) - { - return false; - } - - Factory::getApplication()->triggerEvent('onAfterPurge', array($group)); - - return true; - } - - /** - * Purge an array of cache groups. - * - * @param array $array Array of cache group names. - * - * @return array Array with errors, if they exist. - */ - public function cleanlist($array) - { - $errors = array(); - - foreach ($array as $group) - { - if (!$this->clean($group)) - { - $errors[] = $group; - } - } - - return $errors; - } - - /** - * Purge all cache items. - * - * @return boolean True if successful; false otherwise. - */ - public function purge() - { - try - { - Factory::getCache('')->gc(); - } - catch (CacheConnectingException $exception) - { - return false; - } - catch (UnsupportedCacheException $exception) - { - return false; - } - - Factory::getApplication()->triggerEvent('onAfterPurge', array()); - - return true; - } + /** + * An Array of CacheItems indexed by cache group ID + * + * @var array + */ + protected $_data = array(); + + /** + * Group total + * + * @var integer + */ + protected $_total = null; + + /** + * Pagination object + * + * @var object + */ + protected $_pagination = null; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 3.5 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'group', + 'count', + 'size', + 'client_id', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering Field for ordering. + * @param string $direction Direction of ordering. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'group', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 3.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to get cache data + * + * @return array + */ + public function getData() + { + if (empty($this->_data)) { + try { + $cache = $this->getCache(); + $data = $cache->getAll(); + + if ($data && \count($data) > 0) { + // Process filter by search term. + if ($search = $this->getState('filter.search')) { + foreach ($data as $key => $cacheItem) { + if (stripos($cacheItem->group, $search) === false) { + unset($data[$key]); + } + } + } + + // Process ordering. + $listOrder = $this->getState('list.ordering', 'group'); + $listDirn = $this->getState('list.direction', 'ASC'); + + $this->_data = ArrayHelper::sortObjects($data, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); + + // Process pagination. + $limit = (int) $this->getState('list.limit', 25); + + if ($limit !== 0) { + $start = (int) $this->getState('list.start', 0); + + return \array_slice($this->_data, $start, $limit); + } + } else { + $this->_data = array(); + } + } catch (CacheConnectingException $exception) { + $this->setError(Text::_('COM_CACHE_ERROR_CACHE_CONNECTION_FAILED')); + $this->_data = array(); + } catch (UnsupportedCacheException $exception) { + $this->setError(Text::_('COM_CACHE_ERROR_CACHE_DRIVER_UNSUPPORTED')); + $this->_data = array(); + } + } + + return $this->_data; + } + + /** + * Method to get cache instance. + * + * @return CacheController + */ + public function getCache() + { + $app = Factory::getApplication(); + + $options = array( + 'defaultgroup' => '', + 'storage' => $app->get('cache_handler', ''), + 'caching' => true, + 'cachebase' => $app->get('cache_path', JPATH_CACHE) + ); + + return Cache::getInstance('', $options); + } + + /** + * Get the number of current Cache Groups. + * + * @return integer + */ + public function getTotal() + { + if (empty($this->_total)) { + $this->_total = count($this->getData()); + } + + return $this->_total; + } + + /** + * Method to get a pagination object for the cache. + * + * @return Pagination + */ + public function getPagination() + { + if (empty($this->_pagination)) { + $this->_pagination = new Pagination($this->getTotal(), $this->getState('list.start'), $this->getState('list.limit')); + } + + return $this->_pagination; + } + + /** + * Clean out a cache group as named by param. + * If no param is passed clean all cache groups. + * + * @param string $group Cache group name. + * + * @return boolean True on success, false otherwise + */ + public function clean($group = '') + { + try { + $this->getCache()->clean($group); + } catch (CacheConnectingException $exception) { + return false; + } catch (UnsupportedCacheException $exception) { + return false; + } + + Factory::getApplication()->triggerEvent('onAfterPurge', array($group)); + + return true; + } + + /** + * Purge an array of cache groups. + * + * @param array $array Array of cache group names. + * + * @return array Array with errors, if they exist. + */ + public function cleanlist($array) + { + $errors = array(); + + foreach ($array as $group) { + if (!$this->clean($group)) { + $errors[] = $group; + } + } + + return $errors; + } + + /** + * Purge all cache items. + * + * @return boolean True if successful; false otherwise. + */ + public function purge() + { + try { + Factory::getCache('')->gc(); + } catch (CacheConnectingException $exception) { + return false; + } catch (UnsupportedCacheException $exception) { + return false; + } + + Factory::getApplication()->triggerEvent('onAfterPurge', array()); + + return true; + } } diff --git a/code/administrator/components/com_cache/src/View/Cache/HtmlView.php b/code/administrator/components/com_cache/src/View/Cache/HtmlView.php index b26eaea6..7018a26e 100644 --- a/code/administrator/components/com_cache/src/View/Cache/HtmlView.php +++ b/code/administrator/components/com_cache/src/View/Cache/HtmlView.php @@ -137,7 +137,7 @@ protected function addToolbar(): void ToolbarHelper::divider(); } - if (Factory::getUser()->authorise('core.admin', 'com_cache')) + if ($this->getCurrentUser()->authorise('core.admin', 'com_cache')) { ToolbarHelper::preferences('com_cache'); ToolbarHelper::divider(); diff --git a/code/administrator/components/com_cache/tmpl/cache/default.php b/code/administrator/components/com_cache/tmpl/cache/default.php index 9f53205a..be12a200 100644 --- a/code/administrator/components/com_cache/tmpl/cache/default.php +++ b/code/administrator/components/com_cache/tmpl/cache/default.php @@ -22,6 +22,7 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive') + ->useScript('table.columns') ->useScript('multiselect') ->useScript('com_cache.admin-cache'); diff --git a/code/administrator/components/com_categories/categories.xml b/code/administrator/components/com_categories/categories.xml index db92f5aa..8fa13bf8 100644 --- a/code/administrator/components/com_categories/categories.xml +++ b/code/administrator/components/com_categories/categories.xml @@ -2,7 +2,7 @@ com_categories Joomla! Project - December 2007 + 2007-12 (C) 2007 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_categories/forms/category.xml b/code/administrator/components/com_categories/forms/category.xml index 04617cf6..2ef2ba4b 100644 --- a/code/administrator/components/com_categories/forms/category.xml +++ b/code/administrator/components/com_categories/forms/category.xml @@ -24,7 +24,6 @@ name="asset_id" type="hidden" filter="unset" - label="JFIELD_ASSET_ID_LABEL" /> * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Categories helper. diff --git a/code/administrator/components/com_categories/layouts/joomla/form/field/categoryedit.php b/code/administrator/components/com_categories/layouts/joomla/form/field/categoryedit.php index e4d3b0d7..a94ab5ed 100644 --- a/code/administrator/components/com_categories/layouts/joomla/form/field/categoryedit.php +++ b/code/administrator/components/com_categories/layouts/joomla/form/field/categoryedit.php @@ -1,4 +1,5 @@ value as array - if ($multiple && is_array($value)) - { - if (!count($value)) - { - $value[] = ''; - } - - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} -else -{ - // Create a regular list. - if (count($options) === 0) - { - // All Categories have been deleted, so we need a new category (This will create on save if selected). - $options[0] = new \stdClass; - $options[0]->value = 'Uncategorised'; - $options[0]->text = 'Uncategorised'; - $options[0]->level = '1'; - $options[0]->published = '1'; - $options[0]->lft = '1'; - } - - $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); +if ($readonly) { + $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); + + // E.g. form field type tag sends $this->value as array + if ($multiple && is_array($value)) { + if (!count($value)) { + $value[] = ''; + } + + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else { + // Create a regular list. + if (count($options) === 0) { + // All Categories have been deleted, so we need a new category (This will create on save if selected). + $options[0] = new \stdClass(); + $options[0]->value = 'Uncategorised'; + $options[0]->text = 'Uncategorised'; + $options[0]->level = '1'; + $options[0]->published = '1'; + $options[0]->lft = '1'; + } + + $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); } -if ($refreshPage === true) -{ - $attr2 .= ' data-refresh-catid="' . $refreshCatId . '" data-refresh-section="' . $refreshSection . '"'; - $attr2 .= ' onchange="Joomla.categoryHasChanged(this)"'; +if ($refreshPage === true) { + $attr2 .= ' data-refresh-catid="' . $refreshCatId . '" data-refresh-section="' . $refreshSection . '"'; + $attr2 .= ' onchange="Joomla.categoryHasChanged(this)"'; - Factory::getDocument()->getWebAssetManager() - ->registerAndUseScript('field.category-change', 'layouts/joomla/form/field/category-change.min.js', [], ['defer' => true], ['core']) - ->useScript('webcomponent.core-loader'); + Factory::getDocument()->getWebAssetManager() + ->registerAndUseScript('field.category-change', 'layouts/joomla/form/field/category-change.min.js', [], ['defer' => true], ['core']) + ->useScript('webcomponent.core-loader'); - // Pass the element id to the javascript - Factory::getDocument()->addScriptOptions('category-change', $id); -} -else -{ - $attr2 .= $onchange ? ' onchange="' . $onchange . '"' : ''; + // Pass the element id to the javascript + Factory::getDocument()->addScriptOptions('category-change', $id); +} else { + $attr2 .= $onchange ? ' onchange="' . $onchange . '"' : ''; } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> > diff --git a/code/administrator/components/com_categories/services/provider.php b/code/administrator/components/com_categories/services/provider.php index 2ef22e13..043c4850 100644 --- a/code/administrator/components/com_categories/services/provider.php +++ b/code/administrator/components/com_categories/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Categories')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Categories')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Categories')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Categories')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new CategoriesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new CategoriesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_categories/src/Controller/AjaxController.php b/code/administrator/components/com_categories/src/Controller/AjaxController.php index f0bdbe8e..93704465 100644 --- a/code/administrator/components/com_categories/src/Controller/AjaxController.php +++ b/code/administrator/components/com_categories/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->get('extension'); - - $assocId = $this->input->getInt('assocId', 0); - - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - - return; - } - - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - - $associations = Associations::getAssociations($extension, '#__categories', 'com_categories.item', (int) $assocId, 'id', 'alias', ''); - - unset($associations[$excludeLang]); - - // Add the title to each of the associated records - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_categories/tables'); - $categoryTable = Table::getInstance('Category', 'JTable'); - - foreach ($associations as $lang => $association) - { - $categoryTable->load($association->id); - $associations[$lang]->title = $categoryTable->title; - } - - $countContentLanguages = \count(LanguageHelper::getContentLanguages(array(0, 1), false)); - - if (\count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > \count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } - - echo new JsonResponse($associations, $message); - } - } + /** + * Method to fetch associations of a category + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the category whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return void + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $extension = $this->input->get('extension'); + + $assocId = $this->input->getInt('assocId', 0); + + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + + return; + } + + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + + $associations = Associations::getAssociations($extension, '#__categories', 'com_categories.item', (int) $assocId, 'id', 'alias', ''); + + unset($associations[$excludeLang]); + + // Add the title to each of the associated records + Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_categories/tables'); + $categoryTable = Table::getInstance('Category', 'JTable'); + + foreach ($associations as $lang => $association) { + $categoryTable->load($association->id); + $associations[$lang]->title = $categoryTable->title; + } + + $countContentLanguages = \count(LanguageHelper::getContentLanguages(array(0, 1), false)); + + if (\count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > \count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } + + echo new JsonResponse($associations, $message); + } + } } diff --git a/code/administrator/components/com_categories/src/Controller/CategoriesController.php b/code/administrator/components/com_categories/src/Controller/CategoriesController.php index ad464332..ebed8134 100644 --- a/code/administrator/components/com_categories/src/Controller/CategoriesController.php +++ b/code/administrator/components/com_categories/src/Controller/CategoriesController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Outputs the JSON-encoded amount of published content categories - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Categories'); - $model->setState('filter.published', 1); - $model->setState('filter.extension', 'com_content'); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_CATEGORIES_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_CATEGORIES_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } - - /** - * Rebuild the nested set tree. - * - * @return boolean False on failure or error, true on success. - * - * @since 1.6 - */ - public function rebuild() - { - $this->checkToken(); - - $extension = $this->input->get('extension'); - $this->setRedirect(Route::_('index.php?option=com_categories&view=categories&extension=' . $extension, false)); - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $model */ - $model = $this->getModel(); - - if ($model->rebuild()) - { - // Rebuild succeeded. - $this->setMessage(Text::_('COM_CATEGORIES_REBUILD_SUCCESS')); - - return true; - } - - // Rebuild failed. - $this->setMessage(Text::_('COM_CATEGORIES_REBUILD_FAILURE')); - - return false; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $extension = $this->input->getCmd('extension', null); - - return '&extension=' . $extension; - } + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Category', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Outputs the JSON-encoded amount of published content categories + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Categories'); + $model->setState('filter.published', 1); + $model->setState('filter.extension', 'com_content'); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_CATEGORIES_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_CATEGORIES_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } + + /** + * Rebuild the nested set tree. + * + * @return boolean False on failure or error, true on success. + * + * @since 1.6 + */ + public function rebuild() + { + $this->checkToken(); + + $extension = $this->input->get('extension'); + $this->setRedirect(Route::_('index.php?option=com_categories&view=categories&extension=' . $extension, false)); + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $model */ + $model = $this->getModel(); + + if ($model->rebuild()) { + // Rebuild succeeded. + $this->setMessage(Text::_('COM_CATEGORIES_REBUILD_SUCCESS')); + + return true; + } + + // Rebuild failed. + $this->setMessage(Text::_('COM_CATEGORIES_REBUILD_FAILURE')); + + return false; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $extension = $this->input->getCmd('extension', null); + + return '&extension=' . $extension; + } } diff --git a/code/administrator/components/com_categories/src/Controller/CategoryController.php b/code/administrator/components/com_categories/src/Controller/CategoryController.php index 0a7e7467..ce8ac847 100644 --- a/code/administrator/components/com_categories/src/Controller/CategoryController.php +++ b/code/administrator/components/com_categories/src/Controller/CategoryController.php @@ -1,4 +1,5 @@ extension)) - { - $this->extension = $this->input->get('extension', 'com_content'); - } - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowAdd($data = array()) - { - $user = $this->app->getIdentity(); - - return ($user->authorise('core.create', $this->extension) || \count($user->getAuthorisedCategories($this->extension, 'core.create'))); - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'parent_id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Check "edit" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->extension . '.category.' . $recordId)) - { - return true; - } - - // Check "edit own" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->extension . '.category.' . $recordId)) - { - // Need to do a lookup from the model to get the owner - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - $ownerId = $record->created_user_id; - - // If the owner matches 'me' then do the test. - if ($ownerId == $user->id) - { - return true; - } - } - - return false; - } - - /** - * Override parent save method to store form data with right key as expected by edit category page - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 3.10.3 - */ - public function save($key = null, $urlVar = null) - { - $result = parent::save($key, $urlVar); - - $oldKey = $this->option . '.edit.category.data'; - $newKey = $this->option . '.edit.category.' . substr($this->extension, 4) . '.data'; - $this->app->setUserState($newKey, $this->app->getUserState($oldKey)); - - return $result; - } - - /** - * Override cancel method to clear form data for a failed edit action - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 3.10.3 - */ - public function cancel($key = null) - { - $result = parent::cancel($key); - - $newKey = $this->option . '.edit.category.' . substr($this->extension, 4) . '.data'; - $this->app->setUserState($newKey, null); - - return $result; - } - - /** - * Method to run batch operations. - * - * @param object|null $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 1.6 - */ - public function batch($model = null) - { - $this->checkToken(); - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $model */ - $model = $this->getModel('Category'); - - // Preset the redirect - $this->setRedirect('index.php?option=com_categories&view=categories&extension=' . $this->extension); - - return parent::batch($model); - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer|null $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 1.6 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - - // In case extension is not passed in the URL, get it directly from category instead of default to com_content - if (!$this->input->exists('extension') && $recordId > 0) - { - $table = $this->getModel('Category')->getTable(); - - if ($table->load($recordId)) - { - $this->extension = $table->extension; - } - } - - $append .= '&extension=' . $this->extension; - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 1.6 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&extension=' . $this->extension; - - return $append; - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param \Joomla\CMS\MVC\Model\BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.1 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $item = $model->getItem(); - - if (isset($item->params) && \is_array($item->params)) - { - $registry = new Registry($item->params); - $item->params = (string) $registry; - } - - if (isset($item->metadata) && \is_array($item->metadata)) - { - $registry = new Registry($item->metadata); - $item->metadata = (string) $registry; - } - } + use VersionableControllerTrait; + + /** + * The extension for which the categories apply. + * + * @var string + * @since 1.6 + */ + protected $extension; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param Input|null $input Input + * + * @since 1.6 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, CMSApplication $app = null, Input $input = null) + { + parent::__construct($config, $factory, $app, $input); + + if (empty($this->extension)) { + $this->extension = $this->input->get('extension', 'com_content'); + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $user = $this->app->getIdentity(); + + return ($user->authorise('core.create', $this->extension) || \count($user->getAuthorisedCategories($this->extension, 'core.create'))); + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'parent_id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.category.' . $recordId)) { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.category.' . $recordId)) { + // Need to do a lookup from the model to get the owner + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + $ownerId = $record->created_user_id; + + // If the owner matches 'me' then do the test. + if ($ownerId == $user->id) { + return true; + } + } + + return false; + } + + /** + * Override parent save method to store form data with right key as expected by edit category page + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 3.10.3 + */ + public function save($key = null, $urlVar = null) + { + $result = parent::save($key, $urlVar); + + $oldKey = $this->option . '.edit.category.data'; + $newKey = $this->option . '.edit.category.' . substr($this->extension, 4) . '.data'; + $this->app->setUserState($newKey, $this->app->getUserState($oldKey)); + + return $result; + } + + /** + * Override cancel method to clear form data for a failed edit action + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 3.10.3 + */ + public function cancel($key = null) + { + $result = parent::cancel($key); + + $newKey = $this->option . '.edit.category.' . substr($this->extension, 4) . '.data'; + $this->app->setUserState($newKey, null); + + return $result; + } + + /** + * Method to run batch operations. + * + * @param object|null $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 1.6 + */ + public function batch($model = null) + { + $this->checkToken(); + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $model */ + $model = $this->getModel('Category'); + + // Preset the redirect + $this->setRedirect('index.php?option=com_categories&view=categories&extension=' . $this->extension); + + return parent::batch($model); + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer|null $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 1.6 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + + // In case extension is not passed in the URL, get it directly from category instead of default to com_content + if (!$this->input->exists('extension') && $recordId > 0) { + $table = $this->getModel('Category')->getTable(); + + if ($table->load($recordId)) { + $this->extension = $table->extension; + } + } + + $append .= '&extension=' . $this->extension; + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 1.6 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&extension=' . $this->extension; + + return $append; + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param \Joomla\CMS\MVC\Model\BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.1 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $item = $model->getItem(); + + if (isset($item->params) && \is_array($item->params)) { + $registry = new Registry($item->params); + $item->params = (string) $registry; + } + + if (isset($item->metadata) && \is_array($item->metadata)) { + $registry = new Registry($item->metadata); + $item->metadata = (string) $registry; + } + } } diff --git a/code/administrator/components/com_categories/src/Controller/DisplayController.php b/code/administrator/components/com_categories/src/Controller/DisplayController.php index 5f9ab4ae..12791d94 100644 --- a/code/administrator/components/com_categories/src/Controller/DisplayController.php +++ b/code/administrator/components/com_categories/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ extension)) - { - $this->extension = $this->input->get('extension', 'com_content'); - } - } - - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. - * - * @return static|boolean This object to support chaining. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = array()) - { - // Get the document object. - $document = $this->app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'categories'); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - $id = $this->input->getInt('id'); - - // Check for edit form. - if ($vName == 'category' && $lName == 'edit' && !$this->checkEditId('com_categories.edit.category', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_categories&view=categories&extension=' . $this->extension, false)); - - return false; - } - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - // Get the model for the view. - $model = $this->getModel($vName, 'Administrator', array('name' => $vName . '.' . substr($this->extension, 4))); - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - $view->display(); - } - - return $this; - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'categories'; + + /** + * The extension for which the categories apply. + * + * @var string + * @since 1.6 + */ + protected $extension; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param Input|null $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Guess the Text message prefix. Defaults to the option. + if (empty($this->extension)) { + $this->extension = $this->input->get('extension', 'com_content'); + } + } + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'categories'); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + $id = $this->input->getInt('id'); + + // Check for edit form. + if ($vName == 'category' && $lName == 'edit' && !$this->checkEditId('com_categories.edit.category', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_categories&view=categories&extension=' . $this->extension, false)); + + return false; + } + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + // Get the model for the view. + $model = $this->getModel($vName, 'Administrator', array('name' => $vName . '.' . substr($this->extension, 4))); + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + $view->display(); + } + + return $this; + } } diff --git a/code/administrator/components/com_categories/src/Dispatcher/Dispatcher.php b/code/administrator/components/com_categories/src/Dispatcher/Dispatcher.php index d32c1c0e..68ee3f8f 100644 --- a/code/administrator/components/com_categories/src/Dispatcher/Dispatcher.php +++ b/code/administrator/components/com_categories/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ getApplication()->input->getCmd('extension'); + /** + * Categories have to check for extension permission + * + * @return void + */ + protected function checkAccess() + { + $extension = $this->getApplication()->input->getCmd('extension'); - $parts = explode('.', $extension); + $parts = explode('.', $extension); - // Check the user has permission to access this component if in the backend - if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage', $parts[0])) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + // Check the user has permission to access this component if in the backend + if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage', $parts[0])) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/code/administrator/components/com_categories/src/Extension/CategoriesComponent.php b/code/administrator/components/com_categories/src/Extension/CategoriesComponent.php index 8b2eb72f..7c03e5f6 100644 --- a/code/administrator/components/com_categories/src/Extension/CategoriesComponent.php +++ b/code/administrator/components/com_categories/src/Extension/CategoriesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('categoriesadministrator', new AdministratorService); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('categoriesadministrator', new AdministratorService()); + } } diff --git a/code/administrator/components/com_categories/src/Field/CategoryeditField.php b/code/administrator/components/com_categories/src/Field/CategoryeditField.php index 5b152ad6..ae4e2d88 100644 --- a/code/administrator/components/com_categories/src/Field/CategoryeditField.php +++ b/code/administrator/components/com_categories/src/Field/CategoryeditField.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string|null $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 3.2 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); - - if ($return) - { - $this->allowAdd = isset($this->element['allowAdd']) ? (boolean) $this->element['allowAdd'] : false; - $this->customPrefix = (string) $this->element['customPrefix']; - } - - return $return; - } - - /** - * Method to get certain otherwise inaccessible properties from the form field object. - * - * @param string $name The property name for which to get the value. - * - * @return mixed The property value or null. - * - * @since 3.6 - */ - public function __get($name) - { - switch ($name) - { - case 'allowAdd': - return (bool) $this->$name; - case 'customPrefix': - return $this->$name; - } - - return parent::__get($name); - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 3.6 - */ - public function __set($name, $value) - { - $value = (string) $value; - - switch ($name) - { - case 'allowAdd': - $value = (string) $value; - $this->$name = ($value === 'true' || $value === $name || $value === '1'); - break; - case 'customPrefix': - $this->$name = (string) $value; - break; - default: - parent::__set($name, $value); - } - } - - /** - * Method to get a list of categories that respects access controls and can be used for - * either category assignment or parent category assignment in edit screens. - * Use the parent element to indicate that the field will be used for assigning parent categories. - * - * @return array The field option objects. - * - * @since 1.6 - */ - protected function getOptions() - { - $options = array(); - $published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array(0, 1); - $name = (string) $this->element['name']; - - // Let's get the id for the current item, either category or content item. - $jinput = Factory::getApplication()->input; - - // Load the category options for a given extension. - - // For categories the old category is the category id or 0 for new category. - if ($this->element['parent'] || $jinput->get('option') == 'com_categories') - { - $oldCat = $jinput->get('id', 0); - $oldParent = $this->form->getValue($name, 0); - $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('extension', 'com_content'); - } - else - // For items the old category is the category they are in when opened or 0 if new. - { - $oldCat = $this->form->getValue($name, 0); - $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('option', 'com_content'); - } - - // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category - $oldCat = \is_array($oldCat) - ? (int) reset($oldCat) - : (int) $oldCat; - - $db = Factory::getDbo(); - $user = Factory::getUser(); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.level'), - $db->quoteName('a.published'), - $db->quoteName('a.lft'), - $db->quoteName('a.language'), - ] - ) - ->from($db->quoteName('#__categories', 'a')); - - // Filter by the extension type - if ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') - { - $query->where('(' . $db->quoteName('a.extension') . ' = :extension OR ' . $db->quoteName('a.parent_id') . ' = 0)') - ->bind(':extension', $extension); - } - else - { - $query->where($db->quoteName('a.extension') . ' = :extension') - ->bind(':extension', $extension); - } - - // Filter language - if (!empty($this->element['language'])) - { - if (strpos($this->element['language'], ',') !== false) - { - $language = explode(',', $this->element['language']); - } - else - { - $language = $this->element['language']; - } - - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - - // Filter on the published state - $state = ArrayHelper::toInteger($published); - $query->whereIn($db->quoteName('a.published'), $state); - - // Filter categories on User Access Level - // Filter by access level on categories. - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - $query->order($db->quoteName('a.lft') . ' ASC'); - - // If parent isn't explicitly stated but we are in com_categories assume we want parents - if ($oldCat != 0 && ($this->element['parent'] == true || $jinput->get('option') == 'com_categories')) - { - // Prevent parenting to children of this item. - // To rearrange parents and children move the children up, not the parents down. - $query->join( - 'LEFT', - $db->quoteName('#__categories', 'p'), - $db->quoteName('p.id') . ' = :oldcat' - ) - ->bind(':oldcat', $oldCat, ParameterType::INTEGER) - ->where('NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft') - . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')' - ); - } - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Pad the option text with spaces using depth level as a multiplier. - for ($i = 0, $n = \count($options); $i < $n; $i++) - { - // Translate ROOT - if ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') - { - if ($options[$i]->level == 0) - { - $options[$i]->text = Text::_('JGLOBAL_ROOT_PARENT'); - } - } - - if ($options[$i]->published == 1) - { - $options[$i]->text = str_repeat('- ', !$options[$i]->level ? 0 : $options[$i]->level - 1) . $options[$i]->text; - } - else - { - $options[$i]->text = str_repeat('- ', !$options[$i]->level ? 0 : $options[$i]->level - 1) . '[' . $options[$i]->text . ']'; - } - - // Displays language code if not set to All - if ($options[$i]->language !== '*') - { - $options[$i]->text = $options[$i]->text . ' (' . $options[$i]->language . ')'; - } - } - - // For new items we want a list of categories you are allowed to create in. - if ($oldCat == 0) - { - foreach ($options as $i => $option) - { - /* - * To take save or create in a category you need to have create rights for that category unless the item is already in that category. - * Unset the option if the user isn't authorised for it. In this field assets are always categories. - */ - if ($option->level != 0 && !$user->authorise('core.create', $extension . '.category.' . $option->value)) - { - unset($options[$i]); - } - } - } - // If you have an existing category id things are more complex. - else - { - /* - * If you are only allowed to edit in this category but not edit.state, you should not get any - * option to change the category parent for a category or the category for a content item, - * but you should be able to save in that category. - */ - foreach ($options as $i => $option) - { - $assetKey = $extension . '.category.' . $oldCat; - - if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.edit.state', $assetKey)) - { - unset($options[$i]); - continue; - } - - if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.edit.state', $assetKey)) - { - unset($options[$i]); - continue; - } - - /* - * However, if you can edit.state you can also move this to another category for which you have - * create permission and you should also still be able to save in the current category. - */ - $assetKey = $extension . '.category.' . $option->value; - - if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.create', $assetKey)) - { - unset($options[$i]); - continue; - } - - if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.create', $assetKey)) - { - unset($options[$i]); - } - } - } - - if ($oldCat != 0 && ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') - && !isset($options[0]) - && isset($this->element['show_root'])) - { - $rowQuery = $db->getQuery(true) - ->select( - [ - $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.level'), - $db->quoteName('a.parent_id'), - ] - ) - ->from($db->quoteName('#__categories', 'a')) - ->where($db->quoteName('a.id') . ' = :aid') - ->bind(':aid', $oldCat, ParameterType::INTEGER); - $db->setQuery($rowQuery); - $row = $db->loadObject(); - - if ($row->parent_id == '1') - { - $parent = new \stdClass; - $parent->text = Text::_('JGLOBAL_ROOT_PARENT'); - array_unshift($options, $parent); - } - - array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('JGLOBAL_ROOT'))); - } - - // Merge any additional options in the XML definition. - return array_merge(parent::getOptions(), $options); - } - - /** - * Method to get the field input markup for a generic list. - * Use the multiple attribute to enable multiselect. - * - * @return string The field input markup. - * - * @since 3.6 - */ - protected function getInput() - { - $data = $this->getLayoutData(); - - $data['options'] = $this->getOptions(); - $data['allowCustom'] = $this->allowAdd; - $data['customPrefix'] = $this->customPrefix; - $data['refreshPage'] = (boolean) $this->element['refresh-enabled']; - $data['refreshCatId'] = (string) $this->element['refresh-cat-id']; - $data['refreshSection'] = (string) $this->element['refresh-section']; - - $renderer = $this->getRenderer($this->layout); - $renderer->setComponent('com_categories'); - $renderer->setClient(1); - - return $renderer->render($data); - } + /** + * To allow creation of new categories. + * + * @var integer + * @since 3.6 + */ + protected $allowAdd; + + /** + * Optional prefix for new categories. + * + * @var string + * @since 3.9.11 + */ + protected $customPrefix; + + /** + * A flexible category list that respects access controls + * + * @var string + * @since 1.6 + */ + public $type = 'CategoryEdit'; + + /** + * Name of the layout being used to render the field + * + * @var string + * @since 4.0.0 + */ + protected $layout = 'joomla.form.field.categoryedit'; + + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string|null $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 3.2 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); + + if ($return) { + $this->allowAdd = isset($this->element['allowAdd']) ? (bool) $this->element['allowAdd'] : false; + $this->customPrefix = (string) $this->element['customPrefix']; + } + + return $return; + } + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 3.6 + */ + public function __get($name) + { + switch ($name) { + case 'allowAdd': + return (bool) $this->$name; + case 'customPrefix': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 3.6 + */ + public function __set($name, $value) + { + $value = (string) $value; + + switch ($name) { + case 'allowAdd': + $value = (string) $value; + $this->$name = ($value === 'true' || $value === $name || $value === '1'); + break; + case 'customPrefix': + $this->$name = (string) $value; + break; + default: + parent::__set($name, $value); + } + } + + /** + * Method to get a list of categories that respects access controls and can be used for + * either category assignment or parent category assignment in edit screens. + * Use the parent element to indicate that the field will be used for assigning parent categories. + * + * @return array The field option objects. + * + * @since 1.6 + */ + protected function getOptions() + { + $options = array(); + $published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array(0, 1); + $name = (string) $this->element['name']; + + // Let's get the id for the current item, either category or content item. + $jinput = Factory::getApplication()->input; + + // Load the category options for a given extension. + + // For categories the old category is the category id or 0 for new category. + if ($this->element['parent'] || $jinput->get('option') == 'com_categories') { + $oldCat = $jinput->get('id', 0); + $oldParent = $this->form->getValue($name, 0); + $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('extension', 'com_content'); + } else // For items the old category is the category they are in when opened or 0 if new. + { + $oldCat = $this->form->getValue($name, 0); + $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('option', 'com_content'); + } + + // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category + $oldCat = \is_array($oldCat) + ? (int) reset($oldCat) + : (int) $oldCat; + + $db = $this->getDatabase(); + $user = Factory::getUser(); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.level'), + $db->quoteName('a.published'), + $db->quoteName('a.lft'), + $db->quoteName('a.language'), + ] + ) + ->from($db->quoteName('#__categories', 'a')); + + // Filter by the extension type + if ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') { + $query->where('(' . $db->quoteName('a.extension') . ' = :extension OR ' . $db->quoteName('a.parent_id') . ' = 0)') + ->bind(':extension', $extension); + } else { + $query->where($db->quoteName('a.extension') . ' = :extension') + ->bind(':extension', $extension); + } + + // Filter language + if (!empty($this->element['language'])) { + if (strpos($this->element['language'], ',') !== false) { + $language = explode(',', $this->element['language']); + } else { + $language = $this->element['language']; + } + + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } + + // Filter on the published state + $state = ArrayHelper::toInteger($published); + $query->whereIn($db->quoteName('a.published'), $state); + + // Filter categories on User Access Level + // Filter by access level on categories. + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + $query->order($db->quoteName('a.lft') . ' ASC'); + + // If parent isn't explicitly stated but we are in com_categories assume we want parents + if ($oldCat != 0 && ($this->element['parent'] == true || $jinput->get('option') == 'com_categories')) { + // Prevent parenting to children of this item. + // To rearrange parents and children move the children up, not the parents down. + $query->join( + 'LEFT', + $db->quoteName('#__categories', 'p'), + $db->quoteName('p.id') . ' = :oldcat' + ) + ->bind(':oldcat', $oldCat, ParameterType::INTEGER) + ->where('NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft') + . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')'); + } + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Pad the option text with spaces using depth level as a multiplier. + for ($i = 0, $n = \count($options); $i < $n; $i++) { + // Translate ROOT + if ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') { + if ($options[$i]->level == 0) { + $options[$i]->text = Text::_('JGLOBAL_ROOT_PARENT'); + } + } + + if ($options[$i]->published == 1) { + $options[$i]->text = str_repeat('- ', !$options[$i]->level ? 0 : $options[$i]->level - 1) . $options[$i]->text; + } else { + $options[$i]->text = str_repeat('- ', !$options[$i]->level ? 0 : $options[$i]->level - 1) . '[' . $options[$i]->text . ']'; + } + + // Displays language code if not set to All + if ($options[$i]->language !== '*') { + $options[$i]->text = $options[$i]->text . ' (' . $options[$i]->language . ')'; + } + } + + // For new items we want a list of categories you are allowed to create in. + if ($oldCat == 0) { + foreach ($options as $i => $option) { + /* + * To take save or create in a category you need to have create rights for that category unless the item is already in that category. + * Unset the option if the user isn't authorised for it. In this field assets are always categories. + */ + if ($option->level != 0 && !$user->authorise('core.create', $extension . '.category.' . $option->value)) { + unset($options[$i]); + } + } + } else { + // If you have an existing category id things are more complex. + /* + * If you are only allowed to edit in this category but not edit.state, you should not get any + * option to change the category parent for a category or the category for a content item, + * but you should be able to save in that category. + */ + foreach ($options as $i => $option) { + $assetKey = $extension . '.category.' . $oldCat; + + if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.edit.state', $assetKey)) { + unset($options[$i]); + continue; + } + + if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.edit.state', $assetKey)) { + unset($options[$i]); + continue; + } + + /* + * However, if you can edit.state you can also move this to another category for which you have + * create permission and you should also still be able to save in the current category. + */ + $assetKey = $extension . '.category.' . $option->value; + + if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.create', $assetKey)) { + unset($options[$i]); + continue; + } + + if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.create', $assetKey)) { + unset($options[$i]); + } + } + } + + if ( + $oldCat != 0 && ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') + && !isset($options[0]) + && isset($this->element['show_root']) + ) { + $rowQuery = $db->getQuery(true) + ->select( + [ + $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.level'), + $db->quoteName('a.parent_id'), + ] + ) + ->from($db->quoteName('#__categories', 'a')) + ->where($db->quoteName('a.id') . ' = :aid') + ->bind(':aid', $oldCat, ParameterType::INTEGER); + $db->setQuery($rowQuery); + $row = $db->loadObject(); + + if ($row->parent_id == '1') { + $parent = new \stdClass(); + $parent->text = Text::_('JGLOBAL_ROOT_PARENT'); + array_unshift($options, $parent); + } + + array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('JGLOBAL_ROOT'))); + } + + // Merge any additional options in the XML definition. + return array_merge(parent::getOptions(), $options); + } + + /** + * Method to get the field input markup for a generic list. + * Use the multiple attribute to enable multiselect. + * + * @return string The field input markup. + * + * @since 3.6 + */ + protected function getInput() + { + $data = $this->getLayoutData(); + + $data['options'] = $this->getOptions(); + $data['allowCustom'] = $this->allowAdd; + $data['customPrefix'] = $this->customPrefix; + $data['refreshPage'] = (bool) $this->element['refresh-enabled']; + $data['refreshCatId'] = (string) $this->element['refresh-cat-id']; + $data['refreshSection'] = (string) $this->element['refresh-section']; + + $renderer = $this->getRenderer($this->layout); + $renderer->setComponent('com_categories'); + $renderer->setClient(1); + + return $renderer->render($data); + } } diff --git a/code/administrator/components/com_categories/src/Field/ComponentsCategoryField.php b/code/administrator/components/com_categories/src/Field/ComponentsCategoryField.php index 02ac5b4d..6877bd49 100644 --- a/code/administrator/components/com_categories/src/Field/ComponentsCategoryField.php +++ b/code/administrator/components/com_categories/src/Field/ComponentsCategoryField.php @@ -1,4 +1,5 @@ getQuery(true); - $query->select('DISTINCT ' . $db->quoteName('extension')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('extension') . ' != ' . $db->quote('system')); - - $db->setQuery($query); - $categoryTypes = $db->loadColumn(); - - foreach ($categoryTypes as $categoryType) - { - $option = new \stdClass; - $option->value = $categoryType; - - // Extract the component name and optional section name - $parts = explode('.', $categoryType); - $component = $parts[0]; - $section = (\count($parts) > 1) ? $parts[1] : null; - - // Load component language files - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE) - || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); - - // If the component section string exists, let's use it - if ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) - { - $option->text = Text::_($component_section_key); - } - else - // Else use the component title - { - $option->text = Text::_(strtoupper($component)); - } - - $options[] = $option; - } - - // Sort by name - $options = ArrayHelper::sortObjects($options, 'text', 1, true, true); - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'ComponentsCategory'; + + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + // Initialise variable. + $db = $this->getDatabase(); + $options = array(); + + $query = $db->getQuery(true); + $query->select('DISTINCT ' . $db->quoteName('extension')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('extension') . ' != ' . $db->quote('system')); + + $db->setQuery($query); + $categoryTypes = $db->loadColumn(); + + foreach ($categoryTypes as $categoryType) { + $option = new \stdClass(); + $option->value = $categoryType; + + // Extract the component name and optional section name + $parts = explode('.', $categoryType); + $component = $parts[0]; + $section = (\count($parts) > 1) ? $parts[1] : null; + + // Load component language files + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE) + || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); + + // If the component section string exists, let's use it + if ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) { + $option->text = Text::_($component_section_key); + } else // Else use the component title + { + $option->text = Text::_(strtoupper($component)); + } + + $options[] = $option; + } + + // Sort by name + $options = ArrayHelper::sortObjects($options, 'text', 1, true, true); + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); + + return $options; + } } diff --git a/code/administrator/components/com_categories/src/Field/Modal/CategoryField.php b/code/administrator/components/com_categories/src/Field/Modal/CategoryField.php index 10fb8eb3..05234673 100644 --- a/code/administrator/components/com_categories/src/Field/Modal/CategoryField.php +++ b/code/administrator/components/com_categories/src/Field/Modal/CategoryField.php @@ -1,4 +1,5 @@ element['extension']) - { - $extension = (string) $this->element['extension']; - } - else - { - $extension = (string) Factory::getApplication()->input->get('extension', 'com_content'); - } - - $allowNew = ((string) $this->element['new'] == 'true'); - $allowEdit = ((string) $this->element['edit'] == 'true'); - $allowClear = ((string) $this->element['clear'] != 'false'); - $allowSelect = ((string) $this->element['select'] != 'false'); - $allowPropagate = ((string) $this->element['propagate'] == 'true'); - - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language. - Factory::getLanguage()->load('com_categories', JPATH_ADMINISTRATOR); - - // The active category id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Category_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Modal_Category'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + if ($this->element['extension']) { + $extension = (string) $this->element['extension']; + } else { + $extension = (string) Factory::getApplication()->input->get('extension', 'com_content'); + } + + $allowNew = ((string) $this->element['new'] == 'true'); + $allowEdit = ((string) $this->element['edit'] == 'true'); + $allowClear = ((string) $this->element['clear'] != 'false'); + $allowSelect = ((string) $this->element['select'] != 'false'); + $allowPropagate = ((string) $this->element['propagate'] == 'true'); + + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language. + Factory::getLanguage()->load('com_categories', JPATH_ADMINISTRATOR); + + // The active category id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Category_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectCategory_" . $this->id . " = function (id, title, object) { window.processModalSelect('Category', '" . $this->id . "', id, title, '', object); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkCategories = 'index.php?option=com_categories&view=categories&layout=modal&tmpl=component&' . Session::getFormToken() . '=1' - . '&extension=' . $extension; - $linkCategory = 'index.php?option=com_categories&view=category&layout=modal&tmpl=component&' . Session::getFormToken() . '=1' - . '&extension=' . $extension; - $modalTitle = Text::_('COM_CATEGORIES_SELECT_A_CATEGORY'); - - if (isset($this->element['language'])) - { - $linkCategories .= '&forcedLanguage=' . $this->element['language']; - $linkCategory .= '&forcedLanguage=' . $this->element['language']; - $modalTitle .= ' — ' . $this->element['label']; - } - - $urlSelect = $linkCategories . '&function=jSelectCategory_' . $this->id; - $urlEdit = $linkCategory . '&task=category.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkCategory . '&task=category.add'; - - if ($value) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('id') . ' = :value') - ->bind(':value', $value, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - $title = empty($title) ? Text::_('COM_CATEGORIES_SELECT_A_CATEGORY') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current category display field. - $html = ''; - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select category button. - if ($allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New category button. - if ($allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit category button. - if ($allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear category button. - if ($allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate category button - if ($allowPropagate && \count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) \strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectCategory_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - // Select category modal. - if ($allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New category modal. - if ($allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_CATEGORIES_NEW_CATEGORY'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit category modal. - if ($allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_CATEGORIES_EDIT_CATEGORY'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Note: class='required' for client side validation - $class = $this->required ? ' class="required modal-value"' : ''; - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.7.0 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkCategories = 'index.php?option=com_categories&view=categories&layout=modal&tmpl=component&' . Session::getFormToken() . '=1' + . '&extension=' . $extension; + $linkCategory = 'index.php?option=com_categories&view=category&layout=modal&tmpl=component&' . Session::getFormToken() . '=1' + . '&extension=' . $extension; + $modalTitle = Text::_('COM_CATEGORIES_SELECT_A_CATEGORY'); + + if (isset($this->element['language'])) { + $linkCategories .= '&forcedLanguage=' . $this->element['language']; + $linkCategory .= '&forcedLanguage=' . $this->element['language']; + $modalTitle .= ' — ' . $this->element['label']; + } + + $urlSelect = $linkCategories . '&function=jSelectCategory_' . $this->id; + $urlEdit = $linkCategory . '&task=category.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkCategory . '&task=category.add'; + + if ($value) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = :value') + ->bind(':value', $value, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + $title = empty($title) ? Text::_('COM_CATEGORIES_SELECT_A_CATEGORY') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current category display field. + $html = ''; + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select category button. + if ($allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New category button. + if ($allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit category button. + if ($allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear category button. + if ($allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate category button + if ($allowPropagate && \count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) \strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectCategory_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + // Select category modal. + if ($allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New category modal. + if ($allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_CATEGORIES_NEW_CATEGORY'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit category modal. + if ($allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_CATEGORIES_EDIT_CATEGORY'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Note: class='required' for client side validation + $class = $this->required ? ' class="required modal-value"' : ''; + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.7.0 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/code/administrator/components/com_categories/src/Helper/CategoriesHelper.php b/code/administrator/components/com_categories/src/Helper/CategoriesHelper.php index dfaacc58..cdbb9b87 100644 --- a/code/administrator/components/com_categories/src/Helper/CategoriesHelper.php +++ b/code/administrator/components/com_categories/src/Helper/CategoriesHelper.php @@ -1,4 +1,5 @@ getAuthorisedViewLevels(); - - foreach ($langAssociations as $langAssociation) - { - // Include only published categories with user access - $arrId = explode(':', $langAssociation->id); - $assocId = (int) $arrId[0]; - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName('published')) - ->from($db->quoteName('#__categories')) - ->whereIn($db->quoteName('access'), $groups) - ->where($db->quoteName('id') . ' = :associd') - ->bind(':associd', $assocId, ParameterType::INTEGER); - - $result = (int) $db->setQuery($query)->loadResult(); - - if ($result === 1) - { - $associations[$langAssociation->language] = $langAssociation->id; - } - } - - return $associations; - } - - /** - * Check if Category ID exists otherwise assign to ROOT category. - * - * @param mixed $catid Name or ID of category. - * @param string $extension Extension that triggers this function - * - * @return integer $catid Category ID. - */ - public static function validateCategoryId($catid, $extension) - { - $categoryTable = Table::getInstance('CategoryTable', '\\Joomla\\Component\\Categories\\Administrator\\Table\\'); - - $data = array(); - $data['id'] = $catid; - $data['extension'] = $extension; - - if (!$categoryTable->load($data)) - { - $catid = 0; - } - - return (int) $catid; - } - - /** - * Create new Category from within item view. - * - * @param array $data Array of data for new category. - * - * @return integer - */ - public static function createCategory($data) - { - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - $categoryModel->save($data); - - $catid = $categoryModel->getState('category.id'); - - return $catid; - } + /** + * Gets a list of associations for a given item. + * + * @param integer $pk Content item key. + * @param string $extension Optional extension name. + * + * @return array of associations. + */ + public static function getAssociations($pk, $extension = 'com_content') + { + $langAssociations = Associations::getAssociations($extension, '#__categories', 'com_categories.item', $pk, 'id', 'alias', ''); + $associations = array(); + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + foreach ($langAssociations as $langAssociation) { + // Include only published categories with user access + $arrId = explode(':', $langAssociation->id); + $assocId = (int) $arrId[0]; + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('published')) + ->from($db->quoteName('#__categories')) + ->whereIn($db->quoteName('access'), $groups) + ->where($db->quoteName('id') . ' = :associd') + ->bind(':associd', $assocId, ParameterType::INTEGER); + + $result = (int) $db->setQuery($query)->loadResult(); + + if ($result === 1) { + $associations[$langAssociation->language] = $langAssociation->id; + } + } + + return $associations; + } + + /** + * Check if Category ID exists otherwise assign to ROOT category. + * + * @param mixed $catid Name or ID of category. + * @param string $extension Extension that triggers this function + * + * @return integer $catid Category ID. + */ + public static function validateCategoryId($catid, $extension) + { + $categoryTable = Table::getInstance('CategoryTable', '\\Joomla\\Component\\Categories\\Administrator\\Table\\'); + + $data = array(); + $data['id'] = $catid; + $data['extension'] = $extension; + + if (!$categoryTable->load($data)) { + $catid = 0; + } + + return (int) $catid; + } + + /** + * Create new Category from within item view. + * + * @param array $data Array of data for new category. + * + * @return integer + */ + public static function createCategory($data) + { + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + $categoryModel->save($data); + + $catid = $categoryModel->getState('category.id'); + + return $catid; + } } diff --git a/code/administrator/components/com_categories/src/Helper/CategoryAssociationHelper.php b/code/administrator/components/com_categories/src/Helper/CategoryAssociationHelper.php index 17ada8cf..f9ac0838 100644 --- a/code/administrator/components/com_categories/src/Helper/CategoryAssociationHelper.php +++ b/code/administrator/components/com_categories/src/Helper/CategoryAssociationHelper.php @@ -1,4 +1,5 @@ $item) - { - if (class_exists($helperClassname) && \is_callable(array($helperClassname, 'getCategoryRoute'))) - { - $return[$tag] = $helperClassname::getCategoryRoute($item, $tag, $layout); - } - else - { - $viewLayout = $layout ? '&layout=' . $layout : ''; + foreach ($associations as $tag => $item) { + if (class_exists($helperClassname) && \is_callable(array($helperClassname, 'getCategoryRoute'))) { + $return[$tag] = $helperClassname::getCategoryRoute($item, $tag, $layout); + } else { + $viewLayout = $layout ? '&layout=' . $layout : ''; - $return[$tag] = 'index.php?option=' . $extension . '&view=category&id=' . $item . $viewLayout; - } - } - } + $return[$tag] = 'index.php?option=' . $extension . '&view=category&id=' . $item . $viewLayout; + } + } + } - return $return; - } + return $return; + } } diff --git a/code/administrator/components/com_categories/src/Model/CategoriesModel.php b/code/administrator/components/com_categories/src/Model/CategoriesModel.php index 0e37313b..51e82700 100644 --- a/code/administrator/components/com_categories/src/Model/CategoriesModel.php +++ b/code/administrator/components/com_categories/src/Model/CategoriesModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', 'cmd'); - - $this->setState('filter.extension', $extension); - $parts = explode('.', $extension); - - // Extract the component name - $this->setState('filter.component', $parts[0]); - - // Extract the optional section name - $this->setState('filter.section', (\count($parts) > 1) ? $parts[1] : null); - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . $this->getState('filter.level'); - $id .= ':' . serialize($this->getState('filter.tag')); - - return parent::getStoreId($id); - } - - /** - * Method to get a database query to list categories. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.title, a.alias, a.note, a.published, a.access' . - ', a.checked_out, a.checked_out_time, a.created_user_id' . - ', a.path, a.parent_id, a.level, a.lft, a.rgt' . - ', a.language' - ) - ); - $query->from($db->quoteName('#__categories', 'a')); - - // Join over the language - $query->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - ] - ) - ->join( - 'LEFT', - $db->quoteName('#__languages', 'l'), - $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language') - ); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join( - 'LEFT', - $db->quoteName('#__users', 'uc'), - $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out') - ); - - // Join over the asset groups. - $query->select($db->quoteName('ag.title', 'access_level')) - ->join( - 'LEFT', - $db->quoteName('#__viewlevels', 'ag'), - $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access') - ); - - // Join over the users for the author. - $query->select($db->quoteName('ua.name', 'author_name')) - ->join( - 'LEFT', - $db->quoteName('#__users', 'ua'), - $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_user_id') - ); - - // Join over the associations. - $assoc = $this->getAssoc(); - - if ($assoc) - { - $query->select('COUNT(asso2.id)>1 as association') - ->join( - 'LEFT', - $db->quoteName('#__associations', 'asso'), - $db->quoteName('asso.id') . ' = ' . $db->quoteName('a.id') - . ' AND ' . $db->quoteName('asso.context') . ' = ' . $db->quote('com_categories.item') - ) - ->join( - 'LEFT', - $db->quoteName('#__associations', 'asso2'), - $db->quoteName('asso2.key') . ' = ' . $db->quoteName('asso.key') - ) - ->group('a.id, l.title, uc.name, ag.title, ua.name'); - } - - // Filter by extension - if ($extension = $this->getState('filter.extension')) - { - $query->where($db->quoteName('a.extension') . ' = :extension') - ->bind(':extension', $extension); - } - - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('a.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - // Filter by published state - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->whereIn($db->quoteName('a.published'), [0, 1]); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.title') . ' LIKE :title', - $db->quoteName('a.alias') . ' LIKE :alias', - $db->quoteName('a.note') . ' LIKE :note', - ], - 'OR' - ) - ->bind(':title', $search) - ->bind(':alias', $search) - ->bind(':note', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Filter by a single or group of tags. - $tag = $this->getState('filter.tag'); - $typeAlias = $extension . '.category'; - - // Run simplified query when filtering by one tag. - if (\is_array($tag) && \count($tag) === 1) - { - $tag = $tag[0]; - } - - if ($tag && \is_array($tag)) - { - $tag = ArrayHelper::toInteger($tag); - - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', - $db->quoteName('type_alias') . ' = :typeAlias', - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->bind(':typeAlias', $typeAlias); - } - elseif ($tag = (int) $tag) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->where( - [ - $db->quoteName('tagmap.tag_id') . ' = :tag', - $db->quoteName('tagmap.type_alias') . ' = :typeAlias', - ] - ) - ->bind(':tag', $tag, ParameterType::INTEGER) - ->bind(':typeAlias', $typeAlias); - } - - // Add the list ordering clause - $listOrdering = $this->getState('list.ordering', 'a.lft'); - $listDirn = $db->escape($this->getState('list.direction', 'ASC')); - - if ($listOrdering == 'a.access') - { - $query->order('a.access ' . $listDirn . ', a.lft ' . $listDirn); - } - else - { - $query->order($db->escape($listOrdering) . ' ' . $listDirn); - } - - // Group by on Categories for \JOIN with component tables to count items - $query->group('a.id, + /** + * Does an association exist? Caches the result of getAssoc(). + * + * @var boolean|null + * @since 4.0.5 + */ + private $hasAssociation; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'published', 'a.published', + 'access', 'a.access', 'access_level', + 'language', 'a.language', 'language_title', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'created_time', 'a.created_time', + 'created_user_id', 'a.created_user_id', + 'lft', 'a.lft', + 'rgt', 'a.rgt', + 'level', 'a.level', + 'path', 'a.path', + 'tag', + ); + } + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', 'cmd'); + + $this->setState('filter.extension', $extension); + $parts = explode('.', $extension); + + // Extract the component name + $this->setState('filter.component', $parts[0]); + + // Extract the optional section name + $this->setState('filter.section', (\count($parts) > 1) ? $parts[1] : null); + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . $this->getState('filter.level'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Method to get a database query to list categories. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.title, a.alias, a.note, a.published, a.access' . + ', a.checked_out, a.checked_out_time, a.created_user_id' . + ', a.path, a.parent_id, a.level, a.lft, a.rgt' . + ', a.language' + ) + ); + $query->from($db->quoteName('#__categories', 'a')); + + // Join over the language + $query->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + ] + ) + ->join( + 'LEFT', + $db->quoteName('#__languages', 'l'), + $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language') + ); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'uc'), + $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out') + ); + + // Join over the asset groups. + $query->select($db->quoteName('ag.title', 'access_level')) + ->join( + 'LEFT', + $db->quoteName('#__viewlevels', 'ag'), + $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access') + ); + + // Join over the users for the author. + $query->select($db->quoteName('ua.name', 'author_name')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'ua'), + $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_user_id') + ); + + // Join over the associations. + $assoc = $this->getAssoc(); + + if ($assoc) { + $query->select('COUNT(asso2.id)>1 as association') + ->join( + 'LEFT', + $db->quoteName('#__associations', 'asso'), + $db->quoteName('asso.id') . ' = ' . $db->quoteName('a.id') + . ' AND ' . $db->quoteName('asso.context') . ' = ' . $db->quote('com_categories.item') + ) + ->join( + 'LEFT', + $db->quoteName('#__associations', 'asso2'), + $db->quoteName('asso2.key') . ' = ' . $db->quoteName('asso.key') + ) + ->group('a.id, l.title, uc.name, ag.title, ua.name'); + } + + // Filter by extension + if ($extension = $this->getState('filter.extension')) { + $query->where($db->quoteName('a.extension') . ' = :extension') + ->bind(':extension', $extension); + } + + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('a.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + // Filter by published state + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->whereIn($db->quoteName('a.published'), [0, 1]); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.title') . ' LIKE :title', + $db->quoteName('a.alias') . ' LIKE :alias', + $db->quoteName('a.note') . ' LIKE :note', + ], + 'OR' + ) + ->bind(':title', $search) + ->bind(':alias', $search) + ->bind(':note', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Filter by a single or group of tags. + $tag = $this->getState('filter.tag'); + $typeAlias = $extension . '.category'; + + // Run simplified query when filtering by one tag. + if (\is_array($tag) && \count($tag) === 1) { + $tag = $tag[0]; + } + + if ($tag && \is_array($tag)) { + $tag = ArrayHelper::toInteger($tag); + + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', + $db->quoteName('type_alias') . ' = :typeAlias', + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->bind(':typeAlias', $typeAlias); + } elseif ($tag = (int) $tag) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->where( + [ + $db->quoteName('tagmap.tag_id') . ' = :tag', + $db->quoteName('tagmap.type_alias') . ' = :typeAlias', + ] + ) + ->bind(':tag', $tag, ParameterType::INTEGER) + ->bind(':typeAlias', $typeAlias); + } + + // Add the list ordering clause + $listOrdering = $this->getState('list.ordering', 'a.lft'); + $listDirn = $db->escape($this->getState('list.direction', 'ASC')); + + if ($listOrdering == 'a.access') { + $query->order('a.access ' . $listDirn . ', a.lft ' . $listDirn); + } else { + $query->order($db->escape($listOrdering) . ' ' . $listDirn); + } + + // Group by on Categories for \JOIN with component tables to count items + $query->group('a.id, a.title, a.alias, a.note, @@ -395,125 +373,116 @@ protected function getListQuery() l.image, uc.name, ag.title, - ua.name' - ); - - return $query; - } - - /** - * Method to determine if an association exists - * - * @return boolean True if the association exists - * - * @since 3.0 - */ - public function getAssoc() - { - if (!\is_null($this->hasAssociation)) - { - return $this->hasAssociation; - } - - $extension = $this->getState('filter.extension'); - - $this->hasAssociation = Associations::isEnabled(); - $extension = explode('.', $extension); - $component = array_shift($extension); - $cname = str_replace('com_', '', $component); - - if (!$this->hasAssociation || !$component || !$cname) - { - $this->hasAssociation = false; - - return $this->hasAssociation; - } - - $componentObject = $this->bootComponent($component); - - if ($componentObject instanceof AssociationServiceInterface && $componentObject instanceof CategoryServiceInterface) - { - $this->hasAssociation = true; - - return $this->hasAssociation; - } - - $hname = $cname . 'HelperAssociation'; - \JLoader::register($hname, JPATH_SITE . '/components/' . $component . '/helpers/association.php'); - - /* @codingStandardsIgnoreStart */ - $this->hasAssociation = class_exists($hname) && !empty($hname::$category_association); - /* @codingStandardsIgnoreEnd */ - - return $this->hasAssociation; - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.0.1 - */ - public function getItems() - { - $items = parent::getItems(); - - if ($items != false) - { - $extension = $this->getState('filter.extension'); - - $this->countItems($items, $extension); - } - - return $items; - } - - /** - * Method to load the countItems method from the extensions - * - * @param \stdClass[] $items The category items - * @param string $extension The category extension - * - * @return void - * - * @since 3.5 - */ - public function countItems(&$items, $extension) - { - $parts = explode('.', $extension, 2); - $section = ''; - - if (\count($parts) > 1) - { - $section = $parts[1]; - } - - $component = Factory::getApplication()->bootComponent($parts[0]); - - if ($component instanceof CategoryServiceInterface) - { - $component->countItems($items, $section); - } - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - // Get the extension from the filter - $extension = $this->getState('filter.extension'); - - $query->where($this->_db->quoteName('extension') . ' = :extension') - ->bind(':extension', $extension); - - return $query; - } + ua.name'); + + return $query; + } + + /** + * Method to determine if an association exists + * + * @return boolean True if the association exists + * + * @since 3.0 + */ + public function getAssoc() + { + if (!\is_null($this->hasAssociation)) { + return $this->hasAssociation; + } + + $extension = $this->getState('filter.extension'); + + $this->hasAssociation = Associations::isEnabled(); + $extension = explode('.', $extension); + $component = array_shift($extension); + $cname = str_replace('com_', '', $component); + + if (!$this->hasAssociation || !$component || !$cname) { + $this->hasAssociation = false; + + return $this->hasAssociation; + } + + $componentObject = $this->bootComponent($component); + + if ($componentObject instanceof AssociationServiceInterface && $componentObject instanceof CategoryServiceInterface) { + $this->hasAssociation = true; + + return $this->hasAssociation; + } + + $hname = $cname . 'HelperAssociation'; + \JLoader::register($hname, JPATH_SITE . '/components/' . $component . '/helpers/association.php'); + + $this->hasAssociation = class_exists($hname) && !empty($hname::$category_association); + + return $this->hasAssociation; + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.0.1 + */ + public function getItems() + { + $items = parent::getItems(); + + if ($items != false) { + $extension = $this->getState('filter.extension'); + + $this->countItems($items, $extension); + } + + return $items; + } + + /** + * Method to load the countItems method from the extensions + * + * @param \stdClass[] $items The category items + * @param string $extension The category extension + * + * @return void + * + * @since 3.5 + */ + public function countItems(&$items, $extension) + { + $parts = explode('.', $extension, 2); + $section = ''; + + if (\count($parts) > 1) { + $section = $parts[1]; + } + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof CategoryServiceInterface) { + $component->countItems($items, $section); + } + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + // Get the extension from the filter + $extension = $this->getState('filter.extension'); + + $query->where($this->getDatabase()->quoteName('extension') . ' = :extension') + ->bind(':extension', $extension); + + return $query; + } } diff --git a/code/administrator/components/com_categories/src/Model/CategoryModel.php b/code/administrator/components/com_categories/src/Model/CategoryModel.php index 8b5bb7fd..b0fa0b0d 100644 --- a/code/administrator/components/com_categories/src/Model/CategoryModel.php +++ b/code/administrator/components/com_categories/src/Model/CategoryModel.php @@ -1,4 +1,5 @@ input->get('extension', 'com_content'); - $this->typeAlias = $extension . '.category'; - - // Add a new batch command - $this->batch_commands['flip_ordering'] = 'batchFlipordering'; - - parent::__construct($config, $factory); - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', $record->extension . '.category.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - // Check for existing category. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $record->extension . '.category.' . (int) $record->id); - } - - // New category, so check against the parent. - if (!empty($record->parent_id)) - { - return $user->authorise('core.edit.state', $record->extension . '.category.' . (int) $record->parent_id); - } - - // Default to component settings if neither category nor parent known. - return $user->authorise('core.edit.state', $record->extension); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\Table\Table A Table object - * - * @since 1.6 - */ - public function getTable($type = 'Category', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - $parentId = $app->input->getInt('parent_id'); - $this->setState('category.parent_id', $parentId); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState($this->getName() . '.id', $pk); - - $extension = $app->input->get('extension', 'com_content'); - $this->setState('category.extension', $extension); - $parts = explode('.', $extension); - - // Extract the component name - $this->setState('category.component', $parts[0]); - - // Extract the optional section name - $this->setState('category.section', (\count($parts) > 1) ? $parts[1] : null); - - // Load the parameters. - $params = ComponentHelper::getParams('com_categories'); - $this->setState('params', $params); - } - - /** - * Method to get a category. - * - * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. - * - * @return mixed Category data object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - if ($result = parent::getItem($pk)) - { - // Prime required properties. - if (empty($result->id)) - { - $result->parent_id = $this->getState('category.parent_id'); - $result->extension = $this->getState('category.extension'); - } - - // Convert the metadata field to an array. - $registry = new Registry($result->metadata); - $result->metadata = $registry->toArray(); - - if (!empty($result->id)) - { - $result->tags = new TagsHelper; - $result->tags->getTagIds($result->id, $result->extension . '.category'); - } - } - - $assoc = $this->getAssoc(); - - if ($assoc) - { - if ($result->id != null) - { - $result->associations = ArrayHelper::toInteger(CategoriesHelper::getAssociations($result->id, $result->extension)); - } - else - { - $result->associations = array(); - } - } - - return $result; - } - - /** - * Method to get the row form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A JForm object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - $extension = $this->getState('category.extension'); - $jinput = Factory::getApplication()->input; - - // A workaround to get the extension into the model for save requests. - if (empty($extension) && isset($data['extension'])) - { - $extension = $data['extension']; - $parts = explode('.', $extension); - - $this->setState('category.extension', $extension); - $this->setState('category.component', $parts[0]); - $this->setState('category.section', @$parts[1]); - } - - // Get the form. - $form = $this->loadForm('com_categories.category' . $extension, 'category', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on Edit State access controls. - if (empty($data['extension'])) - { - $data['extension'] = $extension; - } - - $categoryId = $jinput->get('id'); - $parts = explode('.', $extension); - $assetKey = $categoryId ? $extension . '.category.' . $categoryId : $parts[0]; - - if (!Factory::getUser()->authorise('core.edit.state', $assetKey)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - // Don't allow to change the created_user_id user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_user_id', 'filter', 'unset'); - } - - return $form; - } - - /** - * A protected method to get the where clause for the reorder - * This ensures that the row will be moved relative to a row with the same extension - * - * @param Category $table Current table instance - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('extension') . ' = ' . $this->_db->quote($table->extension), - ]; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_categories.edit.' . $this->getName() . '.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Language, Access) in edit form if those have been selected in Category Manager - if (!$data->id) - { - // Check for which extension the Category Manager is used and get selected fields - $extension = substr($app->getUserState('com_categories.categories.filter.extension', ''), 4); - $filters = (array) $app->getUserState('com_categories.categories.' . $extension . '.filter'); - - $data->set( - 'published', - $app->input->getInt( - 'published', - ((isset($filters['published']) && $filters['published'] !== '') ? $filters['published'] : null) - ) - ); - $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); - $data->set( - 'access', - $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) - ); - } - } - - $this->preprocessData('com_categories.category', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see JFormRule - * @see JFilterInput - * @since 3.9.23 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', $data['extension'])) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import. - * - * @return mixed - * - * @since 1.6 - * - * @throws \Exception if there is an error in the form event. - * - * @see \Joomla\CMS\Form\FormField - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $lang = Factory::getLanguage(); - $component = $this->getState('category.component'); - $section = $this->getState('category.section'); - $extension = Factory::getApplication()->input->get('extension', null); - - // Get the component form if it exists - $name = 'category' . ($section ? ('.' . $section) : ''); - - // Looking first in the component forms folder - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); - - // Looking in the component models/forms folder (J! 3) - if (!file_exists($path)) - { - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); - } - - // Old way: looking in the component folder - if (!file_exists($path)) - { - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/$name.xml"); - } - - if (file_exists($path)) - { - $lang->load($component, JPATH_BASE); - $lang->load($component, JPATH_BASE . '/components/' . $component); - - if (!$form->loadFile($path, false)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - $componentInterface = Factory::getApplication()->bootComponent($component); - - if ($componentInterface instanceof CategoryServiceInterface) - { - $componentInterface->prepareForm($form, $data); - } - else - { - // Try to find the component helper. - $eName = str_replace('com_', '', $component); - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/helpers/category.php"); - - if (file_exists($path)) - { - $cName = ucfirst($eName) . ucfirst($section) . 'HelperCategory'; - - \JLoader::register($cName, $path); - - if (class_exists($cName) && \is_callable(array($cName, 'onPrepareForm'))) - { - $lang->load($component, JPATH_BASE, null, false, false) - || $lang->load($component, JPATH_BASE . '/components/' . $component, null, false, false) - || $lang->load($component, JPATH_BASE, $lang->getDefault(), false, false) - || $lang->load($component, JPATH_BASE . '/components/' . $component, $lang->getDefault(), false, false); - \call_user_func_array(array($cName, 'onPrepareForm'), array(&$form)); - - // Check for an error. - if ($form instanceof \Exception) - { - $this->setError($form->getMessage()); - - return false; - } - } - } - } - - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $component); - $form->setFieldAttribute('rules', 'section', $name); - - // Association category items - if ($this->getAssoc()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (\count($languages) > 1) - { - $addform = new \SimpleXMLElement('
'); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_category'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('extension', $extension); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - } - - $form->load($addform, false); - } - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $table = $this->getTable(); - $input = Factory::getApplication()->input; - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); - $isNew = true; - $context = $this->option . '.' . $this->name; - - if (!empty($data['tags']) && $data['tags'][0] != '') - { - $table->newTags = $data['tags']; - } - - // Include the plugins for the save events. - PluginHelper::importPlugin($this->events_map['save']); - - // Load the row if saving an existing category. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - // Set the new parent id if parent id not matched OR while New/Save as Copy . - if ($table->parent_id != $data['parent_id'] || $data['id'] == 0) - { - $table->setLocation($data['parent_id'], 'last-child'); - } - - // Alter the title for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['title'] == $origTable->title) - { - [$title, $alias] = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); - $data['title'] = $title; - $data['alias'] = $alias; - } - else - { - if ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - $data['published'] = 0; - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Bind the rules. - if (isset($data['rules'])) - { - $rules = new Rules($data['rules']); - $table->setRules($rules); - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew, $data)); - - if (\in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - $assoc = $this->getAssoc(); - - if ($assoc) - { - // Adding self to the association - $associations = $data['associations'] ?? array(); - - // Unset any invalid associations - $associations = ArrayHelper::toInteger($associations); - - foreach ($associations as $tag => $id) - { - if (!$id) - { - unset($associations[$tag]); - } - } - - // Detecting all item menus - $allLanguage = $table->language == '*'; - - if ($allLanguage && !empty($associations)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CATEGORIES_ERROR_ALL_LANGUAGE_ASSOCIATED'), 'notice'); - } - - // Get associationskey for edited item - $db = $this->getDbo(); - $id = (int) $table->id; - $query = $db->getQuery(true) - ->select($db->quoteName('key')) - ->from($db->quoteName('#__associations')) - ->where($db->quoteName('context') . ' = :associationscontext') - ->where($db->quoteName('id') . ' = :id') - ->bind(':associationscontext', $this->associationsContext) - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $oldKey = $db->loadResult(); - - if ($associations || $oldKey !== null) - { - $where = []; - - // Deleting old associations for the associated items - $query = $db->getQuery(true) - ->delete($db->quoteName('#__associations')) - ->where($db->quoteName('context') . ' = :associationscontext') - ->bind(':associationscontext', $this->associationsContext); - - if ($associations) - { - $where[] = $db->quoteName('id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')'; - } - - if ($oldKey !== null) - { - $where[] = $db->quoteName('key') . ' = :oldKey'; - $query->bind(':oldKey', $oldKey); - } - - $query->extendWhere('AND', $where, 'OR'); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Adding self to the association - if (!$allLanguage) - { - $associations[$table->language] = (int) $table->id; - } - - if (\count($associations) > 1) - { - // Adding new association for these items - $key = md5(json_encode($associations)); - $query->clear() - ->insert($db->quoteName('#__associations')) - ->columns( - [ - $db->quoteName('id'), - $db->quoteName('context'), - $db->quoteName('key'), - ] - ); - - foreach ($associations as $id) - { - $id = (int) $id; - - $query->values( - implode( - ',', - $query->bindArray( - [$id, $this->associationsContext, $key], - [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] - ) - ) - ); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew, $data)); - - // Rebuild the path for the category: - if (!$table->rebuildPath($table->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Rebuild the paths of the category's children: - if (!$table->rebuild($table->id, $table->lft, $table->level, $table->path)) - { - $this->setError($table->getError()); - - return false; - } - - $this->setState($this->getName() . '.id', $table->id); - - if (Factory::getApplication()->input->get('task') == 'editAssociations') - { - return $this->redirectToAssociations($data); - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the published state of one or more records. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function publish(&$pks, $value = 1) - { - if (parent::publish($pks, $value)) - { - $extension = Factory::getApplication()->input->get('extension'); - - // Include the content plugins for the change of category state event. - PluginHelper::importPlugin('content'); - - // Trigger the onCategoryChangeState event. - Factory::getApplication()->triggerEvent('onCategoryChangeState', array($extension, $pks, $value)); - - return true; - } - } - - /** - * Method rebuild the entire nested set tree. - * - * @return boolean False on failure or error, true otherwise. - * - * @since 1.6 - */ - public function rebuild() - { - // Get an instance of the table object. - $table = $this->getTable(); - - if (!$table->rebuild()) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to save the reordered nested set tree. - * First we save the new order values in the lft values of the changed ids. - * Then we invoke the table rebuild to implement the new ordering. - * - * @param array $idArray An array of primary key ids. - * @param integer $lftArray The lft value - * - * @return boolean False on failure or error, True otherwise - * - * @since 1.6 - */ - public function saveorder($idArray = null, $lftArray = null) - { - // Get an instance of the table object. - $table = $this->getTable(); - - if (!$table->saveorder($idArray, $lftArray)) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Batch flip category ordering. - * - * @param integer $value The new category. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return mixed An array of new IDs on success, boolean false on failure. - * - * @since 3.6.3 - */ - protected function batchFlipordering($value, $pks, $contexts) - { - $successful = array(); - - $db = $this->getDbo(); - $query = $db->getQuery(true); - - /** - * For each category get the max ordering value - * Re-order with max - ordering - */ - foreach ($pks as $id) - { - $query->select('MAX(' . $db->quoteName('ordering') . ')') - ->from($db->quoteName('#__content')) - ->where($db->quoteName('catid') . ' = :catid') - ->bind(':catid', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - $max = (int) $db->loadResult(); - $max++; - - $query->clear(); - - $query->update($db->quoteName('#__content')) - ->set($db->quoteName('ordering') . ' = :max - ' . $db->quoteName('ordering')) - ->where($db->quoteName('catid') . ' = :catid') - ->bind(':max', $max, ParameterType::INTEGER) - ->bind(':catid', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - if ($db->execute()) - { - $successful[] = $id; - } - } - - return empty($successful) ? false : $successful; - } - - /** - * Batch copy categories to a new category. - * - * @param integer $value The new category. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return mixed An array of new IDs on success, boolean false on failure. - * - * @since 1.6 - */ - protected function batchCopy($value, $pks, $contexts) - { - $type = new UCMType; - $this->type = $type->getTypeByAlias($this->typeAlias); - - // $value comes as {parent_id}.{extension} - $parts = explode('.', $value); - $parentId = (int) ArrayHelper::getValue($parts, 0, 1); - - $db = $this->getDbo(); - $extension = Factory::getApplication()->input->get('extension', '', 'word'); - $newIds = array(); - - // Check that the parent exists - if ($parentId) - { - if (!$this->table->load($parentId)) - { - if ($error = $this->table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Non-fatal error - $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); - $parentId = 0; - } - } - - // Check that user has create permission for parent category - if ($parentId == $this->table->getRootId()) - { - $canCreate = $this->user->authorise('core.create', $extension); - } - else - { - $canCreate = $this->user->authorise('core.create', $extension . '.category.' . $parentId); - } - - if (!$canCreate) - { - // Error since user cannot create in parent category - $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); - - return false; - } - } - - // If the parent is 0, set it to the ID of the root item in the tree - if (empty($parentId)) - { - if (!$parentId = $this->table->getRootId()) - { - $this->setError($this->table->getError()); - - return false; - } - // Make sure we can create in root - elseif (!$this->user->authorise('core.create', $extension)) - { - $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); - - return false; - } - } - - // We need to log the parent ID - $parents = array(); - - // Calculate the emergency stop count as a precaution against a runaway loop bug - $query = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('id') . ')') - ->from($db->quoteName('#__categories')); - $db->setQuery($query); - - try - { - $count = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Parent exists so let's proceed - while (!empty($pks) && $count > 0) - { - // Pop the first id off the stack - $pk = array_shift($pks); - - $this->table->reset(); - - // Check that the row actually exists - if (!$this->table->load($pk)) - { - if ($error = $this->table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - // Copy is a bit tricky, because we also need to copy the children - $lft = (int) $this->table->lft; - $rgt = (int) $this->table->rgt; - $query->clear() - ->select($db->quoteName('id')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('lft') . ' > :lft') - ->where($db->quoteName('rgt') . ' < :rgt') - ->bind(':lft', $lft, ParameterType::INTEGER) - ->bind(':rgt', $rgt, ParameterType::INTEGER); - $db->setQuery($query); - $childIds = $db->loadColumn(); - - // Add child ID's to the array only if they aren't already there. - foreach ($childIds as $childId) - { - if (!\in_array($childId, $pks)) - { - $pks[] = $childId; - } - } - - // Make a copy of the old ID, Parent ID and Asset ID - $oldId = $this->table->id; - $oldParentId = $this->table->parent_id; - $oldAssetId = $this->table->asset_id; - - // Reset the id because we are making a copy. - $this->table->id = 0; - - // If we a copying children, the Old ID will turn up in the parents list - // otherwise it's a new top level item - $this->table->parent_id = $parents[$oldParentId] ?? $parentId; - - // Set the new location in the tree for the node. - $this->table->setLocation($this->table->parent_id, 'last-child'); - - // @TODO: Deal with ordering? - // $this->table->ordering = 1; - $this->table->level = null; - $this->table->asset_id = null; - $this->table->lft = null; - $this->table->rgt = null; - - // Alter the title & alias - [$title, $alias] = $this->generateNewTitle($this->table->parent_id, $this->table->alias, $this->table->title); - $this->table->title = $title; - $this->table->alias = $alias; - - // Unpublish because we are making a copy - $this->table->published = 0; - - // Store the row. - if (!$this->table->store()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Get the new item ID - $newId = $this->table->get('id'); - - // Add the new ID to the array - $newIds[$pk] = $newId; - - // Copy rules - $query->clear() - ->update($db->quoteName('#__assets', 't')) - ->join('INNER', - $db->quoteName('#__assets', 's'), - $db->quoteName('s.id') . ' = :oldid' - ) - ->bind(':oldid', $oldAssetId, ParameterType::INTEGER) - ->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules')) - ->where($db->quoteName('t.id') . ' = :assetid') - ->bind(':assetid', $this->table->asset_id, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Now we log the old 'parent' to the new 'parent' - $parents[$oldId] = $this->table->id; - $count--; - } - - // Rebuild the hierarchy. - if (!$this->table->rebuild()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Rebuild the tree path. - if (!$this->table->rebuildPath($this->table->id)) - { - $this->setError($this->table->getError()); - - return false; - } - - return $newIds; - } - - /** - * Batch move categories to a new category. - * - * @param integer $value The new category ID. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True on success. - * - * @since 1.6 - */ - protected function batchMove($value, $pks, $contexts) - { - $parentId = (int) $value; - $type = new UCMType; - $this->type = $type->getTypeByAlias($this->typeAlias); - - $db = $this->getDbo(); - $query = $db->getQuery(true); - $extension = Factory::getApplication()->input->get('extension', '', 'word'); - - // Check that the parent exists. - if ($parentId) - { - if (!$this->table->load($parentId)) - { - if ($error = $this->table->getError()) - { - // Fatal error. - $this->setError($error); - - return false; - } - else - { - // Non-fatal error. - $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); - $parentId = 0; - } - } - - // Check that user has create permission for parent category. - if ($parentId == $this->table->getRootId()) - { - $canCreate = $this->user->authorise('core.create', $extension); - } - else - { - $canCreate = $this->user->authorise('core.create', $extension . '.category.' . $parentId); - } - - if (!$canCreate) - { - // Error since user cannot create in parent category - $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); - - return false; - } - - // Check that user has edit permission for every category being moved - // Note that the entire batch operation fails if any category lacks edit permission - foreach ($pks as $pk) - { - if (!$this->user->authorise('core.edit', $extension . '.category.' . $pk)) - { - // Error since user cannot edit this category - $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_EDIT')); - - return false; - } - } - } - - // We are going to store all the children and just move the category - $children = array(); - - // Parent exists so let's proceed - foreach ($pks as $pk) - { - // Check that the row actually exists - if (!$this->table->load($pk)) - { - if ($error = $this->table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - // Set the new location in the tree for the node. - $this->table->setLocation($parentId, 'last-child'); - - // Check if we are moving to a different parent - if ($parentId != $this->table->parent_id) - { - $lft = (int) $this->table->lft; - $rgt = (int) $this->table->rgt; - - // Add the child node ids to the children array. - $query->clear() - ->select($db->quoteName('id')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('lft') . ' BETWEEN :lft AND :rgt') - ->bind(':lft', $lft, ParameterType::INTEGER) - ->bind(':rgt', $rgt, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $children = array_merge($children, (array) $db->loadColumn()); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - // Store the row. - if (!$this->table->store()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Rebuild the tree path. - if (!$this->table->rebuildPath()) - { - $this->setError($this->table->getError()); - - return false; - } - } - - // Process the child rows - if (!empty($children)) - { - // Remove any duplicates and sanitize ids. - $children = array_unique($children); - $children = ArrayHelper::toInteger($children); - } - - return true; - } - - /** - * Custom clean the cache of com_content and content modules - * - * @param string $group Cache group name. - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - $extension = Factory::getApplication()->input->get('extension'); - - switch ($extension) - { - case 'com_content': - parent::cleanCache('com_content'); - parent::cleanCache('mod_articles_archive'); - parent::cleanCache('mod_articles_categories'); - parent::cleanCache('mod_articles_category'); - parent::cleanCache('mod_articles_latest'); - parent::cleanCache('mod_articles_news'); - parent::cleanCache('mod_articles_popular'); - break; - default: - parent::cleanCache($extension); - break; - } - } - - /** - * Method to change the title & alias. - * - * @param integer $parentId The id of the parent. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 1.7 - */ - protected function generateNewTitle($parentId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) - { - $title = StringHelper::increment($title); - $alias = StringHelper::increment($alias, 'dash'); - } - - return array($title, $alias); - } - - /** - * Method to determine if a category association is available. - * - * @return boolean True if a category association is available; false otherwise. - */ - public function getAssoc() - { - if (!\is_null($this->hasAssociation)) - { - return $this->hasAssociation; - } - - $extension = $this->getState('category.extension'); - - $this->hasAssociation = Associations::isEnabled(); - $extension = explode('.', $extension); - $component = array_shift($extension); - $cname = str_replace('com_', '', $component); - - if (!$this->hasAssociation || !$component || !$cname) - { - $this->hasAssociation = false; - - return $this->hasAssociation; - } - - $componentObject = $this->bootComponent($component); - - if ($componentObject instanceof AssociationServiceInterface && $componentObject instanceof CategoryServiceInterface) - { - $this->hasAssociation = true; - - return $this->hasAssociation; - } - - $hname = $cname . 'HelperAssociation'; - \JLoader::register($hname, JPATH_SITE . '/components/' . $component . '/helpers/association.php'); - - $this->hasAssociation = class_exists($hname) && !empty($hname::$category_association); - - return $this->hasAssociation; - } + use VersionableModelTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_CATEGORIES'; + + /** + * The type alias for this content type. Used for content version history. + * + * @var string + * @since 3.2 + */ + public $typeAlias = null; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_categories.item'; + + /** + * Does an association exist? Caches the result of getAssoc(). + * + * @var boolean|null + * @since 3.10.4 + */ + private $hasAssociation; + + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $extension = Factory::getApplication()->input->get('extension', 'com_content'); + $this->typeAlias = $extension . '.category'; + + // Add a new batch command + $this->batch_commands['flip_ordering'] = 'batchFlipordering'; + + parent::__construct($config, $factory); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + return Factory::getUser()->authorise('core.delete', $record->extension . '.category.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + // Check for existing category. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $record->extension . '.category.' . (int) $record->id); + } + + // New category, so check against the parent. + if (!empty($record->parent_id)) { + return $user->authorise('core.edit.state', $record->extension . '.category.' . (int) $record->parent_id); + } + + // Default to component settings if neither category nor parent known. + return $user->authorise('core.edit.state', $record->extension); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\Table\Table A Table object + * + * @since 1.6 + */ + public function getTable($type = 'Category', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + $parentId = $app->input->getInt('parent_id'); + $this->setState('category.parent_id', $parentId); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState($this->getName() . '.id', $pk); + + $extension = $app->input->get('extension', 'com_content'); + $this->setState('category.extension', $extension); + $parts = explode('.', $extension); + + // Extract the component name + $this->setState('category.component', $parts[0]); + + // Extract the optional section name + $this->setState('category.section', (\count($parts) > 1) ? $parts[1] : null); + + // Load the parameters. + $params = ComponentHelper::getParams('com_categories'); + $this->setState('params', $params); + } + + /** + * Method to get a category. + * + * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. + * + * @return mixed Category data object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + if ($result = parent::getItem($pk)) { + // Prime required properties. + if (empty($result->id)) { + $result->parent_id = $this->getState('category.parent_id'); + $result->extension = $this->getState('category.extension'); + } + + // Convert the metadata field to an array. + $registry = new Registry($result->metadata); + $result->metadata = $registry->toArray(); + + if (!empty($result->id)) { + $result->tags = new TagsHelper(); + $result->tags->getTagIds($result->id, $result->extension . '.category'); + } + } + + $assoc = $this->getAssoc(); + + if ($assoc) { + if ($result->id != null) { + $result->associations = ArrayHelper::toInteger(CategoriesHelper::getAssociations($result->id, $result->extension)); + } else { + $result->associations = array(); + } + } + + return $result; + } + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A JForm object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $extension = $this->getState('category.extension'); + $jinput = Factory::getApplication()->input; + + // A workaround to get the extension into the model for save requests. + if (empty($extension) && isset($data['extension'])) { + $extension = $data['extension']; + $parts = explode('.', $extension); + + $this->setState('category.extension', $extension); + $this->setState('category.component', $parts[0]); + $this->setState('category.section', @$parts[1]); + } + + // Get the form. + $form = $this->loadForm('com_categories.category' . $extension, 'category', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on Edit State access controls. + if (empty($data['extension'])) { + $data['extension'] = $extension; + } + + $categoryId = $jinput->get('id'); + $parts = explode('.', $extension); + $assetKey = $categoryId ? $extension . '.category.' . $categoryId : $parts[0]; + + if (!Factory::getUser()->authorise('core.edit.state', $assetKey)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + // Don't allow to change the created_user_id user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_user_id', 'filter', 'unset'); + } + + return $form; + } + + /** + * A protected method to get the where clause for the reorder + * This ensures that the row will be moved relative to a row with the same extension + * + * @param Category $table Current table instance + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('extension') . ' = ' . $db->quote($table->extension), + ]; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_categories.edit.' . $this->getName() . '.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Language, Access) in edit form if those have been selected in Category Manager + if (!$data->id) { + // Check for which extension the Category Manager is used and get selected fields + $extension = substr($app->getUserState('com_categories.categories.filter.extension', ''), 4); + $filters = (array) $app->getUserState('com_categories.categories.' . $extension . '.filter'); + + $data->set( + 'published', + $app->input->getInt( + 'published', + ((isset($filters['published']) && $filters['published'] !== '') ? $filters['published'] : null) + ) + ); + $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); + $data->set( + 'access', + $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) + ); + } + } + + $this->preprocessData('com_categories.category', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see JFormRule + * @see JFilterInput + * @since 3.9.23 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', $data['extension'])) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import. + * + * @return mixed + * + * @since 1.6 + * + * @throws \Exception if there is an error in the form event. + * + * @see \Joomla\CMS\Form\FormField + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $lang = Factory::getLanguage(); + $component = $this->getState('category.component'); + $section = $this->getState('category.section'); + $extension = Factory::getApplication()->input->get('extension', null); + + // Get the component form if it exists + $name = 'category' . ($section ? ('.' . $section) : ''); + + // Looking first in the component forms folder + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); + + // Looking in the component models/forms folder (J! 3) + if (!file_exists($path)) { + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); + } + + // Old way: looking in the component folder + if (!file_exists($path)) { + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/$name.xml"); + } + + if (file_exists($path)) { + $lang->load($component, JPATH_BASE); + $lang->load($component, JPATH_BASE . '/components/' . $component); + + if (!$form->loadFile($path, false)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + $componentInterface = Factory::getApplication()->bootComponent($component); + + if ($componentInterface instanceof CategoryServiceInterface) { + $componentInterface->prepareForm($form, $data); + } else { + // Try to find the component helper. + $eName = str_replace('com_', '', $component); + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/helpers/category.php"); + + if (file_exists($path)) { + $cName = ucfirst($eName) . ucfirst($section) . 'HelperCategory'; + + \JLoader::register($cName, $path); + + if (class_exists($cName) && \is_callable(array($cName, 'onPrepareForm'))) { + $lang->load($component, JPATH_BASE, null, false, false) + || $lang->load($component, JPATH_BASE . '/components/' . $component, null, false, false) + || $lang->load($component, JPATH_BASE, $lang->getDefault(), false, false) + || $lang->load($component, JPATH_BASE . '/components/' . $component, $lang->getDefault(), false, false); + \call_user_func_array(array($cName, 'onPrepareForm'), array(&$form)); + + // Check for an error. + if ($form instanceof \Exception) { + $this->setError($form->getMessage()); + + return false; + } + } + } + } + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $component); + $form->setFieldAttribute('rules', 'section', $name); + + // Association category items + if ($this->getAssoc()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (\count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_category'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('extension', $extension); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + } + + $form->load($addform, false); + } + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $table = $this->getTable(); + $input = Factory::getApplication()->input; + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); + $isNew = true; + $context = $this->option . '.' . $this->name; + + if (!empty($data['tags']) && $data['tags'][0] != '') { + $table->newTags = $data['tags']; + } + + // Include the plugins for the save events. + PluginHelper::importPlugin($this->events_map['save']); + + // Load the row if saving an existing category. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + // Set the new parent id if parent id not matched OR while New/Save as Copy . + if ($table->parent_id != $data['parent_id'] || $data['id'] == 0) { + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Alter the title for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['title'] == $origTable->title) { + [$title, $alias] = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); + $data['title'] = $title; + $data['alias'] = $alias; + } else { + if ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + $data['published'] = 0; + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Bind the rules. + if (isset($data['rules'])) { + $rules = new Rules($data['rules']); + $table->setRules($rules); + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew, $data)); + + if (\in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + $assoc = $this->getAssoc(); + + if ($assoc) { + // Adding self to the association + $associations = $data['associations'] ?? array(); + + // Unset any invalid associations + $associations = ArrayHelper::toInteger($associations); + + foreach ($associations as $tag => $id) { + if (!$id) { + unset($associations[$tag]); + } + } + + // Detecting all item menus + $allLanguage = $table->language == '*'; + + if ($allLanguage && !empty($associations)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CATEGORIES_ERROR_ALL_LANGUAGE_ASSOCIATED'), 'notice'); + } + + // Get associationskey for edited item + $db = $this->getDatabase(); + $id = (int) $table->id; + $query = $db->getQuery(true) + ->select($db->quoteName('key')) + ->from($db->quoteName('#__associations')) + ->where($db->quoteName('context') . ' = :associationscontext') + ->where($db->quoteName('id') . ' = :id') + ->bind(':associationscontext', $this->associationsContext) + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $oldKey = $db->loadResult(); + + if ($associations || $oldKey !== null) { + $where = []; + + // Deleting old associations for the associated items + $query = $db->getQuery(true) + ->delete($db->quoteName('#__associations')) + ->where($db->quoteName('context') . ' = :associationscontext') + ->bind(':associationscontext', $this->associationsContext); + + if ($associations) { + $where[] = $db->quoteName('id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')'; + } + + if ($oldKey !== null) { + $where[] = $db->quoteName('key') . ' = :oldKey'; + $query->bind(':oldKey', $oldKey); + } + + $query->extendWhere('AND', $where, 'OR'); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Adding self to the association + if (!$allLanguage) { + $associations[$table->language] = (int) $table->id; + } + + if (\count($associations) > 1) { + // Adding new association for these items + $key = md5(json_encode($associations)); + $query->clear() + ->insert($db->quoteName('#__associations')) + ->columns( + [ + $db->quoteName('id'), + $db->quoteName('context'), + $db->quoteName('key'), + ] + ); + + foreach ($associations as $id) { + $id = (int) $id; + + $query->values( + implode( + ',', + $query->bindArray( + [$id, $this->associationsContext, $key], + [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] + ) + ) + ); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew, $data)); + + // Rebuild the path for the category: + if (!$table->rebuildPath($table->id)) { + $this->setError($table->getError()); + + return false; + } + + // Rebuild the paths of the category's children: + if (!$table->rebuild($table->id, $table->lft, $table->level, $table->path)) { + $this->setError($table->getError()); + + return false; + } + + $this->setState($this->getName() . '.id', $table->id); + + if (Factory::getApplication()->input->get('task') == 'editAssociations') { + return $this->redirectToAssociations($data); + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the published state of one or more records. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function publish(&$pks, $value = 1) + { + if (parent::publish($pks, $value)) { + $extension = Factory::getApplication()->input->get('extension'); + + // Include the content plugins for the change of category state event. + PluginHelper::importPlugin('content'); + + // Trigger the onCategoryChangeState event. + Factory::getApplication()->triggerEvent('onCategoryChangeState', array($extension, $pks, $value)); + + return true; + } + } + + /** + * Method rebuild the entire nested set tree. + * + * @return boolean False on failure or error, true otherwise. + * + * @since 1.6 + */ + public function rebuild() + { + // Get an instance of the table object. + $table = $this->getTable(); + + if (!$table->rebuild()) { + $this->setError($table->getError()); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to save the reordered nested set tree. + * First we save the new order values in the lft values of the changed ids. + * Then we invoke the table rebuild to implement the new ordering. + * + * @param array $idArray An array of primary key ids. + * @param integer $lftArray The lft value + * + * @return boolean False on failure or error, True otherwise + * + * @since 1.6 + */ + public function saveorder($idArray = null, $lftArray = null) + { + // Get an instance of the table object. + $table = $this->getTable(); + + if (!$table->saveorder($idArray, $lftArray)) { + $this->setError($table->getError()); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Batch flip category ordering. + * + * @param integer $value The new category. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return mixed An array of new IDs on success, boolean false on failure. + * + * @since 3.6.3 + */ + protected function batchFlipordering($value, $pks, $contexts) + { + $successful = array(); + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + /** + * For each category get the max ordering value + * Re-order with max - ordering + */ + foreach ($pks as $id) { + $query->select('MAX(' . $db->quoteName('ordering') . ')') + ->from($db->quoteName('#__content')) + ->where($db->quoteName('catid') . ' = :catid') + ->bind(':catid', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + $max = (int) $db->loadResult(); + $max++; + + $query->clear(); + + $query->update($db->quoteName('#__content')) + ->set($db->quoteName('ordering') . ' = :max - ' . $db->quoteName('ordering')) + ->where($db->quoteName('catid') . ' = :catid') + ->bind(':max', $max, ParameterType::INTEGER) + ->bind(':catid', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + if ($db->execute()) { + $successful[] = $id; + } + } + + return empty($successful) ? false : $successful; + } + + /** + * Batch copy categories to a new category. + * + * @param integer $value The new category. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return mixed An array of new IDs on success, boolean false on failure. + * + * @since 1.6 + */ + protected function batchCopy($value, $pks, $contexts) + { + $type = new UCMType(); + $this->type = $type->getTypeByAlias($this->typeAlias); + + // $value comes as {parent_id}.{extension} + $parts = explode('.', $value); + $parentId = (int) ArrayHelper::getValue($parts, 0, 1); + + $db = $this->getDatabase(); + $extension = Factory::getApplication()->input->get('extension', '', 'word'); + $newIds = array(); + + // Check that the parent exists + if ($parentId) { + if (!$this->table->load($parentId)) { + if ($error = $this->table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Non-fatal error + $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); + $parentId = 0; + } + } + + // Check that user has create permission for parent category + if ($parentId == $this->table->getRootId()) { + $canCreate = $this->user->authorise('core.create', $extension); + } else { + $canCreate = $this->user->authorise('core.create', $extension . '.category.' . $parentId); + } + + if (!$canCreate) { + // Error since user cannot create in parent category + $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); + + return false; + } + } + + // If the parent is 0, set it to the ID of the root item in the tree + if (empty($parentId)) { + if (!$parentId = $this->table->getRootId()) { + $this->setError($this->table->getError()); + + return false; + } elseif (!$this->user->authorise('core.create', $extension)) { + // Make sure we can create in root + $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); + + return false; + } + } + + // We need to log the parent ID + $parents = array(); + + // Calculate the emergency stop count as a precaution against a runaway loop bug + $query = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('id') . ')') + ->from($db->quoteName('#__categories')); + $db->setQuery($query); + + try { + $count = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Parent exists so let's proceed + while (!empty($pks) && $count > 0) { + // Pop the first id off the stack + $pk = array_shift($pks); + + $this->table->reset(); + + // Check that the row actually exists + if (!$this->table->load($pk)) { + if ($error = $this->table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + // Copy is a bit tricky, because we also need to copy the children + $lft = (int) $this->table->lft; + $rgt = (int) $this->table->rgt; + $query->clear() + ->select($db->quoteName('id')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('lft') . ' > :lft') + ->where($db->quoteName('rgt') . ' < :rgt') + ->bind(':lft', $lft, ParameterType::INTEGER) + ->bind(':rgt', $rgt, ParameterType::INTEGER); + $db->setQuery($query); + $childIds = $db->loadColumn(); + + // Add child ID's to the array only if they aren't already there. + foreach ($childIds as $childId) { + if (!\in_array($childId, $pks)) { + $pks[] = $childId; + } + } + + // Make a copy of the old ID, Parent ID and Asset ID + $oldId = $this->table->id; + $oldParentId = $this->table->parent_id; + $oldAssetId = $this->table->asset_id; + + // Reset the id because we are making a copy. + $this->table->id = 0; + + // If we a copying children, the Old ID will turn up in the parents list + // otherwise it's a new top level item + $this->table->parent_id = $parents[$oldParentId] ?? $parentId; + + // Set the new location in the tree for the node. + $this->table->setLocation($this->table->parent_id, 'last-child'); + + // @TODO: Deal with ordering? + // $this->table->ordering = 1; + $this->table->level = null; + $this->table->asset_id = null; + $this->table->lft = null; + $this->table->rgt = null; + + // Alter the title & alias + [$title, $alias] = $this->generateNewTitle($this->table->parent_id, $this->table->alias, $this->table->title); + $this->table->title = $title; + $this->table->alias = $alias; + + // Unpublish because we are making a copy + $this->table->published = 0; + + // Store the row. + if (!$this->table->store()) { + $this->setError($this->table->getError()); + + return false; + } + + // Get the new item ID + $newId = $this->table->get('id'); + + // Add the new ID to the array + $newIds[$pk] = $newId; + + // Copy rules + $query->clear() + ->update($db->quoteName('#__assets', 't')) + ->join( + 'INNER', + $db->quoteName('#__assets', 's'), + $db->quoteName('s.id') . ' = :oldid' + ) + ->bind(':oldid', $oldAssetId, ParameterType::INTEGER) + ->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules')) + ->where($db->quoteName('t.id') . ' = :assetid') + ->bind(':assetid', $this->table->asset_id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Now we log the old 'parent' to the new 'parent' + $parents[$oldId] = $this->table->id; + $count--; + } + + // Rebuild the hierarchy. + if (!$this->table->rebuild()) { + $this->setError($this->table->getError()); + + return false; + } + + // Rebuild the tree path. + if (!$this->table->rebuildPath($this->table->id)) { + $this->setError($this->table->getError()); + + return false; + } + + return $newIds; + } + + /** + * Batch move categories to a new category. + * + * @param integer $value The new category ID. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True on success. + * + * @since 1.6 + */ + protected function batchMove($value, $pks, $contexts) + { + $parentId = (int) $value; + $type = new UCMType(); + $this->type = $type->getTypeByAlias($this->typeAlias); + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $extension = Factory::getApplication()->input->get('extension', '', 'word'); + + // Check that the parent exists. + if ($parentId) { + if (!$this->table->load($parentId)) { + if ($error = $this->table->getError()) { + // Fatal error. + $this->setError($error); + + return false; + } else { + // Non-fatal error. + $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); + $parentId = 0; + } + } + + // Check that user has create permission for parent category. + if ($parentId == $this->table->getRootId()) { + $canCreate = $this->user->authorise('core.create', $extension); + } else { + $canCreate = $this->user->authorise('core.create', $extension . '.category.' . $parentId); + } + + if (!$canCreate) { + // Error since user cannot create in parent category + $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); + + return false; + } + + // Check that user has edit permission for every category being moved + // Note that the entire batch operation fails if any category lacks edit permission + foreach ($pks as $pk) { + if (!$this->user->authorise('core.edit', $extension . '.category.' . $pk)) { + // Error since user cannot edit this category + $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_EDIT')); + + return false; + } + } + } + + // We are going to store all the children and just move the category + $children = array(); + + // Parent exists so let's proceed + foreach ($pks as $pk) { + // Check that the row actually exists + if (!$this->table->load($pk)) { + if ($error = $this->table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + // Set the new location in the tree for the node. + $this->table->setLocation($parentId, 'last-child'); + + // Check if we are moving to a different parent + if ($parentId != $this->table->parent_id) { + $lft = (int) $this->table->lft; + $rgt = (int) $this->table->rgt; + + // Add the child node ids to the children array. + $query->clear() + ->select($db->quoteName('id')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('lft') . ' BETWEEN :lft AND :rgt') + ->bind(':lft', $lft, ParameterType::INTEGER) + ->bind(':rgt', $rgt, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $children = array_merge($children, (array) $db->loadColumn()); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + // Store the row. + if (!$this->table->store()) { + $this->setError($this->table->getError()); + + return false; + } + + // Rebuild the tree path. + if (!$this->table->rebuildPath()) { + $this->setError($this->table->getError()); + + return false; + } + } + + // Process the child rows + if (!empty($children)) { + // Remove any duplicates and sanitize ids. + $children = array_unique($children); + $children = ArrayHelper::toInteger($children); + } + + return true; + } + + /** + * Custom clean the cache of com_content and content modules + * + * @param string $group Cache group name. + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + $extension = Factory::getApplication()->input->get('extension'); + + switch ($extension) { + case 'com_content': + parent::cleanCache('com_content'); + parent::cleanCache('mod_articles_archive'); + parent::cleanCache('mod_articles_categories'); + parent::cleanCache('mod_articles_category'); + parent::cleanCache('mod_articles_latest'); + parent::cleanCache('mod_articles_news'); + parent::cleanCache('mod_articles_popular'); + break; + default: + parent::cleanCache($extension); + break; + } + } + + /** + * Method to change the title & alias. + * + * @param integer $parentId The id of the parent. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 1.7 + */ + protected function generateNewTitle($parentId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) { + $title = StringHelper::increment($title); + $alias = StringHelper::increment($alias, 'dash'); + } + + return array($title, $alias); + } + + /** + * Method to determine if a category association is available. + * + * @return boolean True if a category association is available; false otherwise. + */ + public function getAssoc() + { + if (!\is_null($this->hasAssociation)) { + return $this->hasAssociation; + } + + $extension = $this->getState('category.extension', ''); + + $this->hasAssociation = Associations::isEnabled(); + $extension = explode('.', $extension); + $component = array_shift($extension); + $cname = str_replace('com_', '', $component); + + if (!$this->hasAssociation || !$component || !$cname) { + $this->hasAssociation = false; + + return $this->hasAssociation; + } + + $componentObject = $this->bootComponent($component); + + if ($componentObject instanceof AssociationServiceInterface && $componentObject instanceof CategoryServiceInterface) { + $this->hasAssociation = true; + + return $this->hasAssociation; + } + + $hname = $cname . 'HelperAssociation'; + \JLoader::register($hname, JPATH_SITE . '/components/' . $component . '/helpers/association.php'); + + $this->hasAssociation = class_exists($hname) && !empty($hname::$category_association); + + return $this->hasAssociation; + } } diff --git a/code/administrator/components/com_categories/src/Service/HTML/AdministratorService.php b/code/administrator/components/com_categories/src/Service/HTML/AdministratorService.php index 0a48842e..b31b270f 100644 --- a/code/administrator/components/com_categories/src/Service/HTML/AdministratorService.php +++ b/code/administrator/components/com_categories/src/Service/HTML/AdministratorService.php @@ -1,4 +1,5 @@ getQuery(true) - ->select( - [ - $db->quoteName('c.id'), - $db->quoteName('c.title'), - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('l.lang_code'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__categories', 'c')) - ->whereIn($db->quoteName('c.id'), array_values($associations)) - ->where($db->quoteName('c.id') . ' != :catid') - ->bind(':catid', $catid, ParameterType::INTEGER) - ->join( - 'LEFT', - $db->quoteName('#__languages', 'l'), - $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code') - ); - $db->setQuery($query); + // Get the associated categories + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('c.id'), + $db->quoteName('c.title'), + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('l.lang_code'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__categories', 'c')) + ->whereIn($db->quoteName('c.id'), array_values($associations)) + ->where($db->quoteName('c.id') . ' != :catid') + ->bind(':catid', $catid, ParameterType::INTEGER) + ->join( + 'LEFT', + $db->quoteName('#__languages', 'l'), + $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code') + ); + $db->setQuery($query); - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500, $e); - } + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500, $e); + } - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_categories&task=category.edit&id=' . (int) $item->id . '&extension=' . $extension); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8'); - $classes = 'badge bg-secondary'; + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_categories&task=category.edit&id=' . (int) $item->id . '&extension=' . $extension); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8'); + $classes = 'badge bg-secondary'; - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } - $html = LayoutHelper::render('joomla.content.associations', $items); - } + $html = LayoutHelper::render('joomla.content.associations', $items); + } - return $html; - } + return $html; + } } diff --git a/code/administrator/components/com_categories/src/Table/CategoryTable.php b/code/administrator/components/com_categories/src/Table/CategoryTable.php index dc63e189..3342fc11 100644 --- a/code/administrator/components/com_categories/src/Table/CategoryTable.php +++ b/code/administrator/components/com_categories/src/Table/CategoryTable.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->assoc = $this->get('Assoc'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Written this way because we only want to call IsEmptyState if no items, to prevent always calling it when not needed. - if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Preprocess the list of items to find ordering divisions. - foreach ($this->items as &$item) - { - $this->ordering[$item->parent_id][] = $item->id; - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In article associations modal we need to remove language filter if forcing a language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @throws \Exception - * @since 1.6 - */ - protected function addToolbar() - { - $categoryId = $this->state->get('filter.category_id'); - $component = $this->state->get('filter.component'); - $section = $this->state->get('filter.section'); - $canDo = ContentHelper::getActions($component, 'category', $categoryId); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - // Avoid nonsense situation. - if ($component == 'com_categories') - { - return; - } - - // Need to load the menu language file as mod_menu hasn't been loaded yet. - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE) - || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); - - // If a component categories title string is present, let's use it. - if ($lang->hasKey($component_title_key = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_TITLE')) - { - $title = Text::_($component_title_key); - } - elseif ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) - // Else if the component section string exists, let's use it. - { - $title = Text::sprintf('COM_CATEGORIES_CATEGORIES_TITLE', $this->escape(Text::_($component_section_key))); - } - else - // Else use the base title - { - $title = Text::_('COM_CATEGORIES_CATEGORIES_BASE_TITLE'); - } - - // Load specific css component - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = $this->document->getWebAssetManager(); - $wa->getRegistry()->addExtensionRegistryFile($component); - - if ($wa->assetExists('style', $component . '.admin-categories')) - { - $wa->useStyle($component . '.admin-categories'); - } - else - { - $wa->registerAndUseStyle($component . '.admin-categories', $component . '/administrator/categories.css'); - } - - // Prepare the toolbar. - ToolbarHelper::title($title, 'folder categories ' . substr($component, 4) . ($section ? "-$section" : '') . '-categories'); - - if ($canDo->get('core.create') || count($user->getAuthorisedCategories($component, 'core.create')) > 0) - { - $toolbar->addNew('category.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('categories.publish')->listCheck(true); - - $childBar->unpublish('categories.unpublish')->listCheck(true); - - $childBar->archive('categories.archive')->listCheck(true); - } - - if ($user->authorise('core.admin')) - { - $childBar->checkin('categories.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - $childBar->trash('categories.trash')->listCheck(true); - } - - // Add a batch button - if ($canDo->get('core.create') - && $canDo->get('core.edit') - && $canDo->get('core.edit.state')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $canDo->get('core.admin')) - { - $toolbar->standardButton('refresh') - ->text('JTOOLBAR_REBUILD') - ->task('categories.rebuild'); - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete', $component)) - { - $toolbar->delete('categories.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences($component); - } - - // Get the component form if it exists for the help key/url - $name = 'category' . ($section ? ('.' . $section) : ''); - - // Looking first in the component forms folder - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); - - // Looking in the component models/forms folder (J! 3) - if (!file_exists($path)) - { - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); - } - - $ref_key = ''; - $url = ''; - - // Look first in form for help key and url - if (file_exists($path)) - { - if (!$xml = simplexml_load_file($path)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - $ref_key = (string) $xml->listhelp['key']; - $url = (string) $xml->listhelp['url']; - } - - if (!$ref_key) - { - // Compute the ref_key if it does exist in the component - $languageKey = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_HELP_KEY'; - - if ($lang->hasKey($languageKey)) - { - $ref_key = $languageKey; - } - else - { - $languageKey = 'JHELP_COMPONENTS_' . strtoupper(substr($component, 4) . ($section ? "_$section" : '')) . '_CATEGORIES'; - - if ($lang->hasKey($languageKey)) - { - $ref_key = $languageKey; - } - } - } - - /* - * Get help for the categories view for the component by - * -remotely searching in a URL defined in the category form - * -remotely searching in a language defined dedicated URL: *component*_HELP_URL - * -locally searching in a component help file if helpURL param exists in the component and is set to '' - * -remotely searching in a component URL if helpURL param exists in the component and is NOT set to '' - */ - if (!$url) - { - if ($lang->hasKey($lang_help_url = strtoupper($component) . '_HELP_URL')) - { - $debug = $lang->setDebug(false); - $url = Text::_($lang_help_url); - $lang->setDebug($debug); - } - } - - ToolbarHelper::help($ref_key, ComponentHelper::getParams($component)->exists('helpURL'), $url); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var object + */ + protected $state; + + /** + * Flag if an association exists + * + * @var boolean + */ + protected $assoc; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string|null $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @throws GenericDataException + * + * @return void + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->assoc = $this->get('Assoc'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Written this way because we only want to call IsEmptyState if no items, to prevent always calling it when not needed. + if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Preprocess the list of items to find ordering divisions. + foreach ($this->items as &$item) { + $this->ordering[$item->parent_id][] = $item->id; + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In article associations modal we need to remove language filter if forcing a language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @throws \Exception + * @since 1.6 + */ + protected function addToolbar() + { + $categoryId = $this->state->get('filter.category_id'); + $component = $this->state->get('filter.component'); + $section = $this->state->get('filter.section'); + $canDo = ContentHelper::getActions($component, 'category', $categoryId); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + // Avoid nonsense situation. + if ($component == 'com_categories') { + return; + } + + // Need to load the menu language file as mod_menu hasn't been loaded yet. + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE) + || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); + + // If a component categories title string is present, let's use it. + if ($lang->hasKey($component_title_key = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_TITLE')) { + $title = Text::_($component_title_key); + } elseif ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) { + // Else if the component section string exists, let's use it. + $title = Text::sprintf('COM_CATEGORIES_CATEGORIES_TITLE', $this->escape(Text::_($component_section_key))); + } else // Else use the base title + { + $title = Text::_('COM_CATEGORIES_CATEGORIES_BASE_TITLE'); + } + + // Load specific css component + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = $this->document->getWebAssetManager(); + $wa->getRegistry()->addExtensionRegistryFile($component); + + if ($wa->assetExists('style', $component . '.admin-categories')) { + $wa->useStyle($component . '.admin-categories'); + } else { + $wa->registerAndUseStyle($component . '.admin-categories', $component . '/administrator/categories.css'); + } + + // Prepare the toolbar. + ToolbarHelper::title($title, 'folder categories ' . substr($component, 4) . ($section ? "-$section" : '') . '-categories'); + + if ($canDo->get('core.create') || count($user->getAuthorisedCategories($component, 'core.create')) > 0) { + $toolbar->addNew('category.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('categories.publish')->listCheck(true); + + $childBar->unpublish('categories.unpublish')->listCheck(true); + + $childBar->archive('categories.archive')->listCheck(true); + } + + if ($user->authorise('core.admin')) { + $childBar->checkin('categories.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + $childBar->trash('categories.trash')->listCheck(true); + } + + // Add a batch button + if ( + $canDo->get('core.create') + && $canDo->get('core.edit') + && $canDo->get('core.edit.state') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $canDo->get('core.admin')) { + $toolbar->standardButton('refresh') + ->text('JTOOLBAR_REBUILD') + ->task('categories.rebuild'); + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete', $component)) { + $toolbar->delete('categories.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences($component); + } + + // Get the component form if it exists for the help key/url + $name = 'category' . ($section ? ('.' . $section) : ''); + + // Looking first in the component forms folder + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); + + // Looking in the component models/forms folder (J! 3) + if (!file_exists($path)) { + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); + } + + $ref_key = ''; + $url = ''; + + // Look first in form for help key and url + if (file_exists($path)) { + if (!$xml = simplexml_load_file($path)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + $ref_key = (string) $xml->listhelp['key']; + $url = (string) $xml->listhelp['url']; + } + + if (!$ref_key) { + // Compute the ref_key if it does exist in the component + $languageKey = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_HELP_KEY'; + + if ($lang->hasKey($languageKey)) { + $ref_key = $languageKey; + } else { + $languageKey = 'JHELP_COMPONENTS_' . strtoupper(substr($component, 4) . ($section ? "_$section" : '')) . '_CATEGORIES'; + + if ($lang->hasKey($languageKey)) { + $ref_key = $languageKey; + } + } + } + + /* + * Get help for the categories view for the component by + * -remotely searching in a URL defined in the category form + * -remotely searching in a language defined dedicated URL: *component*_HELP_URL + * -locally searching in a component help file if helpURL param exists in the component and is set to '' + * -remotely searching in a component URL if helpURL param exists in the component and is NOT set to '' + */ + if (!$url) { + if ($lang->hasKey($lang_help_url = strtoupper($component) . '_HELP_URL')) { + $debug = $lang->setDebug(false); + $url = Text::_($lang_help_url); + $lang->setDebug($debug); + } + } + + ToolbarHelper::help($ref_key, ComponentHelper::getParams($component)->exists('helpURL'), $url); + } } diff --git a/code/administrator/components/com_categories/src/View/Category/HtmlView.php b/code/administrator/components/com_categories/src/View/Category/HtmlView.php index 71d1a501..572acc68 100644 --- a/code/administrator/components/com_categories/src/View/Category/HtmlView.php +++ b/code/administrator/components/com_categories/src/View/Category/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - $section = $this->state->get('category.section') ? $this->state->get('category.section') . '.' : ''; - $this->canDo = ContentHelper::getActions($this->state->get('category.component'), $section . 'category', $this->item->id); - $this->assoc = $this->get('Assoc'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check if we have a content type for this alias - if (!empty(TagsHelper::getTypes('objectList', array($this->state->get('category.extension') . '.category'), true))) - { - $this->checkTags = true; - } - - Factory::getApplication()->input->set('hidemainmenu', true); - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('parent_id', 'language', '*,' . $forcedLanguage); - - // Only allow to select tags with All language or with the forced language. - $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $extension = Factory::getApplication()->input->get('extension'); - $user = Factory::getUser(); - $userId = $user->id; - - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Avoid nonsense situation. - if ($extension == 'com_categories') - { - return; - } - - // The extension can be in the form com_foo.section - $parts = explode('.', $extension); - $component = $parts[0]; - $section = (count($parts) > 1) ? $parts[1] : null; - $componentParams = ComponentHelper::getParams($component); - - // Need to load the menu language file as mod_menu hasn't been loaded yet. - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE) - || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); - - // Get the results for each action. - $canDo = $this->canDo; - - // If a component categories title string is present, let's use it. - if ($lang->hasKey($component_title_key = $component . ($section ? "_$section" : '') . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE')) - { - $title = Text::_($component_title_key); - } - // Else if the component section string exists, let's use it. - elseif ($lang->hasKey($component_section_key = $component . ($section ? "_$section" : ''))) - { - $title = Text::sprintf('COM_CATEGORIES_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') - . '_TITLE', $this->escape(Text::_($component_section_key)) - ); - } - // Else use the base title - else - { - $title = Text::_('COM_CATEGORIES_CATEGORY_BASE_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE'); - } - - // Load specific css component - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = $this->document->getWebAssetManager(); - $wa->getRegistry()->addExtensionRegistryFile($component); - - if ($wa->assetExists('style', $component . '.admin-categories')) - { - $wa->useStyle($component . '.admin-categories'); - } - else - { - $wa->registerAndUseStyle($component . '.admin-categories', $component . '/administrator/categories.css'); - } - - // Prepare the toolbar. - ToolbarHelper::title( - $title, - 'folder category-' . ($isNew ? 'add' : 'edit') - . ' ' . substr($component, 4) . ($section ? "-$section" : '') . '-category-' . ($isNew ? 'add' : 'edit') - ); - - // For new records, check the create permission. - if ($isNew && (count($user->getAuthorisedCategories($component, 'core.create')) > 0)) - { - ToolbarHelper::apply('category.apply'); - ToolbarHelper::saveGroup( - [ - ['save', 'category.save'], - ['save2new', 'category.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('category.cancel'); - } - - // If not checked out, can save the item. - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_user_id == $userId); - - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - ToolbarHelper::apply('category.apply'); - - $toolbarButtons[] = ['save', 'category.save']; - - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'category.save2new']; - } - } - - // If an existing item, can save to a copy. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'category.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('category.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $componentParams->get('save_history', 0) && $itemEditable) - { - $typeAlias = $extension . '.category'; - ToolbarHelper::versions($typeAlias, $this->item->id); - } - - if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) - { - ToolbarHelper::custom('category.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); - } - } - - ToolbarHelper::divider(); - - // Look first in form for help key - $ref_key = (string) $this->form->getXml()->help['key']; - - // Try with a language string - if (!$ref_key) - { - // Compute the ref_key if it does exist in the component - $languageKey = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') . '_HELP_KEY'; - - if ($lang->hasKey($languageKey)) - { - $ref_key = $languageKey; - } - else - { - $languageKey = 'JHELP_COMPONENTS_' - . strtoupper(substr($component, 4) . ($section ? "_$section" : '')) - . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT'); - - if ($lang->hasKey($languageKey)) - { - $ref_key = $languageKey; - } - } - } - - /* - * Get help for the category/section view for the component by - * -remotely searching in a URL defined in the category form - * -remotely searching in a language defined dedicated URL: *component*_HELP_URL - * -locally searching in a component help file if helpURL param exists in the component and is set to '' - * -remotely searching in a component URL if helpURL param exists in the component and is NOT set to '' - */ - $url = (string) $this->form->getXml()->help['url']; - - if (!$url) - { - if ($lang->hasKey($lang_help_url = strtoupper($component) . '_HELP_URL')) - { - $debug = $lang->setDebug(false); - $url = Text::_($lang_help_url); - $lang->setDebug($debug); - } - } - - ToolbarHelper::help($ref_key, $componentParams->exists('helpURL'), $url, $component); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * Flag if an association exists + * + * @var boolean + */ + protected $assoc; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + */ + protected $canDo; + + /** + * Is there a content type associated with this category alias + * + * @var boolean + * @since 4.0.0 + */ + protected $checkTags = false; + + /** + * Display the view. + * + * @param string|null $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $section = $this->state->get('category.section') ? $this->state->get('category.section') . '.' : ''; + $this->canDo = ContentHelper::getActions($this->state->get('category.component'), $section . 'category', $this->item->id); + $this->assoc = $this->get('Assoc'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check if we have a content type for this alias + if (!empty(TagsHelper::getTypes('objectList', array($this->state->get('category.extension') . '.category'), true))) { + $this->checkTags = true; + } + + Factory::getApplication()->input->set('hidemainmenu', true); + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('parent_id', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $extension = Factory::getApplication()->input->get('extension'); + $user = $this->getCurrentUser(); + $userId = $user->id; + + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Avoid nonsense situation. + if ($extension == 'com_categories') { + return; + } + + // The extension can be in the form com_foo.section + $parts = explode('.', $extension); + $component = $parts[0]; + $section = (count($parts) > 1) ? $parts[1] : null; + $componentParams = ComponentHelper::getParams($component); + + // Need to load the menu language file as mod_menu hasn't been loaded yet. + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE) + || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); + + // Get the results for each action. + $canDo = $this->canDo; + + // If a component categories title string is present, let's use it. + if ($lang->hasKey($component_title_key = $component . ($section ? "_$section" : '') . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE')) { + $title = Text::_($component_title_key); + } elseif ($lang->hasKey($component_section_key = $component . ($section ? "_$section" : ''))) { + // Else if the component section string exists, let's use it. + $title = Text::sprintf('COM_CATEGORIES_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') + . '_TITLE', $this->escape(Text::_($component_section_key))); + } else { + // Else use the base title + $title = Text::_('COM_CATEGORIES_CATEGORY_BASE_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE'); + } + + // Load specific css component + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = $this->document->getWebAssetManager(); + $wa->getRegistry()->addExtensionRegistryFile($component); + + if ($wa->assetExists('style', $component . '.admin-categories')) { + $wa->useStyle($component . '.admin-categories'); + } else { + $wa->registerAndUseStyle($component . '.admin-categories', $component . '/administrator/categories.css'); + } + + // Prepare the toolbar. + ToolbarHelper::title( + $title, + 'folder category-' . ($isNew ? 'add' : 'edit') + . ' ' . substr($component, 4) . ($section ? "-$section" : '') . '-category-' . ($isNew ? 'add' : 'edit') + ); + + // For new records, check the create permission. + if ($isNew && (count($user->getAuthorisedCategories($component, 'core.create')) > 0)) { + ToolbarHelper::apply('category.apply'); + ToolbarHelper::saveGroup( + [ + ['save', 'category.save'], + ['save2new', 'category.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('category.cancel'); + } else { + // If not checked out, can save the item. + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_user_id == $userId); + + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + ToolbarHelper::apply('category.apply'); + + $toolbarButtons[] = ['save', 'category.save']; + + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'category.save2new']; + } + } + + // If an existing item, can save to a copy. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'category.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('category.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $componentParams->get('save_history', 0) && $itemEditable) { + $typeAlias = $extension . '.category'; + ToolbarHelper::versions($typeAlias, $this->item->id); + } + + if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + ToolbarHelper::custom('category.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); + } + } + + ToolbarHelper::divider(); + + // Look first in form for help key + $ref_key = (string) $this->form->getXml()->help['key']; + + // Try with a language string + if (!$ref_key) { + // Compute the ref_key if it does exist in the component + $languageKey = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') . '_HELP_KEY'; + + if ($lang->hasKey($languageKey)) { + $ref_key = $languageKey; + } else { + $languageKey = 'JHELP_COMPONENTS_' + . strtoupper(substr($component, 4) . ($section ? "_$section" : '')) + . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT'); + + if ($lang->hasKey($languageKey)) { + $ref_key = $languageKey; + } + } + } + + /* + * Get help for the category/section view for the component by + * -remotely searching in a URL defined in the category form + * -remotely searching in a language defined dedicated URL: *component*_HELP_URL + * -locally searching in a component help file if helpURL param exists in the component and is set to '' + * -remotely searching in a component URL if helpURL param exists in the component and is NOT set to '' + */ + $url = (string) $this->form->getXml()->help['url']; + + if (!$url) { + if ($lang->hasKey($lang_help_url = strtoupper($component) . '_HELP_URL')) { + $debug = $lang->setDebug(false); + $url = Text::_($lang_help_url); + $lang->setDebug($debug); + } + } + + ToolbarHelper::help($ref_key, $componentParams->exists('helpURL'), $url, $component); + } } diff --git a/code/administrator/components/com_categories/tmpl/categories/default.php b/code/administrator/components/com_categories/tmpl/categories/default.php index 73dca084..9a4ecb97 100644 --- a/code/administrator/components/com_categories/tmpl/categories/default.php +++ b/code/administrator/components/com_categories/tmpl/categories/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $userId = $user->get('id'); @@ -32,281 +34,274 @@ $component = $parts[0]; $section = null; -if (count($parts) > 1) -{ - $section = $parts[1]; +if (count($parts) > 1) { + $section = $parts[1]; - $inflector = Inflector::getInstance(); + $inflector = Inflector::getInstance(); - if (!$inflector->isPlural($section)) - { - $section = $inflector->toPlural($section); - } + if (!$inflector->isPlural($section)) { + $section = $inflector->toPlural($section); + } } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_categories&task=categories.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_categories&task=categories.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?> -
-
-
- $this)); - ?> - items)) : ?> -
- - -
- - - - - - - - - - items[0]) && property_exists($this->items[0], 'count_published')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_archived')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> - - - - assoc) : ?> - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="false"> - items as $i => $item) : ?> - authorise('core.edit', $extension . '.category.' . $item->id); - $canCheckin = $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', $extension . '.category.' . $item->id) && $item->created_user_id == $userId; - $canChange = $user->authorise('core.edit.state', $extension . '.category.' . $item->id) && $canCheckin; +
+
+
+ $this)); + ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ + + + + + + + items[0]) && property_exists($this->items[0], 'count_published')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_archived')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> + + + + assoc) : ?> + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + items as $i => $item) : ?> + authorise('core.edit', $extension . '.category.' . $item->id); + $canCheckin = $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', $extension . '.category.' . $item->id) && $item->created_user_id == $userId; + $canChange = $user->authorise('core.edit.state', $extension . '.category.' . $item->id) && $canCheckin; - // Get the parents of item for sorting - if ($item->level > 1) - { - $parentsStr = ''; - $_currentParentId = $item->parent_id; - $parentsStr = ' ' . $_currentParentId; - for ($i2 = 0; $i2 < $item->level; $i2++) - { - foreach ($this->ordering as $k => $v) - { - $v = implode('-', $v); - $v = '-' . $v . '-'; - if (strpos($v, '-' . $_currentParentId . '-') !== false) - { - $parentsStr .= ' ' . $k; - $_currentParentId = $k; - break; - } - } - } - } - else - { - $parentsStr = ''; - } - ?> - - - - - - items[0]) && property_exists($this->items[0], 'count_published')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_archived')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> - - + // Get the parents of item for sorting + if ($item->level > 1) { + $parentsStr = ''; + $_currentParentId = $item->parent_id; + $parentsStr = ' ' . $_currentParentId; + for ($i2 = 0; $i2 < $item->level; $i2++) { + foreach ($this->ordering as $k => $v) { + $v = implode('-', $v); + $v = '-' . $v . '-'; + if (strpos($v, '-' . $_currentParentId . '-') !== false) { + $parentsStr .= ' ' . $k; + $_currentParentId = $k; + break; + } + } + } + } else { + $parentsStr = ''; + } + ?> + + + + + + items[0]) && property_exists($this->items[0], 'count_published')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_archived')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> + + - - assoc) : ?> - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - published, $i, 'categories.', $canChange); ?> - - $item->level)); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'categories.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- - - note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - - -
-
- - count_published; ?> - - - - - count_unpublished; ?> - - - - - count_archived; ?> - - - - - count_trashed; ?> - - -
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + published, $i, 'categories.', $canChange); ?> + + $item->level)); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'categories.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ + + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + + +
+
+ + count_published; ?> + + + + + count_unpublished; ?> + + + + + count_archived; ?> + + + + + count_trashed; ?> + + + - escape($item->access_level); ?> - - association) : ?> - id, $extension); ?> - - - - - id; ?> -
+ + escape($item->access_level); ?> + + assoc) : ?> + + association) : ?> + id, $extension); ?> + + + + + + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', $extension) - && $user->authorise('core.edit', $extension) - && $user->authorise('core.edit.state', $extension)) : ?> - Text::_('COM_CATEGORIES_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', $extension) + && $user->authorise('core.edit', $extension) + && $user->authorise('core.edit.state', $extension) + ) : ?> + Text::_('COM_CATEGORIES_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - - -
-
-
+ + + + + + + diff --git a/code/administrator/components/com_categories/tmpl/categories/default_batch_body.php b/code/administrator/components/com_categories/tmpl/categories/default_batch_body.php index 700addd6..aa432d63 100644 --- a/code/administrator/components/com_categories/tmpl/categories/default_batch_body.php +++ b/code/administrator/components/com_categories/tmpl/categories/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\HTML\HTMLHelper; @@ -19,46 +21,46 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
-
- = 0) : ?> -
-
- $extension, 'addRoot' => true]); ?> -
-
- -
-
- -
-
-
- -
-
-
- -
- -
-
-
-
- +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ $extension, 'addRoot' => true]); ?> +
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+ +
+
+
+
+
diff --git a/code/administrator/components/com_categories/tmpl/categories/default_batch_footer.php b/code/administrator/components/com_categories/tmpl/categories/default_batch_footer.php index 5f37efc7..3f29dd80 100644 --- a/code/administrator/components/com_categories/tmpl/categories/default_batch_footer.php +++ b/code/administrator/components/com_categories/tmpl/categories/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/code/administrator/components/com_categories/tmpl/categories/emptystate.php b/code/administrator/components/com_categories/tmpl/categories/emptystate.php index 03b15bdb..df600d52 100644 --- a/code/administrator/components/com_categories/tmpl/categories/emptystate.php +++ b/code/administrator/components/com_categories/tmpl/categories/emptystate.php @@ -1,4 +1,5 @@ load($component, JPATH_ADMINISTRATOR . '/components/' . $component); // If a component categories title string is present, let's use it. -if ($lang->hasKey($component_title_key = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_TITLE')) -{ - $title = Text::_($component_title_key); -} -// Else if the component section string exists, let's use it -elseif ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) +if ($lang->hasKey($component_title_key = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_TITLE')) { + $title = Text::_($component_title_key); +} elseif ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) { + // Else if the component section string exists, let's use it + $title = Text::sprintf('COM_CATEGORIES_CATEGORIES_TITLE', $this->escape(Text::_($component_section_key))); +} else // Else use the base title { - $title = Text::sprintf('COM_CATEGORIES_CATEGORIES_TITLE', $this->escape(Text::_($component_section_key))); -} -else // Else use the base title -{ - $title = Text::_('COM_CATEGORIES_CATEGORIES_BASE_TITLE'); + $title = Text::_('COM_CATEGORIES_CATEGORIES_BASE_TITLE'); } $displayData = [ - 'textPrefix' => 'COM_CATEGORIES', - 'formURL' => 'index.php?option=com_categories&extension=' . $extension, - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Category', - 'title' => $title, - 'icon' => 'icon-folder categories content-categories', + 'textPrefix' => 'COM_CATEGORIES', + 'formURL' => 'index.php?option=com_categories&extension=' . $extension, + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Category', + 'title' => $title, + 'icon' => 'icon-folder categories content-categories', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', $extension)) -{ - $displayData['createURL'] = 'index.php?option=com_categories&extension=' . $extension . '&task=category.add'; +if (Factory::getApplication()->getIdentity()->authorise('core.create', $extension)) { + $displayData['createURL'] = 'index.php?option=com_categories&extension=' . $extension . '&task=category.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_categories/tmpl/categories/modal.php b/code/administrator/components/com_categories/tmpl/categories/modal.php index 2500c0b4..819957f5 100644 --- a/code/administrator/components/com_categories/tmpl/categories/modal.php +++ b/code/administrator/components/com_categories/tmpl/categories/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } HTMLHelper::_('behavior.core'); @@ -34,114 +34,106 @@ ?>
-
+ - $this)); ?> + $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - ?> - items as $i => $item) : ?> - language && Multilanguage::isEnabled()) - { - $tag = strlen($item->language); - if ($tag == 5) - { - $lang = substr($item->language, 0, 2); - } - elseif ($tag == 6) - { - $lang = substr($item->language, 0, 3); - } - else - { - $lang = ''; - } - } - elseif (!Multilanguage::isEnabled()) - { - $lang = ''; - } - ?> - - - - - - - - - -
- , - , - -
- - - - - - - - - -
- - - - - $item->level)); ?> - - escape($item->title); ?> -
- note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
-
- escape($item->access_level); ?> - - - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + ?> + items as $i => $item) : ?> + language && Multilanguage::isEnabled()) { + $tag = strlen($item->language); + if ($tag == 5) { + $lang = substr($item->language, 0, 2); + } elseif ($tag == 6) { + $lang = substr($item->language, 0, 3); + } else { + $lang = ''; + } + } elseif (!Multilanguage::isEnabled()) { + $lang = ''; + } + ?> + + + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ + + + + $item->level)); ?> + + escape($item->title); ?> +
+ note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
+
+ escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - - + + + + + -
+
diff --git a/code/administrator/components/com_categories/tmpl/category/edit.php b/code/administrator/components/com_categories/tmpl/category/edit.php index 448b4a18..b20338fd 100644 --- a/code/administrator/components/com_categories/tmpl/category/edit.php +++ b/code/administrator/components/com_categories/tmpl/category/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $input = $app->input; @@ -34,14 +35,12 @@ $c = Factory::getApplication()->bootComponent($this->state->get('category.extension')); -if ($c instanceof WorkflowServiceInterface) -{ - $wcontext = $c->getCategoryWorkflowContext($this->state->get('category.section')); +if ($c instanceof WorkflowServiceInterface) { + $wcontext = $c->getCategoryWorkflowContext($this->state->get('category.section')); - if (!$c->isWorkflowActive($wcontext)) - { - $this->ignore_fieldsets[] = 'workflow'; - } + if (!$c->isWorkflowActive($wcontext)) { + $this->ignore_fieldsets[] = 'workflow'; + } } $this->useCoreUI = true; @@ -54,78 +53,77 @@
- - -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
- form->getLabel('description'); ?> - form->getInput('description'); ?> -
-
- -
-
- - - - - - -
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- - - - - -
- -
- -
-
- - - - - - canDo->get('core.admin')) : ?> - - -
- -
- form->getInput('rules'); ?> -
-
- - - - - - form->getInput('extension'); ?> - - - - -
+ + +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> + +
+
+ form->getLabel('description'); ?> + form->getInput('description'); ?> +
+
+ +
+
+ + + + + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + + + + +
+ +
+ +
+
+ + + + + + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + + + form->getInput('extension'); ?> + + + + +
diff --git a/code/administrator/components/com_categories/tmpl/category/modal.php b/code/administrator/components/com_categories/tmpl/category/modal.php index 2adebfb9..87526bec 100644 --- a/code/administrator/components/com_categories/tmpl/category/modal.php +++ b/code/administrator/components/com_categories/tmpl/category/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/code/administrator/components/com_checkin/checkin.xml b/code/administrator/components/com_checkin/checkin.xml index 1494a15c..47a60413 100644 --- a/code/administrator/components/com_checkin/checkin.xml +++ b/code/administrator/components/com_checkin/checkin.xml @@ -2,7 +2,7 @@ com_checkin Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_checkin/services/provider.php b/code/administrator/components/com_checkin/services/provider.php index e4ad52d3..66b8f0fe 100644 --- a/code/administrator/components/com_checkin/services/provider.php +++ b/code/administrator/components/com_checkin/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Checkin')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Checkin')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Checkin')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Checkin')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_checkin/src/Controller/DisplayController.php b/code/administrator/components/com_checkin/src/Controller/DisplayController.php index 34c55127..8f920453 100644 --- a/code/administrator/components/com_checkin/src/Controller/DisplayController.php +++ b/code/administrator/components/com_checkin/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'string'); - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST'), 'warning'); - } - else - { - // Get the model. - /** @var \Joomla\Component\Checkin\Administrator\Model\CheckinModel $model */ - $model = $this->getModel('Checkin'); - - // Checked in the items. - $this->setMessage(Text::plural('COM_CHECKIN_N_ITEMS_CHECKED_IN', $model->checkin($ids))); - } - - $this->setRedirect('index.php?option=com_checkin'); - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_checkin')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Checkin'); - - $amount = (int) count($model->getItems()); - - echo new JsonResponse($amount); - } - - /** - * Method to get the number of locked icons - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getQuickiconContent() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_checkin')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Checkin'); - - $amount = (int) count($model->getItems()); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_CHECKIN_N_QUICKICON_SRONLY', $amount); - - echo new JsonResponse($result); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'checkin'; + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static A \JControllerLegacy object to support chaining. + */ + public function display($cachable = false, $urlparams = array()) + { + return parent::display(); + } + + /** + * Check in a list of items. + * + * @return void + */ + public function checkin() + { + // Check for request forgeries + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'string'); + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST'), 'warning'); + } else { + // Get the model. + /** @var \Joomla\Component\Checkin\Administrator\Model\CheckinModel $model */ + $model = $this->getModel('Checkin'); + + // Checked in the items. + $this->setMessage(Text::plural('COM_CHECKIN_N_ITEMS_CHECKED_IN', $model->checkin($ids))); + } + + $this->setRedirect('index.php?option=com_checkin'); + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_checkin')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Checkin'); + + $amount = (int) count($model->getItems()); + + echo new JsonResponse($amount); + } + + /** + * Method to get the number of locked icons + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getQuickiconContent() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_checkin')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Checkin'); + + $amount = (int) count($model->getItems()); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_CHECKIN_N_QUICKICON_SRONLY', $amount); + + echo new JsonResponse($result); + } } diff --git a/code/administrator/components/com_checkin/src/Model/CheckinModel.php b/code/administrator/components/com_checkin/src/Model/CheckinModel.php index 6987398e..90eb55e5 100644 --- a/code/administrator/components/com_checkin/src/Model/CheckinModel.php +++ b/code/administrator/components/com_checkin/src/Model/CheckinModel.php @@ -1,4 +1,5 @@ getDbo(); - - if (!is_array($ids)) - { - return 0; - } - - // This int will hold the checked item count. - $results = 0; - - $app = Factory::getApplication(); - - foreach ($ids as $tn) - { - // Make sure we get the right tables based on prefix. - if (stripos($tn, $app->get('dbprefix')) !== 0) - { - continue; - } - - $fields = $db->getTableColumns($tn, false); - - if (!(isset($fields['checked_out']) && isset($fields['checked_out_time']))) - { - continue; - } - - $query = $db->getQuery(true) - ->update($db->quoteName($tn)) - ->set($db->quoteName('checked_out') . ' = DEFAULT'); - - if ($fields['checked_out_time']->Null === 'YES') - { - $query->set($db->quoteName('checked_out_time') . ' = NULL'); - } - else - { - $nullDate = $db->getNullDate(); - - $query->set($db->quoteName('checked_out_time') . ' = :checkouttime') - ->bind(':checkouttime', $nullDate); - } - - if ($fields['checked_out']->Null === 'YES') - { - $query->where($db->quoteName('checked_out') . ' IS NOT NULL'); - } - else - { - $query->where($db->quoteName('checked_out') . ' > 0'); - } - - $db->setQuery($query); - - if ($db->execute()) - { - $results = $results + $db->getAffectedRows(); - $app->triggerEvent('onAfterCheckin', array($tn)); - } - } - - return $results; - } - - /** - * Get total of tables - * - * @return integer Total to check-in tables - * - * @since 1.6 - */ - public function getTotal() - { - if (!isset($this->total)) - { - $this->getItems(); - } - - return $this->total; - } - - /** - * Get tables - * - * @return array Checked in table names as keys and checked in item count as values. - * - * @since 1.6 - */ - public function getItems() - { - if (!isset($this->items)) - { - $db = $this->getDbo(); - $tables = $db->getTableList(); - $prefix = Factory::getApplication()->get('dbprefix'); - - // This array will hold table name as key and checked in item count as value. - $results = array(); - - foreach ($tables as $tn) - { - // Make sure we get the right tables based on prefix. - if (stripos($tn, $prefix) !== 0) - { - continue; - } - - if ($this->getState('filter.search') && stripos($tn, $this->getState('filter.search')) === false) - { - continue; - } - - $fields = $db->getTableColumns($tn, false); - - if (!(isset($fields['checked_out']) && isset($fields['checked_out_time']))) - { - continue; - } - - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName($tn)); - - if ($fields['checked_out']->Null === 'YES') - { - $query->where($db->quoteName('checked_out') . ' IS NOT NULL'); - } - else - { - $query->where($db->quoteName('checked_out') . ' > 0'); - } - - $db->setQuery($query); - $count = $db->loadResult(); - - if ($count) - { - $results[$tn] = $count; - } - } - - $this->total = count($results); - - // Order items by table - if ($this->getState('list.ordering') == 'table') - { - if (strtolower($this->getState('list.direction')) == 'asc') - { - ksort($results); - } - else - { - krsort($results); - } - } - // Order items by number of items - else - { - if (strtolower($this->getState('list.direction')) == 'asc') - { - asort($results); - } - else - { - arsort($results); - } - } - - // Pagination - $limit = (int) $this->getState('list.limit'); - - if ($limit !== 0) - { - $this->items = array_slice($results, $this->getState('list.start'), $limit); - } - else - { - $this->items = $results; - } - } - - return $this->items; - } + /** + * Count of the total items checked out + * + * @var integer + */ + protected $total; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'table', + 'count', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note: Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'table', $direction = 'asc') + { + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Checks in requested tables + * + * @param array $ids An array of table names. Optional. + * + * @return mixed The database results or 0 + * + * @since 1.6 + */ + public function checkin($ids = array()) + { + $db = $this->getDatabase(); + + if (!is_array($ids)) { + return 0; + } + + // This int will hold the checked item count. + $results = 0; + + $app = Factory::getApplication(); + + foreach ($ids as $tn) { + // Make sure we get the right tables based on prefix. + if (stripos($tn, $app->get('dbprefix')) !== 0) { + continue; + } + + $fields = $db->getTableColumns($tn, false); + + if (!(isset($fields['checked_out']) && isset($fields['checked_out_time']))) { + continue; + } + + $query = $db->getQuery(true) + ->update($db->quoteName($tn)) + ->set($db->quoteName('checked_out') . ' = DEFAULT'); + + if ($fields['checked_out_time']->Null === 'YES') { + $query->set($db->quoteName('checked_out_time') . ' = NULL'); + } else { + $nullDate = $db->getNullDate(); + + $query->set($db->quoteName('checked_out_time') . ' = :checkouttime') + ->bind(':checkouttime', $nullDate); + } + + if ($fields['checked_out']->Null === 'YES') { + $query->where($db->quoteName('checked_out') . ' IS NOT NULL'); + } else { + $query->where($db->quoteName('checked_out') . ' > 0'); + } + + $db->setQuery($query); + + if ($db->execute()) { + $results = $results + $db->getAffectedRows(); + $app->triggerEvent('onAfterCheckin', array($tn)); + } + } + + return $results; + } + + /** + * Get total of tables + * + * @return integer Total to check-in tables + * + * @since 1.6 + */ + public function getTotal() + { + if (!isset($this->total)) { + $this->getItems(); + } + + return $this->total; + } + + /** + * Get tables + * + * @return array Checked in table names as keys and checked in item count as values. + * + * @since 1.6 + */ + public function getItems() + { + if (!isset($this->items)) { + $db = $this->getDatabase(); + $tables = $db->getTableList(); + $prefix = Factory::getApplication()->get('dbprefix'); + + // This array will hold table name as key and checked in item count as value. + $results = array(); + + foreach ($tables as $tn) { + // Make sure we get the right tables based on prefix. + if (stripos($tn, $prefix) !== 0) { + continue; + } + + if ($this->getState('filter.search') && stripos($tn, $this->getState('filter.search')) === false) { + continue; + } + + $fields = $db->getTableColumns($tn, false); + + if (!(isset($fields['checked_out']) && isset($fields['checked_out_time']))) { + continue; + } + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName($tn)); + + if ($fields['checked_out']->Null === 'YES') { + $query->where($db->quoteName('checked_out') . ' IS NOT NULL'); + } else { + $query->where($db->quoteName('checked_out') . ' > 0'); + } + + $db->setQuery($query); + $count = $db->loadResult(); + + if ($count) { + $results[$tn] = $count; + } + } + + $this->total = count($results); + + // Order items by table + if ($this->getState('list.ordering') == 'table') { + if (strtolower($this->getState('list.direction')) == 'asc') { + ksort($results); + } else { + krsort($results); + } + } else { + // Order items by number of items + if (strtolower($this->getState('list.direction')) == 'asc') { + asort($results); + } else { + arsort($results); + } + } + + // Pagination + $limit = (int) $this->getState('list.limit'); + + if ($limit !== 0) { + $this->items = array_slice($results, $this->getState('list.start'), $limit); + } else { + $this->items = $results; + } + } + + return $this->items; + } } diff --git a/code/administrator/components/com_checkin/src/View/Checkin/HtmlView.php b/code/administrator/components/com_checkin/src/View/Checkin/HtmlView.php index 7bb8218f..c0e05b0e 100644 --- a/code/administrator/components/com_checkin/src/View/Checkin/HtmlView.php +++ b/code/administrator/components/com_checkin/src/View/Checkin/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items)) - { - $this->isEmptyState = true; - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_CHECKIN_GLOBAL_CHECK_IN'), 'check-square'); - - if (!$this->isEmptyState) - { - ToolbarHelper::custom('checkin', 'checkin', '', 'JTOOLBAR_CHECKIN', true); - } - - if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_checkin')) - { - ToolbarHelper::divider(); - ToolbarHelper::preferences('com_checkin'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Maintenance:_Global_Check-in'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items)) { + $this->isEmptyState = true; + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_CHECKIN_GLOBAL_CHECK_IN'), 'check-square'); + + if (!$this->isEmptyState) { + ToolbarHelper::custom('checkin', 'checkin', '', 'JTOOLBAR_CHECKIN', true); + } + + if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_checkin')) { + ToolbarHelper::divider(); + ToolbarHelper::preferences('com_checkin'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Maintenance:_Global_Check-in'); + } } diff --git a/code/administrator/components/com_checkin/tmpl/checkin/default.php b/code/administrator/components/com_checkin/tmpl/checkin/default.php index 930cec72..1718632f 100644 --- a/code/administrator/components/com_checkin/tmpl/checkin/default.php +++ b/code/administrator/components/com_checkin/tmpl/checkin/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
-
-
-
- $this)); ?> - total > 0) : ?> - - - - - - - - - - - - items as $table => $count) : ?> - - - - - - - - -
- , - , - -
- - - - - -
- - - - - -
+
+
+
+ $this)); ?> + total > 0) : ?> + + + + + + + + + + + + items as $table => $count) : ?> + + + + + + + + +
+ , + , + +
+ + + + + +
+ + + + + +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + +
+
+
diff --git a/code/administrator/components/com_checkin/tmpl/checkin/emptystate.php b/code/administrator/components/com_checkin/tmpl/checkin/emptystate.php index 9a858348..640e87e6 100644 --- a/code/administrator/components/com_checkin/tmpl/checkin/emptystate.php +++ b/code/administrator/components/com_checkin/tmpl/checkin/emptystate.php @@ -1,4 +1,5 @@ 'COM_CHECKIN', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Maintenance:_Global_Check-in', - 'icon' => 'icon-check-square', - 'title' => Text::_('COM_CHECKIN_GLOBAL_CHECK_IN'), + 'textPrefix' => 'COM_CHECKIN', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Maintenance:_Global_Check-in', + 'icon' => 'icon-check-square', + 'title' => Text::_('COM_CHECKIN_GLOBAL_CHECK_IN'), ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_config/config.xml b/code/administrator/components/com_config/config.xml index 409d74f4..a0145284 100644 --- a/code/administrator/components/com_config/config.xml +++ b/code/administrator/components/com_config/config.xml @@ -2,7 +2,7 @@ com_config Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_config/forms/application.xml b/code/administrator/components/com_config/forms/application.xml index 2f8c06b3..63d4d459 100644 --- a/code/administrator/components/com_config/forms/application.xml +++ b/code/administrator/components/com_config/forms/application.xml @@ -955,6 +955,20 @@
+ + + + +
registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Config')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Config')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Config')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Config')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Config')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Config')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new ConfigComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new ConfigComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_config/src/Controller/ApplicationController.php b/code/administrator/components/com_config/src/Controller/ApplicationController.php index 9fc6687c..ab8f223e 100644 --- a/code/administrator/components/com_config/src/Controller/ApplicationController.php +++ b/code/administrator/components/com_config/src/Controller/ApplicationController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Cancel operation. - * - * @return void - * - * @since 3.0.0 - */ - public function cancel() - { - $this->setRedirect(Route::_('index.php?option=com_cpanel')); - } - - /** - * Saves the form - * - * @return void|boolean Void on success. Boolean false on fail. - * - * @since 4.0.0 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - - return false; - } - - $this->app->setUserState('com_config.config.global.data', null); - - /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ - $model = $this->getModel('Application', 'Administrator'); - - $data = $this->input->post->get('jform', array(), 'array'); - - // Complete data array if needed - $oldData = $model->getData(); - $data = array_replace($oldData, $data); - - // Get request type - $saveFormat = $this->app->getDocument()->getType(); - - // Handle service requests - if ($saveFormat == 'json') - { - $form = $model->getForm(); - $return = $model->validate($form, $data); - - if ($return === false) - { - $this->app->setHeader('Status', 422, true); - - return false; - } - - return $model->save($return); - } - - // Must load after serving service-requests - $form = $model->getForm(); - - // Validate the posted data. - $return = $model->validate($form, $data); - - // Check for validation errors. - if ($return === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the posted data in the session. - $this->app->setUserState('com_config.config.global.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_config', false)); - - return false; - } - - // Validate database connection data. - $data = $return; - $return = $model->validateDbConnection($data); - - // Check for validation errors. - if ($return === false) - { - /* - * The validateDbConnection method enqueued all messages for us. - */ - - // Save the posted data in the session. - $this->app->setUserState('com_config.config.global.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_config', false)); - - return false; - } - - // Save the validated data in the session. - $this->app->setUserState('com_config.config.global.data', $return); - - // Attempt to save the configuration. - $data = $return; - $return = $model->save($data); - - // Check the return value. - if ($return === false) - { - /* - * The save method enqueued all messages for us, so we just need to redirect back. - */ - - // Save failed, go back to the screen and display a notice. - $this->setRedirect(Route::_('index.php?option=com_config', false)); - - return false; - } - - // Set the success message. - $this->app->enqueueMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); - - // Set the redirect based on the task. - switch ($this->input->getCmd('task')) - { - case 'apply': - $this->setRedirect(Route::_('index.php?option=com_config', false)); - break; - - case 'save': - default: - $this->setRedirect(Route::_('index.php', false)); - break; - } - } - - /** - * Method to remove root in global configuration. - * - * @return boolean - * - * @since 3.2 - */ - public function removeroot() - { - // Check for request forgeries. - if (!Session::checkToken('get')) - { - $this->setRedirect('index.php', Text::_('JINVALID_TOKEN'), 'error'); - - return false; - } - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - - return false; - } - - // Initialise model. - - /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ - $model = $this->getModel('Application', 'Administrator'); - - // Attempt to save the configuration and remove root. - try - { - $model->removeroot(); - } - catch (\RuntimeException $e) - { - // Save failed, go back to the screen and display a notice. - $this->setRedirect('index.php', Text::_('JERROR_SAVE_FAILED', $e->getMessage()), 'error'); - - return false; - } - - // Set the redirect based on the task. - $this->setRedirect(Route::_('index.php'), Text::_('COM_CONFIG_SAVE_SUCCESS')); - - return true; - } - - /** - * Method to send the test mail. - * - * @return void - * - * @since 3.5 - */ - public function sendtestmail() - { - // Send json mime type. - $this->app->mimeType = 'application/json'; - $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); - $this->app->sendHeaders(); - - // Check if user token is valid. - if (!Session::checkToken()) - { - $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); - echo new JsonResponse; - $this->app->close(); - } - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - echo new JsonResponse; - $this->app->close(); - } - - /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ - $model = $this->getModel('Application', 'Administrator'); - - echo new JsonResponse($model->sendTestMail()); - - $this->app->close(); - } - - /** - * Method to GET permission value and give it to the model for storing in the database. - * - * @return void - * - * @since 3.5 - */ - public function store() - { - // Send json mime type. - $this->app->mimeType = 'application/json'; - $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); - $this->app->sendHeaders(); - - // Check if user token is valid. - if (!Session::checkToken('get')) - { - $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); - echo new JsonResponse; - $this->app->close(); - } - - /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ - $model = $this->getModel('Application', 'Administrator'); - echo new JsonResponse($model->storePermissions()); - $this->app->close(); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Map the apply task to the save method. + $this->registerTask('apply', 'save'); + } + + /** + * Cancel operation. + * + * @return void + * + * @since 3.0.0 + */ + public function cancel() + { + $this->setRedirect(Route::_('index.php?option=com_cpanel')); + } + + /** + * Saves the form + * + * @return void|boolean Void on success. Boolean false on fail. + * + * @since 4.0.0 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + + return false; + } + + $this->app->setUserState('com_config.config.global.data', null); + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + + $data = $this->input->post->get('jform', array(), 'array'); + + // Complete data array if needed + $oldData = $model->getData(); + $data = array_replace($oldData, $data); + + // Get request type + $saveFormat = $this->app->getDocument()->getType(); + + // Handle service requests + if ($saveFormat == 'json') { + $form = $model->getForm(); + $return = $model->validate($form, $data); + + if ($return === false) { + $this->app->setHeader('Status', 422, true); + + return false; + } + + return $model->save($return); + } + + // Must load after serving service-requests + $form = $model->getForm(); + + // Validate the posted data. + $return = $model->validate($form, $data); + + // Check for validation errors. + if ($return === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the posted data in the session. + $this->app->setUserState('com_config.config.global.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_config', false)); + + return false; + } + + // Validate database connection data. + $data = $return; + $return = $model->validateDbConnection($data); + + // Check for validation errors. + if ($return === false) { + /* + * The validateDbConnection method enqueued all messages for us. + */ + + // Save the posted data in the session. + $this->app->setUserState('com_config.config.global.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_config', false)); + + return false; + } + + // Save the validated data in the session. + $this->app->setUserState('com_config.config.global.data', $return); + + // Attempt to save the configuration. + $data = $return; + $return = $model->save($data); + + // Check the return value. + if ($return === false) { + /* + * The save method enqueued all messages for us, so we just need to redirect back. + */ + + // Save failed, go back to the screen and display a notice. + $this->setRedirect(Route::_('index.php?option=com_config', false)); + + return false; + } + + // Set the success message. + $this->app->enqueueMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); + + // Set the redirect based on the task. + switch ($this->input->getCmd('task')) { + case 'apply': + $this->setRedirect(Route::_('index.php?option=com_config', false)); + break; + + case 'save': + default: + $this->setRedirect(Route::_('index.php', false)); + break; + } + } + + /** + * Method to remove root in global configuration. + * + * @return boolean + * + * @since 3.2 + */ + public function removeroot() + { + // Check for request forgeries. + if (!Session::checkToken('get')) { + $this->setRedirect('index.php', Text::_('JINVALID_TOKEN'), 'error'); + + return false; + } + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + + return false; + } + + // Initialise model. + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + + // Attempt to save the configuration and remove root. + try { + $model->removeroot(); + } catch (\RuntimeException $e) { + // Save failed, go back to the screen and display a notice. + $this->setRedirect('index.php', Text::_('JERROR_SAVE_FAILED', $e->getMessage()), 'error'); + + return false; + } + + // Set the redirect based on the task. + $this->setRedirect(Route::_('index.php'), Text::_('COM_CONFIG_SAVE_SUCCESS')); + + return true; + } + + /** + * Method to send the test mail. + * + * @return void + * + * @since 3.5 + */ + public function sendtestmail() + { + // Send json mime type. + $this->app->mimeType = 'application/json'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + // Check if user token is valid. + if (!Session::checkToken()) { + $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); + echo new JsonResponse(); + $this->app->close(); + } + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + echo new JsonResponse(); + $this->app->close(); + } + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + + echo new JsonResponse($model->sendTestMail()); + + $this->app->close(); + } + + /** + * Method to GET permission value and give it to the model for storing in the database. + * + * @return void + * + * @since 3.5 + */ + public function store() + { + // Send json mime type. + $this->app->mimeType = 'application/json'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + // Check if user token is valid. + if (!Session::checkToken('get')) { + $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); + echo new JsonResponse(); + $this->app->close(); + } + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + echo new JsonResponse($model->storePermissions()); + $this->app->close(); + } } diff --git a/code/administrator/components/com_config/src/Controller/ComponentController.php b/code/administrator/components/com_config/src/Controller/ComponentController.php index a7718e36..dc9e8dae 100644 --- a/code/administrator/components/com_config/src/Controller/ComponentController.php +++ b/code/administrator/components/com_config/src/Controller/ComponentController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Method to save component configuration. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean - * - * @since 3.2 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - $data = $this->input->get('jform', [], 'ARRAY'); - $id = $this->input->get('id', null, 'INT'); - $option = $this->input->get('component'); - $user = $this->app->getIdentity(); - $context = "$this->option.edit.$this->context.$option"; - - /** @var \Joomla\Component\Config\Administrator\Model\ComponentModel $model */ - $model = $this->getModel('Component', 'Administrator'); - $model->setState('component.option', $option); - $form = $model->getForm(); - - // Make sure com_joomlaupdate and com_privacy can only be accessed by SuperUser - if (\in_array(strtolower($option), ['com_joomlaupdate', 'com_privacy'], true) && !$user->authorise('core.admin')) - { - $this->setRedirect(Route::_('index.php', false), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - } - - // Check if the user is authorised to do this. - if (!$user->authorise('core.admin', $option) && !$user->authorise('core.options', $option)) - { - $this->setRedirect(Route::_('index.php', false), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - } - - // Remove the permissions rules data if user isn't allowed to edit them. - if (!$user->authorise('core.admin', $option) && isset($data['params']) && isset($data['params']['rules'])) - { - unset($data['params']['rules']); - } - - $returnUri = $this->input->post->get('return', null, 'base64'); - - $redirect = ''; - - if (!empty($returnUri)) - { - $redirect = '&return=' . urlencode($returnUri); - } - - // Validate the posted data. - $return = $model->validate($form, $data); - - // Check for validation errors. - if ($return === false) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), - $model->getError(), - 'error' - ); - - return false; - } - - // Attempt to save the configuration. - $data = [ - 'params' => $return, - 'id' => $id, - 'option' => $option, - ]; - - try - { - $model->save($data); - } - catch (\RuntimeException $e) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - // Save failed, go back to the screen and display a notice. - $this->setRedirect( - Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), - Text::_('JERROR_SAVE_FAILED', $e->getMessage()), - 'error' - ); - - return false; - } - - // Clear session data. - $this->app->setUserState($context . '.data', null); - - // Set the redirect based on the task. - switch ($this->input->get('task')) - { - case 'apply': - $this->setRedirect( - Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), - Text::_('COM_CONFIG_SAVE_SUCCESS'), - 'message' - ); - - break; - - case 'save': - $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); - default: - $redirect = 'index.php?option=' . $option; - - if (!empty($returnUri)) - { - $redirect = base64_decode($returnUri); - } - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = Uri::base(); - } - - $this->setRedirect(Route::_($redirect, false)); - } - - return true; - } - - /** - * Method to cancel global configuration component. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean - * - * @since 3.2 - */ - public function cancel($key = null) - { - $component = $this->input->get('component'); - - // Clear session data. - $this->app->setUserState("$this->option.edit.$this->context.$component.data", null); - - // Calculate redirect URL - $returnUri = $this->input->post->get('return', null, 'base64'); - - $redirect = 'index.php?option=' . $component; - - if (!empty($returnUri)) - { - $redirect = base64_decode($returnUri); - } - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = Uri::base(); - } - - $this->setRedirect(Route::_($redirect, false)); - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Map the apply task to the save method. + $this->registerTask('apply', 'save'); + } + + /** + * Method to save component configuration. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean + * + * @since 3.2 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + $data = $this->input->get('jform', [], 'ARRAY'); + $id = $this->input->get('id', null, 'INT'); + $option = $this->input->get('component'); + $user = $this->app->getIdentity(); + $context = "$this->option.edit.$this->context.$option"; + + /** @var \Joomla\Component\Config\Administrator\Model\ComponentModel $model */ + $model = $this->getModel('Component', 'Administrator'); + $model->setState('component.option', $option); + $form = $model->getForm(); + + // Make sure com_joomlaupdate and com_privacy can only be accessed by SuperUser + if (\in_array(strtolower($option), ['com_joomlaupdate', 'com_privacy'], true) && !$user->authorise('core.admin')) { + $this->setRedirect(Route::_('index.php', false), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + } + + // Check if the user is authorised to do this. + if (!$user->authorise('core.admin', $option) && !$user->authorise('core.options', $option)) { + $this->setRedirect(Route::_('index.php', false), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + } + + // Remove the permissions rules data if user isn't allowed to edit them. + if (!$user->authorise('core.admin', $option) && isset($data['params']) && isset($data['params']['rules'])) { + unset($data['params']['rules']); + } + + $returnUri = $this->input->post->get('return', null, 'base64'); + + $redirect = ''; + + if (!empty($returnUri)) { + $redirect = '&return=' . urlencode($returnUri); + } + + // Validate the posted data. + $return = $model->validate($form, $data); + + // Check for validation errors. + if ($return === false) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), + $model->getError(), + 'error' + ); + + return false; + } + + // Attempt to save the configuration. + $data = [ + 'params' => $return, + 'id' => $id, + 'option' => $option, + ]; + + try { + $model->save($data); + } catch (\RuntimeException $e) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + // Save failed, go back to the screen and display a notice. + $this->setRedirect( + Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), + Text::_('JERROR_SAVE_FAILED', $e->getMessage()), + 'error' + ); + + return false; + } + + // Clear session data. + $this->app->setUserState($context . '.data', null); + + // Set the redirect based on the task. + switch ($this->input->get('task')) { + case 'apply': + $this->setRedirect( + Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), + Text::_('COM_CONFIG_SAVE_SUCCESS'), + 'message' + ); + + break; + + case 'save': + $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); + + // No break + + default: + $redirect = 'index.php?option=' . $option; + + if (!empty($returnUri)) { + $redirect = base64_decode($returnUri); + } + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = Uri::base(); + } + + $this->setRedirect(Route::_($redirect, false)); + } + + return true; + } + + /** + * Method to cancel global configuration component. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean + * + * @since 3.2 + */ + public function cancel($key = null) + { + $component = $this->input->get('component'); + + // Clear session data. + $this->app->setUserState("$this->option.edit.$this->context.$component.data", null); + + // Calculate redirect URL + $returnUri = $this->input->post->get('return', null, 'base64'); + + $redirect = 'index.php?option=' . $component; + + if (!empty($returnUri)) { + $redirect = base64_decode($returnUri); + } + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = Uri::base(); + } + + $this->setRedirect(Route::_($redirect, false)); + + return true; + } } diff --git a/code/administrator/components/com_config/src/Controller/DisplayController.php b/code/administrator/components/com_config/src/Controller/DisplayController.php index 2ed6a6f0..6731ba3b 100644 --- a/code/administrator/components/com_config/src/Controller/DisplayController.php +++ b/code/administrator/components/com_config/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('component', ''); - - // Make sure com_joomlaupdate and com_privacy can only be accessed by SuperUser - if (in_array(strtolower($component), array('com_joomlaupdate', 'com_privacy')) - && !$this->app->getIdentity()->authorise('core.admin')) - { - $this->setRedirect(Route::_('index.php'), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - } - - return parent::display($cachable, $urlparams); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'application'; + + + /** + * Typical view method for MVC based architecture + * + * This function is provide as a default implementation, in most cases + * you will need to override it in your own controllers. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see {@link InputFilter::clean()}. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 3.0 + * @throws \Exception + */ + public function display($cachable = false, $urlparams = array()) + { + $component = $this->input->get('component', ''); + + // Make sure com_joomlaupdate and com_privacy can only be accessed by SuperUser + if ( + in_array(strtolower($component), array('com_joomlaupdate', 'com_privacy')) + && !$this->app->getIdentity()->authorise('core.admin') + ) { + $this->setRedirect(Route::_('index.php'), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + } + + return parent::display($cachable, $urlparams); + } } diff --git a/code/administrator/components/com_config/src/Controller/RequestController.php b/code/administrator/components/com_config/src/Controller/RequestController.php index f3121774..6322bda6 100644 --- a/code/administrator/components/com_config/src/Controller/RequestController.php +++ b/code/administrator/components/com_config/src/Controller/RequestController.php @@ -1,4 +1,5 @@ input->getWord('option', 'com_config'); - - if ($this->app->isClient('administrator')) - { - $viewName = $this->input->getWord('view', 'application'); - } - else - { - $viewName = $this->input->getWord('view', 'config'); - } - - // Register the layout paths for the view - $paths = new \SplPriorityQueue; - - if ($this->app->isClient('administrator')) - { - $paths->insert(JPATH_ADMINISTRATOR . '/components/' . $componentFolder . '/view/' . $viewName . '/tmpl', 1); - } - else - { - $paths->insert(JPATH_BASE . '/components/' . $componentFolder . '/view/' . $viewName . '/tmpl', 1); - } - - $model = new \Joomla\Component\Config\Administrator\Model\ApplicationModel; - $component = $model->getState()->get('component.option'); - - // Access check. - if (!$this->app->getIdentity()->authorise('core.admin', $component) - && !$this->app->getIdentity()->authorise('core.options', $component)) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - - return false; - } - - try - { - $data = $model->getData(); - $user = $this->app->getIdentity(); - } - catch (\Exception $e) - { - $this->app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - $this->userIsSuperAdmin = $user->authorise('core.admin'); - - // Required data - $requiredData = array( - 'sitename' => null, - 'offline' => null, - 'access' => null, - 'list_limit' => null, - 'MetaDesc' => null, - 'MetaRights' => null, - 'sef' => null, - 'sitename_pagetitles' => null, - 'debug' => null, - 'debug_lang' => null, - 'error_reporting' => null, - 'mailfrom' => null, - 'fromname' => null - ); - - $data = array_intersect_key($data, $requiredData); - - return json_encode($data); - } + /** + * Execute the controller. + * + * @return mixed A rendered view or false + * + * @since 3.2 + */ + public function getJson() + { + $componentFolder = $this->input->getWord('option', 'com_config'); + + if ($this->app->isClient('administrator')) { + $viewName = $this->input->getWord('view', 'application'); + } else { + $viewName = $this->input->getWord('view', 'config'); + } + + // Register the layout paths for the view + $paths = new \SplPriorityQueue(); + + if ($this->app->isClient('administrator')) { + $paths->insert(JPATH_ADMINISTRATOR . '/components/' . $componentFolder . '/view/' . $viewName . '/tmpl', 1); + } else { + $paths->insert(JPATH_BASE . '/components/' . $componentFolder . '/view/' . $viewName . '/tmpl', 1); + } + + $model = new \Joomla\Component\Config\Administrator\Model\ApplicationModel(); + $component = $model->getState()->get('component.option'); + + // Access check. + if ( + !$this->app->getIdentity()->authorise('core.admin', $component) + && !$this->app->getIdentity()->authorise('core.options', $component) + ) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + + return false; + } + + try { + $data = $model->getData(); + $user = $this->app->getIdentity(); + } catch (\Exception $e) { + $this->app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + $this->userIsSuperAdmin = $user->authorise('core.admin'); + + // Required data + $requiredData = array( + 'sitename' => null, + 'offline' => null, + 'access' => null, + 'list_limit' => null, + 'MetaDesc' => null, + 'MetaRights' => null, + 'sef' => null, + 'sitename_pagetitles' => null, + 'debug' => null, + 'debug_lang' => null, + 'error_reporting' => null, + 'mailfrom' => null, + 'fromname' => null + ); + + $data = array_intersect_key($data, $requiredData); + + return json_encode($data); + } } diff --git a/code/administrator/components/com_config/src/Extension/ConfigComponent.php b/code/administrator/components/com_config/src/Extension/ConfigComponent.php index f275a9d6..9e54525f 100644 --- a/code/administrator/components/com_config/src/Extension/ConfigComponent.php +++ b/code/administrator/components/com_config/src/Extension/ConfigComponent.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('name AS text, element AS value') - ->from('#__extensions') - ->where('enabled >= 1') - ->where('type =' . $db->quote('component')); + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('name AS text, element AS value') + ->from('#__extensions') + ->where('enabled >= 1') + ->where('type =' . $db->quote('component')); - $items = $db->setQuery($query)->loadObjectList(); + $items = $db->setQuery($query)->loadObjectList(); - if ($items) - { - $lang = Factory::getLanguage(); + if ($items) { + $lang = Factory::getLanguage(); - foreach ($items as &$item) - { - // Load language - $extension = $item->value; + foreach ($items as &$item) { + // Load language + $extension = $item->value; - if (File::exists(JPATH_ADMINISTRATOR . '/components/' . $extension . '/config.xml')) - { - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) - || $lang->load("$extension.sys", $source); + if (File::exists(JPATH_ADMINISTRATOR . '/components/' . $extension . '/config.xml')) { + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) + || $lang->load("$extension.sys", $source); - // Translate component name - $item->text = Text::_($item->text); - } - else - { - $item = null; - } - } + // Translate component name + $item->text = Text::_($item->text); + } else { + $item = null; + } + } - // Sort by component name - $items = ArrayHelper::sortObjects(array_filter($items), 'text', 1, true, true); - } + // Sort by component name + $items = ArrayHelper::sortObjects(array_filter($items), 'text', 1, true, true); + } - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $items); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $items); - return $options; - } + return $options; + } } diff --git a/code/administrator/components/com_config/src/Field/FiltersField.php b/code/administrator/components/com_config/src/Field/FiltersField.php index bd93d5cd..e0a5e30d 100644 --- a/code/administrator/components/com_config/src/Field/FiltersField.php +++ b/code/administrator/components/com_config/src/Field/FiltersField.php @@ -1,4 +1,5 @@ getWebAssetManager()->useScript('com_config.filters'); - - // Get the available user groups. - $groups = $this->getUserGroups(); - - // Build the form control. - $html = array(); - - // Open the table. - $html[] = ''; - - // The table heading. - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - - // The table body. - $html[] = ' '; - - foreach ($groups as $group) - { - if (!isset($this->value[$group->value])) - { - $this->value[$group->value] = array('filter_type' => 'BL', 'filter_tags' => '', 'filter_attributes' => ''); - } - - $group_filter = $this->value[$group->value]; - - $group_filter['filter_tags'] = !empty($group_filter['filter_tags']) ? $group_filter['filter_tags'] : ''; - $group_filter['filter_attributes'] = !empty($group_filter['filter_attributes']) ? $group_filter['filter_attributes'] : ''; - - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - } - - $html[] = ' '; - - // Close the table. - $html[] = '
'; - $html[] = ' ' . Text::_('JGLOBAL_FILTER_GROUPS_LABEL') . ''; - $html[] = ' '; - $html[] = ' ' . Text::_('JGLOBAL_FILTER_TYPE_LABEL') . ''; - $html[] = ' '; - $html[] = ' ' . Text::_('JGLOBAL_FILTER_TAGS_LABEL') . ''; - $html[] = ' '; - $html[] = ' ' . Text::_('JGLOBAL_FILTER_ATTRIBUTES_LABEL') . ''; - $html[] = '
'; - $html[] = ' ' . LayoutHelper::render('joomla.html.treeprefix', array('level' => $group->level + 1)) . $group->text; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = '
'; - - return implode("\n", $html); - } - - /** - * A helper to get the list of user groups. - * - * @return array - * - * @since 1.6 - */ - protected function getUserGroups() - { - // Get a database object. - $db = Factory::getDbo(); - - // Get the user groups from the database. - $query = $db->getQuery(true); - $query->select('a.id AS value, a.title AS text, COUNT(DISTINCT b.id) AS level, a.parent_id as parent'); - $query->from('#__usergroups AS a'); - $query->join('LEFT', '#__usergroups AS b on a.lft > b.lft AND a.rgt < b.rgt'); - $query->group('a.id, a.title, a.lft'); - $query->order('a.lft ASC'); - $db->setQuery($query); - $options = $db->loadObjectList(); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + public $type = 'Filters'; + + /** + * Method to get the field input markup. + * + * @todo: Add access check. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + // Add translation string for notification + Text::script('COM_CONFIG_TEXT_FILTERS_NOTE'); + + // Add Javascript + Factory::getDocument()->getWebAssetManager()->useScript('com_config.filters'); + + // Get the available user groups. + $groups = $this->getUserGroups(); + + // Build the form control. + $html = array(); + + // Open the table. + $html[] = ''; + + // The table heading. + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + + // The table body. + $html[] = ' '; + + foreach ($groups as $group) { + if (!isset($this->value[$group->value])) { + $this->value[$group->value] = array('filter_type' => 'BL', 'filter_tags' => '', 'filter_attributes' => ''); + } + + $group_filter = $this->value[$group->value]; + + $group_filter['filter_tags'] = !empty($group_filter['filter_tags']) ? $group_filter['filter_tags'] : ''; + $group_filter['filter_attributes'] = !empty($group_filter['filter_attributes']) ? $group_filter['filter_attributes'] : ''; + + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + } + + $html[] = ' '; + + // Close the table. + $html[] = '
'; + $html[] = ' ' . Text::_('JGLOBAL_FILTER_GROUPS_LABEL') . ''; + $html[] = ' '; + $html[] = ' ' . Text::_('JGLOBAL_FILTER_TYPE_LABEL') . ''; + $html[] = ' '; + $html[] = ' ' . Text::_('JGLOBAL_FILTER_TAGS_LABEL') . ''; + $html[] = ' '; + $html[] = ' ' . Text::_('JGLOBAL_FILTER_ATTRIBUTES_LABEL') . ''; + $html[] = '
'; + $html[] = ' ' . LayoutHelper::render('joomla.html.treeprefix', array('level' => $group->level + 1)) . $group->text; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = '
'; + + return implode("\n", $html); + } + + /** + * A helper to get the list of user groups. + * + * @return array + * + * @since 1.6 + */ + protected function getUserGroups() + { + // Get a database object. + $db = $this->getDatabase(); + + // Get the user groups from the database. + $query = $db->getQuery(true); + $query->select('a.id AS value, a.title AS text, COUNT(DISTINCT b.id) AS level, a.parent_id as parent'); + $query->from('#__usergroups AS a'); + $query->join('LEFT', '#__usergroups AS b on a.lft > b.lft AND a.rgt < b.rgt'); + $query->group('a.id, a.title, a.lft'); + $query->order('a.lft ASC'); + $db->setQuery($query); + $options = $db->loadObjectList(); + + return $options; + } } diff --git a/code/administrator/components/com_config/src/Helper/ConfigHelper.php b/code/administrator/components/com_config/src/Helper/ConfigHelper.php index ee5ed8b3..1e573792 100644 --- a/code/administrator/components/com_config/src/Helper/ConfigHelper.php +++ b/code/administrator/components/com_config/src/Helper/ConfigHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('element') - ->from('#__extensions') - ->where('type = ' . $db->quote('component')) - ->where('enabled = 1'); - $db->setQuery($query); - $result = $db->loadColumn(); - - return $result; - } - - /** - * Returns true if the component has configuration options. - * - * @param string $component Component name - * - * @return boolean - * - * @since 3.0 - */ - public static function hasComponentConfig($component) - { - return is_file(JPATH_ADMINISTRATOR . '/components/' . $component . '/config.xml'); - } - - /** - * Returns an array of all components with configuration options. - * Optionally return only those components for which the current user has 'core.manage' rights. - * - * @param boolean $authCheck True to restrict to components where current user has 'core.manage' rights. - * - * @return array - * - * @since 3.0 - */ - public static function getComponentsWithConfig($authCheck = true) - { - $result = array(); - $components = self::getAllComponents(); - $user = Factory::getUser(); - - // Remove com_config from the array as that may have weird side effects - $components = array_diff($components, array('com_config')); - - foreach ($components as $component) - { - if (self::hasComponentConfig($component) && (!$authCheck || $user->authorise('core.manage', $component))) - { - self::loadLanguageForComponent($component); - $result[$component] = ApplicationHelper::stringURLSafe(Text::_($component)) . '_' . $component; - } - } - - asort($result); - - return array_keys($result); - } - - /** - * Load the sys language for the given component. - * - * @param array $components Array of component names. - * - * @return void - * - * @since 3.0 - */ - public static function loadLanguageForComponents($components) - { - foreach ($components as $component) - { - self::loadLanguageForComponent($component); - } - } - - /** - * Load the sys language for the given component. - * - * @param string $component component name. - * - * @return void - * - * @since 3.5 - */ - public static function loadLanguageForComponent($component) - { - if (empty($component)) - { - return; - } - - $lang = Factory::getLanguage(); - - // Load the core file then - // Load extension-local file. - $lang->load($component . '.sys', JPATH_BASE) - || $lang->load($component . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component); - } + /** + * Get an array of all enabled components. + * + * @return array + * + * @since 3.0 + */ + public static function getAllComponents() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('element') + ->from('#__extensions') + ->where('type = ' . $db->quote('component')) + ->where('enabled = 1'); + $db->setQuery($query); + $result = $db->loadColumn(); + + return $result; + } + + /** + * Returns true if the component has configuration options. + * + * @param string $component Component name + * + * @return boolean + * + * @since 3.0 + */ + public static function hasComponentConfig($component) + { + return is_file(JPATH_ADMINISTRATOR . '/components/' . $component . '/config.xml'); + } + + /** + * Returns an array of all components with configuration options. + * Optionally return only those components for which the current user has 'core.manage' rights. + * + * @param boolean $authCheck True to restrict to components where current user has 'core.manage' rights. + * + * @return array + * + * @since 3.0 + */ + public static function getComponentsWithConfig($authCheck = true) + { + $result = array(); + $components = self::getAllComponents(); + $user = Factory::getUser(); + + // Remove com_config from the array as that may have weird side effects + $components = array_diff($components, array('com_config')); + + foreach ($components as $component) { + if (self::hasComponentConfig($component) && (!$authCheck || $user->authorise('core.manage', $component))) { + self::loadLanguageForComponent($component); + $result[$component] = ApplicationHelper::stringURLSafe(Text::_($component)) . '_' . $component; + } + } + + asort($result); + + return array_keys($result); + } + + /** + * Load the sys language for the given component. + * + * @param array $components Array of component names. + * + * @return void + * + * @since 3.0 + */ + public static function loadLanguageForComponents($components) + { + foreach ($components as $component) { + self::loadLanguageForComponent($component); + } + } + + /** + * Load the sys language for the given component. + * + * @param string $component component name. + * + * @return void + * + * @since 3.5 + */ + public static function loadLanguageForComponent($component) + { + if (empty($component)) { + return; + } + + $lang = Factory::getLanguage(); + + // Load the core file then + // Load extension-local file. + $lang->load($component . '.sys', JPATH_BASE) + || $lang->load($component . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component); + } } diff --git a/code/administrator/components/com_config/src/Model/ApplicationModel.php b/code/administrator/components/com_config/src/Model/ApplicationModel.php index c907e1bb..e2182240 100644 --- a/code/administrator/components/com_config/src/Model/ApplicationModel.php +++ b/code/administrator/components/com_config/src/Model/ApplicationModel.php @@ -1,4 +1,5 @@ loadForm('com_config.application', 'application', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the configuration data. - * - * This method will load the global configuration data straight from - * JConfig. If configuration data has been saved in the session, that - * data will be merged into the original data, overwriting it. - * - * @return array An array containing all global config data. - * - * @since 1.6 - */ - public function getData() - { - // Get the config data. - $config = new \JConfig; - $data = ArrayHelper::fromObject($config); - - // Get the correct driver at runtime - $data['dbtype'] = Factory::getDbo()->getName(); - - // Prime the asset_id for the rules. - $data['asset_id'] = 1; - - // Get the text filter data - $params = ComponentHelper::getParams('com_config'); - $data['filters'] = ArrayHelper::fromObject($params->get('filters')); - - // If no filter data found, get from com_content (update of 1.6/1.7 site) - if (empty($data['filters'])) - { - $contentParams = ComponentHelper::getParams('com_content'); - $data['filters'] = ArrayHelper::fromObject($contentParams->get('filters')); - } - - // Check for data in the session. - $temp = Factory::getApplication()->getUserState('com_config.config.global.data'); - - // Merge in the session data. - if (!empty($temp)) - { - // $temp can sometimes be an object, and we need it to be an array - if (is_object($temp)) - { - $temp = ArrayHelper::fromObject($temp); - } - - $data = array_merge($temp, $data); - } - - // Correct error_reporting value, since we removed "development", the "maximum" should be set instead - // @TODO: This can be removed in 5.0 - if (!empty($data['error_reporting']) && $data['error_reporting'] === 'development') - { - $data['error_reporting'] = 'maximum'; - } - - return $data; - } - - /** - * Method to validate the db connection properties. - * - * @param array $data An array containing all global config data. - * - * @return array|boolean Array with the validated global config data or boolean false on a validation failure. - * - * @since 4.0.0 - */ - public function validateDbConnection($data) - { - // Validate database connection encryption options - if ((int) $data['dbencryption'] === 0) - { - // Reset unused options - if (!empty($data['dbsslkey'])) - { - $data['dbsslkey'] = ''; - } - - if (!empty($data['dbsslcert'])) - { - $data['dbsslcert'] = ''; - } - - if ((bool) $data['dbsslverifyservercert'] === true) - { - $data['dbsslverifyservercert'] = false; - } - - if (!empty($data['dbsslca'])) - { - $data['dbsslca'] = ''; - } - - if (!empty($data['dbsslcipher'])) - { - $data['dbsslcipher'] = ''; - } - } - else - { - // Check localhost - if (strtolower($data['host']) === 'localhost') - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_LOCALHOST'), 'error'); - - return false; - } - - // Check CA file and folder depending on database type if server certificate verification - if ((bool) $data['dbsslverifyservercert'] === true) - { - if (empty($data['dbsslca'])) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CA_LABEL') - ), - 'error' - ); - - return false; - } - - if (!File::exists(Path::clean($data['dbsslca']))) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CA_LABEL') - ), - 'error' - ); - - return false; - } - } - else - { - // Reset unused option - if (!empty($data['dbsslca'])) - { - $data['dbsslca'] = ''; - } - } - - // Check key and certificate if two-way encryption - if ((int) $data['dbencryption'] === 2) - { - if (empty($data['dbsslkey'])) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_KEY_LABEL') - ), - 'error' - ); - - return false; - } - - if (!File::exists(Path::clean($data['dbsslkey']))) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_KEY_LABEL') - ), - 'error' - ); - - return false; - } - - if (empty($data['dbsslcert'])) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CERT_LABEL') - ), - 'error' - ); - - return false; - } - - if (!File::exists(Path::clean($data['dbsslcert']))) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CERT_LABEL') - ), - 'error' - ); - - return false; - } - } - else - { - // Reset unused options - if (!empty($data['dbsslkey'])) - { - $data['dbsslkey'] = ''; - } - - if (!empty($data['dbsslcert'])) - { - $data['dbsslcert'] = ''; - } - } - } - - return $data; - } - - /** - * Method to save the configuration data. - * - * @param array $data An array containing all global config data. - * - * @return boolean True on success, false on failure. - * - * @since 1.6 - */ - public function save($data) - { - $app = Factory::getApplication(); - - // Try to load the values from the configuration file - foreach ($this->protectedConfigurationFields as $fieldKey) - { - if (!isset($data[$fieldKey])) - { - $data[$fieldKey] = $app->get($fieldKey, ''); - } - } - - // Check that we aren't setting wrong database configuration - $options = array( - 'driver' => $data['dbtype'], - 'host' => $data['host'], - 'user' => $data['user'], - 'password' => $data['password'], - 'database' => $data['db'], - 'prefix' => $data['dbprefix'], - ); - - if ((int) $data['dbencryption'] !== 0) - { - $options['ssl'] = [ - 'enable' => true, - 'verify_server_cert' => (bool) $data['dbsslverifyservercert'], - ]; - - foreach (['cipher', 'ca', 'key', 'cert'] as $value) - { - $confVal = trim($data['dbssl' . $value]); - - if ($confVal !== '') - { - $options['ssl'][$value] = $confVal; - } - } - } - - try - { - $revisedDbo = DatabaseDriver::getInstance($options); - $revisedDbo->getVersion(); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_DATABASE_NOT_AVAILABLE', $e->getCode(), $e->getMessage()), 'error'); - - return false; - } - - if ((int) $data['dbencryption'] !== 0 && empty($revisedDbo->getConnectionEncryption())) - { - if ($revisedDbo->isConnectionEncryptionSupported()) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_CONN_NOT_ENCRYPT'), 'error'); - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_SRV_NOT_SUPPORTS'), 'error'); - } - - return false; - } - - // Check if we can set the Force SSL option - if ((int) $data['force_ssl'] !== 0 && (int) $data['force_ssl'] !== (int) $app->get('force_ssl', '0')) - { - try - { - // Make an HTTPS request to check if the site is available in HTTPS. - $host = Uri::getInstance()->getHost(); - $options = new Registry; - $options->set('userAgent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0'); - - // Do not check for valid server certificate here, leave this to the user, moreover disable using a proxy if any is configured. - $options->set('transport.curl', - array( - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_SSL_VERIFYHOST => false, - CURLOPT_PROXY => null, - CURLOPT_PROXYUSERPWD => null, - ) - ); - $response = HttpFactory::getHttp($options)->get('https://' . $host . Uri::root(true) . '/', array('Host' => $host), 10); - - // If available in HTTPS check also the status code. - if (!in_array($response->code, array(200, 503, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 401), true)) - { - throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_SSL_NOT_AVAILABLE_HTTP_CODE')); - } - } - catch (\RuntimeException $e) - { - $data['force_ssl'] = 0; - - // Also update the user state - $app->setUserState('com_config.config.global.data.force_ssl', 0); - - // Inform the user - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_SSL_NOT_AVAILABLE', $e->getMessage()), 'warning'); - } - } - - // Save the rules - if (isset($data['rules'])) - { - $rules = new Rules($data['rules']); - - // Check that we aren't removing our Super User permission - // Need to get groups from database, since they might have changed - $myGroups = Access::getGroupsByUser(Factory::getUser()->get('id')); - $myRules = $rules->getData(); - $hasSuperAdmin = $myRules['core.admin']->allow($myGroups); - - if (!$hasSuperAdmin) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_REMOVING_SUPER_ADMIN'), 'error'); - - return false; - } - - $asset = Table::getInstance('asset'); - - if ($asset->loadByName('root.1')) - { - $asset->rules = (string) $rules; - - if (!$asset->check() || !$asset->store()) - { - $app->enqueueMessage($asset->getError(), 'error'); - - return false; - } - } - else - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_ROOT_ASSET_NOT_FOUND'), 'error'); - - return false; - } - - unset($data['rules']); - } - - // Save the text filters - if (isset($data['filters'])) - { - $registry = new Registry(array('filters' => $data['filters'])); - - $extension = Table::getInstance('extension'); - - // Get extension_id - $extensionId = $extension->find(array('name' => 'com_config')); - - if ($extension->load((int) $extensionId)) - { - $extension->params = (string) $registry; - - if (!$extension->check() || !$extension->store()) - { - $app->enqueueMessage($extension->getError(), 'error'); - - return false; - } - } - else - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIG_EXTENSION_NOT_FOUND'), 'error'); - - return false; - } - - unset($data['filters']); - } - - // Get the previous configuration. - $prev = new \JConfig; - $prev = ArrayHelper::fromObject($prev); - - // Merge the new data in. We do this to preserve values that were not in the form. - $data = array_merge($prev, $data); - - /* - * Perform miscellaneous options based on configuration settings/changes. - */ - - // Escape the offline message if present. - if (isset($data['offline_message'])) - { - $data['offline_message'] = OutputFilter::ampReplace($data['offline_message']); - } - - // Purge the database session table if we are changing to the database handler. - if ($prev['session_handler'] != 'database' && $data['session_handler'] == 'database') - { - $query = $this->_db->getQuery(true) - ->delete($this->_db->quoteName('#__session')) - ->where($this->_db->quoteName('time') . ' < ' . (time() - 1)); - $this->_db->setQuery($query); - $this->_db->execute(); - } - - // Purge the database session table if we are disabling session metadata - if ($prev['session_metadata'] == 1 && $data['session_metadata'] == 0) - { - try - { - // If we are are using the session handler, purge the extra columns, otherwise truncate the whole session table - if ($data['session_handler'] === 'database') - { - $revisedDbo->setQuery( - $revisedDbo->getQuery(true) - ->update('#__session') - ->set( - [ - $revisedDbo->quoteName('client_id') . ' = 0', - $revisedDbo->quoteName('guest') . ' = NULL', - $revisedDbo->quoteName('userid') . ' = NULL', - $revisedDbo->quoteName('username') . ' = NULL', - ] - ) - )->execute(); - } - else - { - $revisedDbo->truncateTable('#__session'); - } - } - catch (\RuntimeException $e) - { - /* - * The database API logs errors on failures so we don't need to add any error handling mechanisms here. - * Also, this data won't be added or checked anymore once the configuration is saved, so it'll purge itself - * through normal garbage collection anyway or if not using the database handler someone can purge the - * table on their own. Either way, carry on Soldier! - */ - } - } - - // Ensure custom session file path exists or try to create it if changed - if (!empty($data['session_filesystem_path'])) - { - $currentPath = $prev['session_filesystem_path'] ?? null; - - if ($currentPath) - { - $currentPath = Path::clean($currentPath); - } - - $data['session_filesystem_path'] = Path::clean($data['session_filesystem_path']); - - if ($currentPath !== $data['session_filesystem_path']) - { - if (!Folder::exists($data['session_filesystem_path']) && !Folder::create($data['session_filesystem_path'])) - { - try - { - Log::add( - Text::sprintf( - 'COM_CONFIG_ERROR_CUSTOM_SESSION_FILESYSTEM_PATH_NOTWRITABLE_USING_DEFAULT', - $data['session_filesystem_path'] - ), - Log::WARNING, - 'jerror' - ); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_CUSTOM_SESSION_FILESYSTEM_PATH_NOTWRITABLE_USING_DEFAULT', - $data['session_filesystem_path'] - ), - 'warning' - ); - } - - $data['session_filesystem_path'] = $currentPath; - } - } - } - - // Set the shared session configuration - if (isset($data['shared_session'])) - { - $currentShared = $prev['shared_session'] ?? '0'; - - // Has the user enabled shared sessions? - if ($data['shared_session'] == 1 && $currentShared == 0) - { - // Generate a random shared session name - $data['session_name'] = UserHelper::genRandomPassword(16); - } - - // Has the user disabled shared sessions? - if ($data['shared_session'] == 0 && $currentShared == 1) - { - // Remove the session name value - unset($data['session_name']); - } - } - - // Set the shared session configuration - if (isset($data['shared_session'])) - { - $currentShared = $prev['shared_session'] ?? '0'; - - // Has the user enabled shared sessions? - if ($data['shared_session'] == 1 && $currentShared == 0) - { - // Generate a random shared session name - $data['session_name'] = UserHelper::genRandomPassword(16); - } - - // Has the user disabled shared sessions? - if ($data['shared_session'] == 0 && $currentShared == 1) - { - // Remove the session name value - unset($data['session_name']); - } - } - - if (empty($data['cache_handler'])) - { - $data['caching'] = 0; - } - - /* - * Look for a custom cache_path - * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default - */ - if (!empty($data['cache_path'])) - { - $path = $data['cache_path']; - } - elseif (!empty($prev['cache_path'])) - { - $path = $prev['cache_path']; - } - else - { - $path = JPATH_CACHE; - } - - // Give a warning if the cache-folder can not be opened - if ($data['caching'] > 0 && $data['cache_handler'] == 'file' && @opendir($path) == false) - { - $error = true; - - // If a custom path is in use, try using the system default instead of disabling cache - if ($path !== JPATH_CACHE && @opendir(JPATH_CACHE) != false) - { - try - { - Log::add( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_CACHE_PATH_NOTWRITABLE_USING_DEFAULT', $path, JPATH_CACHE), - Log::WARNING, - 'jerror' - ); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_CACHE_PATH_NOTWRITABLE_USING_DEFAULT', $path, JPATH_CACHE), - 'warning' - ); - } - - $path = JPATH_CACHE; - $error = false; - - $data['cache_path'] = ''; - } - - if ($error) - { - try - { - Log::add(Text::sprintf('COM_CONFIG_ERROR_CACHE_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_CACHE_PATH_NOTWRITABLE', $path), 'warning'); - } - - $data['caching'] = 0; - } - } - - // Did the user remove their custom cache path? Don't save the variable to the config - if (empty($data['cache_path'])) - { - unset($data['cache_path']); - } - - // Clean the cache if disabled but previously enabled or changing cache handlers; these operations use the `$prev` data already in memory - if ((!$data['caching'] && $prev['caching']) || $data['cache_handler'] !== $prev['cache_handler']) - { - try - { - Factory::getCache()->clean(); - } - catch (CacheConnectingException $exception) - { - try - { - Log::add(Text::_('COM_CONFIG_ERROR_CACHE_CONNECTION_FAILED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CACHE_CONNECTION_FAILED'), 'warning'); - } - } - catch (UnsupportedCacheException $exception) - { - try - { - Log::add(Text::_('COM_CONFIG_ERROR_CACHE_DRIVER_UNSUPPORTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CACHE_DRIVER_UNSUPPORTED'), 'warning'); - } - } - } - - /* - * Look for a custom tmp_path - * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default - */ - $defaultTmpPath = JPATH_ROOT . '/tmp'; - - if (!empty($data['tmp_path'])) - { - $path = $data['tmp_path']; - } - elseif (!empty($prev['tmp_path'])) - { - $path = $prev['tmp_path']; - } - else - { - $path = $defaultTmpPath; - } - - $path = Path::clean($path); - - // Give a warning if the tmp-folder is not valid or not writable - if (!is_dir($path) || !is_writable($path)) - { - $error = true; - - // If a custom path is in use, try using the system default tmp path - if ($path !== $defaultTmpPath && is_dir($defaultTmpPath) && is_writable($defaultTmpPath)) - { - try - { - Log::add( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_TEMP_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultTmpPath), - Log::WARNING, - 'jerror' - ); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_TEMP_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultTmpPath), - 'warning' - ); - } - - $error = false; - - $data['tmp_path'] = $defaultTmpPath; - } - - if ($error) - { - try - { - Log::add(Text::sprintf('COM_CONFIG_ERROR_TMP_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_TMP_PATH_NOTWRITABLE', $path), 'warning'); - } - } - } - - /* - * Look for a custom log_path - * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default - */ - $defaultLogPath = JPATH_ADMINISTRATOR . '/logs'; - - if (!empty($data['log_path'])) - { - $path = $data['log_path']; - } - elseif (!empty($prev['log_path'])) - { - $path = $prev['log_path']; - } - else - { - $path = $defaultLogPath; - } - - $path = Path::clean($path); - - // Give a warning if the log-folder is not valid or not writable - if (!is_dir($path) || !is_writable($path)) - { - $error = true; - - // If a custom path is in use, try using the system default log path - if ($path !== $defaultLogPath && is_dir($defaultLogPath) && is_writable($defaultLogPath)) - { - try - { - Log::add( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_LOG_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultLogPath), - Log::WARNING, - 'jerror' - ); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_LOG_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultLogPath), - 'warning' - ); - } - - $error = false; - $data['log_path'] = $defaultLogPath; - } - - if ($error) - { - try - { - Log::add(Text::sprintf('COM_CONFIG_ERROR_LOG_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_LOG_PATH_NOTWRITABLE', $path), 'warning'); - } - } - } - - // Create the new configuration object. - $config = new Registry($data); - - // Overwrite webservices cors settings - $app->set('cors', $data['cors']); - $app->set('cors_allow_origin', $data['cors_allow_origin']); - $app->set('cors_allow_headers', $data['cors_allow_headers']); - $app->set('cors_allow_methods', $data['cors_allow_methods']); - - // Clear cache of com_config component. - $this->cleanCache('_system'); - - $result = $app->triggerEvent('onApplicationBeforeSave', array($config)); - - // Store the data. - if (in_array(false, $result, true)) - { - throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_UNKNOWN_BEFORE_SAVING')); - } - - // Write the configuration file. - $result = $this->writeConfigFile($config); - - // Trigger the after save event. - $app->triggerEvent('onApplicationAfterSave', array($config)); - - return $result; - } - - /** - * Method to unset the root_user value from configuration data. - * - * This method will load the global configuration data straight from - * JConfig and remove the root_user value for security, then save the configuration. - * - * @return boolean True on success, false on failure. - * - * @since 1.6 - */ - public function removeroot() - { - $app = Factory::getApplication(); - - // Get the previous configuration. - $prev = new \JConfig; - $prev = ArrayHelper::fromObject($prev); - - // Create the new configuration object, and unset the root_user property - unset($prev['root_user']); - $config = new Registry($prev); - - $result = $app->triggerEvent('onApplicationBeforeSave', array($config)); - - // Store the data. - if (in_array(false, $result, true)) - { - throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_UNKNOWN_BEFORE_SAVING')); - } - - // Write the configuration file. - $result = $this->writeConfigFile($config); - - // Trigger the after save event. - $app->triggerEvent('onApplicationAfterSave', array($config)); - - return $result; - } - - /** - * Method to write the configuration to a file. - * - * @param Registry $config A Registry object containing all global config data. - * - * @return boolean True on success, false on failure. - * - * @since 2.5.4 - * @throws \RuntimeException - */ - private function writeConfigFile(Registry $config) - { - // Set the configuration file path. - $file = JPATH_CONFIGURATION . '/configuration.php'; - - $app = Factory::getApplication(); - - // Attempt to make the file writeable. - if (Path::isOwner($file) && !Path::setPermissions($file, '0644')) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'notice'); - } - - // Attempt to write the configuration file as a PHP class named JConfig. - $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); - - if (!File::write($file, $configuration)) - { - throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_WRITE_FAILED')); - } - - // Attempt to make the file unwriteable. - if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'notice'); - } - - return true; - } - - /** - * Method to store the permission values in the asset table. - * - * This method will get an array with permission key value pairs and transform it - * into json and update the asset table in the database. - * - * @param string $permission Need an array with Permissions (component, rule, value and title) - * - * @return array|bool A list of result data or false on failure. - * - * @since 3.5 - */ - public function storePermissions($permission = null) - { - $app = Factory::getApplication(); - $user = Factory::getUser(); - - if (is_null($permission)) - { - // Get data from input. - $permission = array( - 'component' => $app->input->Json->get('comp'), - 'action' => $app->input->Json->get('action'), - 'rule' => $app->input->Json->get('rule'), - 'value' => $app->input->Json->get('value'), - 'title' => $app->input->Json->get('title', '', 'RAW') - ); - } - - // We are creating a new item so we don't have an item id so don't allow. - if (substr($permission['component'], -6) === '.false') - { - $app->enqueueMessage(Text::_('JLIB_RULES_SAVE_BEFORE_CHANGE_PERMISSIONS'), 'error'); - - return false; - } - - // Check if the user is authorized to do this. - if (!$user->authorise('core.admin', $permission['component'])) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - - return false; - } - - $permission['component'] = empty($permission['component']) ? 'root.1' : $permission['component']; - - // Current view is global config? - $isGlobalConfig = $permission['component'] === 'root.1'; - - // Check if changed group has Super User permissions. - $isSuperUserGroupBefore = Access::checkGroup($permission['rule'], 'core.admin'); - - // Check if current user belongs to changed group. - $currentUserBelongsToGroup = in_array((int) $permission['rule'], $user->groups) ? true : false; - - // Get current user groups tree. - $currentUserGroupsTree = Access::getGroupsByUser($user->id, true); - - // Check if current user belongs to changed group. - $currentUserSuperUser = $user->authorise('core.admin'); - - // If user is not Super User cannot change the permissions of a group it belongs to. - if (!$currentUserSuperUser && $currentUserBelongsToGroup) - { - $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_OWN_GROUPS'), 'error'); - - return false; - } - - // If user is not Super User cannot change the permissions of a group it belongs to. - if (!$currentUserSuperUser && in_array((int) $permission['rule'], $currentUserGroupsTree)) - { - $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_OWN_PARENT_GROUPS'), 'error'); - - return false; - } - - // If user is not Super User cannot change the permissions of a Super User Group. - if (!$currentUserSuperUser && $isSuperUserGroupBefore && !$currentUserBelongsToGroup) - { - $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_SUPER_USER'), 'error'); - - return false; - } - - // If user is not Super User cannot change the Super User permissions in any group it belongs to. - if ($isSuperUserGroupBefore && $currentUserBelongsToGroup && $permission['action'] === 'core.admin') - { - $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF'), 'error'); - - return false; - } - - try - { - /** @var Asset $asset */ - $asset = Table::getInstance('asset'); - $result = $asset->loadByName($permission['component']); - - if ($result === false) - { - $data = array($permission['action'] => array($permission['rule'] => $permission['value'])); - - $rules = new Rules($data); - $asset->rules = (string) $rules; - $asset->name = (string) $permission['component']; - $asset->title = (string) $permission['title']; - - // Get the parent asset id so we have a correct tree. - /** @var Asset $parentAsset */ - $parentAsset = Table::getInstance('Asset'); - - if (strpos($asset->name, '.') !== false) - { - $assetParts = explode('.', $asset->name); - $parentAsset->loadByName($assetParts[0]); - $parentAssetId = $parentAsset->id; - } - else - { - $parentAssetId = $parentAsset->getRootId(); - } - - /** - * @todo: incorrect ACL stored - * When changing a permission of an item that doesn't have a row in the asset table the row a new row is created. - * This works fine for item <-> component <-> global config scenario and component <-> global config scenario. - * But doesn't work properly for item <-> section(s) <-> component <-> global config scenario, - * because a wrong parent asset id (the component) is stored. - * Happens when there is no row in the asset table (ex: deleted or not created on update). - */ - - $asset->setLocation($parentAssetId, 'last-child'); - } - else - { - // Decode the rule settings. - $temp = json_decode($asset->rules, true); - - // Check if a new value is to be set. - if (isset($permission['value'])) - { - // Check if we already have an action entry. - if (!isset($temp[$permission['action']])) - { - $temp[$permission['action']] = array(); - } - - // Check if we already have a rule entry. - if (!isset($temp[$permission['action']][$permission['rule']])) - { - $temp[$permission['action']][$permission['rule']] = array(); - } - - // Set the new permission. - $temp[$permission['action']][$permission['rule']] = (int) $permission['value']; - - // Check if we have an inherited setting. - if ($permission['value'] === '') - { - unset($temp[$permission['action']][$permission['rule']]); - } - - // Check if we have any rules. - if (!$temp[$permission['action']]) - { - unset($temp[$permission['action']]); - } - } - else - { - // There is no value so remove the action as it's not needed. - unset($temp[$permission['action']]); - } - - $asset->rules = json_encode($temp, JSON_FORCE_OBJECT); - } - - if (!$asset->check() || !$asset->store()) - { - $app->enqueueMessage(Text::_('JLIB_UNKNOWN'), 'error'); - - return false; - } - } - catch (\Exception $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // All checks done. - $result = array( - 'text' => '', - 'class' => '', - 'result' => true, - ); - - // Show the current effective calculated permission considering current group, path and cascade. - - try - { - // Get the asset id by the name of the component. - $query = $this->_db->getQuery(true) - ->select($this->_db->quoteName('id')) - ->from($this->_db->quoteName('#__assets')) - ->where($this->_db->quoteName('name') . ' = :component') - ->bind(':component', $permission['component']); - - $this->_db->setQuery($query); - - $assetId = (int) $this->_db->loadResult(); - - // Fetch the parent asset id. - $parentAssetId = null; - - /** - * @todo: incorrect info - * When creating a new item (not saving) it uses the calculated permissions from the component (item <-> component <-> global config). - * But if we have a section too (item <-> section(s) <-> component <-> global config) this is not correct. - * Also, currently it uses the component permission, but should use the calculated permissions for a child of the component/section. - */ - - // If not in global config we need the parent_id asset to calculate permissions. - if (!$isGlobalConfig) - { - // In this case we need to get the component rules too. - $query->clear() - ->select($this->_db->quoteName('parent_id')) - ->from($this->_db->quoteName('#__assets')) - ->where($this->_db->quoteName('id') . ' = :assetid') - ->bind(':assetid', $assetId, ParameterType::INTEGER); - - $this->_db->setQuery($query); - - $parentAssetId = (int) $this->_db->loadResult(); - } - - // Get the group parent id of the current group. - $rule = (int) $permission['rule']; - $query->clear() - ->select($this->_db->quoteName('parent_id')) - ->from($this->_db->quoteName('#__usergroups')) - ->where($this->_db->quoteName('id') . ' = :rule') - ->bind(':rule', $rule, ParameterType::INTEGER); - - $this->_db->setQuery($query); - - $parentGroupId = (int) $this->_db->loadResult(); - - // Count the number of child groups of the current group. - $query->clear() - ->select('COUNT(' . $this->_db->quoteName('id') . ')') - ->from($this->_db->quoteName('#__usergroups')) - ->where($this->_db->quoteName('parent_id') . ' = :rule') - ->bind(':rule', $rule, ParameterType::INTEGER); - - $this->_db->setQuery($query); - - $totalChildGroups = (int) $this->_db->loadResult(); - } - catch (\Exception $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Clear access statistics. - Access::clearStatics(); - - // After current group permission is changed we need to check again if the group has Super User permissions. - $isSuperUserGroupAfter = Access::checkGroup($permission['rule'], 'core.admin'); - - // Get the rule for just this asset (non-recursive) and get the actual setting for the action for this group. - $assetRule = Access::getAssetRules($assetId, false, false)->allow($permission['action'], $permission['rule']); - - // Get the group, group parent id, and group global config recursive calculated permission for the chosen action. - $inheritedGroupRule = Access::checkGroup($permission['rule'], $permission['action'], $assetId); - - if (!empty($parentAssetId)) - { - $inheritedGroupParentAssetRule = Access::checkGroup($permission['rule'], $permission['action'], $parentAssetId); - } - else - { - $inheritedGroupParentAssetRule = null; - } - - $inheritedParentGroupRule = !empty($parentGroupId) ? Access::checkGroup($parentGroupId, $permission['action'], $assetId) : null; - - // Current group is a Super User group, so calculated setting is "Allowed (Super User)". - if ($isSuperUserGroupAfter) - { - $result['class'] = 'badge bg-success'; - $result['text'] = '' . Text::_('JLIB_RULES_ALLOWED_ADMIN'); - } - // Not super user. - else - { - // First get the real recursive calculated setting and add (Inherited) to it. - - // If recursive calculated setting is "Denied" or null. Calculated permission is "Not Allowed (Inherited)". - if ($inheritedGroupRule === null || $inheritedGroupRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_INHERITED'); - } - // If recursive calculated setting is "Allowed". Calculated permission is "Allowed (Inherited)". - else - { - $result['class'] = 'badge bg-success'; - $result['text'] = Text::_('JLIB_RULES_ALLOWED_INHERITED'); - } - - // Second part: Overwrite the calculated permissions labels if there is an explicit permission in the current group. - - /** - * @todo: incorrect info - * If a component has a permission that doesn't exists in global config (ex: frontend editing in com_modules) by default - * we get "Not Allowed (Inherited)" when we should get "Not Allowed (Default)". - */ - - // If there is an explicit permission "Not Allowed". Calculated permission is "Not Allowed". - if ($assetRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED'); - } - // If there is an explicit permission is "Allowed". Calculated permission is "Allowed". - elseif ($assetRule === true) - { - $result['class'] = 'badge bg-success'; - $result['text'] = Text::_('JLIB_RULES_ALLOWED'); - } - - // Third part: Overwrite the calculated permissions labels for special cases. - - // Global configuration with "Not Set" permission. Calculated permission is "Not Allowed (Default)". - if (empty($parentGroupId) && $isGlobalConfig === true && $assetRule === null) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_DEFAULT'); - } - - /** - * Component/Item with explicit "Denied" permission at parent Asset (Category, Component or Global config) configuration. - * Or some parent group has an explicit "Denied". - * Calculated permission is "Not Allowed (Locked)". - */ - elseif ($inheritedGroupParentAssetRule === false || $inheritedParentGroupRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = '' . Text::_('JLIB_RULES_NOT_ALLOWED_LOCKED'); - } - } - - // If removed or added super user from group, we need to refresh the page to recalculate all settings. - if ($isSuperUserGroupBefore != $isSuperUserGroupAfter) - { - $app->enqueueMessage(Text::_('JLIB_RULES_NOTICE_RECALCULATE_GROUP_PERMISSIONS'), 'notice'); - } - - // If this group has child groups, we need to refresh the page to recalculate the child settings. - if ($totalChildGroups > 0) - { - $app->enqueueMessage(Text::_('JLIB_RULES_NOTICE_RECALCULATE_GROUP_CHILDS_PERMISSIONS'), 'notice'); - } - - return $result; - } - - /** - * Method to send a test mail which is called via an AJAX request - * - * @return boolean - * - * @since 3.5 - */ - public function sendTestMail() - { - // Set the new values to test with the current settings - $app = Factory::getApplication(); - $user = Factory::getUser(); - $input = $app->input->json; - $smtppass = $input->get('smtppass', null, 'RAW'); - - $app->set('smtpauth', $input->get('smtpauth')); - $app->set('smtpuser', $input->get('smtpuser', '', 'STRING')); - $app->set('smtphost', $input->get('smtphost')); - $app->set('smtpsecure', $input->get('smtpsecure')); - $app->set('smtpport', $input->get('smtpport')); - $app->set('mailfrom', $input->get('mailfrom', '', 'STRING')); - $app->set('fromname', $input->get('fromname', '', 'STRING')); - $app->set('mailer', $input->get('mailer')); - $app->set('mailonline', $input->get('mailonline')); - - // Use smtppass only if it was submitted - if ($smtppass !== null) - { - $app->set('smtppass', $smtppass); - } - - $mail = Factory::getMailer(); - - // Prepare email and try to send it - $mailer = new MailTemplate('com_config.test_mail', $user->getParam('language', $app->get('language')), $mail); - $mailer->addTemplateData( - array( - 'sitename' => $app->get('sitename'), - 'method' => Text::_('COM_CONFIG_SENDMAIL_METHOD_' . strtoupper($mail->Mailer)) - ) - ); - $mailer->addRecipient($app->get('mailfrom'), $app->get('fromname')); - - try - { - $mailSent = $mailer->send(); - } - catch (MailDisabledException | phpMailerException $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - if ($mailSent === true) - { - $methodName = Text::_('COM_CONFIG_SENDMAIL_METHOD_' . strtoupper($mail->Mailer)); - - // If JMail send the mail using PHP Mail as fallback. - if ($mail->Mailer !== $app->get('mailer')) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_SENDMAIL_SUCCESS_FALLBACK', $app->get('mailfrom'), $methodName), 'warning'); - } - else - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_SENDMAIL_SUCCESS', $app->get('mailfrom'), $methodName), 'message'); - } - - return true; - } - - $app->enqueueMessage(Text::_('COM_CONFIG_SENDMAIL_ERROR'), 'error'); - - return false; - } + /** + * Array of protected password fields from the configuration.php + * + * @var array + * @since 3.9.23 + */ + private $protectedConfigurationFields = array('password', 'secret', 'smtppass', 'redis_server_auth', 'session_redis_server_auth'); + + /** + * Method to get a form object. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return mixed A JForm object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_config.application', 'application', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the configuration data. + * + * This method will load the global configuration data straight from + * JConfig. If configuration data has been saved in the session, that + * data will be merged into the original data, overwriting it. + * + * @return array An array containing all global config data. + * + * @since 1.6 + */ + public function getData() + { + // Get the config data. + $config = new \JConfig(); + $data = ArrayHelper::fromObject($config); + + // Get the correct driver at runtime + $data['dbtype'] = $this->getDatabase()->getName(); + + // Prime the asset_id for the rules. + $data['asset_id'] = 1; + + // Get the text filter data + $params = ComponentHelper::getParams('com_config'); + $data['filters'] = ArrayHelper::fromObject($params->get('filters')); + + // If no filter data found, get from com_content (update of 1.6/1.7 site) + if (empty($data['filters'])) { + $contentParams = ComponentHelper::getParams('com_content'); + $data['filters'] = ArrayHelper::fromObject($contentParams->get('filters')); + } + + // Check for data in the session. + $temp = Factory::getApplication()->getUserState('com_config.config.global.data'); + + // Merge in the session data. + if (!empty($temp)) { + // $temp can sometimes be an object, and we need it to be an array + if (is_object($temp)) { + $temp = ArrayHelper::fromObject($temp); + } + + $data = array_merge($temp, $data); + } + + // Correct error_reporting value, since we removed "development", the "maximum" should be set instead + // @TODO: This can be removed in 5.0 + if (!empty($data['error_reporting']) && $data['error_reporting'] === 'development') { + $data['error_reporting'] = 'maximum'; + } + + return $data; + } + + /** + * Method to validate the db connection properties. + * + * @param array $data An array containing all global config data. + * + * @return array|boolean Array with the validated global config data or boolean false on a validation failure. + * + * @since 4.0.0 + */ + public function validateDbConnection($data) + { + // Validate database connection encryption options + if ((int) $data['dbencryption'] === 0) { + // Reset unused options + if (!empty($data['dbsslkey'])) { + $data['dbsslkey'] = ''; + } + + if (!empty($data['dbsslcert'])) { + $data['dbsslcert'] = ''; + } + + if ((bool) $data['dbsslverifyservercert'] === true) { + $data['dbsslverifyservercert'] = false; + } + + if (!empty($data['dbsslca'])) { + $data['dbsslca'] = ''; + } + + if (!empty($data['dbsslcipher'])) { + $data['dbsslcipher'] = ''; + } + } else { + // Check localhost + if (strtolower($data['host']) === 'localhost') { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_LOCALHOST'), 'error'); + + return false; + } + + // Check CA file and folder depending on database type if server certificate verification + if ((bool) $data['dbsslverifyservercert'] === true) { + if (empty($data['dbsslca'])) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CA_LABEL') + ), + 'error' + ); + + return false; + } + + if (!File::exists(Path::clean($data['dbsslca']))) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CA_LABEL') + ), + 'error' + ); + + return false; + } + } else { + // Reset unused option + if (!empty($data['dbsslca'])) { + $data['dbsslca'] = ''; + } + } + + // Check key and certificate if two-way encryption + if ((int) $data['dbencryption'] === 2) { + if (empty($data['dbsslkey'])) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_KEY_LABEL') + ), + 'error' + ); + + return false; + } + + if (!File::exists(Path::clean($data['dbsslkey']))) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_KEY_LABEL') + ), + 'error' + ); + + return false; + } + + if (empty($data['dbsslcert'])) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CERT_LABEL') + ), + 'error' + ); + + return false; + } + + if (!File::exists(Path::clean($data['dbsslcert']))) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CERT_LABEL') + ), + 'error' + ); + + return false; + } + } else { + // Reset unused options + if (!empty($data['dbsslkey'])) { + $data['dbsslkey'] = ''; + } + + if (!empty($data['dbsslcert'])) { + $data['dbsslcert'] = ''; + } + } + } + + return $data; + } + + /** + * Method to save the configuration data. + * + * @param array $data An array containing all global config data. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + */ + public function save($data) + { + $app = Factory::getApplication(); + + // Try to load the values from the configuration file + foreach ($this->protectedConfigurationFields as $fieldKey) { + if (!isset($data[$fieldKey])) { + $data[$fieldKey] = $app->get($fieldKey, ''); + } + } + + // Check that we aren't setting wrong database configuration + $options = array( + 'driver' => $data['dbtype'], + 'host' => $data['host'], + 'user' => $data['user'], + 'password' => $data['password'], + 'database' => $data['db'], + 'prefix' => $data['dbprefix'], + ); + + if ((int) $data['dbencryption'] !== 0) { + $options['ssl'] = [ + 'enable' => true, + 'verify_server_cert' => (bool) $data['dbsslverifyservercert'], + ]; + + foreach (['cipher', 'ca', 'key', 'cert'] as $value) { + $confVal = trim($data['dbssl' . $value]); + + if ($confVal !== '') { + $options['ssl'][$value] = $confVal; + } + } + } + + try { + $revisedDbo = DatabaseDriver::getInstance($options); + $revisedDbo->getVersion(); + } catch (\Exception $e) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_DATABASE_NOT_AVAILABLE', $e->getCode(), $e->getMessage()), 'error'); + + return false; + } + + if ((int) $data['dbencryption'] !== 0 && empty($revisedDbo->getConnectionEncryption())) { + if ($revisedDbo->isConnectionEncryptionSupported()) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_CONN_NOT_ENCRYPT'), 'error'); + } else { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_SRV_NOT_SUPPORTS'), 'error'); + } + + return false; + } + + // Check if we can set the Force SSL option + if ((int) $data['force_ssl'] !== 0 && (int) $data['force_ssl'] !== (int) $app->get('force_ssl', '0')) { + try { + // Make an HTTPS request to check if the site is available in HTTPS. + $host = Uri::getInstance()->getHost(); + $options = new Registry(); + $options->set('userAgent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0'); + + // Do not check for valid server certificate here, leave this to the user, moreover disable using a proxy if any is configured. + $options->set( + 'transport.curl', + array( + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_PROXY => null, + CURLOPT_PROXYUSERPWD => null, + ) + ); + $response = HttpFactory::getHttp($options)->get('https://' . $host . Uri::root(true) . '/', array('Host' => $host), 10); + + // If available in HTTPS check also the status code. + if (!in_array($response->code, array(200, 503, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 401), true)) { + throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_SSL_NOT_AVAILABLE_HTTP_CODE')); + } + } catch (\RuntimeException $e) { + $data['force_ssl'] = 0; + + // Also update the user state + $app->setUserState('com_config.config.global.data.force_ssl', 0); + + // Inform the user + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_SSL_NOT_AVAILABLE', $e->getMessage()), 'warning'); + } + } + + // Save the rules + if (isset($data['rules'])) { + $rules = new Rules($data['rules']); + + // Check that we aren't removing our Super User permission + // Need to get groups from database, since they might have changed + $myGroups = Access::getGroupsByUser(Factory::getUser()->get('id')); + $myRules = $rules->getData(); + $hasSuperAdmin = $myRules['core.admin']->allow($myGroups); + + if (!$hasSuperAdmin) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_REMOVING_SUPER_ADMIN'), 'error'); + + return false; + } + + $asset = Table::getInstance('asset'); + + if ($asset->loadByName('root.1')) { + $asset->rules = (string) $rules; + + if (!$asset->check() || !$asset->store()) { + $app->enqueueMessage($asset->getError(), 'error'); + + return false; + } + } else { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_ROOT_ASSET_NOT_FOUND'), 'error'); + + return false; + } + + unset($data['rules']); + } + + // Save the text filters + if (isset($data['filters'])) { + $registry = new Registry(array('filters' => $data['filters'])); + + $extension = Table::getInstance('extension'); + + // Get extension_id + $extensionId = $extension->find(array('name' => 'com_config')); + + if ($extension->load((int) $extensionId)) { + $extension->params = (string) $registry; + + if (!$extension->check() || !$extension->store()) { + $app->enqueueMessage($extension->getError(), 'error'); + + return false; + } + } else { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIG_EXTENSION_NOT_FOUND'), 'error'); + + return false; + } + + unset($data['filters']); + } + + // Get the previous configuration. + $prev = new \JConfig(); + $prev = ArrayHelper::fromObject($prev); + + // Merge the new data in. We do this to preserve values that were not in the form. + $data = array_merge($prev, $data); + + /* + * Perform miscellaneous options based on configuration settings/changes. + */ + + // Escape the offline message if present. + if (isset($data['offline_message'])) { + $data['offline_message'] = OutputFilter::ampReplace($data['offline_message']); + } + + // Purge the database session table if we are changing to the database handler. + if ($prev['session_handler'] != 'database' && $data['session_handler'] == 'database') { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__session')) + ->where($db->quoteName('time') . ' < ' . (time() - 1)); + $db->setQuery($query); + $db->execute(); + } + + // Purge the database session table if we are disabling session metadata + if ($prev['session_metadata'] == 1 && $data['session_metadata'] == 0) { + try { + // If we are are using the session handler, purge the extra columns, otherwise truncate the whole session table + if ($data['session_handler'] === 'database') { + $revisedDbo->setQuery( + $revisedDbo->getQuery(true) + ->update('#__session') + ->set( + [ + $revisedDbo->quoteName('client_id') . ' = 0', + $revisedDbo->quoteName('guest') . ' = NULL', + $revisedDbo->quoteName('userid') . ' = NULL', + $revisedDbo->quoteName('username') . ' = NULL', + ] + ) + )->execute(); + } else { + $revisedDbo->truncateTable('#__session'); + } + } catch (\RuntimeException $e) { + /* + * The database API logs errors on failures so we don't need to add any error handling mechanisms here. + * Also, this data won't be added or checked anymore once the configuration is saved, so it'll purge itself + * through normal garbage collection anyway or if not using the database handler someone can purge the + * table on their own. Either way, carry on Soldier! + */ + } + } + + // Ensure custom session file path exists or try to create it if changed + if (!empty($data['session_filesystem_path'])) { + $currentPath = $prev['session_filesystem_path'] ?? null; + + if ($currentPath) { + $currentPath = Path::clean($currentPath); + } + + $data['session_filesystem_path'] = Path::clean($data['session_filesystem_path']); + + if ($currentPath !== $data['session_filesystem_path']) { + if (!Folder::exists($data['session_filesystem_path']) && !Folder::create($data['session_filesystem_path'])) { + try { + Log::add( + Text::sprintf( + 'COM_CONFIG_ERROR_CUSTOM_SESSION_FILESYSTEM_PATH_NOTWRITABLE_USING_DEFAULT', + $data['session_filesystem_path'] + ), + Log::WARNING, + 'jerror' + ); + } catch (\RuntimeException $logException) { + $app->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_CUSTOM_SESSION_FILESYSTEM_PATH_NOTWRITABLE_USING_DEFAULT', + $data['session_filesystem_path'] + ), + 'warning' + ); + } + + $data['session_filesystem_path'] = $currentPath; + } + } + } + + // Set the shared session configuration + if (isset($data['shared_session'])) { + $currentShared = $prev['shared_session'] ?? '0'; + + // Has the user enabled shared sessions? + if ($data['shared_session'] == 1 && $currentShared == 0) { + // Generate a random shared session name + $data['session_name'] = UserHelper::genRandomPassword(16); + } + + // Has the user disabled shared sessions? + if ($data['shared_session'] == 0 && $currentShared == 1) { + // Remove the session name value + unset($data['session_name']); + } + } + + // Set the shared session configuration + if (isset($data['shared_session'])) { + $currentShared = $prev['shared_session'] ?? '0'; + + // Has the user enabled shared sessions? + if ($data['shared_session'] == 1 && $currentShared == 0) { + // Generate a random shared session name + $data['session_name'] = UserHelper::genRandomPassword(16); + } + + // Has the user disabled shared sessions? + if ($data['shared_session'] == 0 && $currentShared == 1) { + // Remove the session name value + unset($data['session_name']); + } + } + + if (empty($data['cache_handler'])) { + $data['caching'] = 0; + } + + /* + * Look for a custom cache_path + * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default + */ + if (!empty($data['cache_path'])) { + $path = $data['cache_path']; + } elseif (!empty($prev['cache_path'])) { + $path = $prev['cache_path']; + } else { + $path = JPATH_CACHE; + } + + // Give a warning if the cache-folder can not be opened + if ($data['caching'] > 0 && $data['cache_handler'] == 'file' && @opendir($path) == false) { + $error = true; + + // If a custom path is in use, try using the system default instead of disabling cache + if ($path !== JPATH_CACHE && @opendir(JPATH_CACHE) != false) { + try { + Log::add( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_CACHE_PATH_NOTWRITABLE_USING_DEFAULT', $path, JPATH_CACHE), + Log::WARNING, + 'jerror' + ); + } catch (\RuntimeException $logException) { + $app->enqueueMessage( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_CACHE_PATH_NOTWRITABLE_USING_DEFAULT', $path, JPATH_CACHE), + 'warning' + ); + } + + $path = JPATH_CACHE; + $error = false; + + $data['cache_path'] = ''; + } + + if ($error) { + try { + Log::add(Text::sprintf('COM_CONFIG_ERROR_CACHE_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_CACHE_PATH_NOTWRITABLE', $path), 'warning'); + } + + $data['caching'] = 0; + } + } + + // Did the user remove their custom cache path? Don't save the variable to the config + if (empty($data['cache_path'])) { + unset($data['cache_path']); + } + + // Clean the cache if disabled but previously enabled or changing cache handlers; these operations use the `$prev` data already in memory + if ((!$data['caching'] && $prev['caching']) || $data['cache_handler'] !== $prev['cache_handler']) { + try { + Factory::getCache()->clean(); + } catch (CacheConnectingException $exception) { + try { + Log::add(Text::_('COM_CONFIG_ERROR_CACHE_CONNECTION_FAILED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $logException) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CACHE_CONNECTION_FAILED'), 'warning'); + } + } catch (UnsupportedCacheException $exception) { + try { + Log::add(Text::_('COM_CONFIG_ERROR_CACHE_DRIVER_UNSUPPORTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $logException) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CACHE_DRIVER_UNSUPPORTED'), 'warning'); + } + } + } + + /* + * Look for a custom tmp_path + * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default + */ + $defaultTmpPath = JPATH_ROOT . '/tmp'; + + if (!empty($data['tmp_path'])) { + $path = $data['tmp_path']; + } elseif (!empty($prev['tmp_path'])) { + $path = $prev['tmp_path']; + } else { + $path = $defaultTmpPath; + } + + $path = Path::clean($path); + + // Give a warning if the tmp-folder is not valid or not writable + if (!is_dir($path) || !is_writable($path)) { + $error = true; + + // If a custom path is in use, try using the system default tmp path + if ($path !== $defaultTmpPath && is_dir($defaultTmpPath) && is_writable($defaultTmpPath)) { + try { + Log::add( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_TEMP_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultTmpPath), + Log::WARNING, + 'jerror' + ); + } catch (\RuntimeException $logException) { + $app->enqueueMessage( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_TEMP_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultTmpPath), + 'warning' + ); + } + + $error = false; + + $data['tmp_path'] = $defaultTmpPath; + } + + if ($error) { + try { + Log::add(Text::sprintf('COM_CONFIG_ERROR_TMP_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_TMP_PATH_NOTWRITABLE', $path), 'warning'); + } + } + } + + /* + * Look for a custom log_path + * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default + */ + $defaultLogPath = JPATH_ADMINISTRATOR . '/logs'; + + if (!empty($data['log_path'])) { + $path = $data['log_path']; + } elseif (!empty($prev['log_path'])) { + $path = $prev['log_path']; + } else { + $path = $defaultLogPath; + } + + $path = Path::clean($path); + + // Give a warning if the log-folder is not valid or not writable + if (!is_dir($path) || !is_writable($path)) { + $error = true; + + // If a custom path is in use, try using the system default log path + if ($path !== $defaultLogPath && is_dir($defaultLogPath) && is_writable($defaultLogPath)) { + try { + Log::add( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_LOG_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultLogPath), + Log::WARNING, + 'jerror' + ); + } catch (\RuntimeException $logException) { + $app->enqueueMessage( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_LOG_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultLogPath), + 'warning' + ); + } + + $error = false; + $data['log_path'] = $defaultLogPath; + } + + if ($error) { + try { + Log::add(Text::sprintf('COM_CONFIG_ERROR_LOG_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_LOG_PATH_NOTWRITABLE', $path), 'warning'); + } + } + } + + // Create the new configuration object. + $config = new Registry($data); + + // Overwrite webservices cors settings + $app->set('cors', $data['cors']); + $app->set('cors_allow_origin', $data['cors_allow_origin']); + $app->set('cors_allow_headers', $data['cors_allow_headers']); + $app->set('cors_allow_methods', $data['cors_allow_methods']); + + // Clear cache of com_config component. + $this->cleanCache('_system'); + + $result = $app->triggerEvent('onApplicationBeforeSave', array($config)); + + // Store the data. + if (in_array(false, $result, true)) { + throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_UNKNOWN_BEFORE_SAVING')); + } + + // Write the configuration file. + $result = $this->writeConfigFile($config); + + // Trigger the after save event. + $app->triggerEvent('onApplicationAfterSave', array($config)); + + return $result; + } + + /** + * Method to unset the root_user value from configuration data. + * + * This method will load the global configuration data straight from + * JConfig and remove the root_user value for security, then save the configuration. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + */ + public function removeroot() + { + $app = Factory::getApplication(); + + // Get the previous configuration. + $prev = new \JConfig(); + $prev = ArrayHelper::fromObject($prev); + + // Create the new configuration object, and unset the root_user property + unset($prev['root_user']); + $config = new Registry($prev); + + $result = $app->triggerEvent('onApplicationBeforeSave', array($config)); + + // Store the data. + if (in_array(false, $result, true)) { + throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_UNKNOWN_BEFORE_SAVING')); + } + + // Write the configuration file. + $result = $this->writeConfigFile($config); + + // Trigger the after save event. + $app->triggerEvent('onApplicationAfterSave', array($config)); + + return $result; + } + + /** + * Method to write the configuration to a file. + * + * @param Registry $config A Registry object containing all global config data. + * + * @return boolean True on success, false on failure. + * + * @since 2.5.4 + * @throws \RuntimeException + */ + private function writeConfigFile(Registry $config) + { + // Set the configuration file path. + $file = JPATH_CONFIGURATION . '/configuration.php'; + + $app = Factory::getApplication(); + + // Attempt to make the file writeable. + if (Path::isOwner($file) && !Path::setPermissions($file, '0644')) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'notice'); + } + + // Attempt to write the configuration file as a PHP class named JConfig. + $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); + + if (!File::write($file, $configuration)) { + throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_WRITE_FAILED')); + } + + // Attempt to make the file unwriteable. + if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'notice'); + } + + return true; + } + + /** + * Method to store the permission values in the asset table. + * + * This method will get an array with permission key value pairs and transform it + * into json and update the asset table in the database. + * + * @param string $permission Need an array with Permissions (component, rule, value and title) + * + * @return array|bool A list of result data or false on failure. + * + * @since 3.5 + */ + public function storePermissions($permission = null) + { + $app = Factory::getApplication(); + $user = Factory::getUser(); + + if (is_null($permission)) { + // Get data from input. + $permission = array( + 'component' => $app->input->Json->get('comp'), + 'action' => $app->input->Json->get('action'), + 'rule' => $app->input->Json->get('rule'), + 'value' => $app->input->Json->get('value'), + 'title' => $app->input->Json->get('title', '', 'RAW') + ); + } + + // We are creating a new item so we don't have an item id so don't allow. + if (substr($permission['component'], -6) === '.false') { + $app->enqueueMessage(Text::_('JLIB_RULES_SAVE_BEFORE_CHANGE_PERMISSIONS'), 'error'); + + return false; + } + + // Check if the user is authorized to do this. + if (!$user->authorise('core.admin', $permission['component'])) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + + return false; + } + + $permission['component'] = empty($permission['component']) ? 'root.1' : $permission['component']; + + // Current view is global config? + $isGlobalConfig = $permission['component'] === 'root.1'; + + // Check if changed group has Super User permissions. + $isSuperUserGroupBefore = Access::checkGroup($permission['rule'], 'core.admin'); + + // Check if current user belongs to changed group. + $currentUserBelongsToGroup = in_array((int) $permission['rule'], $user->groups) ? true : false; + + // Get current user groups tree. + $currentUserGroupsTree = Access::getGroupsByUser($user->id, true); + + // Check if current user belongs to changed group. + $currentUserSuperUser = $user->authorise('core.admin'); + + // If user is not Super User cannot change the permissions of a group it belongs to. + if (!$currentUserSuperUser && $currentUserBelongsToGroup) { + $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_OWN_GROUPS'), 'error'); + + return false; + } + + // If user is not Super User cannot change the permissions of a group it belongs to. + if (!$currentUserSuperUser && in_array((int) $permission['rule'], $currentUserGroupsTree)) { + $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_OWN_PARENT_GROUPS'), 'error'); + + return false; + } + + // If user is not Super User cannot change the permissions of a Super User Group. + if (!$currentUserSuperUser && $isSuperUserGroupBefore && !$currentUserBelongsToGroup) { + $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_SUPER_USER'), 'error'); + + return false; + } + + // If user is not Super User cannot change the Super User permissions in any group it belongs to. + if ($isSuperUserGroupBefore && $currentUserBelongsToGroup && $permission['action'] === 'core.admin') { + $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF'), 'error'); + + return false; + } + + try { + /** @var Asset $asset */ + $asset = Table::getInstance('asset'); + $result = $asset->loadByName($permission['component']); + + if ($result === false) { + $data = array($permission['action'] => array($permission['rule'] => $permission['value'])); + + $rules = new Rules($data); + $asset->rules = (string) $rules; + $asset->name = (string) $permission['component']; + $asset->title = (string) $permission['title']; + + // Get the parent asset id so we have a correct tree. + /** @var Asset $parentAsset */ + $parentAsset = Table::getInstance('Asset'); + + if (strpos($asset->name, '.') !== false) { + $assetParts = explode('.', $asset->name); + $parentAsset->loadByName($assetParts[0]); + $parentAssetId = $parentAsset->id; + } else { + $parentAssetId = $parentAsset->getRootId(); + } + + /** + * @todo: incorrect ACL stored + * When changing a permission of an item that doesn't have a row in the asset table the row a new row is created. + * This works fine for item <-> component <-> global config scenario and component <-> global config scenario. + * But doesn't work properly for item <-> section(s) <-> component <-> global config scenario, + * because a wrong parent asset id (the component) is stored. + * Happens when there is no row in the asset table (ex: deleted or not created on update). + */ + + $asset->setLocation($parentAssetId, 'last-child'); + } else { + // Decode the rule settings. + $temp = json_decode($asset->rules, true); + + // Check if a new value is to be set. + if (isset($permission['value'])) { + // Check if we already have an action entry. + if (!isset($temp[$permission['action']])) { + $temp[$permission['action']] = array(); + } + + // Check if we already have a rule entry. + if (!isset($temp[$permission['action']][$permission['rule']])) { + $temp[$permission['action']][$permission['rule']] = array(); + } + + // Set the new permission. + $temp[$permission['action']][$permission['rule']] = (int) $permission['value']; + + // Check if we have an inherited setting. + if ($permission['value'] === '') { + unset($temp[$permission['action']][$permission['rule']]); + } + + // Check if we have any rules. + if (!$temp[$permission['action']]) { + unset($temp[$permission['action']]); + } + } else { + // There is no value so remove the action as it's not needed. + unset($temp[$permission['action']]); + } + + $asset->rules = json_encode($temp, JSON_FORCE_OBJECT); + } + + if (!$asset->check() || !$asset->store()) { + $app->enqueueMessage(Text::_('JLIB_UNKNOWN'), 'error'); + + return false; + } + } catch (\Exception $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // All checks done. + $result = array( + 'text' => '', + 'class' => '', + 'result' => true, + ); + + // Show the current effective calculated permission considering current group, path and cascade. + + try { + // The database instance + $db = $this->getDatabase(); + + // Get the asset id by the name of the component. + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = :component') + ->bind(':component', $permission['component']); + + $db->setQuery($query); + + $assetId = (int) $db->loadResult(); + + // Fetch the parent asset id. + $parentAssetId = null; + + /** + * @todo: incorrect info + * When creating a new item (not saving) it uses the calculated permissions from the component (item <-> component <-> global config). + * But if we have a section too (item <-> section(s) <-> component <-> global config) this is not correct. + * Also, currently it uses the component permission, but should use the calculated permissions for a child of the component/section. + */ + + // If not in global config we need the parent_id asset to calculate permissions. + if (!$isGlobalConfig) { + // In this case we need to get the component rules too. + $query->clear() + ->select($db->quoteName('parent_id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('id') . ' = :assetid') + ->bind(':assetid', $assetId, ParameterType::INTEGER); + + $db->setQuery($query); + + $parentAssetId = (int) $db->loadResult(); + } + + // Get the group parent id of the current group. + $rule = (int) $permission['rule']; + $query->clear() + ->select($db->quoteName('parent_id')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :rule') + ->bind(':rule', $rule, ParameterType::INTEGER); + + $db->setQuery($query); + + $parentGroupId = (int) $db->loadResult(); + + // Count the number of child groups of the current group. + $query->clear() + ->select('COUNT(' . $db->quoteName('id') . ')') + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('parent_id') . ' = :rule') + ->bind(':rule', $rule, ParameterType::INTEGER); + + $db->setQuery($query); + + $totalChildGroups = (int) $db->loadResult(); + } catch (\Exception $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Clear access statistics. + Access::clearStatics(); + + // After current group permission is changed we need to check again if the group has Super User permissions. + $isSuperUserGroupAfter = Access::checkGroup($permission['rule'], 'core.admin'); + + // Get the rule for just this asset (non-recursive) and get the actual setting for the action for this group. + $assetRule = Access::getAssetRules($assetId, false, false)->allow($permission['action'], $permission['rule']); + + // Get the group, group parent id, and group global config recursive calculated permission for the chosen action. + $inheritedGroupRule = Access::checkGroup($permission['rule'], $permission['action'], $assetId); + + if (!empty($parentAssetId)) { + $inheritedGroupParentAssetRule = Access::checkGroup($permission['rule'], $permission['action'], $parentAssetId); + } else { + $inheritedGroupParentAssetRule = null; + } + + $inheritedParentGroupRule = !empty($parentGroupId) ? Access::checkGroup($parentGroupId, $permission['action'], $assetId) : null; + + // Current group is a Super User group, so calculated setting is "Allowed (Super User)". + if ($isSuperUserGroupAfter) { + $result['class'] = 'badge bg-success'; + $result['text'] = '' . Text::_('JLIB_RULES_ALLOWED_ADMIN'); + } else { + // Not super user. + // First get the real recursive calculated setting and add (Inherited) to it. + + // If recursive calculated setting is "Denied" or null. Calculated permission is "Not Allowed (Inherited)". + if ($inheritedGroupRule === null || $inheritedGroupRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_INHERITED'); + } else { + // If recursive calculated setting is "Allowed". Calculated permission is "Allowed (Inherited)". + $result['class'] = 'badge bg-success'; + $result['text'] = Text::_('JLIB_RULES_ALLOWED_INHERITED'); + } + + // Second part: Overwrite the calculated permissions labels if there is an explicit permission in the current group. + + /** + * @todo: incorrect info + * If a component has a permission that doesn't exists in global config (ex: frontend editing in com_modules) by default + * we get "Not Allowed (Inherited)" when we should get "Not Allowed (Default)". + */ + + // If there is an explicit permission "Not Allowed". Calculated permission is "Not Allowed". + if ($assetRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED'); + } elseif ($assetRule === true) { + // If there is an explicit permission is "Allowed". Calculated permission is "Allowed". + $result['class'] = 'badge bg-success'; + $result['text'] = Text::_('JLIB_RULES_ALLOWED'); + } + + // Third part: Overwrite the calculated permissions labels for special cases. + + // Global configuration with "Not Set" permission. Calculated permission is "Not Allowed (Default)". + if (empty($parentGroupId) && $isGlobalConfig === true && $assetRule === null) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_DEFAULT'); + } elseif ($inheritedGroupParentAssetRule === false || $inheritedParentGroupRule === false) { + /** + * Component/Item with explicit "Denied" permission at parent Asset (Category, Component or Global config) configuration. + * Or some parent group has an explicit "Denied". + * Calculated permission is "Not Allowed (Locked)". + */ + $result['class'] = 'badge bg-danger'; + $result['text'] = '' . Text::_('JLIB_RULES_NOT_ALLOWED_LOCKED'); + } + } + + // If removed or added super user from group, we need to refresh the page to recalculate all settings. + if ($isSuperUserGroupBefore != $isSuperUserGroupAfter) { + $app->enqueueMessage(Text::_('JLIB_RULES_NOTICE_RECALCULATE_GROUP_PERMISSIONS'), 'notice'); + } + + // If this group has child groups, we need to refresh the page to recalculate the child settings. + if ($totalChildGroups > 0) { + $app->enqueueMessage(Text::_('JLIB_RULES_NOTICE_RECALCULATE_GROUP_CHILDS_PERMISSIONS'), 'notice'); + } + + return $result; + } + + /** + * Method to send a test mail which is called via an AJAX request + * + * @return boolean + * + * @since 3.5 + */ + public function sendTestMail() + { + // Set the new values to test with the current settings + $app = Factory::getApplication(); + $user = Factory::getUser(); + $input = $app->input->json; + $smtppass = $input->get('smtppass', null, 'RAW'); + + $app->set('smtpauth', $input->get('smtpauth')); + $app->set('smtpuser', $input->get('smtpuser', '', 'STRING')); + $app->set('smtphost', $input->get('smtphost')); + $app->set('smtpsecure', $input->get('smtpsecure')); + $app->set('smtpport', $input->get('smtpport')); + $app->set('mailfrom', $input->get('mailfrom', '', 'STRING')); + $app->set('fromname', $input->get('fromname', '', 'STRING')); + $app->set('mailer', $input->get('mailer')); + $app->set('mailonline', $input->get('mailonline')); + + // Use smtppass only if it was submitted + if ($smtppass !== null) { + $app->set('smtppass', $smtppass); + } + + $mail = Factory::getMailer(); + + // Prepare email and try to send it + $mailer = new MailTemplate('com_config.test_mail', $user->getParam('language', $app->get('language')), $mail); + $mailer->addTemplateData( + array( + 'sitename' => $app->get('sitename'), + 'method' => Text::_('COM_CONFIG_SENDMAIL_METHOD_' . strtoupper($mail->Mailer)) + ) + ); + $mailer->addRecipient($app->get('mailfrom'), $app->get('fromname')); + + try { + $mailSent = $mailer->send(); + } catch (MailDisabledException | phpMailerException $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + if ($mailSent === true) { + $methodName = Text::_('COM_CONFIG_SENDMAIL_METHOD_' . strtoupper($mail->Mailer)); + + // If JMail send the mail using PHP Mail as fallback. + if ($mail->Mailer !== $app->get('mailer')) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_SENDMAIL_SUCCESS_FALLBACK', $app->get('mailfrom'), $methodName), 'warning'); + } else { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_SENDMAIL_SUCCESS', $app->get('mailfrom'), $methodName), 'message'); + } + + return true; + } + + $app->enqueueMessage(Text::_('COM_CONFIG_SENDMAIL_ERROR'), 'error'); + + return false; + } } diff --git a/code/administrator/components/com_config/src/Model/ComponentModel.php b/code/administrator/components/com_config/src/Model/ComponentModel.php index 34e97a70..acafcdb4 100644 --- a/code/administrator/components/com_config/src/Model/ComponentModel.php +++ b/code/administrator/components/com_config/src/Model/ComponentModel.php @@ -1,4 +1,5 @@ input; - - // Set the component (option) we are dealing with. - $component = $input->get('component'); - - $this->state->set('component.option', $component); - - // Set an alternative path for the configuration file. - if ($path = $input->getString('path')) - { - $path = Path::clean(JPATH_SITE . '/' . $path); - Path::check($path); - $this->state->set('component.path', $path); - } - } - - /** - * Method to get a form object. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return mixed A JForm object on success, false on failure - * - * @since 3.2 - */ - public function getForm($data = array(), $loadData = true) - { - $state = $this->getState(); - $option = $state->get('component.option'); - - if ($path = $state->get('component.path')) - { - // Add the search path for the admin component config.xml file. - Form::addFormPath($path); - } - else - { - // Add the search path for the admin component config.xml file. - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/' . $option); - } - - // Get the form. - $form = $this->loadForm( - 'com_config.component', - 'config', - array('control' => 'jform', 'load_data' => $loadData), - false, - '/config' - ); - - if (empty($form)) - { - return false; - } - - $lang = Factory::getLanguage(); - $lang->load($option, JPATH_BASE) - || $lang->load($option, JPATH_BASE . "/components/$option"); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - $option = $this->getState()->get('component.option'); - - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_config.edit.component.' . $option . '.data', []); - - if (empty($data)) - { - return $this->getComponent()->getParams()->toArray(); - } - - return $data; - } - - /** - * Get the component information. - * - * @return object - * - * @since 3.2 - */ - public function getComponent() - { - $state = $this->getState(); - $option = $state->get('component.option'); - - // Load common and local language files. - $lang = Factory::getLanguage(); - $lang->load($option, JPATH_BASE) - || $lang->load($option, JPATH_BASE . "/components/$option"); - - $result = ComponentHelper::getComponent($option); - - return $result; - } - - /** - * Method to save the configuration data. - * - * @param array $data An array containing all global config data. - * - * @return boolean True on success, false on failure. - * - * @since 3.2 - * @throws \RuntimeException - */ - public function save($data) - { - $table = Table::getInstance('extension'); - $context = $this->option . '.' . $this->name; - PluginHelper::importPlugin('extension'); - - // Check super user group. - if (isset($data['params']) && !Factory::getUser()->authorise('core.admin')) - { - $form = $this->getForm(array(), false); - - foreach ($form->getFieldsets() as $fieldset) - { - foreach ($form->getFieldset($fieldset->name) as $field) - { - if ($field->type === 'UserGroupList' && isset($data['params'][$field->fieldname]) - && (int) $field->getAttribute('checksuperusergroup', 0) === 1 - && Access::checkGroup($data['params'][$field->fieldname], 'core.admin')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED')); - } - } - } - } - - // Save the rules. - if (isset($data['params']) && isset($data['params']['rules'])) - { - if (!Factory::getUser()->authorise('core.admin', $data['option'])) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED')); - } - - $rules = new Rules($data['params']['rules']); - $asset = Table::getInstance('asset'); - - if (!$asset->loadByName($data['option'])) - { - $root = Table::getInstance('asset'); - $root->loadByName('root.1'); - $asset->name = $data['option']; - $asset->title = $data['option']; - $asset->setLocation($root->id, 'last-child'); - } - - $asset->rules = (string) $rules; - - if (!$asset->check() || !$asset->store()) - { - throw new \RuntimeException($asset->getError()); - } - - // We don't need this anymore - unset($data['option']); - unset($data['params']['rules']); - } - - // Load the previous Data - if (!$table->load($data['id'])) - { - throw new \RuntimeException($table->getError()); - } - - unset($data['id']); - - // Bind the data. - if (!$table->bind($data)) - { - throw new \RuntimeException($table->getError()); - } - - // Check the data. - if (!$table->check()) - { - throw new \RuntimeException($table->getError()); - } - - $result = Factory::getApplication()->triggerEvent('onExtensionBeforeSave', array($context, $table, false)); - - // Store the data. - if (in_array(false, $result, true) || !$table->store()) - { - throw new \RuntimeException($table->getError()); - } - - Factory::getApplication()->triggerEvent('onExtensionAfterSave', array($context, $table, false)); - - // Clean the component cache. - $this->cleanCache('_system'); - - return true; - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.2 + */ + protected function populateState() + { + $input = Factory::getApplication()->input; + + // Set the component (option) we are dealing with. + $component = $input->get('component'); + + $this->state->set('component.option', $component); + + // Set an alternative path for the configuration file. + if ($path = $input->getString('path')) { + $path = Path::clean(JPATH_SITE . '/' . $path); + Path::check($path); + $this->state->set('component.path', $path); + } + } + + /** + * Method to get a form object. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return mixed A JForm object on success, false on failure + * + * @since 3.2 + */ + public function getForm($data = array(), $loadData = true) + { + $state = $this->getState(); + $option = $state->get('component.option'); + + if ($path = $state->get('component.path')) { + // Add the search path for the admin component config.xml file. + Form::addFormPath($path); + } else { + // Add the search path for the admin component config.xml file. + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/' . $option); + } + + // Get the form. + $form = $this->loadForm( + 'com_config.component', + 'config', + array('control' => 'jform', 'load_data' => $loadData), + false, + '/config' + ); + + if (empty($form)) { + return false; + } + + $lang = Factory::getLanguage(); + $lang->load($option, JPATH_BASE) + || $lang->load($option, JPATH_BASE . "/components/$option"); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + $option = $this->getState()->get('component.option'); + + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_config.edit.component.' . $option . '.data', []); + + if (empty($data)) { + return $this->getComponent()->getParams()->toArray(); + } + + return $data; + } + + /** + * Get the component information. + * + * @return object + * + * @since 3.2 + */ + public function getComponent() + { + $state = $this->getState(); + $option = $state->get('component.option'); + + // Load common and local language files. + $lang = Factory::getLanguage(); + $lang->load($option, JPATH_BASE) + || $lang->load($option, JPATH_BASE . "/components/$option"); + + $result = ComponentHelper::getComponent($option); + + return $result; + } + + /** + * Method to save the configuration data. + * + * @param array $data An array containing all global config data. + * + * @return boolean True on success, false on failure. + * + * @since 3.2 + * @throws \RuntimeException + */ + public function save($data) + { + $table = Table::getInstance('extension'); + $context = $this->option . '.' . $this->name; + PluginHelper::importPlugin('extension'); + + // Check super user group. + if (isset($data['params']) && !Factory::getUser()->authorise('core.admin')) { + $form = $this->getForm(array(), false); + + foreach ($form->getFieldsets() as $fieldset) { + foreach ($form->getFieldset($fieldset->name) as $field) { + if ( + $field->type === 'UserGroupList' && isset($data['params'][$field->fieldname]) + && (int) $field->getAttribute('checksuperusergroup', 0) === 1 + && Access::checkGroup($data['params'][$field->fieldname], 'core.admin') + ) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED')); + } + } + } + } + + // Save the rules. + if (isset($data['params']) && isset($data['params']['rules'])) { + if (!Factory::getUser()->authorise('core.admin', $data['option'])) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED')); + } + + $rules = new Rules($data['params']['rules']); + $asset = Table::getInstance('asset'); + + if (!$asset->loadByName($data['option'])) { + $root = Table::getInstance('asset'); + $root->loadByName('root.1'); + $asset->name = $data['option']; + $asset->title = $data['option']; + $asset->setLocation($root->id, 'last-child'); + } + + $asset->rules = (string) $rules; + + if (!$asset->check() || !$asset->store()) { + throw new \RuntimeException($asset->getError()); + } + + // We don't need this anymore + unset($data['option']); + unset($data['params']['rules']); + } + + // Load the previous Data + if (!$table->load($data['id'])) { + throw new \RuntimeException($table->getError()); + } + + unset($data['id']); + + // Bind the data. + if (!$table->bind($data)) { + throw new \RuntimeException($table->getError()); + } + + // Check the data. + if (!$table->check()) { + throw new \RuntimeException($table->getError()); + } + + $result = Factory::getApplication()->triggerEvent('onExtensionBeforeSave', array($context, $table, false)); + + // Store the data. + if (in_array(false, $result, true) || !$table->store()) { + throw new \RuntimeException($table->getError()); + } + + Factory::getApplication()->triggerEvent('onExtensionAfterSave', array($context, $table, false)); + + // Clean the component cache. + $this->cleanCache('_system'); + + return true; + } } diff --git a/code/administrator/components/com_config/src/View/Application/HtmlView.php b/code/administrator/components/com_config/src/View/Application/HtmlView.php index c750bffa..a3cda985 100644 --- a/code/administrator/components/com_config/src/View/Application/HtmlView.php +++ b/code/administrator/components/com_config/src/View/Application/HtmlView.php @@ -1,4 +1,5 @@ get('form'); - $data = $this->get('data'); - $user = Factory::getUser(); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return; - } - - // Bind data - if ($form && $data) - { - $form->bind($data); - } - - // Get the params for com_users. - $usersParams = ComponentHelper::getParams('com_users'); - - // Get the params for com_media. - $mediaParams = ComponentHelper::getParams('com_media'); - - $this->form = &$form; - $this->data = &$data; - $this->usersParams = &$usersParams; - $this->mediaParams = &$mediaParams; - $this->components = ConfigHelper::getComponentsWithConfig(); - ConfigHelper::loadLanguageForComponents($this->components); - - $this->userIsSuperAdmin = $user->authorise('core.admin'); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.2 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_CONFIG_GLOBAL_CONFIGURATION'), 'cog config'); - ToolbarHelper::apply('application.apply'); - ToolbarHelper::divider(); - ToolbarHelper::save('application.save'); - ToolbarHelper::divider(); - ToolbarHelper::cancel('application.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::divider(); - ToolbarHelper::inlinehelp(); - ToolbarHelper::help('Site_Global_Configuration'); - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * @since 3.2 + */ + public $state; + + /** + * The form object + * + * @var \Joomla\CMS\Form\Form + * @since 3.2 + */ + public $form; + + /** + * The data to be displayed in the form + * + * @var array + * @since 3.2 + */ + public $data; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see \JViewLegacy::loadTemplate() + * @since 3.0 + */ + public function display($tpl = null) + { + try { + // Load Form and Data + $form = $this->get('form'); + $data = $this->get('data'); + $user = $this->getCurrentUser(); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return; + } + + // Bind data + if ($form && $data) { + $form->bind($data); + } + + // Get the params for com_users. + $usersParams = ComponentHelper::getParams('com_users'); + + // Get the params for com_media. + $mediaParams = ComponentHelper::getParams('com_media'); + + $this->form = &$form; + $this->data = &$data; + $this->usersParams = &$usersParams; + $this->mediaParams = &$mediaParams; + $this->components = ConfigHelper::getComponentsWithConfig(); + ConfigHelper::loadLanguageForComponents($this->components); + + $this->userIsSuperAdmin = $user->authorise('core.admin'); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.2 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_CONFIG_GLOBAL_CONFIGURATION'), 'cog config'); + ToolbarHelper::apply('application.apply'); + ToolbarHelper::divider(); + ToolbarHelper::save('application.save'); + ToolbarHelper::divider(); + ToolbarHelper::cancel('application.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::divider(); + ToolbarHelper::inlinehelp(); + ToolbarHelper::help('Site_Global_Configuration'); + } } diff --git a/code/administrator/components/com_config/src/View/Component/HtmlView.php b/code/administrator/components/com_config/src/View/Component/HtmlView.php index fa0996f9..e9568dfe 100644 --- a/code/administrator/components/com_config/src/View/Component/HtmlView.php +++ b/code/administrator/components/com_config/src/View/Component/HtmlView.php @@ -1,4 +1,5 @@ get('component'); - - if (!$component->enabled) - { - return; - } - - $form = $this->get('form'); - $user = Factory::getUser(); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return; - } - - $this->fieldsets = $form ? $form->getFieldsets() : null; - $this->formControl = $form ? $form->getFormControl() : null; - - // Don't show permissions fieldset if not authorised. - if (!$user->authorise('core.admin', $component->option) && isset($this->fieldsets['permissions'])) - { - unset($this->fieldsets['permissions']); - } - - $this->form = &$form; - $this->component = &$component; - - $this->components = ConfigHelper::getComponentsWithConfig(); - - $this->userIsSuperAdmin = $user->authorise('core.admin'); - $this->currentComponent = Factory::getApplication()->input->get('component'); - $this->return = Factory::getApplication()->input->get('return', '', 'base64'); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.2 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_($this->component->option . '_configuration'), 'cog config'); - ToolbarHelper::apply('component.apply'); - ToolbarHelper::divider(); - ToolbarHelper::save('component.save'); - ToolbarHelper::divider(); - ToolbarHelper::cancel('component.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::divider(); - - $inlinehelp = (string) $this->form->getXml()->config->inlinehelp['button'] == 'show' ?: false; - $targetClass = (string) $this->form->getXml()->config->inlinehelp['targetclass'] ?: 'hide-aware-inline-help'; - - if ($inlinehelp) - { - ToolbarHelper::inlinehelp($targetClass); - } - - $helpUrl = $this->form->getData()->get('helpURL'); - $helpKey = (string) $this->form->getXml()->config->help['key']; - - // Try with legacy language key - if (!$helpKey) - { - $language = Factory::getApplication()->getLanguage(); - $languageKey = 'JHELP_COMPONENTS_' . strtoupper($this->currentComponent) . '_OPTIONS'; - - if ($language->hasKey($languageKey)) - { - $helpKey = $languageKey; - } - } - - ToolbarHelper::help($helpKey, (boolean) $helpUrl, null, $this->currentComponent); - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * @since 3.2 + */ + public $state; + + /** + * The form object + * + * @var \Joomla\CMS\Form\Form + * @since 3.2 + */ + public $form; + + /** + * An object with the information for the component + * + * @var \Joomla\CMS\Component\ComponentRecord + * @since 3.2 + */ + public $component; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see \JViewLegacy::loadTemplate() + * @since 3.2 + */ + public function display($tpl = null) + { + try { + $component = $this->get('component'); + + if (!$component->enabled) { + return; + } + + $form = $this->get('form'); + $user = $this->getCurrentUser(); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return; + } + + $this->fieldsets = $form ? $form->getFieldsets() : null; + $this->formControl = $form ? $form->getFormControl() : null; + + // Don't show permissions fieldset if not authorised. + if (!$user->authorise('core.admin', $component->option) && isset($this->fieldsets['permissions'])) { + unset($this->fieldsets['permissions']); + } + + $this->form = &$form; + $this->component = &$component; + + $this->components = ConfigHelper::getComponentsWithConfig(); + + $this->userIsSuperAdmin = $user->authorise('core.admin'); + $this->currentComponent = Factory::getApplication()->input->get('component'); + $this->return = Factory::getApplication()->input->get('return', '', 'base64'); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.2 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_($this->component->option . '_configuration'), 'cog config'); + ToolbarHelper::apply('component.apply'); + ToolbarHelper::divider(); + ToolbarHelper::save('component.save'); + ToolbarHelper::divider(); + ToolbarHelper::cancel('component.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::divider(); + + $inlinehelp = (string) $this->form->getXml()->config->inlinehelp['button'] == 'show' ?: false; + $targetClass = (string) $this->form->getXml()->config->inlinehelp['targetclass'] ?: 'hide-aware-inline-help'; + + if ($inlinehelp) { + ToolbarHelper::inlinehelp($targetClass); + } + + $helpUrl = $this->form->getData()->get('helpURL'); + $helpKey = (string) $this->form->getXml()->config->help['key']; + + // Try with legacy language key + if (!$helpKey) { + $language = Factory::getApplication()->getLanguage(); + $languageKey = 'JHELP_COMPONENTS_' . strtoupper($this->currentComponent) . '_OPTIONS'; + + if ($language->hasKey($languageKey)) { + $helpKey = $languageKey; + } + } + + ToolbarHelper::help($helpKey, (bool) $helpUrl, null, $this->currentComponent); + } } diff --git a/code/administrator/components/com_config/tmpl/application/default.php b/code/administrator/components/com_config/tmpl/application/default.php index 9dba06f4..9cc769e0 100644 --- a/code/administrator/components/com_config/tmpl/application/default.php +++ b/code/administrator/components/com_config/tmpl/application/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); // Load JS message titles Text::script('ERROR'); @@ -26,56 +27,56 @@ ?>
-
- -
- 'page-site', 'recall' => true, 'breakpoint' => 768]); ?> - - loadTemplate('site'); ?> - loadTemplate('metadata'); ?> - loadTemplate('seo'); ?> - loadTemplate('cookie'); ?> - +
+ +
+ 'page-site', 'recall' => true, 'breakpoint' => 768]); ?> + + loadTemplate('site'); ?> + loadTemplate('metadata'); ?> + loadTemplate('seo'); ?> + loadTemplate('cookie'); ?> + - - loadTemplate('debug'); ?> - loadTemplate('cache'); ?> - loadTemplate('session'); ?> - + + loadTemplate('debug'); ?> + loadTemplate('cache'); ?> + loadTemplate('session'); ?> + - - loadTemplate('server'); ?> - loadTemplate('locale'); ?> - loadTemplate('webservices'); ?> - loadTemplate('proxy'); ?> - loadTemplate('database'); ?> - loadTemplate('mail'); ?> - + + loadTemplate('server'); ?> + loadTemplate('locale'); ?> + loadTemplate('webservices'); ?> + loadTemplate('proxy'); ?> + loadTemplate('database'); ?> + loadTemplate('mail'); ?> + - - loadTemplate('logging'); ?> - loadTemplate('logging_custom'); ?> - + + loadTemplate('logging'); ?> + loadTemplate('logging_custom'); ?> + - - loadTemplate('filters'); ?> - + + loadTemplate('filters'); ?> + - - loadTemplate('permissions'); ?> - - + + loadTemplate('permissions'); ?> + + - - -
-
+ + +
+
diff --git a/code/administrator/components/com_config/tmpl/application/default_cache.php b/code/administrator/components/com_config/tmpl/application/default_cache.php index 79d8bb00..621f93e6 100644 --- a/code/administrator/components/com_config/tmpl/application/default_cache.php +++ b/code/administrator/components/com_config/tmpl/application/default_cache.php @@ -1,4 +1,5 @@ document->getWebAssetManager() - ->useScript('webcomponent.field-send-test-mail'); + ->useScript('webcomponent.field-send-test-mail'); // Load JavaScript message titles Text::script('ERROR'); @@ -42,9 +43,9 @@ ?> - + - + diff --git a/code/administrator/components/com_config/tmpl/application/default_metadata.php b/code/administrator/components/com_config/tmpl/application/default_metadata.php index 912499a3..75f9eeb1 100644 --- a/code/administrator/components/com_config/tmpl/application/default_metadata.php +++ b/code/administrator/components/com_config/tmpl/application/default_metadata.php @@ -1,4 +1,5 @@ diff --git a/code/administrator/components/com_config/tmpl/application/default_permissions.php b/code/administrator/components/com_config/tmpl/application/default_permissions.php index 83cd3c43..929e62b1 100644 --- a/code/administrator/components/com_config/tmpl/application/default_permissions.php +++ b/code/administrator/components/com_config/tmpl/application/default_permissions.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('form.validate') - ->useScript('keepalive'); + ->useScript('keepalive'); -if ($this->fieldsets) -{ - HTMLHelper::_('bootstrap.framework'); +if ($this->fieldsets) { + HTMLHelper::_('bootstrap.framework'); } $xml = $this->form->getXml(); ?>
-
- - - - -
- fieldsets) : ?> - - - true, 'breakpoint' => 768]); ?> - - fieldsets as $name => $fieldSet) : ?> - xpath('//fieldset[@name="' . $name . '"]/fieldset'); - $hasParent = $xml->xpath('//fieldset/fieldset[@name="' . $name . '"]'); - $isGrandchild = $xml->xpath('//fieldset/fieldset/fieldset[@name="' . $name . '"]'); - ?> - - - showon)) : ?> - useScript('showon'); ?> - showon, $this->formControl)) . '\''; ?> - - - label) ? 'COM_CONFIG_' . $name . '_FIELDSET_LABEL' : $fieldSet->label; ?> - - -
- label); ?> -
- - - - 1) : ?> -
-
- - - - - - - - - - - - -
- label); ?> -
- - - - - description)) : ?> -
- - description); ?> -
- - - - form->renderFieldset($name, $name === 'permissions' ? ['hiddenLabel' => true, 'class' => 'revert-controls'] : []); ?> - - - -
-
- - - - - - 1) : ?> -
-
- - - - - - - -
- - -
- - - - - - - - - +
+ + + + +
+ fieldsets) : ?> + + + true, 'breakpoint' => 768]); ?> + + fieldsets as $name => $fieldSet) : ?> + xpath('//fieldset[@name="' . $name . '"]/fieldset'); + $hasParent = $xml->xpath('//fieldset/fieldset[@name="' . $name . '"]'); + $isGrandchild = $xml->xpath('//fieldset/fieldset/fieldset[@name="' . $name . '"]'); + ?> + + + showon)) : ?> + useScript('showon'); ?> + showon, $this->formControl)) . '\''; ?> + + + label) ? 'COM_CONFIG_' . $name . '_FIELDSET_LABEL' : $fieldSet->label; ?> + + +
+ label); ?> +
+ + + 1) : ?> +
+
+ + + + + + + + + + + +
+ label); ?> +
+ + + + + description)) : ?> +
+ + description); ?> +
+ + + + form->renderFieldset($name, $name === 'permissions' ? ['hiddenLabel' => true, 'class' => 'revert-controls'] : []); ?> + + + +
+
+ + + + + 1) : ?> +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + diff --git a/code/administrator/components/com_config/tmpl/component/default_navigation.php b/code/administrator/components/com_config/tmpl/component/default_navigation.php index 7f8f9a68..2eb65444 100644 --- a/code/administrator/components/com_config/tmpl/component/default_navigation.php +++ b/code/administrator/components/com_config/tmpl/component/default_navigation.php @@ -1,4 +1,5 @@ diff --git a/code/administrator/components/com_contact/contact.xml b/code/administrator/components/com_contact/contact.xml index 4a358a93..3c32d2eb 100644 --- a/code/administrator/components/com_contact/contact.xml +++ b/code/administrator/components/com_contact/contact.xml @@ -2,7 +2,7 @@ com_contact Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_contact/helpers/contact.php b/code/administrator/components/com_contact/helpers/contact.php index 26229413..a0fdb57c 100644 --- a/code/administrator/components/com_contact/helpers/contact.php +++ b/code/administrator/components/com_contact/helpers/contact.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Contact component helper. diff --git a/code/administrator/components/com_contact/services/provider.php b/code/administrator/components/com_contact/services/provider.php index d9d3d0c3..21f9104d 100644 --- a/code/administrator/components/com_contact/services/provider.php +++ b/code/administrator/components/com_contact/services/provider.php @@ -1,4 +1,5 @@ set(AssociationExtensionInterface::class, new AssociationsHelper); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->set(AssociationExtensionInterface::class, new AssociationsHelper()); - $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Contact')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Contact')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Contact')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Contact')); + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Contact')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Contact')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Contact')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Contact')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new ContactComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new ContactComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_contact/src/Controller/AjaxController.php b/code/administrator/components/com_contact/src/Controller/AjaxController.php index 1aff1047..85837cee 100644 --- a/code/administrator/components/com_contact/src/Controller/AjaxController.php +++ b/code/administrator/components/com_contact/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->getInt('assocId', 0); + /** + * Method to fetch associations of a contact + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the contact whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return void + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', (int) $assocId); + $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', (int) $assocId); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - $contactTable = $this->factory->createTable('Contact', 'Administrator'); + // Add the title to each of the associated records + $contactTable = $this->factory->createTable('Contact', 'Administrator'); - foreach ($associations as $lang => $association) - { - $contactTable->load($association->id); - $associations[$lang]->title = $contactTable->name; - } + foreach ($associations as $lang => $association) { + $contactTable->load($association->id); + $associations[$lang]->title = $contactTable->name; + } - $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/code/administrator/components/com_contact/src/Controller/ContactController.php b/code/administrator/components/com_contact/src/Controller/ContactController.php index 0817e424..0f85704c 100644 --- a/code/administrator/components/com_contact/src/Controller/ContactController.php +++ b/code/administrator/components/com_contact/src/Controller/ContactController.php @@ -1,4 +1,5 @@ input->getInt('filter_category_id'), 'int'); - - if ($categoryId) - { - // If the category has been passed in the URL check it. - return $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); - } - - // In the absence of better information, revert to the component permissions. - return parent::allowAdd($data); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - - // Since there is no asset tracking, fallback to the component permissions. - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Get the item. - $item = $this->getModel()->getItem($recordId); - - // Since there is no item, return false. - if (empty($item)) - { - return false; - } - - $user = $this->app->getIdentity(); - - // Check if can edit own core.edit.own. - $canEditOwn = $user->authorise('core.edit.own', $this->option . '.category.' . (int) $item->catid) && $item->created_by == $user->id; - - // Check the category core.edit permissions. - return $canEditOwn || $user->authorise('core.edit', $this->option . '.category.' . (int) $item->catid); - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - /** @var \Joomla\Component\Contact\Administrator\Model\ContactModel $model */ - $model = $this->getModel('Contact', 'Administrator', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_contact&view=contacts' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } - - /** - * Function that allows child controller access to model data - * after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 4.1.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = []) - { - if ($this->getTask() === 'save2menu') - { - $editState = []; - - $id = $model->getState('contact.id'); - - $link = 'index.php?option=com_contact&view=contact'; - $type = 'component'; - - $editState['id'] = $id; - $editState['link'] = $link; - $editState['title'] = $model->getItem($id)->name; - $editState['type'] = $type; - $editState['request']['id'] = $id; - - $this->app->setUserState( - 'com_menus.edit.item', - [ - 'data' => $editState, - 'type' => $type, - 'link' => $link, - ] - ); - - $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); - } - } + use VersionableControllerTrait; + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('filter_category_id'), 'int'); + + if ($categoryId) { + // If the category has been passed in the URL check it. + return $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); + } + + // In the absence of better information, revert to the component permissions. + return parent::allowAdd($data); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + + // Since there is no asset tracking, fallback to the component permissions. + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Get the item. + $item = $this->getModel()->getItem($recordId); + + // Since there is no item, return false. + if (empty($item)) { + return false; + } + + $user = $this->app->getIdentity(); + + // Check if can edit own core.edit.own. + $canEditOwn = $user->authorise('core.edit.own', $this->option . '.category.' . (int) $item->catid) && $item->created_by == $user->id; + + // Check the category core.edit permissions. + return $canEditOwn || $user->authorise('core.edit', $this->option . '.category.' . (int) $item->catid); + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + /** @var \Joomla\Component\Contact\Administrator\Model\ContactModel $model */ + $model = $this->getModel('Contact', 'Administrator', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_contact&view=contacts' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } + + /** + * Function that allows child controller access to model data + * after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 4.1.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = []) + { + if ($this->getTask() === 'save2menu') { + $editState = []; + + $id = $model->getState('contact.id'); + + $link = 'index.php?option=com_contact&view=contact'; + $type = 'component'; + + $editState['id'] = $id; + $editState['link'] = $link; + $editState['title'] = $model->getItem($id)->name; + $editState['type'] = $type; + $editState['request']['id'] = $id; + + $this->app->setUserState( + 'com_menus.edit.item', + [ + 'data' => $editState, + 'type' => $type, + 'link' => $link, + ] + ); + + $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); + } + } } diff --git a/code/administrator/components/com_contact/src/Controller/ContactsController.php b/code/administrator/components/com_contact/src/Controller/ContactsController.php index 953b454c..06e5aec9 100644 --- a/code/administrator/components/com_contact/src/Controller/ContactsController.php +++ b/code/administrator/components/com_contact/src/Controller/ContactsController.php @@ -1,4 +1,5 @@ registerTask('unfeatured', 'featured'); - } - - /** - * Method to toggle the featured setting of a list of contacts. - * - * @return void - * - * @since 1.6 - */ - public function featured() - { - // Check for request forgeries - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('featured' => 1, 'unfeatured' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - // Get the model. - /** @var \Joomla\Component\Contact\Administrator\Model\ContactModel $model */ - $model = $this->getModel(); - - // Access checks. - foreach ($ids as $i => $id) - { - // Remove zero value resulting from input filter - if ($id === 0) - { - unset($ids[$i]); - - continue; - } - - $item = $model->getItem($id); - - if (!$this->app->getIdentity()->authorise('core.edit.state', 'com_contact.category.' . (int) $item->catid)) - { - // Prune items that you can't change. - unset($ids[$i]); - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); - } - } - - if (empty($ids)) - { - $message = null; - - $this->app->enqueueMessage(Text::_('COM_CONTACT_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Publish the items. - if (!$model->featured($ids, $value)) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - - if ($value == 1) - { - $message = Text::plural('COM_CONTACT_N_ITEMS_FEATURED', count($ids)); - } - else - { - $message = Text::plural('COM_CONTACT_N_ITEMS_UNFEATURED', count($ids)); - } - } - - $this->setRedirect('index.php?option=com_contact&view=contacts', $message); - } - - /** - * Proxy for getModel. - * - * @param string $name The name of the model. - * @param string $prefix The prefix for the PHP class name. - * @param array $config Array of configuration parameters. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel - * - * @since 1.6 - */ - public function getModel($name = 'Contact', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('unfeatured', 'featured'); + } + + /** + * Method to toggle the featured setting of a list of contacts. + * + * @return void + * + * @since 1.6 + */ + public function featured() + { + // Check for request forgeries + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('featured' => 1, 'unfeatured' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + // Get the model. + /** @var \Joomla\Component\Contact\Administrator\Model\ContactModel $model */ + $model = $this->getModel(); + + // Access checks. + foreach ($ids as $i => $id) { + // Remove zero value resulting from input filter + if ($id === 0) { + unset($ids[$i]); + + continue; + } + + $item = $model->getItem($id); + + if (!$this->app->getIdentity()->authorise('core.edit.state', 'com_contact.category.' . (int) $item->catid)) { + // Prune items that you can't change. + unset($ids[$i]); + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); + } + } + + if (empty($ids)) { + $message = null; + + $this->app->enqueueMessage(Text::_('COM_CONTACT_NO_ITEM_SELECTED'), 'warning'); + } else { + // Publish the items. + if (!$model->featured($ids, $value)) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } + + if ($value == 1) { + $message = Text::plural('COM_CONTACT_N_ITEMS_FEATURED', count($ids)); + } else { + $message = Text::plural('COM_CONTACT_N_ITEMS_UNFEATURED', count($ids)); + } + } + + $this->setRedirect('index.php?option=com_contact&view=contacts', $message); + } + + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 1.6 + */ + public function getModel($name = 'Contact', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_contact/src/Controller/DisplayController.php b/code/administrator/components/com_contact/src/Controller/DisplayController.php index 81f436b1..e16b9fd9 100644 --- a/code/administrator/components/com_contact/src/Controller/DisplayController.php +++ b/code/administrator/components/com_contact/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', $this->default_view); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static |boolean This object to support chaining. False on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', $this->default_view); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'contact' && $layout == 'edit' && !$this->checkEditId('com_contact.edit.contact', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'contact' && $layout == 'edit' && !$this->checkEditId('com_contact.edit.contact', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_contact&view=contacts', false)); + $this->setRedirect(Route::_('index.php?option=com_contact&view=contacts', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/code/administrator/components/com_contact/src/Extension/ContactComponent.php b/code/administrator/components/com_contact/src/Extension/ContactComponent.php index 3bcf27ef..436c230c 100644 --- a/code/administrator/components/com_contact/src/Extension/ContactComponent.php +++ b/code/administrator/components/com_contact/src/Extension/ContactComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('contactadministrator', new AdministratorService); - $this->getRegistry()->register('contacticon', new Icon); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('contactadministrator', new AdministratorService()); + $this->getRegistry()->register('contacticon', new Icon($container->get(UserFactoryInterface::class))); + } - /** - * Returns a valid section for the given section. If it is not valid then null - * is returned. - * - * @param string $section The section to get the mapping for - * @param object $item The item - * - * @return string|null The new section - * - * @since 4.0.0 - */ - public function validateSection($section, $item = null) - { - if (Factory::getApplication()->isClient('site') && $section == 'contact' && $item instanceof Form) - { - // The contact form needs to be the mail section - $section = 'mail'; - } + /** + * Returns a valid section for the given section. If it is not valid then null + * is returned. + * + * @param string $section The section to get the mapping for + * @param object $item The item + * + * @return string|null The new section + * + * @since 4.0.0 + */ + public function validateSection($section, $item = null) + { + if (Factory::getApplication()->isClient('site') && $section == 'contact' && $item instanceof Form) { + // The contact form needs to be the mail section + $section = 'mail'; + } - if (Factory::getApplication()->isClient('site') && ($section === 'category' || $section === 'form')) - { - // The contact form needs to be the mail section - $section = 'contact'; - } + if (Factory::getApplication()->isClient('site') && ($section === 'category' || $section === 'form')) { + // The contact form needs to be the mail section + $section = 'contact'; + } - if ($section !== 'mail' && $section !== 'contact') - { - // We don't know other sections - return null; - } + if ($section !== 'mail' && $section !== 'contact') { + // We don't know other sections + return null; + } - return $section; - } + return $section; + } - /** - * Returns valid contexts - * - * @return array - * - * @since 4.0.0 - */ - public function getContexts(): array - { - Factory::getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); + /** + * Returns valid contexts + * + * @return array + * + * @since 4.0.0 + */ + public function getContexts(): array + { + Factory::getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); - $contexts = array( - 'com_contact.contact' => Text::_('COM_CONTACT_FIELDS_CONTEXT_CONTACT'), - 'com_contact.mail' => Text::_('COM_CONTACT_FIELDS_CONTEXT_MAIL'), - 'com_contact.categories' => Text::_('JCATEGORY') - ); + $contexts = array( + 'com_contact.contact' => Text::_('COM_CONTACT_FIELDS_CONTEXT_CONTACT'), + 'com_contact.mail' => Text::_('COM_CONTACT_FIELDS_CONTEXT_MAIL'), + 'com_contact.categories' => Text::_('JCATEGORY') + ); - return $contexts; - } + return $contexts; + } - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return ($section === 'category' ? 'categories' : 'contact_details'); - } + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return ($section === 'category' ? 'categories' : 'contact_details'); + } - /** - * Returns the state column for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getStateColumnForSection(string $section = null) - { - return 'published'; - } + /** + * Returns the state column for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getStateColumnForSection(string $section = null) + { + return 'published'; + } } diff --git a/code/administrator/components/com_contact/src/Field/Modal/ContactField.php b/code/administrator/components/com_contact/src/Field/Modal/ContactField.php index 1130bfe0..03e3008d 100644 --- a/code/administrator/components/com_contact/src/Field/Modal/ContactField.php +++ b/code/administrator/components/com_contact/src/Field/Modal/ContactField.php @@ -1,4 +1,5 @@ element['new'] == 'true'); - $allowEdit = ((string) $this->element['edit'] == 'true'); - $allowClear = ((string) $this->element['clear'] != 'false'); - $allowSelect = ((string) $this->element['select'] != 'false'); - $allowPropagate = ((string) $this->element['propagate'] == 'true'); - - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language - Factory::getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); - - // The active contact id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Contact_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Modal_Contact'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $allowNew = ((string) $this->element['new'] == 'true'); + $allowEdit = ((string) $this->element['edit'] == 'true'); + $allowClear = ((string) $this->element['clear'] != 'false'); + $allowSelect = ((string) $this->element['select'] != 'false'); + $allowPropagate = ((string) $this->element['propagate'] == 'true'); + + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language + Factory::getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); + + // The active contact id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Contact_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectContact_" . $this->id . " = function (id, title, object) { window.processModalSelect('Contact', '" . $this->id . "', id, title, '', object); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkContacts = 'index.php?option=com_contact&view=contacts&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $linkContact = 'index.php?option=com_contact&view=contact&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $modalTitle = Text::_('COM_CONTACT_SELECT_A_CONTACT'); - - if (isset($this->element['language'])) - { - $linkContacts .= '&forcedLanguage=' . $this->element['language']; - $linkContact .= '&forcedLanguage=' . $this->element['language']; - $modalTitle .= ' — ' . $this->element['label']; - } - - $urlSelect = $linkContacts . '&function=jSelectContact_' . $this->id; - $urlEdit = $linkContact . '&task=contact.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkContact . '&task=contact.add'; - - if ($value) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__contact_details')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $value, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - $title = empty($title) ? Text::_('COM_CONTACT_SELECT_A_CONTACT') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current contact display field. - $html = ''; - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select contact button - if ($allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New contact button - if ($allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit contact button - if ($allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear contact button - if ($allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate contact button - if ($allowPropagate && count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectContact_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - // Select contact modal - if ($allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New contact modal - if ($allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_CONTACT_NEW_CONTACT'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit contact modal. - if ($allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_CONTACT_EDIT_CONTACT'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Note: class='required' for client side validation. - $class = $this->required ? ' class="required modal-value"' : ''; - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.4 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkContacts = 'index.php?option=com_contact&view=contacts&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $linkContact = 'index.php?option=com_contact&view=contact&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $modalTitle = Text::_('COM_CONTACT_SELECT_A_CONTACT'); + + if (isset($this->element['language'])) { + $linkContacts .= '&forcedLanguage=' . $this->element['language']; + $linkContact .= '&forcedLanguage=' . $this->element['language']; + $modalTitle .= ' — ' . $this->element['label']; + } + + $urlSelect = $linkContacts . '&function=jSelectContact_' . $this->id; + $urlEdit = $linkContact . '&task=contact.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkContact . '&task=contact.add'; + + if ($value) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__contact_details')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $value, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + $title = empty($title) ? Text::_('COM_CONTACT_SELECT_A_CONTACT') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current contact display field. + $html = ''; + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select contact button + if ($allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New contact button + if ($allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit contact button + if ($allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear contact button + if ($allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate contact button + if ($allowPropagate && count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectContact_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + // Select contact modal + if ($allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New contact modal + if ($allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_CONTACT_NEW_CONTACT'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit contact modal. + if ($allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_CONTACT_EDIT_CONTACT'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Note: class='required' for client side validation. + $class = $this->required ? ' class="required modal-value"' : ''; + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.4 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/code/administrator/components/com_contact/src/Helper/AssociationsHelper.php b/code/administrator/components/com_contact/src/Helper/AssociationsHelper.php index 87551616..b3279ebf 100644 --- a/code/administrator/components/com_contact/src/Helper/AssociationsHelper.php +++ b/code/administrator/components/com_contact/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getType($typeName); - - $context = $this->extension . '.item'; - $catidField = 'catid'; - - if ($typeName === 'category') - { - $context = 'com_categories.item'; - $catidField = ''; - } - - // Get the associations. - $associations = Associations::getAssociations( - $this->extension, - $type['tables']['a'], - $context, - $id, - 'id', - 'alias', - $catidField - ); - - return $associations; - } - - /** - * Get item information - * - * @param string $typeName The item type - * @param int $id The id of item for which we need the associated items - * - * @return Table|null - * - * @since 3.7.0 - */ - public function getItem($typeName, $id) - { - if (empty($id)) - { - return null; - } - - $table = null; - - switch ($typeName) - { - case 'contact': - $table = Table::getInstance('ContactTable', 'Joomla\\Component\\Contact\\Administrator\\Table\\'); - break; - - case 'category': - $table = Table::getInstance('Category'); - break; - } - - if (empty($table)) - { - return null; - } - - $table->load($id); - - return $table; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - if (in_array($typeName, $this->itemTypes)) - { - switch ($typeName) - { - case 'contact': - $fields['title'] = 'a.name'; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['category'] = true; - $support['save2copy'] = true; - - $tables = array( - 'a' => '#__contact_details' - ); - - $title = 'contact'; - break; - - case 'category': - $fields['created_user_id'] = 'a.created_user_id'; - $fields['ordering'] = 'a.lft'; - $fields['level'] = 'a.level'; - $fields['catid'] = ''; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['level'] = true; - - $tables = array( - 'a' => '#__categories' - ); - - $title = 'category'; - break; - } - } - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_contact'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array('contact', 'category'); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = true; + + /** + * Method to get the associations for a given item. + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 4.0.0 + */ + public function getAssociationsForItem($id = 0, $view = null) + { + return AssociationHelper::getAssociations($id, $view); + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociations($typeName, $id) + { + $type = $this->getType($typeName); + + $context = $this->extension . '.item'; + $catidField = 'catid'; + + if ($typeName === 'category') { + $context = 'com_categories.item'; + $catidField = ''; + } + + // Get the associations. + $associations = Associations::getAssociations( + $this->extension, + $type['tables']['a'], + $context, + $id, + 'id', + 'alias', + $catidField + ); + + return $associations; + } + + /** + * Get item information + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return Table|null + * + * @since 3.7.0 + */ + public function getItem($typeName, $id) + { + if (empty($id)) { + return null; + } + + $table = null; + + switch ($typeName) { + case 'contact': + $table = Table::getInstance('ContactTable', 'Joomla\\Component\\Contact\\Administrator\\Table\\'); + break; + + case 'category': + $table = Table::getInstance('Category'); + break; + } + + if (empty($table)) { + return null; + } + + $table->load($id); + + return $table; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + if (in_array($typeName, $this->itemTypes)) { + switch ($typeName) { + case 'contact': + $fields['title'] = 'a.name'; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['category'] = true; + $support['save2copy'] = true; + + $tables = array( + 'a' => '#__contact_details' + ); + + $title = 'contact'; + break; + + case 'category': + $fields['created_user_id'] = 'a.created_user_id'; + $fields['ordering'] = 'a.lft'; + $fields['level'] = 'a.level'; + $fields['catid'] = ''; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['level'] = true; + + $tables = array( + 'a' => '#__categories' + ); + + $title = 'category'; + break; + } + } + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } } diff --git a/code/administrator/components/com_contact/src/Helper/ContactHelper.php b/code/administrator/components/com_contact/src/Helper/ContactHelper.php index 8928555c..9c13f39d 100644 --- a/code/administrator/components/com_contact/src/Helper/ContactHelper.php +++ b/code/administrator/components/com_contact/src/Helper/ContactHelper.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage', - 'tag' => 'batchTag', - 'user_id' => 'batchUser', - ); - - /** - * Name of the form - * - * @var string - * @since 4.0.0 - */ - protected $formName = 'contact'; - - /** - * Batch change a linked user. - * - * @param integer $value The new value matching a User ID. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - protected function batchUser($value, $pks, $contexts) - { - foreach ($pks as $pk) - { - if ($this->user->authorise('core.edit', $contexts[$pk])) - { - $this->table->reset(); - $this->table->load($pk); - $this->table->user_id = (int) $value; - - if (!$this->table->store()) - { - $this->setError($this->table->getError()); - - return false; - } - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', 'com_contact.category.' . (int) $record->catid); - } - - /** - * Method to test whether a record can have its state edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - // Check against the category. - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.edit.state', 'com_contact.category.' . (int) $record->catid); - } - - // Default to component settings if category not known. - return parent::canEditState($record); - } - - /** - * Method to get the row form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - Form::addFieldPath(JPATH_ADMINISTRATOR . '/components/com_users/models/fields'); - - // Get the form. - $form = $this->loadForm('com_contact.' . $this->formName, $this->formName, array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('featured', 'disabled', 'true'); - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('featured', 'filter', 'unset'); - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - if ($item = parent::getItem($pk)) - { - // Convert the metadata field to an array. - $registry = new Registry($item->metadata); - $item->metadata = $registry->toArray(); - } - - // Load associated contact items - $assoc = Associations::isEnabled(); - - if ($assoc) - { - $item->associations = array(); - - if ($item->id != null) - { - $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $item->id); - - foreach ($associations as $tag => $association) - { - $item->associations[$tag] = $association->id; - } - } - } - - // Load item tags - if (!empty($item->id)) - { - $item->tags = new TagsHelper; - $item->tags->getTagIds($item->id, 'com_contact.contact'); - } - - return $item; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $app = Factory::getApplication(); - - // Check the session for previously entered form data. - $data = $app->getUserState('com_contact.edit.contact.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Prime some default values. - if ($this->getState('contact.id') == 0) - { - $data->set('catid', $app->input->get('catid', $app->getUserState('com_contact.contacts.filter.category_id'), 'int')); - } - } - - $this->preprocessData('com_contact.contact', $data); - - return $data; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 3.0 - */ - public function save($data) - { - $input = Factory::getApplication()->input; - - // Create new category, if needed. - $createCategory = true; - - // If category ID is provided, check if it's valid. - if (is_numeric($data['catid']) && $data['catid']) - { - $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_contact'); - } - - // Save New Category - if ($createCategory && $this->canCreateCategory()) - { - $category = [ - // Remove #new# prefix, if exists. - 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], - 'parent_id' => 1, - 'extension' => 'com_contact', - 'language' => $data['language'], - 'published' => 1, - ]; - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - - // Create new category. - if (!$categoryModel->save($category)) - { - $this->setError($categoryModel->getError()); - - return false; - } - - // Get the Category ID. - $data['catid'] = $categoryModel->getState('category.id'); - } - - // Alter the name for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['name'] == $origTable->name) - { - list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); - $data['name'] = $name; - $data['alias'] = $alias; - } - else - { - if ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - $data['published'] = 0; - } - - $links = array('linka', 'linkb', 'linkc', 'linkd', 'linke'); - - foreach ($links as $link) - { - if (!empty($data['params'][$link])) - { - $data['params'][$link] = PunycodeHelper::urlToPunycode($data['params'][$link]); - } - } - - return parent::save($data); - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param \Joomla\CMS\Table\Table $table The Table object - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - $date = Factory::getDate()->toSql(); - - $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); - - $table->generateAlias(); - - if (empty($table->id)) - { - // Set the values - $table->created = $date; - - // Set ordering to the last item if not set - if (empty($table->ordering)) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('MAX(ordering)') - ->from($db->quoteName('#__contact_details')); - $db->setQuery($query); - $max = $db->loadResult(); - - $table->ordering = $max + 1; - } - } - else - { - // Set the values - $table->modified = $date; - $table->modified_by = Factory::getUser()->id; - } - - // Increment the content version number. - $table->version++; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param \Joomla\CMS\Table\Table $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('catid') . ' = ' . (int) $table->catid, - ]; - } - - /** - * Preprocess the form. - * - * @param Form $form Form object. - * @param object $data Data object. - * @param string $group Group name. - * - * @return void - * - * @since 3.0.3 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - if ($this->canCreateCategory()) - { - $form->setFieldAttribute('catid', 'allowAdd', 'true'); - - // Add a prefix for categories created on the fly. - $form->setFieldAttribute('catid', 'customPrefix', '#new#'); - } - - // Association contact items - if (Associations::isEnabled()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (count($languages) > 1) - { - $addform = new \SimpleXMLElement('
'); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_contact'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - } - - $form->load($addform, false); - } - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to toggle the featured setting of contacts. - * - * @param array $pks The ids of the items to toggle. - * @param integer $value The value to toggle to. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function featured($pks, $value = 0) - { - // Sanitize the ids. - $pks = ArrayHelper::toInteger((array) $pks); - - if (empty($pks)) - { - $this->setError(Text::_('COM_CONTACT_NO_ITEM_SELECTED')); - - return false; - } - - $table = $this->getTable(); - - try - { - $db = $this->getDbo(); - - $query = $db->getQuery(true); - $query->update($db->quoteName('#__contact_details')); - $query->set($db->quoteName('featured') . ' = :featured'); - $query->whereIn($db->quoteName('id'), $pks); - $query->bind(':featured', $value, ParameterType::INTEGER); - - $db->setQuery($query); - - $db->execute(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $table->reorder(); - - // Clean component's cache - $this->cleanCache(); - - return true; - } - - /** - * Is the user allowed to create an on the fly category? - * - * @return boolean - * - * @since 3.6.1 - */ - private function canCreateCategory() - { - return Factory::getUser()->authorise('core.create', 'com_contact'); - } + use VersionableModelTrait; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_contact.contact'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_contact.item'; + + /** + * Batch copy/move command. If set to false, the batch copy/move command is not supported + * + * @var string + */ + protected $batch_copymove = 'category_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage', + 'tag' => 'batchTag', + 'user_id' => 'batchUser', + ); + + /** + * Name of the form + * + * @var string + * @since 4.0.0 + */ + protected $formName = 'contact'; + + /** + * Batch change a linked user. + * + * @param integer $value The new value matching a User ID. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + protected function batchUser($value, $pks, $contexts) + { + foreach ($pks as $pk) { + if ($this->user->authorise('core.edit', $contexts[$pk])) { + $this->table->reset(); + $this->table->load($pk); + $this->table->user_id = (int) $value; + + if (!$this->table->store()) { + $this->setError($this->table->getError()); + + return false; + } + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + return Factory::getUser()->authorise('core.delete', 'com_contact.category.' . (int) $record->catid); + } + + /** + * Method to test whether a record can have its state edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + // Check against the category. + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.edit.state', 'com_contact.category.' . (int) $record->catid); + } + + // Default to component settings if category not known. + return parent::canEditState($record); + } + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + Form::addFieldPath(JPATH_ADMINISTRATOR . '/components/com_users/models/fields'); + + // Get the form. + $form = $this->loadForm('com_contact.' . $this->formName, $this->formName, array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('featured', 'disabled', 'true'); + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('featured', 'filter', 'unset'); + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + if ($item = parent::getItem($pk)) { + // Convert the metadata field to an array. + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + } + + // Load associated contact items + $assoc = Associations::isEnabled(); + + if ($assoc) { + $item->associations = array(); + + if ($item->id != null) { + $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $item->id); + + foreach ($associations as $tag => $association) { + $item->associations[$tag] = $association->id; + } + } + } + + // Load item tags + if (!empty($item->id)) { + $item->tags = new TagsHelper(); + $item->tags->getTagIds($item->id, 'com_contact.contact'); + } + + return $item; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $app = Factory::getApplication(); + + // Check the session for previously entered form data. + $data = $app->getUserState('com_contact.edit.contact.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Prime some default values. + if ($this->getState('contact.id') == 0) { + $data->set('catid', $app->input->get('catid', $app->getUserState('com_contact.contacts.filter.category_id'), 'int')); + } + } + + $this->preprocessData('com_contact.contact', $data); + + return $data; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 3.0 + */ + public function save($data) + { + $input = Factory::getApplication()->input; + + // Create new category, if needed. + $createCategory = true; + + // If category ID is provided, check if it's valid. + if (is_numeric($data['catid']) && $data['catid']) { + $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_contact'); + } + + // Save New Category + if ($createCategory && $this->canCreateCategory()) { + $category = [ + // Remove #new# prefix, if exists. + 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], + 'parent_id' => 1, + 'extension' => 'com_contact', + 'language' => $data['language'], + 'published' => 1, + ]; + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + + // Create new category. + if (!$categoryModel->save($category)) { + $this->setError($categoryModel->getError()); + + return false; + } + + // Get the Category ID. + $data['catid'] = $categoryModel->getState('category.id'); + } + + // Alter the name for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['name'] == $origTable->name) { + list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); + $data['name'] = $name; + $data['alias'] = $alias; + } else { + if ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + $data['published'] = 0; + } + + $links = array('linka', 'linkb', 'linkc', 'linkd', 'linke'); + + foreach ($links as $link) { + if (!empty($data['params'][$link])) { + $data['params'][$link] = PunycodeHelper::urlToPunycode($data['params'][$link]); + } + } + + return parent::save($data); + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param \Joomla\CMS\Table\Table $table The Table object + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + $date = Factory::getDate()->toSql(); + + $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); + + $table->generateAlias(); + + if (empty($table->id)) { + // Set the values + $table->created = $date; + + // Set ordering to the last item if not set + if (empty($table->ordering)) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('MAX(ordering)') + ->from($db->quoteName('#__contact_details')); + $db->setQuery($query); + $max = $db->loadResult(); + + $table->ordering = $max + 1; + } + } else { + // Set the values + $table->modified = $date; + $table->modified_by = Factory::getUser()->id; + } + + // Increment the content version number. + $table->version++; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param \Joomla\CMS\Table\Table $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, + ]; + } + + /** + * Preprocess the form. + * + * @param Form $form Form object. + * @param object $data Data object. + * @param string $group Group name. + * + * @return void + * + * @since 3.0.3 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + if ($this->canCreateCategory()) { + $form->setFieldAttribute('catid', 'allowAdd', 'true'); + + // Add a prefix for categories created on the fly. + $form->setFieldAttribute('catid', 'customPrefix', '#new#'); + } + + // Association contact items + if (Associations::isEnabled()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_contact'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + } + + $form->load($addform, false); + } + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to toggle the featured setting of contacts. + * + * @param array $pks The ids of the items to toggle. + * @param integer $value The value to toggle to. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function featured($pks, $value = 0) + { + // Sanitize the ids. + $pks = ArrayHelper::toInteger((array) $pks); + + if (empty($pks)) { + $this->setError(Text::_('COM_CONTACT_NO_ITEM_SELECTED')); + + return false; + } + + $table = $this->getTable(); + + try { + $db = $this->getDatabase(); + + $query = $db->getQuery(true); + $query->update($db->quoteName('#__contact_details')); + $query->set($db->quoteName('featured') . ' = :featured'); + $query->whereIn($db->quoteName('id'), $pks); + $query->bind(':featured', $value, ParameterType::INTEGER); + + $db->setQuery($query); + + $db->execute(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $table->reorder(); + + // Clean component's cache + $this->cleanCache(); + + return true; + } + + /** + * Is the user allowed to create an on the fly category? + * + * @return boolean + * + * @since 3.6.1 + */ + private function canCreateCategory() + { + return Factory::getUser()->authorise('core.create', 'com_contact'); + } } diff --git a/code/administrator/components/com_contact/src/Model/ContactsModel.php b/code/administrator/components/com_contact/src/Model/ContactsModel.php index edd39679..471b6e2e 100644 --- a/code/administrator/components/com_contact/src/Model/ContactsModel.php +++ b/code/administrator/components/com_contact/src/Model/ContactsModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . serialize($this->getState('filter.category_id')); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . serialize($this->getState('filter.tag')); - $id .= ':' . $this->getState('filter.level'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select( - $db->quoteName( - explode( - ', ', - $this->getState( - 'list.select', - 'a.id, a.name, a.alias, a.checked_out, a.checked_out_time, a.catid, a.user_id' . - ', a.published, a.access, a.created, a.created_by, a.ordering, a.featured, a.language' . - ', a.publish_up, a.publish_down' - ) - ) - ) - ); - $query->from($db->quoteName('#__contact_details', 'a')); - - // Join over the users for the linked user. - $query->select( - array( - $db->quoteName('ul.name', 'linked_user'), - $db->quoteName('ul.email') - ) - ) - ->join( - 'LEFT', - $db->quoteName('#__users', 'ul') . ' ON ' . $db->quoteName('ul.id') . ' = ' . $db->quoteName('a.user_id') - ); - - // Join over the language - $query->select($db->quoteName('l.title', 'language_title')) - ->select($db->quoteName('l.image', 'language_image')) - ->join( - 'LEFT', - $db->quoteName('#__languages', 'l') . ' ON ' . $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language') - ); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join( - 'LEFT', - $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out') - ); - - // Join over the asset groups. - $query->select($db->quoteName('ag.title', 'access_level')) - ->join( - 'LEFT', - $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access') - ); - - // Join over the categories. - $query->select($db->quoteName('c.title', 'category_title')) - ->join( - 'LEFT', - $db->quoteName('#__categories', 'c') . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid') - ); - - // Join over the associations. - if (Associations::isEnabled()) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') - ->from($db->quoteName('#__associations', 'asso1')) - ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) - ->where( - [ - $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('asso1.context') . ' = ' . $db->quote('com_contact.item'), - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); - } - - // Filter by featured. - $featured = (string) $this->getState('filter.featured'); - - if (in_array($featured, ['0','1'])) - { - $query->where($db->quoteName('a.featured') . ' = ' . (int) $featured); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access'); - $query->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $query->whereIn($db->quoteName('a.access'), $user->getAuthorisedViewLevels()); - } - - // Filter by published state - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $query->where($db->quoteName('a.published') . ' = :published'); - $query->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where('(' . $db->quoteName('a.published') . ' = 0 OR ' . $db->quoteName('a.published') . ' = 1)'); - } - - // Filter by search in name. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . trim($search) . '%'; - $query->where( - '(' . $db->quoteName('a.name') . ' LIKE :name OR ' . $db->quoteName('a.alias') . ' LIKE :alias)' - ); - $query->bind(':name', $search); - $query->bind(':alias', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language'); - $query->bind(':language', $language); - } - - // Filter by a single or group of tags. - $tag = $this->getState('filter.tag'); - - // Run simplified query when filtering by one tag. - if (\is_array($tag) && \count($tag) === 1) - { - $tag = $tag[0]; - } - - if ($tag && \is_array($tag)) - { - $tag = ArrayHelper::toInteger($tag); - - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', - $db->quoteName('type_alias') . ' = ' . $db->quote('com_contact.contact'), - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ); - } - elseif ($tag = (int) $tag) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->where( - [ - $db->quoteName('tagmap.tag_id') . ' = :tag', - $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_contact.contact'), - ] - ) - ->bind(':tag', $tag, ParameterType::INTEGER); - } - - // Filter by categories and by level - $categoryId = $this->getState('filter.category_id', array()); - $level = $this->getState('filter.level'); - - if (!is_array($categoryId)) - { - $categoryId = $categoryId ? array($categoryId) : array(); - } - - // Case: Using both categories filter and by level filter - if (count($categoryId)) - { - $categoryId = ArrayHelper::toInteger($categoryId); - $categoryTable = Table::getInstance('Category', 'JTable'); - $subCatItemsWhere = array(); - - // @todo: Convert to prepared statement - foreach ($categoryId as $filter_catid) - { - $categoryTable->load($filter_catid); - $subCatItemsWhere[] = '(' . - ($level ? 'c.level <= ' . ((int) $level + (int) $categoryTable->level - 1) . ' AND ' : '') . - 'c.lft >= ' . (int) $categoryTable->lft . ' AND ' . - 'c.rgt <= ' . (int) $categoryTable->rgt . ')'; - } - - $query->where('(' . implode(' OR ', $subCatItemsWhere) . ')'); - } - - // Case: Using only the by level filter - elseif ($level) - { - $query->where($db->quoteName('c.level') . ' <= :level'); - $query->bind(':level', $level, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'a.name'); - $orderDirn = $this->state->get('list.direction', 'asc'); - - if ($orderCol == 'a.ordering' || $orderCol == 'category_title') - { - $orderCol = $db->quoteName('c.title') . ' ' . $orderDirn . ', ' . $db->quoteName('a.ordering'); - } - - $query->order($db->escape($orderCol . ' ' . $orderDirn)); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_id', 'category_title', + 'user_id', 'a.user_id', + 'published', 'a.published', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'created_by', 'a.created_by', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'language', 'a.language', 'language_title', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'ul.name', 'linked_user', + 'tag', + 'level', 'c.level', + ); + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . serialize($this->getState('filter.tag')); + $id .= ':' . $this->getState('filter.level'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select( + $db->quoteName( + explode( + ', ', + $this->getState( + 'list.select', + 'a.id, a.name, a.alias, a.checked_out, a.checked_out_time, a.catid, a.user_id' . + ', a.published, a.access, a.created, a.created_by, a.ordering, a.featured, a.language' . + ', a.publish_up, a.publish_down' + ) + ) + ) + ); + $query->from($db->quoteName('#__contact_details', 'a')); + + // Join over the users for the linked user. + $query->select( + array( + $db->quoteName('ul.name', 'linked_user'), + $db->quoteName('ul.email') + ) + ) + ->join( + 'LEFT', + $db->quoteName('#__users', 'ul') . ' ON ' . $db->quoteName('ul.id') . ' = ' . $db->quoteName('a.user_id') + ); + + // Join over the language + $query->select($db->quoteName('l.title', 'language_title')) + ->select($db->quoteName('l.image', 'language_image')) + ->join( + 'LEFT', + $db->quoteName('#__languages', 'l') . ' ON ' . $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language') + ); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out') + ); + + // Join over the asset groups. + $query->select($db->quoteName('ag.title', 'access_level')) + ->join( + 'LEFT', + $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access') + ); + + // Join over the categories. + $query->select($db->quoteName('c.title', 'category_title')) + ->join( + 'LEFT', + $db->quoteName('#__categories', 'c') . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid') + ); + + // Join over the associations. + if (Associations::isEnabled()) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') + ->from($db->quoteName('#__associations', 'asso1')) + ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) + ->where( + [ + $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('asso1.context') . ' = ' . $db->quote('com_contact.item'), + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); + } + + // Filter by featured. + $featured = (string) $this->getState('filter.featured'); + + if (in_array($featured, ['0','1'])) { + $query->where($db->quoteName('a.featured') . ' = ' . (int) $featured); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access'); + $query->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $query->whereIn($db->quoteName('a.access'), $user->getAuthorisedViewLevels()); + } + + // Filter by published state + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $query->where($db->quoteName('a.published') . ' = :published'); + $query->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where('(' . $db->quoteName('a.published') . ' = 0 OR ' . $db->quoteName('a.published') . ' = 1)'); + } + + // Filter by search in name. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $search, ParameterType::INTEGER); + } else { + $search = '%' . trim($search) . '%'; + $query->where( + '(' . $db->quoteName('a.name') . ' LIKE :name OR ' . $db->quoteName('a.alias') . ' LIKE :alias)' + ); + $query->bind(':name', $search); + $query->bind(':alias', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language'); + $query->bind(':language', $language); + } + + // Filter by a single or group of tags. + $tag = $this->getState('filter.tag'); + + // Run simplified query when filtering by one tag. + if (\is_array($tag) && \count($tag) === 1) { + $tag = $tag[0]; + } + + if ($tag && \is_array($tag)) { + $tag = ArrayHelper::toInteger($tag); + + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', + $db->quoteName('type_alias') . ' = ' . $db->quote('com_contact.contact'), + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ); + } elseif ($tag = (int) $tag) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->where( + [ + $db->quoteName('tagmap.tag_id') . ' = :tag', + $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_contact.contact'), + ] + ) + ->bind(':tag', $tag, ParameterType::INTEGER); + } + + // Filter by categories and by level + $categoryId = $this->getState('filter.category_id', array()); + $level = $this->getState('filter.level'); + + if (!is_array($categoryId)) { + $categoryId = $categoryId ? array($categoryId) : array(); + } + + // Case: Using both categories filter and by level filter + if (count($categoryId)) { + $categoryId = ArrayHelper::toInteger($categoryId); + $categoryTable = Table::getInstance('Category', 'JTable'); + $subCatItemsWhere = array(); + + // @todo: Convert to prepared statement + foreach ($categoryId as $filter_catid) { + $categoryTable->load($filter_catid); + $subCatItemsWhere[] = '(' . + ($level ? 'c.level <= ' . ((int) $level + (int) $categoryTable->level - 1) . ' AND ' : '') . + 'c.lft >= ' . (int) $categoryTable->lft . ' AND ' . + 'c.rgt <= ' . (int) $categoryTable->rgt . ')'; + } + + $query->where('(' . implode(' OR ', $subCatItemsWhere) . ')'); + } elseif ($level) { + // Case: Using only the by level filter + $query->where($db->quoteName('c.level') . ' <= :level'); + $query->bind(':level', $level, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'a.name'); + $orderDirn = $this->state->get('list.direction', 'asc'); + + if ($orderCol == 'a.ordering' || $orderCol == 'category_title') { + $orderCol = $db->quoteName('c.title') . ' ' . $orderDirn . ', ' . $db->quoteName('a.ordering'); + } + + $query->order($db->escape($orderCol . ' ' . $orderDirn)); + + return $query; + } } diff --git a/code/administrator/components/com_contact/src/Service/HTML/AdministratorService.php b/code/administrator/components/com_contact/src/Service/HTML/AdministratorService.php index 7255f693..6f9ad1e0 100644 --- a/code/administrator/components/com_contact/src/Service/HTML/AdministratorService.php +++ b/code/administrator/components/com_contact/src/Service/HTML/AdministratorService.php @@ -1,4 +1,5 @@ $associated) - { - $associations[$tag] = (int) $associated->id; - } - - // Get the associated contact items - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('c.id'), - $db->quoteName('c.name', 'title'), - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('lang_code'), - $db->quoteName('cat.title', 'category_title'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__contact_details', 'c')) - ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) - ->whereIn($db->quoteName('c.id'), array_values($associations)) - ->where($db->quoteName('c.id') . ' != :id') - ->bind(':id', $contactid, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500, $e); - } - - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); - - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_contact&task=contact.edit&id=' . (int) $item->id); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); - $classes = 'badge bg-secondary'; - - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } - - $html = LayoutHelper::render('joomla.content.associations', $items); - } - - return $html; - } - - /** - * Show the featured/not-featured icon. - * - * @param integer $value The featured value. - * @param integer $i Id of the item. - * @param boolean $canChange Whether the value can be changed or not. - * - * @return string The anchor tag to toggle featured/unfeatured contacts. - * - * @since 1.6 - */ - public function featured($value, $i, $canChange = true) - { - // Array of image, task, title, action - $states = array( - 0 => array('unfeatured', 'contacts.featured', 'COM_CONTACT_UNFEATURED', 'JGLOBAL_ITEM_FEATURE'), - 1 => array('featured', 'contacts.unfeatured', 'JFEATURED', 'JGLOBAL_ITEM_UNFEATURE'), - ); - $state = ArrayHelper::getValue($states, (int) $value, $states[1]); - $icon = $state[0] === 'featured' ? 'star featured' : 'circle'; - $onclick = 'onclick="return Joomla.listItemTask(\'cb' . $i . '\',\'' . $state[1] . '\')"'; - $tooltipText = Text::_($state[3]); - - if (!$canChange) - { - $onclick = 'disabled'; - $tooltipText = Text::_($state[2]); - } - - $html = '' - . ''; - - return $html; - } + /** + * Get the associated language flags + * + * @param integer $contactid The item id to search associations + * + * @return string The language HTML + * + * @throws \Exception + */ + public function association($contactid) + { + // Defaults + $html = ''; + + // Get the associations + if ($associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $contactid)) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } + + // Get the associated contact items + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('c.id'), + $db->quoteName('c.name', 'title'), + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('lang_code'), + $db->quoteName('cat.title', 'category_title'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__contact_details', 'c')) + ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) + ->whereIn($db->quoteName('c.id'), array_values($associations)) + ->where($db->quoteName('c.id') . ' != :id') + ->bind(':id', $contactid, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500, $e); + } + + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); + + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_contact&task=contact.edit&id=' . (int) $item->id); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); + $classes = 'badge bg-secondary'; + + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } + + $html = LayoutHelper::render('joomla.content.associations', $items); + } + + return $html; + } + + /** + * Show the featured/not-featured icon. + * + * @param integer $value The featured value. + * @param integer $i Id of the item. + * @param boolean $canChange Whether the value can be changed or not. + * + * @return string The anchor tag to toggle featured/unfeatured contacts. + * + * @since 1.6 + */ + public function featured($value, $i, $canChange = true) + { + // Array of image, task, title, action + $states = array( + 0 => array('unfeatured', 'contacts.featured', 'COM_CONTACT_UNFEATURED', 'JGLOBAL_ITEM_FEATURE'), + 1 => array('featured', 'contacts.unfeatured', 'JFEATURED', 'JGLOBAL_ITEM_UNFEATURE'), + ); + $state = ArrayHelper::getValue($states, (int) $value, $states[1]); + $icon = $state[0] === 'featured' ? 'star featured' : 'circle'; + $onclick = 'onclick="return Joomla.listItemTask(\'cb' . $i . '\',\'' . $state[1] . '\')"'; + $tooltipText = Text::_($state[3]); + + if (!$canChange) { + $onclick = 'disabled'; + $tooltipText = Text::_($state[2]); + } + + $html = '' + . ''; + + return $html; + } } diff --git a/code/administrator/components/com_contact/src/Service/HTML/Icon.php b/code/administrator/components/com_contact/src/Service/HTML/Icon.php index e826cdcc..fb02b18c 100644 --- a/code/administrator/components/com_contact/src/Service/HTML/Icon.php +++ b/code/administrator/components/com_contact/src/Service/HTML/Icon.php @@ -1,4 +1,5 @@ id; - - $text = ''; - - if ($params->get('show_icons')) - { - $text .= ''; - } - - $text .= Text::_('COM_CONTACT_NEW_CONTACT'); - - // Add the button classes to the attribs array - if (isset($attribs['class'])) - { - $attribs['class'] .= ' btn btn-primary'; - } - else - { - $attribs['class'] = 'btn btn-primary'; - } - - $button = HTMLHelper::_('link', Route::_($url), $text, $attribs); - - return $button; - } - - /** - * Display an edit icon for the contact. - * - * This icon will not display in a popup window, nor if the contact is trashed. - * Edit access checks must be performed in the calling code. - * - * @param object $contact The contact information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML for the contact edit icon. - * - * @since 4.0.0 - */ - public static function edit($contact, $params, $attribs = array(), $legacy = false) - { - $user = Factory::getUser(); - $uri = Uri::getInstance(); - - // Ignore if in a popup window. - if ($params && $params->get('popup')) - { - return ''; - } - - // Ignore if the state is negative (trashed). - if ($contact->published < 0) - { - return ''; - } - - // Show checked_out icon if the contact is checked out by a different user - if (property_exists($contact, 'checked_out') - && property_exists($contact, 'checked_out_time') - && !is_null($contact->checked_out) - && $contact->checked_out !== $user->get('id')) - { - $checkoutUser = Factory::getUser($contact->checked_out); - $date = HTMLHelper::_('date', $contact->checked_out_time); - $tooltip = Text::sprintf('COM_CONTACT_CHECKED_OUT_BY', $checkoutUser->name) - . '
' . $date; - - $text = LayoutHelper::render('joomla.content.icons.edit_lock', array('contact' => $contact, 'tooltip' => $tooltip, 'legacy' => $legacy)); - - $attribs['aria-describedby'] = 'editcontact-' . (int) $contact->id; - $output = HTMLHelper::_('link', '#', $text, $attribs); - - return $output; - } - - $contactUrl = RouteHelper::getContactRoute($contact->slug, $contact->catid, $contact->language); - $url = $contactUrl . '&task=contact.edit&id=' . $contact->id . '&return=' . base64_encode($uri); - - if ((int) $contact->published === 0) - { - $tooltip = Text::_('COM_CONTACT_EDIT_UNPUBLISHED_CONTACT'); - } - else - { - $tooltip = Text::_('COM_CONTACT_EDIT_PUBLISHED_CONTACT'); - } - - $nowDate = strtotime(Factory::getDate()); - $icon = $contact->published ? 'edit' : 'eye-slash'; - - if (($contact->publish_up !== null && strtotime($contact->publish_up) > $nowDate) - || ($contact->publish_down !== null && strtotime($contact->publish_down) < $nowDate - && $contact->publish_down !== Factory::getDbo()->getNullDate())) - { - $icon = 'eye-slash'; - } - - $aria_described = 'editcontact-' . (int) $contact->id; - - $text = ''; - $text .= Text::_('JGLOBAL_EDIT'); - $text .= ''; - - $attribs['aria-describedby'] = $aria_described; - $output = HTMLHelper::_('link', Route::_($url), $text, $attribs); - - return $output; - } + /** + * The user factory + * + * @var UserFactoryInterface + * + * @since 4.2.0 + */ + private $userFactory; + + /** + * Service constructor + * + * @param UserFactoryInterface $userFactory The userFactory + * + * @since 4.0.0 + */ + public function __construct(UserFactoryInterface $userFactory) + { + $this->userFactory = $userFactory; + } + + /** + * Method to generate a link to the create item page for the given category + * + * @param object $category The category information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * + * @return string The HTML markup for the create item link + * + * @since 4.0.0 + */ + public function create($category, $params, $attribs = array()) + { + $uri = Uri::getInstance(); + + $url = 'index.php?option=com_contact&task=contact.add&return=' . base64_encode($uri) . '&id=0&catid=' . $category->id; + + $text = ''; + + if ($params->get('show_icons')) { + $text .= ''; + } + + $text .= Text::_('COM_CONTACT_NEW_CONTACT'); + + // Add the button classes to the attribs array + if (isset($attribs['class'])) { + $attribs['class'] .= ' btn btn-primary'; + } else { + $attribs['class'] = 'btn btn-primary'; + } + + $button = HTMLHelper::_('link', Route::_($url), $text, $attribs); + + return $button; + } + + /** + * Display an edit icon for the contact. + * + * This icon will not display in a popup window, nor if the contact is trashed. + * Edit access checks must be performed in the calling code. + * + * @param object $contact The contact information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML for the contact edit icon. + * + * @since 4.0.0 + */ + public function edit($contact, $params, $attribs = array(), $legacy = false) + { + $user = Factory::getUser(); + $uri = Uri::getInstance(); + + // Ignore if in a popup window. + if ($params && $params->get('popup')) { + return ''; + } + + // Ignore if the state is negative (trashed). + if ($contact->published < 0) { + return ''; + } + + // Show checked_out icon if the contact is checked out by a different user + if ( + property_exists($contact, 'checked_out') + && property_exists($contact, 'checked_out_time') + && !is_null($contact->checked_out) + && $contact->checked_out !== $user->get('id') + ) { + $checkoutUser = $this->userFactory->loadUserById($contact->checked_out); + $date = HTMLHelper::_('date', $contact->checked_out_time); + $tooltip = Text::sprintf('COM_CONTACT_CHECKED_OUT_BY', $checkoutUser->name) + . '
' . $date; + + $text = LayoutHelper::render('joomla.content.icons.edit_lock', array('contact' => $contact, 'tooltip' => $tooltip, 'legacy' => $legacy)); + + $attribs['aria-describedby'] = 'editcontact-' . (int) $contact->id; + $output = HTMLHelper::_('link', '#', $text, $attribs); + + return $output; + } + + $contactUrl = RouteHelper::getContactRoute($contact->slug, $contact->catid, $contact->language); + $url = $contactUrl . '&task=contact.edit&id=' . $contact->id . '&return=' . base64_encode($uri); + + if ((int) $contact->published === 0) { + $tooltip = Text::_('COM_CONTACT_EDIT_UNPUBLISHED_CONTACT'); + } else { + $tooltip = Text::_('COM_CONTACT_EDIT_PUBLISHED_CONTACT'); + } + + $nowDate = strtotime(Factory::getDate()); + $icon = $contact->published ? 'edit' : 'eye-slash'; + + if ( + ($contact->publish_up !== null && strtotime($contact->publish_up) > $nowDate) + || ($contact->publish_down !== null && strtotime($contact->publish_down) < $nowDate) + ) { + $icon = 'eye-slash'; + } + + $aria_described = 'editcontact-' . (int) $contact->id; + + $text = ''; + $text .= Text::_('JGLOBAL_EDIT'); + $text .= ''; + + $attribs['aria-describedby'] = $aria_described; + $output = HTMLHelper::_('link', Route::_($url), $text, $attribs); + + return $output; + } } diff --git a/code/administrator/components/com_contact/src/Table/ContactTable.php b/code/administrator/components/com_contact/src/Table/ContactTable.php index b1f86ee8..85c248ab 100644 --- a/code/administrator/components/com_contact/src/Table/ContactTable.php +++ b/code/administrator/components/com_contact/src/Table/ContactTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_contact.contact'; - - parent::__construct('#__contact_details', 'id', $db); - - $this->setColumnAlias('title', 'name'); - } - - /** - * Stores a contact. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success, false on failure. - * - * @since 1.6 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate()->toSql(); - $userId = Factory::getUser()->id; - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = $date; - } - - if ($this->id) - { - // Existing item - $this->modified_by = $userId; - $this->modified = $date; - } - else - { - // Field created_by field can be set by the user, so we don't touch it if it's set. - if (empty($this->created_by)) - { - $this->created_by = $userId; - } - - if (!(int) $this->modified) - { - $this->modified = $date; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $userId; - } - } - - // Store utf8 email as punycode - $this->email_to = PunycodeHelper::emailToPunycode($this->email_to); - - // Convert IDN urls to punycode - $this->webpage = PunycodeHelper::urlToPunycode($this->webpage); - - // Verify that the alias is unique - $table = Table::getInstance('ContactTable', __NAMESPACE__ . '\\', array('dbo' => $this->getDbo())); - - if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_CONTACT_ERROR_UNIQUE_ALIAS')); - - return false; - } - - return parent::store($updateNulls); - } - - /** - * Overloaded check function - * - * @return boolean True on success, false on failure - * - * @see \JTable::check - * @since 1.5 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $this->default_con = (int) $this->default_con; - - if (InputFilter::checkAttribute(array('href', $this->webpage))) - { - $this->setError(Text::_('COM_CONTACT_WARNING_PROVIDE_VALID_URL')); - - return false; - } - - // Check for valid name - if (trim($this->name) == '') - { - $this->setError(Text::_('COM_CONTACT_WARNING_PROVIDE_VALID_NAME')); - - return false; - } - - // Generate a valid alias - $this->generateAlias(); - - // Check for a valid category. - if (!$this->catid = (int) $this->catid) - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); - - return false; - } - - // Sanity check for user_id - if (!$this->user_id) - { - $this->user_id = 0; - } - - // Check the publish down date is not earlier than publish up. - if ((int) $this->publish_down > 0 && $this->publish_down < $this->publish_up) - { - $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); - - return false; - } - - if (!$this->id) - { - // Hits must be zero on a new item - $this->hits = 0; - } - - // Clean up description -- eliminate quotes and <> brackets - if (!empty($this->metadesc)) - { - // Only process if not empty - $badCharacters = array("\"", '<', '>'); - $this->metadesc = StringHelper::str_ireplace($badCharacters, '', $this->metadesc); - } - else - { - $this->metadesc = ''; - } - - if (empty($this->params)) - { - $this->params = '{}'; - } - - if (empty($this->metadata)) - { - $this->metadata = '{}'; - } - - // Set publish_up, publish_down to null if not set - if (!$this->publish_up) - { - $this->publish_up = null; - } - - if (!$this->publish_down) - { - $this->publish_down = null; - } - - if (!$this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - - return true; - } - - /** - * Generate a valid alias from title / date. - * Remains public to be able to check for duplicated alias before saving - * - * @return string - */ - public function generateAlias() - { - if (empty($this->alias)) - { - $this->alias = $this->name; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); - - if (trim(str_replace('-', '', $this->alias)) == '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - return $this->alias; - } - - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + use TaggableTableTrait; + + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Ensure the params and metadata in json encoded in the bind method + * + * @var array + * @since 3.3 + */ + protected $_jsonEncode = array('params', 'metadata'); + + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 1.0 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_contact.contact'; + + parent::__construct('#__contact_details', 'id', $db); + + $this->setColumnAlias('title', 'name'); + } + + /** + * Stores a contact. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate()->toSql(); + $userId = Factory::getUser()->id; + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = $date; + } + + if ($this->id) { + // Existing item + $this->modified_by = $userId; + $this->modified = $date; + } else { + // Field created_by field can be set by the user, so we don't touch it if it's set. + if (empty($this->created_by)) { + $this->created_by = $userId; + } + + if (!(int) $this->modified) { + $this->modified = $date; + } + + if (empty($this->modified_by)) { + $this->modified_by = $userId; + } + } + + // Store utf8 email as punycode + if ($this->email_to !== null) { + $this->email_to = PunycodeHelper::emailToPunycode($this->email_to); + } + + // Convert IDN urls to punycode + if ($this->webpage !== null) { + $this->webpage = PunycodeHelper::urlToPunycode($this->webpage); + } + + // Verify that the alias is unique + $table = Table::getInstance('ContactTable', __NAMESPACE__ . '\\', array('dbo' => $this->getDbo())); + + if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_CONTACT_ERROR_UNIQUE_ALIAS')); + + return false; + } + + return parent::store($updateNulls); + } + + /** + * Overloaded check function + * + * @return boolean True on success, false on failure + * + * @see \JTable::check + * @since 1.5 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->default_con = (int) $this->default_con; + + if ($this->webpage !== null && InputFilter::checkAttribute(array('href', $this->webpage))) { + $this->setError(Text::_('COM_CONTACT_WARNING_PROVIDE_VALID_URL')); + + return false; + } + + // Check for valid name + if (trim($this->name) == '') { + $this->setError(Text::_('COM_CONTACT_WARNING_PROVIDE_VALID_NAME')); + + return false; + } + + // Generate a valid alias + $this->generateAlias(); + + // Check for a valid category. + if (!$this->catid = (int) $this->catid) { + $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); + + return false; + } + + // Sanity check for user_id + if (!$this->user_id) { + $this->user_id = 0; + } + + // Check the publish down date is not earlier than publish up. + if ((int) $this->publish_down > 0 && $this->publish_down < $this->publish_up) { + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); + + return false; + } + + if (!$this->id) { + // Hits must be zero on a new item + $this->hits = 0; + } + + // Clean up description -- eliminate quotes and <> brackets + if (!empty($this->metadesc)) { + // Only process if not empty + $badCharacters = array("\"", '<', '>'); + $this->metadesc = StringHelper::str_ireplace($badCharacters, '', $this->metadesc); + } else { + $this->metadesc = ''; + } + + if (empty($this->params)) { + $this->params = '{}'; + } + + if (empty($this->metadata)) { + $this->metadata = '{}'; + } + + // Set publish_up, publish_down to null if not set + if (!$this->publish_up) { + $this->publish_up = null; + } + + if (!$this->publish_down) { + $this->publish_down = null; + } + + if (!$this->modified) { + $this->modified = $this->created; + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + + return true; + } + + /** + * Generate a valid alias from title / date. + * Remains public to be able to check for duplicated alias before saving + * + * @return string + */ + public function generateAlias() + { + if (empty($this->alias)) { + $this->alias = $this->name; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); + + if (trim(str_replace('-', '', $this->alias)) == '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + return $this->alias; + } + + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/code/administrator/components/com_contact/src/View/Contact/HtmlView.php b/code/administrator/components/com_contact/src/View/Contact/HtmlView.php index 018cde07..eef25e75 100644 --- a/code/administrator/components/com_contact/src/View/Contact/HtmlView.php +++ b/code/administrator/components/com_contact/src/View/Contact/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); - - // Only allow to select tags with All language or with the forced language. - $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = Factory::getUser(); - $userId = $user->id; - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_contact', 'category', $this->item->catid); - - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title($isNew ? Text::_('COM_CONTACT_MANAGER_CONTACT_NEW') : Text::_('COM_CONTACT_MANAGER_CONTACT_EDIT'), 'address-book contact'); - - // Build the actions for new and existing records. - if ($isNew) - { - // For new records, check the create permission. - if (count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) - { - ToolbarHelper::apply('contact.apply'); - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) use ($user) - { - $childBar->save('contact.save'); - - if ($user->authorise('core.create', 'com_menus.menu')) - { - $childBar->save('contact.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); - } - - $childBar->save2new('contact.save2new'); - } - ); - } - - ToolbarHelper::cancel('contact.cancel'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - $toolbar->apply('contact.apply'); - } - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) - { - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - $childBar->save('contact.save'); - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $childBar->save2new('contact.save2new'); - } - } - - // If checked out, we can still save2menu - if ($user->authorise('core.create', 'com_menus.menu')) - { - $childBar->save('contact.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); - } - - // If checked out, we can still save - if ($canDo->get('core.create')) - { - $childBar->save2copy('contact.save2copy'); - } - } - ); - - ToolbarHelper::cancel('contact.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) - { - ToolbarHelper::versions('com_contact.contact', $this->item->id); - } - - if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) - { - ToolbarHelper::custom('contact.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Contacts:_New_or_Edit'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + // Initialise variables. + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_contact', 'category', $this->item->catid); + + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title($isNew ? Text::_('COM_CONTACT_MANAGER_CONTACT_NEW') : Text::_('COM_CONTACT_MANAGER_CONTACT_EDIT'), 'address-book contact'); + + // Build the actions for new and existing records. + if ($isNew) { + // For new records, check the create permission. + if (count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) { + ToolbarHelper::apply('contact.apply'); + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) use ($user) { + $childBar->save('contact.save'); + + if ($user->authorise('core.create', 'com_menus.menu')) { + $childBar->save('contact.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); + } + + $childBar->save2new('contact.save2new'); + } + ); + } + + ToolbarHelper::cancel('contact.cancel'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + $toolbar->apply('contact.apply'); + } + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) { + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + $childBar->save('contact.save'); + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $childBar->save2new('contact.save2new'); + } + } + + // If checked out, we can still save2menu + if ($user->authorise('core.create', 'com_menus.menu')) { + $childBar->save('contact.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); + } + + // If checked out, we can still save + if ($canDo->get('core.create')) { + $childBar->save2copy('contact.save2copy'); + } + } + ); + + ToolbarHelper::cancel('contact.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) { + ToolbarHelper::versions('com_contact.contact', $this->item->id); + } + + if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + ToolbarHelper::custom('contact.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Contacts:_New_or_Edit'); + } } diff --git a/code/administrator/components/com_contact/src/View/Contacts/HtmlView.php b/code/administrator/components/com_contact/src/View/Contacts/HtmlView.php index a03a9a06..1b672f1d 100644 --- a/code/administrator/components/com_contact/src/View/Contacts/HtmlView.php +++ b/code/administrator/components/com_contact/src/View/Contacts/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Preprocess the list of items to find ordering divisions. - // @todo: Complete the ordering stuff with nested sets - foreach ($this->items as &$item) - { - $item->order_up = true; - $item->order_dn = true; - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In article associations modal we need to remove language filter if forcing a language. - // We also need to change the category filter to show show categories with All or the forced language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - - // One last changes needed is to change the category filter to just show categories with All language or with the forced language. - $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_contact', 'category', $this->state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_CONTACT_MANAGER_CONTACTS'), 'address-book contact'); - - if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) - { - $toolbar->addNew('contact.add'); - } - - if (!$this->isEmptyState && $canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('contacts.publish')->listCheck(true); - - $childBar->unpublish('contacts.unpublish')->listCheck(true); - - $childBar->standardButton('featured') - ->text('JFEATURE') - ->task('contacts.featured') - ->listCheck(true); - $childBar->standardButton('unfeatured') - ->text('JUNFEATURE') - ->task('contacts.unfeatured') - ->listCheck(true); - - $childBar->archive('contacts.archive')->listCheck(true); - - if ($user->authorise('core.admin')) - { - $childBar->checkin('contacts.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') != -2) - { - $childBar->trash('contacts.trash')->listCheck(true); - } - - // Add a batch button - if ($user->authorise('core.create', 'com_contact') - && $user->authorise('core.edit', 'com_contact') - && $user->authorise('core.edit.state', 'com_contact')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('contacts.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($user->authorise('core.admin', 'com_contact') || $user->authorise('core.options', 'com_contact')) - { - $toolbar->preferences('com_contact'); - } - - $toolbar->help('Contacts'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Preprocess the list of items to find ordering divisions. + // @todo: Complete the ordering stuff with nested sets + foreach ($this->items as &$item) { + $item->order_up = true; + $item->order_dn = true; + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In article associations modal we need to remove language filter if forcing a language. + // We also need to change the category filter to show show categories with All or the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + + // One last changes needed is to change the category filter to just show categories with All language or with the forced language. + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_contact', 'category', $this->state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_CONTACT_MANAGER_CONTACTS'), 'address-book contact'); + + if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) { + $toolbar->addNew('contact.add'); + } + + if (!$this->isEmptyState && $canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('contacts.publish')->listCheck(true); + + $childBar->unpublish('contacts.unpublish')->listCheck(true); + + $childBar->standardButton('featured') + ->text('JFEATURE') + ->task('contacts.featured') + ->listCheck(true); + $childBar->standardButton('unfeatured') + ->text('JUNFEATURE') + ->task('contacts.unfeatured') + ->listCheck(true); + + $childBar->archive('contacts.archive')->listCheck(true); + + if ($user->authorise('core.admin')) { + $childBar->checkin('contacts.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') != -2) { + $childBar->trash('contacts.trash')->listCheck(true); + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_contact') + && $user->authorise('core.edit', 'com_contact') + && $user->authorise('core.edit.state', 'com_contact') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('contacts.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_contact') || $user->authorise('core.options', 'com_contact')) { + $toolbar->preferences('com_contact'); + } + + $toolbar->help('Contacts'); + } } diff --git a/code/administrator/components/com_contact/tmpl/contact/edit.php b/code/administrator/components/com_contact/tmpl/contact/edit.php index 61f2b057..6798cc6e 100644 --- a/code/administrator/components/com_contact/tmpl/contact/edit.php +++ b/code/administrator/components/com_contact/tmpl/contact/edit.php @@ -20,7 +20,7 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $input = $app->input; @@ -39,96 +39,96 @@ - - -
- 'details', 'recall' => true, 'breakpoint' => 768)); ?> - - item->id) ? Text::_('COM_CONTACT_NEW_CONTACT') : Text::_('COM_CONTACT_EDIT_CONTACT')); ?> -
-
-
-
- form->renderField('user_id'); ?> - form->renderField('image'); ?> - form->renderField('con_position'); ?> - form->renderField('email_to'); ?> - form->renderField('address'); ?> - form->renderField('suburb'); ?> - form->renderField('state'); ?> - form->renderField('postcode'); ?> - form->renderField('country'); ?> -
-
- form->renderField('telephone'); ?> - form->renderField('mobile'); ?> - form->renderField('fax'); ?> - form->renderField('webpage'); ?> - form->renderField('sortname1'); ?> - form->renderField('sortname2'); ?> - form->renderField('sortname3'); ?> -
-
-
-
- -
-
- - - -
-
-
- form->getField('misc')->title; ?> -
- form->getLabel('misc'); ?> - form->getInput('misc'); ?> -
-
-
-
- - - - - -
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- - - - -
- -
- -
-
- - - - - - -
- - - + + +
+ 'details', 'recall' => true, 'breakpoint' => 768)); ?> + + item->id) ? Text::_('COM_CONTACT_NEW_CONTACT') : Text::_('COM_CONTACT_EDIT_CONTACT')); ?> +
+
+
+
+ form->renderField('user_id'); ?> + form->renderField('image'); ?> + form->renderField('con_position'); ?> + form->renderField('email_to'); ?> + form->renderField('address'); ?> + form->renderField('suburb'); ?> + form->renderField('state'); ?> + form->renderField('postcode'); ?> + form->renderField('country'); ?> +
+
+ form->renderField('telephone'); ?> + form->renderField('mobile'); ?> + form->renderField('fax'); ?> + form->renderField('webpage'); ?> + form->renderField('sortname1'); ?> + form->renderField('sortname2'); ?> + form->renderField('sortname3'); ?> +
+
+
+
+ +
+
+ + + +
+
+
+ form->getField('misc')->title; ?> +
+ form->getLabel('misc'); ?> + form->getInput('misc'); ?> +
+
+
+
+ + + + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + + + +
+ +
+ +
+
+ + + + + + +
+ + + diff --git a/code/administrator/components/com_contact/tmpl/contact/modal.php b/code/administrator/components/com_contact/tmpl/contact/modal.php index cf70ffd3..04740bf4 100644 --- a/code/administrator/components/com_contact/tmpl/contact/modal.php +++ b/code/administrator/components/com_contact/tmpl/contact/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/code/administrator/components/com_contact/tmpl/contacts/default.php b/code/administrator/components/com_contact/tmpl/contacts/default.php index cc1ddbd1..0dafb822 100644 --- a/code/administrator/components/com_contact/tmpl/contacts/default.php +++ b/code/administrator/components/com_contact/tmpl/contacts/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $userId = $user->get('id'); @@ -29,180 +31,181 @@ $saveOrder = $listOrder == 'a.ordering'; $assoc = Associations::isEnabled(); -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_contact&task=contacts.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_contact&task=contacts.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
-
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items); - foreach ($this->items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_contact.category.' . $item->catid); - $canEdit = $user->authorise('core.edit', 'com_contact.category.' . $item->catid); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', 'com_contact.category.' . $item->catid) && $item->created_by == $userId; - $canChange = $user->authorise('core.edit.state', 'com_contact.category.' . $item->catid) && $canCheckin; +
+
+
+ $this)); ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items); + foreach ($this->items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_contact.category.' . $item->catid); + $canEdit = $user->authorise('core.edit', 'com_contact.category.' . $item->catid); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', 'com_contact.category.' . $item->catid) && $item->created_by == $userId; + $canChange = $user->authorise('core.edit.state', 'com_contact.category.' . $item->catid) && $canCheckin; - $item->cat_link = Route::_('index.php?option=com_categories&extension=com_contact&task=edit&type=other&id=' . $item->catid); - ?> - - - - - + $item->cat_link = Route::_('index.php?option=com_categories&extension=com_contact&task=edit&type=other&id=' . $item->catid); + ?> + + + + + - - - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + +
- id, false, 'cid', 'cb', $item->name); ?> - - - - - - - - - - featured, $i, $canChange); ?> - - published, $i, 'contacts.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> -
+ id, false, 'cid', 'cb', $item->name); ?> + + + + + + + + + + featured, $i, $canChange); ?> + + published, $i, 'contacts.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + -
- checked_out) : ?> - editor, $item->checked_out_time, 'contacts.', $canCheckin); ?> - - - - escape($item->name); ?> - - escape($item->name); ?> - -
- escape($item->alias)); ?> -
-
- escape($item->category_title); ?> -
-
-
- linked_user)) : ?> - linked_user; ?> -
email; ?>
- -
- access_level; ?> - - association) : ?> - id); ?> - - - - - id; ?> -
+ +
+ checked_out) : ?> + editor, $item->checked_out_time, 'contacts.', $canCheckin); ?> + + + + escape($item->name); ?> + + escape($item->name); ?> + +
+ escape($item->alias)); ?> +
+
+ escape($item->category_title); ?> +
+
+ + + linked_user)) : ?> + linked_user; ?> +
email; ?>
+ + + + access_level; ?> + + + + association) : ?> + id); ?> + + + + + + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_contact') - && $user->authorise('core.edit', 'com_contact') - && $user->authorise('core.edit.state', 'com_contact')) : ?> - Text::_('COM_CONTACT_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - - - - -
-
-
+ + authorise('core.create', 'com_contact') + && $user->authorise('core.edit', 'com_contact') + && $user->authorise('core.edit.state', 'com_contact') + ) : ?> + Text::_('COM_CONTACT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + + + +
diff --git a/code/administrator/components/com_contact/tmpl/contacts/default_batch_body.php b/code/administrator/components/com_contact/tmpl/contacts/default_batch_body.php index 4d5bcd36..101087e4 100644 --- a/code/administrator/components/com_contact/tmpl/contacts/default_batch_body.php +++ b/code/administrator/components/com_contact/tmpl/contacts/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Multilanguage; @@ -16,37 +18,37 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
-
- = 0) : ?> -
-
- 'com_contact']); ?> -
-
- -
-
- -
-
-
-
- $noUser]); ?> -
-
-
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ 'com_contact']); ?> +
+
+ +
+
+ +
+
+
+
+ $noUser]); ?> +
+
+
diff --git a/code/administrator/components/com_contact/tmpl/contacts/default_batch_footer.php b/code/administrator/components/com_contact/tmpl/contacts/default_batch_footer.php index 3b64d567..440ed84e 100644 --- a/code/administrator/components/com_contact/tmpl/contacts/default_batch_footer.php +++ b/code/administrator/components/com_contact/tmpl/contacts/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/code/administrator/components/com_contact/tmpl/contacts/emptystate.php b/code/administrator/components/com_contact/tmpl/contacts/emptystate.php index cf18d797..5cfb9038 100644 --- a/code/administrator/components/com_contact/tmpl/contacts/emptystate.php +++ b/code/administrator/components/com_contact/tmpl/contacts/emptystate.php @@ -1,4 +1,5 @@ 'COM_CONTACT', - 'formURL' => 'index.php?option=com_contact', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Contacts', - 'icon' => 'icon-address-book contact', + 'textPrefix' => 'COM_CONTACT', + 'formURL' => 'index.php?option=com_contact', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Contacts', + 'icon' => 'icon-address-book contact', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_contact') || count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_contact&task=contact.add'; +if ($user->authorise('core.create', 'com_contact') || count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_contact&task=contact.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_contact/tmpl/contacts/modal.php b/code/administrator/components/com_contact/tmpl/contacts/modal.php index 72b644fe..6f5a1115 100644 --- a/code/administrator/components/com_contact/tmpl/contacts/modal.php +++ b/code/administrator/components/com_contact/tmpl/contacts/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -36,128 +36,120 @@ $onclick = $this->escape($function); $multilang = Multilanguage::isEnabled(); -if (!empty($editor)) -{ - // This view is used also in com_menus. Load the xtd script only if the editor is set! - $this->document->addScriptOptions('xtd-contacts', array('editor' => $editor)); - $onclick = "jSelectContact"; +if (!empty($editor)) { + // This view is used also in com_menus. Load the xtd script only if the editor is set! + $this->document->addScriptOptions('xtd-contacts', array('editor' => $editor)); + $onclick = "jSelectContact"; } ?>
-
+ - $this)); ?> + $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - ?> - items as $i => $item) : ?> - language && $multilang) - { - $tag = strlen($item->language); - if ($tag == 5) - { - $lang = substr($item->language, 0, 2); - } - elseif ($tag == 6) - { - $lang = substr($item->language, 0, 3); - } - else { - $lang = ''; - } - } - elseif (!$multilang) - { - $lang = ''; - } - ?> - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - -
- - - - - - escape($item->name); ?> - -
- escape($item->category_title); ?> -
-
- linked_user)) : ?> - linked_user; ?> - - - escape($item->access_level); ?> - - - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + ?> + items as $i => $item) : ?> + language && $multilang) { + $tag = strlen($item->language); + if ($tag == 5) { + $lang = substr($item->language, 0, 2); + } elseif ($tag == 6) { + $lang = substr($item->language, 0, 3); + } else { + $lang = ''; + } + } elseif (!$multilang) { + $lang = ''; + } + ?> + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+ + + + + + escape($item->name); ?> + +
+ escape($item->category_title); ?> +
+
+ linked_user)) : ?> + linked_user; ?> + + + escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - + + + -
+
diff --git a/code/administrator/components/com_content/content.xml b/code/administrator/components/com_content/content.xml index 52e5581a..f50d9f4a 100644 --- a/code/administrator/components/com_content/content.xml +++ b/code/administrator/components/com_content/content.xml @@ -2,7 +2,7 @@ com_content Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_content/helpers/content.php b/code/administrator/components/com_content/helpers/content.php index ffa37af2..bb726ef3 100644 --- a/code/administrator/components/com_content/helpers/content.php +++ b/code/administrator/components/com_content/helpers/content.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Content component helper. diff --git a/code/administrator/components/com_content/services/provider.php b/code/administrator/components/com_content/services/provider.php index a3960c5b..72b91c17 100644 --- a/code/administrator/components/com_content/services/provider.php +++ b/code/administrator/components/com_content/services/provider.php @@ -1,4 +1,5 @@ set(AssociationExtensionInterface::class, new AssociationsHelper); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->set(AssociationExtensionInterface::class, new AssociationsHelper()); - $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Content')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Content')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Content')); + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Content')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Content')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Content')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new ContentComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new ContentComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_content/src/Controller/AjaxController.php b/code/administrator/components/com_content/src/Controller/AjaxController.php index ddbe1add..e4fcb362 100644 --- a/code/administrator/components/com_content/src/Controller/AjaxController.php +++ b/code/administrator/components/com_content/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->getInt('assocId', 0); + /** + * Method to fetch associations of an article + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the article whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return null + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', (int) $assocId); + $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', (int) $assocId); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - $contentTable = Table::getInstance('Content', 'JTable'); + // Add the title to each of the associated records + $contentTable = Table::getInstance('Content', 'JTable'); - foreach ($associations as $lang => $association) - { - $contentTable->load($association->id); - $associations[$lang]->title = $contentTable->title; - } + foreach ($associations as $lang => $association) { + $contentTable->load($association->id); + $associations[$lang]->title = $contentTable->title; + } - $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/code/administrator/components/com_content/src/Controller/ArticleController.php b/code/administrator/components/com_content/src/Controller/ArticleController.php index 4147cb5b..fe0082cf 100644 --- a/code/administrator/components/com_content/src/Controller/ArticleController.php +++ b/code/administrator/components/com_content/src/Controller/ArticleController.php @@ -1,4 +1,5 @@ input->get('return') == 'featured') - { - $this->view_list = 'featured'; - $this->view_item = 'article&return=featured'; - } - } - - /** - * Function that allows child controller access to model data - * after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 4.0.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - if ($this->getTask() === 'save2menu') - { - $editState = []; - - $id = $model->getState('article.id'); - - $link = 'index.php?option=com_content&view=article'; - $type = 'component'; - - $editState['id'] = $id; - $editState['link'] = $link; - $editState['title'] = $model->getItem($id)->title; - $editState['type'] = $type; - $editState['request']['id'] = $id; - - $this->app->setUserState('com_menus.edit.item', array( - 'data' => $editState, - 'type' => $type, - 'link' => $link) - ); - - $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); - } - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowAdd($data = array()) - { - $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('filter_category_id'), 'int'); - - if ($categoryId) - { - // If the category has been passed in the data or URL check it. - return $this->app->getIdentity()->authorise('core.create', 'com_content.category.' . $categoryId); - } - - // In the absence of better information, revert to the component permissions. - return parent::allowAdd(); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Zero record (id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($user->authorise('core.edit', 'com_content.article.' . $recordId)) - { - return true; - } - - // Check edit own on the record asset (explicit or inherited) - if ($user->authorise('core.edit.own', 'com_content.article.' . $recordId)) - { - // Existing record already has an owner, get it - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - // Grant if current user is owner of the record - return $user->id == $record->created_by; - } - - return false; - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 1.6 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ - $model = $this->getModel('Article', 'Administrator', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_content&view=articles' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } + use VersionableControllerTrait; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // An article edit form can come from the articles or featured view. + // Adjust the redirect view on the value of 'return' in the request. + if ($this->input->get('return') == 'featured') { + $this->view_list = 'featured'; + $this->view_item = 'article&return=featured'; + } + } + + /** + * Function that allows child controller access to model data + * after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 4.0.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + if ($this->getTask() === 'save2menu') { + $editState = []; + + $id = $model->getState('article.id'); + + $link = 'index.php?option=com_content&view=article'; + $type = 'component'; + + $editState['id'] = $id; + $editState['link'] = $link; + $editState['title'] = $model->getItem($id)->title; + $editState['type'] = $type; + $editState['request']['id'] = $id; + + $this->app->setUserState('com_menus.edit.item', array( + 'data' => $editState, + 'type' => $type, + 'link' => $link)); + + $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); + } + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('filter_category_id'), 'int'); + + if ($categoryId) { + // If the category has been passed in the data or URL check it. + return $this->app->getIdentity()->authorise('core.create', 'com_content.category.' . $categoryId); + } + + // In the absence of better information, revert to the component permissions. + return parent::allowAdd(); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Zero record (id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($user->authorise('core.edit', 'com_content.article.' . $recordId)) { + return true; + } + + // Check edit own on the record asset (explicit or inherited) + if ($user->authorise('core.edit.own', 'com_content.article.' . $recordId)) { + // Existing record already has an owner, get it + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + // Grant if current user is owner of the record + return $user->id == $record->created_by; + } + + return false; + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 1.6 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ + $model = $this->getModel('Article', 'Administrator', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_content&view=articles' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } } diff --git a/code/administrator/components/com_content/src/Controller/ArticlesController.php b/code/administrator/components/com_content/src/Controller/ArticlesController.php index b70165be..3c8628b0 100644 --- a/code/administrator/components/com_content/src/Controller/ArticlesController.php +++ b/code/administrator/components/com_content/src/Controller/ArticlesController.php @@ -1,4 +1,5 @@ input->get('view') == 'featured') - { - $this->view_list = 'featured'; - } - - $this->registerTask('unfeatured', 'featured'); - } - - /** - * Method to toggle the featured setting of a list of articles. - * - * @return void - * - * @since 1.6 - */ - public function featured() - { - // Check for request forgeries - $this->checkToken(); - - $user = $this->app->getIdentity(); - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('featured' => 1, 'unfeatured' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list . $this->getRedirectToListAppend(); - - // Access checks. - foreach ($ids as $i => $id) - { - // Remove zero value resulting from input filter - if ($id === 0) - { - unset($ids[$i]); - - continue; - } - - if (!$user->authorise('core.edit.state', 'com_content.article.' . (int) $id)) - { - // Prune items that you can't change. - unset($ids[$i]); - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); - } - } - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); - - $this->setRedirect(Route::_($redirectUrl, false)); - - return; - } - - // Get the model. - /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ - $model = $this->getModel(); - - // Publish the items. - if (!$model->featured($ids, $value)) - { - $this->setRedirect(Route::_($redirectUrl, false), $model->getError(), 'error'); - - return; - } - - if ($value == 1) - { - $message = Text::plural('COM_CONTENT_N_ITEMS_FEATURED', count($ids)); - } - else - { - $message = Text::plural('COM_CONTENT_N_ITEMS_UNFEATURED', count($ids)); - } - - $this->setRedirect(Route::_($redirectUrl, false), $message); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel - * - * @since 1.6 - */ - public function getModel($name = 'Article', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to get the JSON-encoded amount of published articles - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('articles'); - - $model->setState('filter.published', 1); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_CONTENT_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_CONTENT_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Articles default form can come from the articles or featured view. + // Adjust the redirect view on the value of 'view' in the request. + if ($this->input->get('view') == 'featured') { + $this->view_list = 'featured'; + } + + $this->registerTask('unfeatured', 'featured'); + } + + /** + * Method to toggle the featured setting of a list of articles. + * + * @return void + * + * @since 1.6 + */ + public function featured() + { + // Check for request forgeries + $this->checkToken(); + + $user = $this->app->getIdentity(); + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('featured' => 1, 'unfeatured' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list . $this->getRedirectToListAppend(); + + // Access checks. + foreach ($ids as $i => $id) { + // Remove zero value resulting from input filter + if ($id === 0) { + unset($ids[$i]); + + continue; + } + + if (!$user->authorise('core.edit.state', 'com_content.article.' . (int) $id)) { + // Prune items that you can't change. + unset($ids[$i]); + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); + } + } + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); + + $this->setRedirect(Route::_($redirectUrl, false)); + + return; + } + + // Get the model. + /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ + $model = $this->getModel(); + + // Publish the items. + if (!$model->featured($ids, $value)) { + $this->setRedirect(Route::_($redirectUrl, false), $model->getError(), 'error'); + + return; + } + + if ($value == 1) { + $message = Text::plural('COM_CONTENT_N_ITEMS_FEATURED', count($ids)); + } else { + $message = Text::plural('COM_CONTENT_N_ITEMS_UNFEATURED', count($ids)); + } + + $this->setRedirect(Route::_($redirectUrl, false), $message); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 1.6 + */ + public function getModel($name = 'Article', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to get the JSON-encoded amount of published articles + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('articles'); + + $model->setState('filter.published', 1); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_CONTENT_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_CONTENT_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } } diff --git a/code/administrator/components/com_content/src/Controller/DisplayController.php b/code/administrator/components/com_content/src/Controller/DisplayController.php index a36e2ec7..aa1e416b 100644 --- a/code/administrator/components/com_content/src/Controller/DisplayController.php +++ b/code/administrator/components/com_content/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'articles'); - $layout = $this->input->get('layout', 'articles'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return BaseController|boolean This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'articles'); + $layout = $this->input->get('layout', 'articles'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'article' && $layout == 'edit' && !$this->checkEditId('com_content.edit.article', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'article' && $layout == 'edit' && !$this->checkEditId('com_content.edit.article', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_content&view=articles', false)); + $this->setRedirect(Route::_('index.php?option=com_content&view=articles', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/code/administrator/components/com_content/src/Controller/FeaturedController.php b/code/administrator/components/com_content/src/Controller/FeaturedController.php index 4f47f55b..b67b0a9c 100644 --- a/code/administrator/components/com_content/src/Controller/FeaturedController.php +++ b/code/administrator/components/com_content/src/Controller/FeaturedController.php @@ -1,4 +1,5 @@ checkToken(); + /** + * Removes an item. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + // Check for request forgeries + $this->checkToken(); - $user = $this->app->getIdentity(); - $ids = (array) $this->input->get('cid', array(), 'int'); + $user = $this->app->getIdentity(); + $ids = (array) $this->input->get('cid', array(), 'int'); - // Access checks. - foreach ($ids as $i => $id) - { - // Remove zero value resulting from input filter - if ($id === 0) - { - unset($ids[$i]); + // Access checks. + foreach ($ids as $i => $id) { + // Remove zero value resulting from input filter + if ($id === 0) { + unset($ids[$i]); - continue; - } + continue; + } - if (!$user->authorise('core.delete', 'com_content.article.' . (int) $id)) - { - // Prune items that you can't delete. - unset($ids[$i]); - $this->app->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'notice'); - } - } + if (!$user->authorise('core.delete', 'com_content.article.' . (int) $id)) { + // Prune items that you can't delete. + unset($ids[$i]); + $this->app->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'notice'); + } + } - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); - } - else - { - /** @var \Joomla\Component\Content\Administrator\Model\FeatureModel $model */ - $model = $this->getModel(); + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); + } else { + /** @var \Joomla\Component\Content\Administrator\Model\FeatureModel $model */ + $model = $this->getModel(); - // Remove the items. - if (!$model->featured($ids, 0)) - { - $this->app->enqueueMessage($model->getError(), 'error'); - } - } + // Remove the items. + if (!$model->featured($ids, 0)) { + $this->app->enqueueMessage($model->getError(), 'error'); + } + } - $this->setRedirect('index.php?option=com_content&view=featured'); - } + $this->setRedirect('index.php?option=com_content&view=featured'); + } - /** - * Method to publish a list of articles. - * - * @return void - * - * @since 1.0 - */ - public function publish() - { - parent::publish(); + /** + * Method to publish a list of articles. + * + * @return void + * + * @since 1.0 + */ + public function publish() + { + parent::publish(); - $this->setRedirect('index.php?option=com_content&view=featured'); - } + $this->setRedirect('index.php?option=com_content&view=featured'); + } - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 1.6 - */ - public function getModel($name = 'Feature', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Feature', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_content/src/Event/Model/FeatureEvent.php b/code/administrator/components/com_content/src/Event/Model/FeatureEvent.php index eddcede2..4b03ecb8 100644 --- a/code/administrator/components/com_content/src/Event/Model/FeatureEvent.php +++ b/code/administrator/components/com_content/src/Event/Model/FeatureEvent.php @@ -1,4 +1,5 @@ name is required but has not been provided"); - } + /** + * Constructor. + * + * @param string $name The event name. + * @param array $arguments The event arguments. + * + * @throws BadMethodCallException + * + * @since 4.0.0 + */ + public function __construct($name, array $arguments = array()) + { + if (!isset($arguments['extension'])) { + throw new BadMethodCallException("Argument 'extension' of event $this->name is required but has not been provided"); + } - if (!isset($arguments['extension']) || !is_string($arguments['extension'])) - { - throw new BadMethodCallException("Argument 'extension' of event $this->name is not of type 'string'"); - } + if (!isset($arguments['extension']) || !is_string($arguments['extension'])) { + throw new BadMethodCallException("Argument 'extension' of event $this->name is not of type 'string'"); + } - if (strpos($arguments['extension'], '.') === false) - { - throw new BadMethodCallException("Argument 'extension' of event $this->name has wrong format. Valid format: 'component.section'"); - } + if (strpos($arguments['extension'], '.') === false) { + throw new BadMethodCallException("Argument 'extension' of event $this->name has wrong format. Valid format: 'component.section'"); + } - if (!\array_key_exists('extensionName', $arguments) || !\array_key_exists('section', $arguments)) - { - $parts = explode('.', $arguments['extension']); + if (!\array_key_exists('extensionName', $arguments) || !\array_key_exists('section', $arguments)) { + $parts = explode('.', $arguments['extension']); - $arguments['extensionName'] = $arguments['extensionName'] ?? $parts[0]; - $arguments['section'] = $arguments['section'] ?? $parts[1]; - } + $arguments['extensionName'] = $arguments['extensionName'] ?? $parts[0]; + $arguments['section'] = $arguments['section'] ?? $parts[1]; + } - if (!isset($arguments['pks']) || !is_array($arguments['pks'])) - { - throw new BadMethodCallException("Argument 'pks' of event $this->name is not of type 'array'"); - } + if (!isset($arguments['pks']) || !is_array($arguments['pks'])) { + throw new BadMethodCallException("Argument 'pks' of event $this->name is not of type 'array'"); + } - if (!isset($arguments['value']) || !is_numeric($arguments['value'])) - { - throw new BadMethodCallException("Argument 'value' of event $this->name is not of type 'numeric'"); - } + if (!isset($arguments['value']) || !is_numeric($arguments['value'])) { + throw new BadMethodCallException("Argument 'value' of event $this->name is not of type 'numeric'"); + } - $arguments['value'] = (int) $arguments['value']; + $arguments['value'] = (int) $arguments['value']; - if ($arguments['value'] !== 0 && $arguments['value'] !== 1) - { - throw new BadMethodCallException("Argument 'value' of event $this->name is not 0 or 1"); - } + if ($arguments['value'] !== 0 && $arguments['value'] !== 1) { + throw new BadMethodCallException("Argument 'value' of event $this->name is not 0 or 1"); + } - parent::__construct($name, $arguments); - } + parent::__construct($name, $arguments); + } - /** - * Set used parameter to true - * - * @param bool $value The value to set - * - * @return void - * - * @since 4.0.0 - */ - public function setAbort(string $reason) - { - $this->arguments['abort'] = true; - $this->arguments['abortReason'] = $reason; - } + /** + * Set used parameter to true + * + * @param bool $value The value to set + * + * @return void + * + * @since 4.0.0 + */ + public function setAbort(string $reason) + { + $this->arguments['abort'] = true; + $this->arguments['abortReason'] = $reason; + } } diff --git a/code/administrator/components/com_content/src/Extension/ContentComponent.php b/code/administrator/components/com_content/src/Extension/ContentComponent.php index 3d2a0373..7b71986a 100644 --- a/code/administrator/components/com_content/src/Extension/ContentComponent.php +++ b/code/administrator/components/com_content/src/Extension/ContentComponent.php @@ -1,4 +1,5 @@ true, - 'core.state' => true, - ]; - - /** - * The trashed condition - * - * @since 4.0.0 - */ - const CONDITION_NAMES = [ - self::CONDITION_PUBLISHED => 'JPUBLISHED', - self::CONDITION_UNPUBLISHED => 'JUNPUBLISHED', - self::CONDITION_ARCHIVED => 'JARCHIVED', - self::CONDITION_TRASHED => 'JTRASHED', - ]; - - /** - * The archived condition - * - * @since 4.0.0 - */ - const CONDITION_ARCHIVED = 2; - - /** - * The published condition - * - * @since 4.0.0 - */ - const CONDITION_PUBLISHED = 1; - - /** - * The unpublished condition - * - * @since 4.0.0 - */ - const CONDITION_UNPUBLISHED = 0; - - /** - * The trashed condition - * - * @since 4.0.0 - */ - const CONDITION_TRASHED = -2; - - /** - * Booting the extension. This is the function to set up the environment of the extension like - * registering new class loaders, etc. - * - * If required, some initial set up can be done from services of the container, eg. - * registering HTML services. - * - * @param ContainerInterface $container The container - * - * @return void - * - * @since 4.0.0 - */ - public function boot(ContainerInterface $container) - { - $this->getRegistry()->register('contentadministrator', new AdministratorService); - $this->getRegistry()->register('contenticon', new Icon); - - // The layout joomla.content.icons does need a general icon service - $this->getRegistry()->register('icon', $this->getRegistry()->getService('contenticon')); - } - - /** - * Returns a valid section for the given section. If it is not valid then null - * is returned. - * - * @param string $section The section to get the mapping for - * @param object $item The item - * - * @return string|null The new section - * - * @since 4.0.0 - */ - public function validateSection($section, $item = null) - { - if (Factory::getApplication()->isClient('site')) - { - // On the front end we need to map some sections - switch ($section) - { - // Editing an article - case 'form': - - // Category list view - case 'featured': - case 'category': - $section = 'article'; - } - } - - if ($section != 'article') - { - // We don't know other sections - return null; - } - - return $section; - } - - /** - * Returns valid contexts - * - * @return array - * - * @since 4.0.0 - */ - public function getContexts(): array - { - Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); - - $contexts = array( - 'com_content.article' => Text::_('COM_CONTENT'), - 'com_content.categories' => Text::_('JCATEGORY') - ); - - return $contexts; - } - - /** - * Returns valid contexts - * - * @return array - * - * @since 4.0.0 - */ - public function getWorkflowContexts(): array - { - Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); - - $contexts = array( - 'com_content.article' => Text::_('COM_CONTENT') - ); - - return $contexts; - } - - /** - * Returns the workflow context based on the given category section - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - public function getCategoryWorkflowContext(?string $section = null): string - { - $context = $this->getWorkflowContexts(); - - return array_key_first($context); - } - - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return '#__content'; - } - - /** - * Returns a table name for the state association - * - * @param string $section An optional section to separate different areas in the component - * - * @return string - * - * @since 4.0.0 - */ - public function getWorkflowTableBySection(?string $section = null): string - { - return '#__content'; - } - - /** - * Returns the model name, based on the context - * - * @param string $context The context of the workflow - * - * @return string - */ - public function getModelName($context): string - { - $parts = explode('.', $context); - - if (count($parts) < 2) - { - return ''; - } - - array_shift($parts); - - $modelname = array_shift($parts); - - if ($modelname === 'article' && Factory::getApplication()->isClient('site')) - { - return 'Form'; - } - elseif ($modelname === 'featured' && Factory::getApplication()->isClient('administrator')) - { - return 'Article'; - } - - return ucfirst($modelname); - } - - /** - * Method to filter transitions by given id of state. - * - * @param array $transitions The Transitions to filter - * @param int $pk Id of the state - * - * @return array - * - * @since 4.0.0 - */ - public function filterTransitions(array $transitions, int $pk): array - { - return ContentHelper::filterTransitions($transitions, $pk); - } - - /** - * Adds Count Items for Category Manager. - * - * @param \stdClass[] $items The category objects - * @param string $section The section - * - * @return void - * - * @since 4.0.0 - */ - public function countItems(array $items, string $section) - { - $config = (object) array( - 'related_tbl' => 'content', - 'state_col' => 'state', - 'group_col' => 'catid', - 'relation_type' => 'category_or_group', - 'uses_workflows' => true, - 'workflows_component' => 'com_content' - ); - - LibraryContentHelper::countRelations($items, $config); - } - - /** - * Adds Count Items for Tag Manager. - * - * @param \stdClass[] $items The content objects - * @param string $extension The name of the active view. - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function countTagItems(array $items, string $extension) - { - $parts = explode('.', $extension); - $section = count($parts) > 1 ? $parts[1] : null; - - $config = (object) array( - 'related_tbl' => ($section === 'category' ? 'categories' : 'content'), - 'state_col' => ($section === 'category' ? 'published' : 'state'), - 'group_col' => 'tag_id', - 'extension' => $extension, - 'relation_type' => 'tag_assigments', - ); - - LibraryContentHelper::countRelations($items, $config); - } - - /** - * Prepares the category form - * - * @param Form $form The form to prepare - * @param array|object $data The form data - * - * @return void - */ - public function prepareForm(Form $form, $data) - { - ContentHelper::onPrepareForm($form, $data); - } + use AssociationServiceTrait; + use RouterServiceTrait; + use HTMLRegistryAwareTrait; + use WorkflowServiceTrait; + use CategoryServiceTrait, TagServiceTrait { + CategoryServiceTrait::getTableNameForSection insteadof TagServiceTrait; + CategoryServiceTrait::getStateColumnForSection insteadof TagServiceTrait; + } + + /** @var array Supported functionality */ + protected $supportedFunctionality = [ + 'core.featured' => true, + 'core.state' => true, + ]; + + /** + * The trashed condition + * + * @since 4.0.0 + */ + public const CONDITION_NAMES = [ + self::CONDITION_PUBLISHED => 'JPUBLISHED', + self::CONDITION_UNPUBLISHED => 'JUNPUBLISHED', + self::CONDITION_ARCHIVED => 'JARCHIVED', + self::CONDITION_TRASHED => 'JTRASHED', + ]; + + /** + * The archived condition + * + * @since 4.0.0 + */ + public const CONDITION_ARCHIVED = 2; + + /** + * The published condition + * + * @since 4.0.0 + */ + public const CONDITION_PUBLISHED = 1; + + /** + * The unpublished condition + * + * @since 4.0.0 + */ + public const CONDITION_UNPUBLISHED = 0; + + /** + * The trashed condition + * + * @since 4.0.0 + */ + public const CONDITION_TRASHED = -2; + + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('contentadministrator', new AdministratorService()); + $this->getRegistry()->register('contenticon', new Icon()); + + // The layout joomla.content.icons does need a general icon service + $this->getRegistry()->register('icon', $this->getRegistry()->getService('contenticon')); + } + + /** + * Returns a valid section for the given section. If it is not valid then null + * is returned. + * + * @param string $section The section to get the mapping for + * @param object $item The item + * + * @return string|null The new section + * + * @since 4.0.0 + */ + public function validateSection($section, $item = null) + { + if (Factory::getApplication()->isClient('site')) { + // On the front end we need to map some sections + switch ($section) { + // Editing an article + case 'form': + // Category list view + case 'featured': + case 'category': + $section = 'article'; + } + } + + if ($section != 'article') { + // We don't know other sections + return null; + } + + return $section; + } + + /** + * Returns valid contexts + * + * @return array + * + * @since 4.0.0 + */ + public function getContexts(): array + { + Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); + + $contexts = array( + 'com_content.article' => Text::_('COM_CONTENT'), + 'com_content.categories' => Text::_('JCATEGORY') + ); + + return $contexts; + } + + /** + * Returns valid contexts + * + * @return array + * + * @since 4.0.0 + */ + public function getWorkflowContexts(): array + { + Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); + + $contexts = array( + 'com_content.article' => Text::_('COM_CONTENT') + ); + + return $contexts; + } + + /** + * Returns the workflow context based on the given category section + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + public function getCategoryWorkflowContext(?string $section = null): string + { + $context = $this->getWorkflowContexts(); + + return array_key_first($context); + } + + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return '#__content'; + } + + /** + * Returns a table name for the state association + * + * @param string $section An optional section to separate different areas in the component + * + * @return string + * + * @since 4.0.0 + */ + public function getWorkflowTableBySection(?string $section = null): string + { + return '#__content'; + } + + /** + * Returns the model name, based on the context + * + * @param string $context The context of the workflow + * + * @return string + */ + public function getModelName($context): string + { + $parts = explode('.', $context); + + if (count($parts) < 2) { + return ''; + } + + array_shift($parts); + + $modelname = array_shift($parts); + + if ($modelname === 'article' && Factory::getApplication()->isClient('site')) { + return 'Form'; + } elseif ($modelname === 'featured' && Factory::getApplication()->isClient('administrator')) { + return 'Article'; + } + + return ucfirst($modelname); + } + + /** + * Method to filter transitions by given id of state. + * + * @param array $transitions The Transitions to filter + * @param int $pk Id of the state + * + * @return array + * + * @since 4.0.0 + */ + public function filterTransitions(array $transitions, int $pk): array + { + return ContentHelper::filterTransitions($transitions, $pk); + } + + /** + * Adds Count Items for Category Manager. + * + * @param \stdClass[] $items The category objects + * @param string $section The section + * + * @return void + * + * @since 4.0.0 + */ + public function countItems(array $items, string $section) + { + $config = (object) array( + 'related_tbl' => 'content', + 'state_col' => 'state', + 'group_col' => 'catid', + 'relation_type' => 'category_or_group', + 'uses_workflows' => true, + 'workflows_component' => 'com_content' + ); + + LibraryContentHelper::countRelations($items, $config); + } + + /** + * Adds Count Items for Tag Manager. + * + * @param \stdClass[] $items The content objects + * @param string $extension The name of the active view. + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function countTagItems(array $items, string $extension) + { + $parts = explode('.', $extension); + $section = count($parts) > 1 ? $parts[1] : null; + + $config = (object) array( + 'related_tbl' => ($section === 'category' ? 'categories' : 'content'), + 'state_col' => ($section === 'category' ? 'published' : 'state'), + 'group_col' => 'tag_id', + 'extension' => $extension, + 'relation_type' => 'tag_assigments', + ); + + LibraryContentHelper::countRelations($items, $config); + } + + /** + * Prepares the category form + * + * @param Form $form The form to prepare + * @param array|object $data The form data + * + * @return void + */ + public function prepareForm(Form $form, $data) + { + ContentHelper::onPrepareForm($form, $data); + } } diff --git a/code/administrator/components/com_content/src/Field/AssocField.php b/code/administrator/components/com_content/src/Field/AssocField.php index 7746abea..5ac843a7 100644 --- a/code/administrator/components/com_content/src/Field/AssocField.php +++ b/code/administrator/components/com_content/src/Field/AssocField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see AssocField::setup() - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - if (!Associations::isEnabled()) - { - return false; - } + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see AssocField::setup() + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + if (!Associations::isEnabled()) { + return false; + } - return parent::setup($element, $value, $group); - } + return parent::setup($element, $value, $group); + } } diff --git a/code/administrator/components/com_content/src/Field/Modal/ArticleField.php b/code/administrator/components/com_content/src/Field/Modal/ArticleField.php index 70993041..99518755 100644 --- a/code/administrator/components/com_content/src/Field/Modal/ArticleField.php +++ b/code/administrator/components/com_content/src/Field/Modal/ArticleField.php @@ -1,4 +1,5 @@ element['new'] == 'true'); - $allowEdit = ((string) $this->element['edit'] == 'true'); - $allowClear = ((string) $this->element['clear'] != 'false'); - $allowSelect = ((string) $this->element['select'] != 'false'); - $allowPropagate = ((string) $this->element['propagate'] == 'true'); - - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language - Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); - - // The active article id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Article_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Modal_Article'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $allowNew = ((string) $this->element['new'] == 'true'); + $allowEdit = ((string) $this->element['edit'] == 'true'); + $allowClear = ((string) $this->element['clear'] != 'false'); + $allowSelect = ((string) $this->element['select'] != 'false'); + $allowPropagate = ((string) $this->element['propagate'] == 'true'); + + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language + Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); + + // The active article id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Article_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectArticle_" . $this->id . " = function (id, title, catid, object, url, language) { window.processModalSelect('Article', '" . $this->id . "', id, title, catid, object, url, language); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkArticles = 'index.php?option=com_content&view=articles&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $linkArticle = 'index.php?option=com_content&view=article&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - - if (isset($this->element['language'])) - { - $linkArticles .= '&forcedLanguage=' . $this->element['language']; - $linkArticle .= '&forcedLanguage=' . $this->element['language']; - $modalTitle = Text::_('COM_CONTENT_SELECT_AN_ARTICLE') . ' — ' . $this->element['label']; - } - else - { - $modalTitle = Text::_('COM_CONTENT_SELECT_AN_ARTICLE'); - } - - $urlSelect = $linkArticles . '&function=jSelectArticle_' . $this->id; - $urlEdit = $linkArticle . '&task=article.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkArticle . '&task=article.add'; - - if ($value) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__content')) - ->where($db->quoteName('id') . ' = :value') - ->bind(':value', $value, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - $title = empty($title) ? Text::_('COM_CONTENT_SELECT_AN_ARTICLE') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current article display field. - $html = ''; - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select article button - if ($allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New article button - if ($allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit article button - if ($allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear article button - if ($allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate article button - if ($allowPropagate && count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectArticle_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - // Select article modal - if ($allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New article modal - if ($allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_CONTENT_NEW_ARTICLE'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit article modal - if ($allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_CONTENT_EDIT_ARTICLE'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Note: class='required' for client side validation. - $class = $this->required ? ' class="required modal-value"' : ''; - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.4 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkArticles = 'index.php?option=com_content&view=articles&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $linkArticle = 'index.php?option=com_content&view=article&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + + if (isset($this->element['language'])) { + $linkArticles .= '&forcedLanguage=' . $this->element['language']; + $linkArticle .= '&forcedLanguage=' . $this->element['language']; + $modalTitle = Text::_('COM_CONTENT_SELECT_AN_ARTICLE') . ' — ' . $this->element['label']; + } else { + $modalTitle = Text::_('COM_CONTENT_SELECT_AN_ARTICLE'); + } + + $urlSelect = $linkArticles . '&function=jSelectArticle_' . $this->id; + $urlEdit = $linkArticle . '&task=article.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkArticle . '&task=article.add'; + + if ($value) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__content')) + ->where($db->quoteName('id') . ' = :value') + ->bind(':value', $value, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + $title = empty($title) ? Text::_('COM_CONTENT_SELECT_AN_ARTICLE') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current article display field. + $html = ''; + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select article button + if ($allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New article button + if ($allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit article button + if ($allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear article button + if ($allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate article button + if ($allowPropagate && count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectArticle_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + // Select article modal + if ($allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New article modal + if ($allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_CONTENT_NEW_ARTICLE'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit article modal + if ($allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_CONTENT_EDIT_ARTICLE'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Note: class='required' for client side validation. + $class = $this->required ? ' class="required modal-value"' : ''; + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.4 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/code/administrator/components/com_content/src/Field/VotelistField.php b/code/administrator/components/com_content/src/Field/VotelistField.php index 5aa280b4..35ab56c4 100644 --- a/code/administrator/components/com_content/src/Field/VotelistField.php +++ b/code/administrator/components/com_content/src/Field/VotelistField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - // Requires vote plugin enabled - if (!PluginHelper::isEnabled('content', 'vote')) - { - return false; - } + /** + * Method to attach a form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + // Requires vote plugin enabled + if (!PluginHelper::isEnabled('content', 'vote')) { + return false; + } - return parent::setup($element, $value, $group); - } + return parent::setup($element, $value, $group); + } } diff --git a/code/administrator/components/com_content/src/Field/VoteradioField.php b/code/administrator/components/com_content/src/Field/VoteradioField.php index c9e8bd69..5e02579a 100644 --- a/code/administrator/components/com_content/src/Field/VoteradioField.php +++ b/code/administrator/components/com_content/src/Field/VoteradioField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - // Requires vote plugin enabled - if (!PluginHelper::isEnabled('content', 'vote')) - { - return false; - } + /** + * Method to attach a form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + // Requires vote plugin enabled + if (!PluginHelper::isEnabled('content', 'vote')) { + return false; + } - return parent::setup($element, $value, $group); - } + return parent::setup($element, $value, $group); + } } diff --git a/code/administrator/components/com_content/src/Helper/AssociationsHelper.php b/code/administrator/components/com_content/src/Helper/AssociationsHelper.php index 4640ae4a..ab7670a4 100644 --- a/code/administrator/components/com_content/src/Helper/AssociationsHelper.php +++ b/code/administrator/components/com_content/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getType($typeName); - - $context = $this->extension . '.item'; - $catidField = 'catid'; - - if ($typeName === 'category') - { - $context = 'com_categories.item'; - $catidField = ''; - } - - // Get the associations. - $associations = Associations::getAssociations( - $this->extension, - $type['tables']['a'], - $context, - $id, - 'id', - 'alias', - $catidField - ); - - return $associations; - } - - /** - * Get item information - * - * @param string $typeName The item type - * @param int $id The id of item for which we need the associated items - * - * @return Table|null - * - * @since 3.7.0 - */ - public function getItem($typeName, $id) - { - if (empty($id)) - { - return null; - } - - $table = null; - - switch ($typeName) - { - case 'article': - $table = Table::getInstance('Content'); - break; - - case 'category': - $table = Table::getInstance('Category'); - break; - } - - if (is_null($table)) - { - return null; - } - - $table->load($id); - - return $table; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - if (in_array($typeName, $this->itemTypes)) - { - switch ($typeName) - { - case 'article': - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['category'] = true; - $support['save2copy'] = true; - - $tables = array( - 'a' => '#__content' - ); - - $title = 'article'; - break; - - case 'category': - $fields['created_user_id'] = 'a.created_user_id'; - $fields['ordering'] = 'a.lft'; - $fields['level'] = 'a.level'; - $fields['catid'] = ''; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['level'] = true; - - $tables = array( - 'a' => '#__categories' - ); - - $title = 'category'; - break; - } - } - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_content'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array('article', 'category'); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = true; + + /** + * Method to get the associations for a given item. + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 4.0.0 + */ + public function getAssociationsForItem($id = 0, $view = null) + { + return AssociationHelper::getAssociations($id, $view); + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociations($typeName, $id) + { + $type = $this->getType($typeName); + + $context = $this->extension . '.item'; + $catidField = 'catid'; + + if ($typeName === 'category') { + $context = 'com_categories.item'; + $catidField = ''; + } + + // Get the associations. + $associations = Associations::getAssociations( + $this->extension, + $type['tables']['a'], + $context, + $id, + 'id', + 'alias', + $catidField + ); + + return $associations; + } + + /** + * Get item information + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return Table|null + * + * @since 3.7.0 + */ + public function getItem($typeName, $id) + { + if (empty($id)) { + return null; + } + + $table = null; + + switch ($typeName) { + case 'article': + $table = Table::getInstance('Content'); + break; + + case 'category': + $table = Table::getInstance('Category'); + break; + } + + if (is_null($table)) { + return null; + } + + $table->load($id); + + return $table; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + if (in_array($typeName, $this->itemTypes)) { + switch ($typeName) { + case 'article': + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['category'] = true; + $support['save2copy'] = true; + + $tables = array( + 'a' => '#__content' + ); + + $title = 'article'; + break; + + case 'category': + $fields['created_user_id'] = 'a.created_user_id'; + $fields['ordering'] = 'a.lft'; + $fields['level'] = 'a.level'; + $fields['catid'] = ''; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['level'] = true; + + $tables = array( + 'a' => '#__categories' + ); + + $title = 'category'; + break; + } + } + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } } diff --git a/code/administrator/components/com_content/src/Helper/ContentHelper.php b/code/administrator/components/com_content/src/Helper/ContentHelper.php index 3d26fc91..1e292c5e 100644 --- a/code/administrator/components/com_content/src/Helper/ContentHelper.php +++ b/code/administrator/components/com_content/src/Helper/ContentHelper.php @@ -1,4 +1,5 @@ getQuery(true); - - $query->select('id') - ->from($db->quoteName('#__content')) - ->where($db->quoteName('state') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $states = $db->loadResult(); - - return empty($states); - } - - /** - * Method to filter transitions by given id of state - * - * @param array $transitions Array of transitions - * @param int $pk Id of state - * @param int $workflowId Id of the workflow - * - * @return array - * - * @since 4.0.0 - */ - public static function filterTransitions(array $transitions, int $pk, int $workflowId = 0): array - { - return array_values( - array_filter( - $transitions, - function ($var) use ($pk, $workflowId) - { - return in_array($var['from_stage_id'], [-1, $pk]) && $workflowId == $var['workflow_id']; - } - ) - ); - } - - /** - * Prepares a form - * - * @param Form $form The form to change - * @param array|object $data The form data - * - * @return void - */ - public static function onPrepareForm(Form $form, $data) - { - if ($form->getName() != 'com_categories.categorycom_content') - { - return; - } - - $db = Factory::getDbo(); - - $data = (array) $data; - - // Make workflows translatable - Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); - - $form->setFieldAttribute('workflow_id', 'default', 'inherit'); - - $component = Factory::getApplication()->bootComponent('com_content'); - - if (!$component instanceof WorkflowServiceInterface - || !$component->isWorkflowActive('com_content.article')) - { - $form->removeField('workflow_id', 'params'); - - return; - } - - $query = $db->getQuery(true); - - $query->select($db->quoteName('title')) - ->from($db->quoteName('#__workflows')) - ->where( - [ - $db->quoteName('default') . ' = 1', - $db->quoteName('published') . ' = 1', - $db->quoteName('extension') . ' = ' . $db->quote('com_content.article'), - ] - ); - - $defaulttitle = $db->setQuery($query)->loadResult(); - - $option = Text::_('COM_WORKFLOW_INHERIT_WORKFLOW_NEW'); - - if (!empty($data['id'])) - { - $category = new Category($db); - - $categories = $category->getPath((int) $data['id']); - - // Remove the current category, because we search for inheritance from parent. - array_pop($categories); - - $option = Text::sprintf('COM_WORKFLOW_INHERIT_WORKFLOW', Text::_($defaulttitle)); - - if (!empty($categories)) - { - $categories = array_reverse($categories); - - $query = $db->getQuery(true); - - $query->select($db->quoteName('title')) - ->from($db->quoteName('#__workflows')) - ->where( - [ - $db->quoteName('id') . ' = :workflowId', - $db->quoteName('published') . ' = 1', - $db->quoteName('extension') . ' = ' . $db->quote('com_content.article'), - ] - ) - ->bind(':workflowId', $workflow_id, ParameterType::INTEGER); - - $db->setQuery($query); - - foreach ($categories as $cat) - { - $cat->params = new Registry($cat->params); - - $workflow_id = $cat->params->get('workflow_id'); - - if ($workflow_id == 'inherit') - { - continue; - } - elseif ($workflow_id == 'use_default') - { - break; - } - elseif ($workflow_id = (int) $workflow_id) - { - $title = $db->loadResult(); - - if (!is_null($title)) - { - $option = Text::sprintf('COM_WORKFLOW_INHERIT_WORKFLOW', Text::_($title)); - - break; - } - } - } - } - } + /** + * Check if state can be deleted + * + * @param int $id Id of state to delete + * + * @return boolean + * + * @since 4.0.0 + */ + public static function canDeleteState(int $id): bool + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select('id') + ->from($db->quoteName('#__content')) + ->where($db->quoteName('state') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $states = $db->loadResult(); + + return empty($states); + } + + /** + * Method to filter transitions by given id of state + * + * @param array $transitions Array of transitions + * @param int $pk Id of state + * @param int $workflowId Id of the workflow + * + * @return array + * + * @since 4.0.0 + */ + public static function filterTransitions(array $transitions, int $pk, int $workflowId = 0): array + { + return array_values( + array_filter( + $transitions, + function ($var) use ($pk, $workflowId) { + return in_array($var['from_stage_id'], [-1, $pk]) && $workflowId == $var['workflow_id']; + } + ) + ); + } + + /** + * Prepares a form + * + * @param Form $form The form to change + * @param array|object $data The form data + * + * @return void + */ + public static function onPrepareForm(Form $form, $data) + { + if ($form->getName() != 'com_categories.categorycom_content') { + return; + } + + $db = Factory::getDbo(); + + $data = (array) $data; + + // Make workflows translatable + Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); + + $form->setFieldAttribute('workflow_id', 'default', 'inherit'); + + $component = Factory::getApplication()->bootComponent('com_content'); + + if ( + !$component instanceof WorkflowServiceInterface + || !$component->isWorkflowActive('com_content.article') + ) { + $form->removeField('workflow_id', 'params'); + + return; + } + + $query = $db->getQuery(true); + + $query->select($db->quoteName('title')) + ->from($db->quoteName('#__workflows')) + ->where( + [ + $db->quoteName('default') . ' = 1', + $db->quoteName('published') . ' = 1', + $db->quoteName('extension') . ' = ' . $db->quote('com_content.article'), + ] + ); + + $defaulttitle = $db->setQuery($query)->loadResult(); + + $option = Text::_('COM_WORKFLOW_INHERIT_WORKFLOW_NEW'); + + if (!empty($data['id'])) { + $category = new Category($db); + + $categories = $category->getPath((int) $data['id']); + + // Remove the current category, because we search for inheritance from parent. + array_pop($categories); + + $option = Text::sprintf('COM_WORKFLOW_INHERIT_WORKFLOW', Text::_($defaulttitle)); + + if (!empty($categories)) { + $categories = array_reverse($categories); + + $query = $db->getQuery(true); + + $query->select($db->quoteName('title')) + ->from($db->quoteName('#__workflows')) + ->where( + [ + $db->quoteName('id') . ' = :workflowId', + $db->quoteName('published') . ' = 1', + $db->quoteName('extension') . ' = ' . $db->quote('com_content.article'), + ] + ) + ->bind(':workflowId', $workflow_id, ParameterType::INTEGER); + + $db->setQuery($query); + + foreach ($categories as $cat) { + $cat->params = new Registry($cat->params); + + $workflow_id = $cat->params->get('workflow_id'); + + if ($workflow_id == 'inherit') { + continue; + } elseif ($workflow_id == 'use_default') { + break; + } elseif ($workflow_id = (int) $workflow_id) { + $title = $db->loadResult(); + + if (!is_null($title)) { + $option = Text::sprintf('COM_WORKFLOW_INHERIT_WORKFLOW', Text::_($title)); + + break; + } + } + } + } + } - $field = $form->getField('workflow_id', 'params'); + $field = $form->getField('workflow_id', 'params'); - $field->addOption($option, ['value' => 'inherit']); + $field->addOption($option, ['value' => 'inherit']); - $field->addOption(Text::sprintf('COM_WORKFLOW_USE_DEFAULT_WORKFLOW', Text::_($defaulttitle)), ['value' => 'use_default']); + $field->addOption(Text::sprintf('COM_WORKFLOW_USE_DEFAULT_WORKFLOW', Text::_($defaulttitle)), ['value' => 'use_default']); - $field->addOption('- ' . Text::_('COM_CONTENT_WORKFLOWS') . ' -', ['disabled' => 'true']); - } + $field->addOption('- ' . Text::_('COM_CONTENT_WORKFLOWS') . ' -', ['disabled' => 'true']); + } } diff --git a/code/administrator/components/com_content/src/Model/ArticleModel.php b/code/administrator/components/com_content/src/Model/ArticleModel.php index 8163148b..e7e47fb6 100644 --- a/code/administrator/components/com_content/src/Model/ArticleModel.php +++ b/code/administrator/components/com_content/src/Model/ArticleModel.php @@ -1,4 +1,5 @@ 'content'], - $config['events_map'] - ); - - parent::__construct($config, $factory, $formFactory); - - // Set the featured status change events - $this->event_before_change_featured = $config['event_before_change_featured'] ?? $this->event_before_change_featured; - $this->event_before_change_featured = $this->event_before_change_featured ?? 'onContentBeforeChangeFeatured'; - $this->event_after_change_featured = $config['event_after_change_featured'] ?? $this->event_after_change_featured; - $this->event_after_change_featured = $this->event_after_change_featured ?? 'onContentAfterChangeFeatured'; - - $this->setUpWorkflow('com_content.article'); - } - - /** - * Function that can be overridden to do any data cleanup after batch copying data - * - * @param TableInterface $table The table object containing the newly created item - * @param integer $newId The id of the new item - * @param integer $oldId The original item id - * - * @return void - * - * @since 3.8.12 - */ - protected function cleanupPostBatchCopy(TableInterface $table, $newId, $oldId) - { - // Check if the article was featured and update the #__content_frontpage table - if ($table->featured == 1) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('featured_up'), - $db->quoteName('featured_down'), - ] - ) - ->from($db->quoteName('#__content_frontpage')) - ->where($db->quoteName('content_id') . ' = :oldId') - ->bind(':oldId', $oldId, ParameterType::INTEGER); - - $featured = $db->setQuery($query)->loadObject(); - - if ($featured) - { - $query = $db->getQuery(true) - ->insert($db->quoteName('#__content_frontpage')) - ->values(':newId, 0, :featuredUp, :featuredDown') - ->bind(':newId', $newId, ParameterType::INTEGER) - ->bind(':featuredUp', $featured->featured_up, $featured->featured_up ? ParameterType::STRING : ParameterType::NULL) - ->bind(':featuredDown', $featured->featured_down, $featured->featured_down ? ParameterType::STRING : ParameterType::NULL); - - $db->setQuery($query); - $db->execute(); - } - } - - $this->workflowCleanupBatchMove($oldId, $newId); - - $oldItem = $this->getTable(); - $oldItem->load($oldId); - $fields = FieldsHelper::getFields('com_content.article', $oldItem, true); - - $fieldsData = array(); - - if (!empty($fields)) - { - $fieldsData['com_fields'] = array(); - - foreach ($fields as $field) - { - $fieldsData['com_fields'][$field->name] = $field->rawvalue; - } - } - - Factory::getApplication()->triggerEvent('onContentAfterSave', array('com_content.article', &$this->table, false, $fieldsData)); - } - - /** - * Batch move categories to a new category. - * - * @param integer $value The new category ID. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True on success. - * - * @since 3.8.6 - */ - protected function batchMove($value, $pks, $contexts) - { - if (empty($this->batchSet)) - { - // Set some needed variables. - $this->user = Factory::getUser(); - $this->table = $this->getTable(); - $this->tableClassName = get_class($this->table); - $this->contentType = new UCMType; - $this->type = $this->contentType->getTypeByTable($this->tableClassName); - } - - $categoryId = (int) $value; - - if (!$this->checkCategoryId($categoryId)) - { - return false; - } - - PluginHelper::importPlugin('system'); - - // Parent exists so we proceed - foreach ($pks as $pk) - { - if (!$this->user->authorise('core.edit', $contexts[$pk])) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - - // Check that the row actually exists - if (!$this->table->load($pk)) - { - if ($error = $this->table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JLIB_APPLICATION_ERROR_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - $fields = FieldsHelper::getFields('com_content.article', $this->table, true); - - $fieldsData = array(); - - if (!empty($fields)) - { - $fieldsData['com_fields'] = array(); - - foreach ($fields as $field) - { - $fieldsData['com_fields'][$field->name] = $field->rawvalue; - } - } - - // Set the new category ID - $this->table->catid = $categoryId; - - // We don't want to modify tags - so remove the associated tags helper - if ($this->table instanceof TaggableTableInterface) - { - $this->table->clearTagsHelper(); - } - - // Check the row. - if (!$this->table->check()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Store the row. - if (!$this->table->store()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Run event for moved article - Factory::getApplication()->triggerEvent('onContentAfterSave', array('com_content.article', &$this->table, false, $fieldsData)); - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || ($record->state != -2)) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', 'com_content.article.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - // Check for existing article. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', 'com_content.article.' . (int) $record->id); - } - - // New article, so check against the category. - if (!empty($record->catid)) - { - return $user->authorise('core.edit.state', 'com_content.category.' . (int) $record->catid); - } - - // Default to component settings if neither article nor category known. - return parent::canEditState($record); - } - - /** - * Prepare and sanitise the table data prior to saving. - * - * @param \Joomla\CMS\Table\Table $table A Table object. - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - // Set the publish date to now - if ($table->state == Workflow::CONDITION_PUBLISHED && (int) $table->publish_up == 0) - { - $table->publish_up = Factory::getDate()->toSql(); - } - - if ($table->state == Workflow::CONDITION_PUBLISHED && intval($table->publish_down) == 0) - { - $table->publish_down = null; - } - - // Increment the content version number. - $table->version++; - - // Reorder the articles within the category so the new article is first - if (empty($table->id)) - { - $table->reorder('catid = ' . (int) $table->catid . ' AND state >= 0'); - } - } - - /** - * Method to change the published state of one or more records. - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function publish(&$pks, $value = 1) - { - $this->workflowBeforeStageChange(); - - return parent::publish($pks, $value); - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - */ - public function getItem($pk = null) - { - if ($item = parent::getItem($pk)) - { - // Convert the params field to an array. - $registry = new Registry($item->attribs); - $item->attribs = $registry->toArray(); - - // Convert the metadata field to an array. - $registry = new Registry($item->metadata); - $item->metadata = $registry->toArray(); - - // Convert the images field to an array. - $registry = new Registry($item->images); - $item->images = $registry->toArray(); - - // Convert the urls field to an array. - $registry = new Registry($item->urls); - $item->urls = $registry->toArray(); - - $item->articletext = ($item->fulltext !== null && trim($item->fulltext) != '') ? $item->introtext . '
' . $item->fulltext : $item->introtext; - - if (!empty($item->id)) - { - $item->tags = new TagsHelper; - $item->tags->getTagIds($item->id, 'com_content.article'); - - $item->featured_up = null; - $item->featured_down = null; - - if ($item->featured) - { - // Get featured dates. - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('featured_up'), - $db->quoteName('featured_down'), - ] - ) - ->from($db->quoteName('#__content_frontpage')) - ->where($db->quoteName('content_id') . ' = :id') - ->bind(':id', $item->id, ParameterType::INTEGER); - - $featured = $db->setQuery($query)->loadObject(); - - if ($featured) - { - $item->featured_up = $featured->featured_up; - $item->featured_down = $featured->featured_down; - } - } - } - } - - // Load associated content items - $assoc = Associations::isEnabled(); - - if ($assoc) - { - $item->associations = array(); - - if ($item->id != null) - { - $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $item->id); - - foreach ($associations as $tag => $association) - { - $item->associations[$tag] = $association->id; - } - } - } - - return $item; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - $app = Factory::getApplication(); - - // Get the form. - $form = $this->loadForm('com_content.article', 'article', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Object uses for checking edit state permission of article - $record = new \stdClass; - - // Get ID of the article from input, for frontend, we use a_id while backend uses id - $articleIdFromInput = $app->isClient('site') - ? $app->input->getInt('a_id', 0) - : $app->input->getInt('id', 0); - - // On edit article, we get ID of article from article.id state, but on save, we use data from input - $id = (int) $this->getState('article.id', $articleIdFromInput); - - $record->id = $id; - - // For new articles we load the potential state + associations - if ($id == 0 && $formField = $form->getField('catid')) - { - $assignedCatids = $data['catid'] ?? $form->getValue('catid'); - - $assignedCatids = is_array($assignedCatids) - ? (int) reset($assignedCatids) - : (int) $assignedCatids; - - // Try to get the category from the category field - if (empty($assignedCatids)) - { - $assignedCatids = $formField->getAttribute('default', null); - - if (!$assignedCatids) - { - // Choose the first category available - $catOptions = $formField->options; - - if ($catOptions && !empty($catOptions[0]->value)) - { - $assignedCatids = (int) $catOptions[0]->value; - } - } - } - - // Activate the reload of the form when category is changed - $form->setFieldAttribute('catid', 'refresh-enabled', true); - $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); - $form->setFieldAttribute('catid', 'refresh-section', 'article'); - - // Store ID of the category uses for edit state permission check - $record->catid = $assignedCatids; - } - else - { - // Get the category which the article is being added to - if (!empty($data['catid'])) - { - $catId = (int) $data['catid']; - } - else - { - $catIds = $form->getValue('catid'); - - $catId = is_array($catIds) - ? (int) reset($catIds) - : (int) $catIds; - - if (!$catId) - { - $catId = (int) $form->getFieldAttribute('catid', 'default', 0); - } - } - - $record->catid = $catId; - } - - // Modify the form based on Edit State access controls. - if (!$this->canEditState($record)) - { - // Disable fields for display. - $form->setFieldAttribute('featured', 'disabled', 'true'); - $form->setFieldAttribute('featured_up', 'disabled', 'true'); - $form->setFieldAttribute('featured_down', 'disabled', 'true'); - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('publish_up', 'disabled', 'true'); - $form->setFieldAttribute('publish_down', 'disabled', 'true'); - $form->setFieldAttribute('state', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is an article you can edit. - $form->setFieldAttribute('featured', 'filter', 'unset'); - $form->setFieldAttribute('featured_up', 'filter', 'unset'); - $form->setFieldAttribute('featured_down', 'filter', 'unset'); - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('publish_up', 'filter', 'unset'); - $form->setFieldAttribute('publish_down', 'filter', 'unset'); - $form->setFieldAttribute('state', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_content.edit.article.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Category, Language, Access) in edit form if those have been selected in Article Manager: Articles - if ($this->getState('article.id') == 0) - { - $filters = (array) $app->getUserState('com_content.articles.filter'); - $data->set( - 'state', - $app->input->getInt( - 'state', - ((isset($filters['published']) && $filters['published'] !== '') ? $filters['published'] : null) - ) - ); - $data->set('catid', $app->input->getInt('catid', (!empty($filters['category_id']) ? $filters['category_id'] : null))); - - if ($app->isClient('administrator')) - { - $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); - } - - $data->set('access', - $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) - ); - } - } - - // If there are params fieldsets in the form it will fail with a registry object - if (isset($data->params) && $data->params instanceof Registry) - { - $data->params = $data->params->toArray(); - } - - $this->preprocessData('com_content.article', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see \Joomla\CMS\Form\FormRule - * @see JFilterInput - * @since 3.7.0 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_content')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $app = Factory::getApplication(); - $input = $app->input; - $filter = InputFilter::getInstance(); - - if (isset($data['metadata']) && isset($data['metadata']['author'])) - { - $data['metadata']['author'] = $filter->clean($data['metadata']['author'], 'TRIM'); - } - - if (isset($data['created_by_alias'])) - { - $data['created_by_alias'] = $filter->clean($data['created_by_alias'], 'TRIM'); - } - - if (isset($data['images']) && is_array($data['images'])) - { - $registry = new Registry($data['images']); - - $data['images'] = (string) $registry; - } - - $this->workflowBeforeSave(); - - // Create new category, if needed. - $createCategory = true; - - if (is_null($data['catid'])) - { - // When there is no catid passed don't try to create one - $createCategory = false; - } - - // If category ID is provided, check if it's valid. - if (is_numeric($data['catid']) && $data['catid']) - { - $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_content'); - } - - // Save New Category - if ($createCategory && $this->canCreateCategory()) - { - $category = [ - // Remove #new# prefix, if exists. - 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], - 'parent_id' => 1, - 'extension' => 'com_content', - 'language' => $data['language'], - 'published' => 1, - ]; - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - - // Create new category. - if (!$categoryModel->save($category)) - { - $this->setError($categoryModel->getError()); - - return false; - } - - // Get the Category ID. - $data['catid'] = $categoryModel->getState('category.id'); - } - - if (isset($data['urls']) && is_array($data['urls'])) - { - $check = $input->post->get('jform', array(), 'array'); - - foreach ($data['urls'] as $i => $url) - { - if ($url != false && ($i == 'urla' || $i == 'urlb' || $i == 'urlc')) - { - if (preg_match('~^#[a-zA-Z]{1}[a-zA-Z0-9-_:.]*$~', $check['urls'][$i]) == 1) - { - $data['urls'][$i] = $check['urls'][$i]; - } - else - { - $data['urls'][$i] = PunycodeHelper::urlToPunycode($url); - } - } - } - - unset($check); - - $registry = new Registry($data['urls']); - - $data['urls'] = (string) $registry; - } - - // Alter the title for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = $this->getTable(); - - if ($app->isClient('site')) - { - $origTable->load($input->getInt('a_id')); - - if ($origTable->title === $data['title']) - { - /** - * If title of article is not changed, set alias to original article alias so that Joomla! will generate - * new Title and Alias for the copied article - */ - $data['alias'] = $origTable->alias; - } - else - { - $data['alias'] = ''; - } - } - else - { - $origTable->load($input->getInt('id')); - } - - if ($data['title'] == $origTable->title) - { - list($title, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['title']); - $data['title'] = $title; - $data['alias'] = $alias; - } - elseif ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - // Automatic handling of alias for empty fields - if (in_array($input->get('task'), array('apply', 'save', 'save2new')) && (!isset($data['id']) || (int) $data['id'] == 0)) - { - if ($data['alias'] == null) - { - if ($app->get('unicodeslugs') == 1) - { - $data['alias'] = OutputFilter::stringUrlUnicodeSlug($data['title']); - } - else - { - $data['alias'] = OutputFilter::stringURLSafe($data['title']); - } - - $table = $this->getTable(); - - if ($table->load(array('alias' => $data['alias'], 'catid' => $data['catid']))) - { - $msg = Text::_('COM_CONTENT_SAVE_WARNING'); - } - - list($title, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['title']); - $data['alias'] = $alias; - - if (isset($msg)) - { - $app->enqueueMessage($msg, 'warning'); - } - } - } - - if (parent::save($data)) - { - // Check if featured is set and if not managed by workflow - if (isset($data['featured']) && !$this->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article')) - { - if (!$this->featured( - $this->getState($this->getName() . '.id'), - $data['featured'], - $data['featured_up'] ?? null, - $data['featured_down'] ?? null - )) - { - return false; - } - } - - $this->workflowAfterSave($data); - - return true; - } - - return false; - } - - /** - * Method to toggle the featured setting of articles. - * - * @param array $pks The ids of the items to toggle. - * @param integer $value The value to toggle to. - * @param string|Date $featuredUp The date which item featured up. - * @param string|Date $featuredDown The date which item featured down. - * - * @return boolean True on success. - */ - public function featured($pks, $value = 0, $featuredUp = null, $featuredDown = null) - { - // Sanitize the ids. - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - $value = (int) $value; - $context = $this->option . '.' . $this->name; - - $this->workflowBeforeStageChange(); - - // Include the plugins for the change of state event. - PluginHelper::importPlugin($this->events_map['featured']); - - // Convert empty strings to null for the query. - if ($featuredUp === '') - { - $featuredUp = null; - } - - if ($featuredDown === '') - { - $featuredDown = null; - } - - if (empty($pks)) - { - $this->setError(Text::_('COM_CONTENT_NO_ITEM_SELECTED')); - - return false; - } - - $table = $this->getTable('Featured', 'Administrator'); - - // Trigger the before change state event. - $eventResult = Factory::getApplication()->getDispatcher()->dispatch( - $this->event_before_change_featured, - AbstractEvent::create( - $this->event_before_change_featured, - [ - 'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent', - 'subject' => $this, - 'extension' => $context, - 'pks' => $pks, - 'value' => $value, - ] - ) - ); - - if ($eventResult->getArgument('abort', false)) - { - $this->setError(Text::_($eventResult->getArgument('abortReason'))); - - return false; - } - - try - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__content')) - ->set($db->quoteName('featured') . ' = :featured') - ->whereIn($db->quoteName('id'), $pks) - ->bind(':featured', $value, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - if ($value === 0) - { - // Adjust the mapping table. - // Clear the existing features settings. - $query = $db->getQuery(true) - ->delete($db->quoteName('#__content_frontpage')) - ->whereIn($db->quoteName('content_id'), $pks); - $db->setQuery($query); - $db->execute(); - } - else - { - // First, we find out which of our new featured articles are already featured. - $query = $db->getQuery(true) - ->select($db->quoteName('content_id')) - ->from($db->quoteName('#__content_frontpage')) - ->whereIn($db->quoteName('content_id'), $pks); - $db->setQuery($query); - - $oldFeatured = $db->loadColumn(); - - // Update old featured articles - if (count($oldFeatured)) - { - $query = $db->getQuery(true) - ->update($db->quoteName('#__content_frontpage')) - ->set( - [ - $db->quoteName('featured_up') . ' = :featuredUp', - $db->quoteName('featured_down') . ' = :featuredDown', - ] - ) - ->whereIn($db->quoteName('content_id'), $oldFeatured) - ->bind(':featuredUp', $featuredUp, $featuredUp ? ParameterType::STRING : ParameterType::NULL) - ->bind(':featuredDown', $featuredDown, $featuredDown ? ParameterType::STRING : ParameterType::NULL); - $db->setQuery($query); - $db->execute(); - } - - // We diff the arrays to get a list of the articles that are newly featured - $newFeatured = array_diff($pks, $oldFeatured); - - // Featuring. - if ($newFeatured) - { - $query = $db->getQuery(true) - ->insert($db->quoteName('#__content_frontpage')) - ->columns( - [ - $db->quoteName('content_id'), - $db->quoteName('ordering'), - $db->quoteName('featured_up'), - $db->quoteName('featured_down'), - ] - ); - - $dataTypes = [ - ParameterType::INTEGER, - ParameterType::INTEGER, - $featuredUp ? ParameterType::STRING : ParameterType::NULL, - $featuredDown ? ParameterType::STRING : ParameterType::NULL, - ]; - - foreach ($newFeatured as $pk) - { - $query->values(implode(',', $query->bindArray([$pk, 0, $featuredUp, $featuredDown], $dataTypes))); - } - - $db->setQuery($query); - $db->execute(); - } - } - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $table->reorder(); - - // Trigger the change state event. - Factory::getApplication()->getDispatcher()->dispatch( - $this->event_after_change_featured, - AbstractEvent::create( - $this->event_after_change_featured, - [ - 'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent', - 'subject' => $this, - 'extension' => $context, - 'pks' => $pks, - 'value' => $value, - ] - ) - ); - - $this->cleanCache(); - - return true; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('catid') . ' = ' . (int) $table->catid, - ]; - } - - /** - * Allows preprocessing of the Form object. - * - * @param Form $form The form object - * @param array $data The data to be merged into the form object - * @param string $group The plugin group to be executed - * - * @return void - * - * @since 3.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - if ($this->canCreateCategory()) - { - $form->setFieldAttribute('catid', 'allowAdd', 'true'); - - // Add a prefix for categories created on the fly. - $form->setFieldAttribute('catid', 'customPrefix', '#new#'); - } - - // Association content items - if (Associations::isEnabled()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (count($languages) > 1) - { - $addform = new \SimpleXMLElement('
'); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_article'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - } - - $form->load($addform, false); - } - } - - $this->workflowPreprocessForm($form, $data); - - parent::preprocessForm($form, $data, $group); - } - - /** - * Custom clean the cache of com_content and content modules - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_content'); - parent::cleanCache('mod_articles_archive'); - parent::cleanCache('mod_articles_categories'); - parent::cleanCache('mod_articles_category'); - parent::cleanCache('mod_articles_latest'); - parent::cleanCache('mod_articles_news'); - parent::cleanCache('mod_articles_popular'); - } - - /** - * Void hit function for pagebreak when editing content from frontend - * - * @return void - * - * @since 3.6.0 - */ - public function hit() - { - } - - /** - * Is the user allowed to create an on the fly category? - * - * @return boolean - * - * @since 3.6.1 - */ - private function canCreateCategory() - { - return Factory::getUser()->authorise('core.create', 'com_content'); - } - - /** - * Delete #__content_frontpage items if the deleted articles was featured - * - * @param object $pks The primary key related to the contents that was deleted. - * - * @return boolean - * - * @since 3.7.0 - */ - public function delete(&$pks) - { - $return = parent::delete($pks); - - if ($return) - { - // Now check to see if this articles was featured if so delete it from the #__content_frontpage table - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__content_frontpage')) - ->whereIn($db->quoteName('content_id'), $pks); - $db->setQuery($query); - $db->execute(); - - $this->workflow->deleteAssociation($pks); - } - - return $return; - } + use WorkflowBehaviorTrait; + use VersionableModelTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_CONTENT'; + + /** + * The type alias for this content type (for example, 'com_content.article'). + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_content.article'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_content.item'; + + /** + * The event to trigger before changing featured status one or more items. + * + * @var string + * @since 4.0.0 + */ + protected $event_before_change_featured = null; + + /** + * The event to trigger after changing featured status one or more items. + * + * @var string + * @since 4.0.0 + */ + protected $event_after_change_featured = null; + + /** + * Constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * @param FormFactoryInterface $formFactory The form factory. + * + * @since 1.6 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config['events_map'] = $config['events_map'] ?? []; + + $config['events_map'] = array_merge( + ['featured' => 'content'], + $config['events_map'] + ); + + parent::__construct($config, $factory, $formFactory); + + // Set the featured status change events + $this->event_before_change_featured = $config['event_before_change_featured'] ?? $this->event_before_change_featured; + $this->event_before_change_featured = $this->event_before_change_featured ?? 'onContentBeforeChangeFeatured'; + $this->event_after_change_featured = $config['event_after_change_featured'] ?? $this->event_after_change_featured; + $this->event_after_change_featured = $this->event_after_change_featured ?? 'onContentAfterChangeFeatured'; + + $this->setUpWorkflow('com_content.article'); + } + + /** + * Function that can be overridden to do any data cleanup after batch copying data + * + * @param TableInterface $table The table object containing the newly created item + * @param integer $newId The id of the new item + * @param integer $oldId The original item id + * + * @return void + * + * @since 3.8.12 + */ + protected function cleanupPostBatchCopy(TableInterface $table, $newId, $oldId) + { + // Check if the article was featured and update the #__content_frontpage table + if ($table->featured == 1) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('featured_up'), + $db->quoteName('featured_down'), + ] + ) + ->from($db->quoteName('#__content_frontpage')) + ->where($db->quoteName('content_id') . ' = :oldId') + ->bind(':oldId', $oldId, ParameterType::INTEGER); + + $featured = $db->setQuery($query)->loadObject(); + + if ($featured) { + $query = $db->getQuery(true) + ->insert($db->quoteName('#__content_frontpage')) + ->values(':newId, 0, :featuredUp, :featuredDown') + ->bind(':newId', $newId, ParameterType::INTEGER) + ->bind(':featuredUp', $featured->featured_up, $featured->featured_up ? ParameterType::STRING : ParameterType::NULL) + ->bind(':featuredDown', $featured->featured_down, $featured->featured_down ? ParameterType::STRING : ParameterType::NULL); + + $db->setQuery($query); + $db->execute(); + } + } + + $this->workflowCleanupBatchMove($oldId, $newId); + + $oldItem = $this->getTable(); + $oldItem->load($oldId); + $fields = FieldsHelper::getFields('com_content.article', $oldItem, true); + + $fieldsData = array(); + + if (!empty($fields)) { + $fieldsData['com_fields'] = array(); + + foreach ($fields as $field) { + $fieldsData['com_fields'][$field->name] = $field->rawvalue; + } + } + + Factory::getApplication()->triggerEvent('onContentAfterSave', array('com_content.article', &$this->table, false, $fieldsData)); + } + + /** + * Batch move categories to a new category. + * + * @param integer $value The new category ID. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True on success. + * + * @since 3.8.6 + */ + protected function batchMove($value, $pks, $contexts) + { + if (empty($this->batchSet)) { + // Set some needed variables. + $this->user = Factory::getUser(); + $this->table = $this->getTable(); + $this->tableClassName = get_class($this->table); + $this->contentType = new UCMType(); + $this->type = $this->contentType->getTypeByTable($this->tableClassName); + } + + $categoryId = (int) $value; + + if (!$this->checkCategoryId($categoryId)) { + return false; + } + + PluginHelper::importPlugin('system'); + + // Parent exists so we proceed + foreach ($pks as $pk) { + if (!$this->user->authorise('core.edit', $contexts[$pk])) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + + // Check that the row actually exists + if (!$this->table->load($pk)) { + if ($error = $this->table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JLIB_APPLICATION_ERROR_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + $fields = FieldsHelper::getFields('com_content.article', $this->table, true); + + $fieldsData = array(); + + if (!empty($fields)) { + $fieldsData['com_fields'] = array(); + + foreach ($fields as $field) { + $fieldsData['com_fields'][$field->name] = $field->rawvalue; + } + } + + // Set the new category ID + $this->table->catid = $categoryId; + + // We don't want to modify tags - so remove the associated tags helper + if ($this->table instanceof TaggableTableInterface) { + $this->table->clearTagsHelper(); + } + + // Check the row. + if (!$this->table->check()) { + $this->setError($this->table->getError()); + + return false; + } + + // Store the row. + if (!$this->table->store()) { + $this->setError($this->table->getError()); + + return false; + } + + // Run event for moved article + Factory::getApplication()->triggerEvent('onContentAfterSave', array('com_content.article', &$this->table, false, $fieldsData)); + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || ($record->state != -2)) { + return false; + } + + return Factory::getUser()->authorise('core.delete', 'com_content.article.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + // Check for existing article. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', 'com_content.article.' . (int) $record->id); + } + + // New article, so check against the category. + if (!empty($record->catid)) { + return $user->authorise('core.edit.state', 'com_content.category.' . (int) $record->catid); + } + + // Default to component settings if neither article nor category known. + return parent::canEditState($record); + } + + /** + * Prepare and sanitise the table data prior to saving. + * + * @param \Joomla\CMS\Table\Table $table A Table object. + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + // Set the publish date to now + if ($table->state == Workflow::CONDITION_PUBLISHED && (int) $table->publish_up == 0) { + $table->publish_up = Factory::getDate()->toSql(); + } + + if ($table->state == Workflow::CONDITION_PUBLISHED && intval($table->publish_down) == 0) { + $table->publish_down = null; + } + + // Increment the content version number. + $table->version++; + + // Reorder the articles within the category so the new article is first + if (empty($table->id)) { + $table->reorder('catid = ' . (int) $table->catid . ' AND state >= 0'); + } + } + + /** + * Method to change the published state of one or more records. + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function publish(&$pks, $value = 1) + { + $this->workflowBeforeStageChange(); + + return parent::publish($pks, $value); + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + */ + public function getItem($pk = null) + { + if ($item = parent::getItem($pk)) { + // Convert the params field to an array. + $registry = new Registry($item->attribs); + $item->attribs = $registry->toArray(); + + // Convert the metadata field to an array. + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + + // Convert the images field to an array. + $registry = new Registry($item->images); + $item->images = $registry->toArray(); + + // Convert the urls field to an array. + $registry = new Registry($item->urls); + $item->urls = $registry->toArray(); + + $item->articletext = ($item->fulltext !== null && trim($item->fulltext) != '') ? $item->introtext . '
' . $item->fulltext : $item->introtext; + + if (!empty($item->id)) { + $item->tags = new TagsHelper(); + $item->tags->getTagIds($item->id, 'com_content.article'); + + $item->featured_up = null; + $item->featured_down = null; + + if ($item->featured) { + // Get featured dates. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('featured_up'), + $db->quoteName('featured_down'), + ] + ) + ->from($db->quoteName('#__content_frontpage')) + ->where($db->quoteName('content_id') . ' = :id') + ->bind(':id', $item->id, ParameterType::INTEGER); + + $featured = $db->setQuery($query)->loadObject(); + + if ($featured) { + $item->featured_up = $featured->featured_up; + $item->featured_down = $featured->featured_down; + } + } + } + } + + // Load associated content items + $assoc = Associations::isEnabled(); + + if ($assoc) { + $item->associations = array(); + + if ($item->id != null) { + $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $item->id); + + foreach ($associations as $tag => $association) { + $item->associations[$tag] = $association->id; + } + } + } + + return $item; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $app = Factory::getApplication(); + + // Get the form. + $form = $this->loadForm('com_content.article', 'article', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Object uses for checking edit state permission of article + $record = new \stdClass(); + + // Get ID of the article from input, for frontend, we use a_id while backend uses id + $articleIdFromInput = $app->isClient('site') + ? $app->input->getInt('a_id', 0) + : $app->input->getInt('id', 0); + + // On edit article, we get ID of article from article.id state, but on save, we use data from input + $id = (int) $this->getState('article.id', $articleIdFromInput); + + $record->id = $id; + + // For new articles we load the potential state + associations + if ($id == 0 && $formField = $form->getField('catid')) { + $assignedCatids = $data['catid'] ?? $form->getValue('catid'); + + $assignedCatids = is_array($assignedCatids) + ? (int) reset($assignedCatids) + : (int) $assignedCatids; + + // Try to get the category from the category field + if (empty($assignedCatids)) { + $assignedCatids = $formField->getAttribute('default', null); + + if (!$assignedCatids) { + // Choose the first category available + $catOptions = $formField->options; + + if ($catOptions && !empty($catOptions[0]->value)) { + $assignedCatids = (int) $catOptions[0]->value; + } + } + } + + // Activate the reload of the form when category is changed + $form->setFieldAttribute('catid', 'refresh-enabled', true); + $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); + $form->setFieldAttribute('catid', 'refresh-section', 'article'); + + // Store ID of the category uses for edit state permission check + $record->catid = $assignedCatids; + } else { + // Get the category which the article is being added to + if (!empty($data['catid'])) { + $catId = (int) $data['catid']; + } else { + $catIds = $form->getValue('catid'); + + $catId = is_array($catIds) + ? (int) reset($catIds) + : (int) $catIds; + + if (!$catId) { + $catId = (int) $form->getFieldAttribute('catid', 'default', 0); + } + } + + $record->catid = $catId; + } + + // Modify the form based on Edit State access controls. + if (!$this->canEditState($record)) { + // Disable fields for display. + $form->setFieldAttribute('featured', 'disabled', 'true'); + $form->setFieldAttribute('featured_up', 'disabled', 'true'); + $form->setFieldAttribute('featured_down', 'disabled', 'true'); + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is an article you can edit. + $form->setFieldAttribute('featured', 'filter', 'unset'); + $form->setFieldAttribute('featured_up', 'filter', 'unset'); + $form->setFieldAttribute('featured_down', 'filter', 'unset'); + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_content.edit.article.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Category, Language, Access) in edit form if those have been selected in Article Manager: Articles + if ($this->getState('article.id') == 0) { + $filters = (array) $app->getUserState('com_content.articles.filter'); + $data->set( + 'state', + $app->input->getInt( + 'state', + ((isset($filters['published']) && $filters['published'] !== '') ? $filters['published'] : null) + ) + ); + $data->set('catid', $app->input->getInt('catid', (!empty($filters['category_id']) ? $filters['category_id'] : null))); + + if ($app->isClient('administrator')) { + $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); + } + + $data->set( + 'access', + $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) + ); + } + } + + // If there are params fieldsets in the form it will fail with a registry object + if (isset($data->params) && $data->params instanceof Registry) { + $data->params = $data->params->toArray(); + } + + $this->preprocessData('com_content.article', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see \Joomla\CMS\Form\FormRule + * @see JFilterInput + * @since 3.7.0 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_content')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $app = Factory::getApplication(); + $input = $app->input; + $filter = InputFilter::getInstance(); + + if (isset($data['metadata']) && isset($data['metadata']['author'])) { + $data['metadata']['author'] = $filter->clean($data['metadata']['author'], 'TRIM'); + } + + if (isset($data['created_by_alias'])) { + $data['created_by_alias'] = $filter->clean($data['created_by_alias'], 'TRIM'); + } + + if (isset($data['images']) && is_array($data['images'])) { + $registry = new Registry($data['images']); + + $data['images'] = (string) $registry; + } + + $this->workflowBeforeSave(); + + // Create new category, if needed. + $createCategory = true; + + if (is_null($data['catid'])) { + // When there is no catid passed don't try to create one + $createCategory = false; + } + + // If category ID is provided, check if it's valid. + if (is_numeric($data['catid']) && $data['catid']) { + $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_content'); + } + + // Save New Category + if ($createCategory && $this->canCreateCategory()) { + $category = [ + // Remove #new# prefix, if exists. + 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], + 'parent_id' => 1, + 'extension' => 'com_content', + 'language' => $data['language'], + 'published' => 1, + ]; + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + + // Create new category. + if (!$categoryModel->save($category)) { + $this->setError($categoryModel->getError()); + + return false; + } + + // Get the Category ID. + $data['catid'] = $categoryModel->getState('category.id'); + } + + if (isset($data['urls']) && is_array($data['urls'])) { + $check = $input->post->get('jform', array(), 'array'); + + foreach ($data['urls'] as $i => $url) { + if ($url != false && ($i == 'urla' || $i == 'urlb' || $i == 'urlc')) { + if (preg_match('~^#[a-zA-Z]{1}[a-zA-Z0-9-_:.]*$~', $check['urls'][$i]) == 1) { + $data['urls'][$i] = $check['urls'][$i]; + } else { + $data['urls'][$i] = PunycodeHelper::urlToPunycode($url); + } + } + } + + unset($check); + + $registry = new Registry($data['urls']); + + $data['urls'] = (string) $registry; + } + + // Alter the title for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = $this->getTable(); + + if ($app->isClient('site')) { + $origTable->load($input->getInt('a_id')); + + if ($origTable->title === $data['title']) { + /** + * If title of article is not changed, set alias to original article alias so that Joomla! will generate + * new Title and Alias for the copied article + */ + $data['alias'] = $origTable->alias; + } else { + $data['alias'] = ''; + } + } else { + $origTable->load($input->getInt('id')); + } + + if ($data['title'] == $origTable->title) { + list($title, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['title']); + $data['title'] = $title; + $data['alias'] = $alias; + } elseif ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + // Automatic handling of alias for empty fields + if (in_array($input->get('task'), array('apply', 'save', 'save2new')) && (!isset($data['id']) || (int) $data['id'] == 0)) { + if ($data['alias'] == null) { + if ($app->get('unicodeslugs') == 1) { + $data['alias'] = OutputFilter::stringUrlUnicodeSlug($data['title']); + } else { + $data['alias'] = OutputFilter::stringURLSafe($data['title']); + } + + $table = $this->getTable(); + + if ($table->load(array('alias' => $data['alias'], 'catid' => $data['catid']))) { + $msg = Text::_('COM_CONTENT_SAVE_WARNING'); + } + + list($title, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['title']); + $data['alias'] = $alias; + + if (isset($msg)) { + $app->enqueueMessage($msg, 'warning'); + } + } + } + + if (parent::save($data)) { + // Check if featured is set and if not managed by workflow + if (isset($data['featured']) && !$this->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article')) { + if ( + !$this->featured( + $this->getState($this->getName() . '.id'), + $data['featured'], + $data['featured_up'] ?? null, + $data['featured_down'] ?? null + ) + ) { + return false; + } + } + + $this->workflowAfterSave($data); + + return true; + } + + return false; + } + + /** + * Method to toggle the featured setting of articles. + * + * @param array $pks The ids of the items to toggle. + * @param integer $value The value to toggle to. + * @param string|Date $featuredUp The date which item featured up. + * @param string|Date $featuredDown The date which item featured down. + * + * @return boolean True on success. + */ + public function featured($pks, $value = 0, $featuredUp = null, $featuredDown = null) + { + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + $value = (int) $value; + $context = $this->option . '.' . $this->name; + + $this->workflowBeforeStageChange(); + + // Include the plugins for the change of state event. + PluginHelper::importPlugin($this->events_map['featured']); + + // Convert empty strings to null for the query. + if ($featuredUp === '') { + $featuredUp = null; + } + + if ($featuredDown === '') { + $featuredDown = null; + } + + if (empty($pks)) { + $this->setError(Text::_('COM_CONTENT_NO_ITEM_SELECTED')); + + return false; + } + + $table = $this->getTable('Featured', 'Administrator'); + + // Trigger the before change state event. + $eventResult = Factory::getApplication()->getDispatcher()->dispatch( + $this->event_before_change_featured, + AbstractEvent::create( + $this->event_before_change_featured, + [ + 'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent', + 'subject' => $this, + 'extension' => $context, + 'pks' => $pks, + 'value' => $value, + ] + ) + ); + + if ($eventResult->getArgument('abort', false)) { + $this->setError(Text::_($eventResult->getArgument('abortReason'))); + + return false; + } + + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__content')) + ->set($db->quoteName('featured') . ' = :featured') + ->whereIn($db->quoteName('id'), $pks) + ->bind(':featured', $value, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + if ($value === 0) { + // Adjust the mapping table. + // Clear the existing features settings. + $query = $db->getQuery(true) + ->delete($db->quoteName('#__content_frontpage')) + ->whereIn($db->quoteName('content_id'), $pks); + $db->setQuery($query); + $db->execute(); + } else { + // First, we find out which of our new featured articles are already featured. + $query = $db->getQuery(true) + ->select($db->quoteName('content_id')) + ->from($db->quoteName('#__content_frontpage')) + ->whereIn($db->quoteName('content_id'), $pks); + $db->setQuery($query); + + $oldFeatured = $db->loadColumn(); + + // Update old featured articles + if (count($oldFeatured)) { + $query = $db->getQuery(true) + ->update($db->quoteName('#__content_frontpage')) + ->set( + [ + $db->quoteName('featured_up') . ' = :featuredUp', + $db->quoteName('featured_down') . ' = :featuredDown', + ] + ) + ->whereIn($db->quoteName('content_id'), $oldFeatured) + ->bind(':featuredUp', $featuredUp, $featuredUp ? ParameterType::STRING : ParameterType::NULL) + ->bind(':featuredDown', $featuredDown, $featuredDown ? ParameterType::STRING : ParameterType::NULL); + $db->setQuery($query); + $db->execute(); + } + + // We diff the arrays to get a list of the articles that are newly featured + $newFeatured = array_diff($pks, $oldFeatured); + + // Featuring. + if ($newFeatured) { + $query = $db->getQuery(true) + ->insert($db->quoteName('#__content_frontpage')) + ->columns( + [ + $db->quoteName('content_id'), + $db->quoteName('ordering'), + $db->quoteName('featured_up'), + $db->quoteName('featured_down'), + ] + ); + + $dataTypes = [ + ParameterType::INTEGER, + ParameterType::INTEGER, + $featuredUp ? ParameterType::STRING : ParameterType::NULL, + $featuredDown ? ParameterType::STRING : ParameterType::NULL, + ]; + + foreach ($newFeatured as $pk) { + $query->values(implode(',', $query->bindArray([$pk, 0, $featuredUp, $featuredDown], $dataTypes))); + } + + $db->setQuery($query); + $db->execute(); + } + } + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $table->reorder(); + + // Trigger the change state event. + Factory::getApplication()->getDispatcher()->dispatch( + $this->event_after_change_featured, + AbstractEvent::create( + $this->event_after_change_featured, + [ + 'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent', + 'subject' => $this, + 'extension' => $context, + 'pks' => $pks, + 'value' => $value, + ] + ) + ); + + $this->cleanCache(); + + return true; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, + ]; + } + + /** + * Allows preprocessing of the Form object. + * + * @param Form $form The form object + * @param array $data The data to be merged into the form object + * @param string $group The plugin group to be executed + * + * @return void + * + * @since 3.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + if ($this->canCreateCategory()) { + $form->setFieldAttribute('catid', 'allowAdd', 'true'); + + // Add a prefix for categories created on the fly. + $form->setFieldAttribute('catid', 'customPrefix', '#new#'); + } + + // Association content items + if (Associations::isEnabled()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_article'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + } + + $form->load($addform, false); + } + } + + $this->workflowPreprocessForm($form, $data); + + parent::preprocessForm($form, $data, $group); + } + + /** + * Custom clean the cache of com_content and content modules + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_content'); + parent::cleanCache('mod_articles_archive'); + parent::cleanCache('mod_articles_categories'); + parent::cleanCache('mod_articles_category'); + parent::cleanCache('mod_articles_latest'); + parent::cleanCache('mod_articles_news'); + parent::cleanCache('mod_articles_popular'); + } + + /** + * Void hit function for pagebreak when editing content from frontend + * + * @return void + * + * @since 3.6.0 + */ + public function hit() + { + } + + /** + * Is the user allowed to create an on the fly category? + * + * @return boolean + * + * @since 3.6.1 + */ + private function canCreateCategory() + { + return Factory::getUser()->authorise('core.create', 'com_content'); + } + + /** + * Delete #__content_frontpage items if the deleted articles was featured + * + * @param object $pks The primary key related to the contents that was deleted. + * + * @return boolean + * + * @since 3.7.0 + */ + public function delete(&$pks) + { + $return = parent::delete($pks); + + if ($return) { + // Now check to see if this articles was featured if so delete it from the #__content_frontpage table + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__content_frontpage')) + ->whereIn($db->quoteName('content_id'), $pks); + $db->setQuery($query); + $db->execute(); + + $this->workflow->deleteAssociation($pks); + } + + return $return; + } } diff --git a/code/administrator/components/com_content/src/Model/ArticlesModel.php b/code/administrator/components/com_content/src/Model/ArticlesModel.php index 8c9f46e0..280d4e84 100644 --- a/code/administrator/components/com_content/src/Model/ArticlesModel.php +++ b/code/administrator/components/com_content/src/Model/ArticlesModel.php @@ -1,4 +1,5 @@ get('workflow_enabled')) - { - $form->removeField('stage', 'filter'); - } - else - { - $ordering = $form->getField('fullordering', 'list'); - - $ordering->addOption('JSTAGE_ASC', ['value' => 'ws.title ASC']); - $ordering->addOption('JSTAGE_DESC', ['value' => 'ws.title DESC']); - } - - return $form; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.id', $direction = 'desc') - { - $app = Factory::getApplication(); - - $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); - $this->setState('filter.search', $search); - - $featured = $this->getUserStateFromRequest($this->context . '.filter.featured', 'filter_featured', ''); - $this->setState('filter.featured', $featured); - - $published = $this->getUserStateFromRequest($this->context . '.filter.published', 'filter_published', ''); - $this->setState('filter.published', $published); - - $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); - $this->setState('filter.level', $level); - - $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); - $this->setState('filter.language', $language); - - $formSubmitted = $app->input->post->get('form_submitted'); - - // Gets the value of a user state variable and sets it in the session - $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); - $this->getUserStateFromRequest($this->context . '.filter.author_id', 'filter_author_id'); - $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id'); - $this->getUserStateFromRequest($this->context . '.filter.tag', 'filter_tag', ''); - - if ($formSubmitted) - { - $access = $app->input->post->get('access'); - $this->setState('filter.access', $access); - - $authorId = $app->input->post->get('author_id'); - $this->setState('filter.author_id', $authorId); - - $categoryId = $app->input->post->get('category_id'); - $this->setState('filter.category_id', $categoryId); - - $tag = $app->input->post->get('tag'); - $this->setState('filter.tag', $tag); - } - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - $this->setState('filter.forcedLanguage', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . serialize($this->getState('filter.access')); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . serialize($this->getState('filter.category_id')); - $id .= ':' . serialize($this->getState('filter.author_id')); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . serialize($this->getState('filter.tag')); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - $params = ComponentHelper::getParams('com_content'); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.asset_id'), - $db->quoteName('a.title'), - $db->quoteName('a.alias'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.catid'), - $db->quoteName('a.state'), - $db->quoteName('a.access'), - $db->quoteName('a.created'), - $db->quoteName('a.created_by'), - $db->quoteName('a.created_by_alias'), - $db->quoteName('a.modified'), - $db->quoteName('a.ordering'), - $db->quoteName('a.featured'), - $db->quoteName('a.language'), - $db->quoteName('a.hits'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - $db->quoteName('a.introtext'), - $db->quoteName('a.fulltext'), - $db->quoteName('a.note'), - $db->quoteName('a.images'), - $db->quoteName('a.metakey'), - $db->quoteName('a.metadesc'), - $db->quoteName('a.metadata'), - $db->quoteName('a.version'), - ] - ) - ) - ->select( - [ - $db->quoteName('fp.featured_up'), - $db->quoteName('fp.featured_down'), - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('uc.name', 'editor'), - $db->quoteName('ag.title', 'access_level'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('c.created_user_id', 'category_uid'), - $db->quoteName('c.level', 'category_level'), - $db->quoteName('parent.title', 'parent_category_title'), - $db->quoteName('parent.id', 'parent_category_id'), - $db->quoteName('parent.created_user_id', 'parent_category_uid'), - $db->quoteName('parent.level', 'parent_category_level'), - $db->quoteName('ua.name', 'author_name'), - $db->quoteName('wa.stage_id', 'stage_id'), - $db->quoteName('ws.title', 'stage_title'), - $db->quoteName('ws.workflow_id', 'workflow_id'), - $db->quoteName('w.title', 'workflow_title'), - ] - ) - ->from($db->quoteName('#__content', 'a')) - ->where($db->quoteName('wa.extension') . ' = ' . $db->quote('com_content.article')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) - ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) - ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')) - ->join('INNER', $db->quoteName('#__workflow_associations', 'wa'), $db->quoteName('wa.item_id') . ' = ' . $db->quoteName('a.id')) - ->join('INNER', $db->quoteName('#__workflow_stages', 'ws'), $db->quoteName('ws.id') . ' = ' . $db->quoteName('wa.stage_id')) - ->join('INNER', $db->quoteName('#__workflows', 'w'), $db->quoteName('w.id') . ' = ' . $db->quoteName('ws.workflow_id')); - - if (PluginHelper::isEnabled('content', 'vote')) - { - $query->select( - [ - 'COALESCE(NULLIF(ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 0), 0), 0)' - . ' AS ' . $db->quoteName('rating'), - 'COALESCE(NULLIF(' . $db->quoteName('v.rating_count') . ', 0), 0) AS ' . $db->quoteName('rating_count'), - ] - ) - ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')); - } - - // Join over the associations. - if (Associations::isEnabled()) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') - ->from($db->quoteName('#__associations', 'asso1')) - ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) - ->where( - [ - $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('asso1.context') . ' = ' . $db->quote('com_content.item'), - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); - } - - // Filter by access level. - $access = $this->getState('filter.access'); - - if (is_numeric($access)) - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - elseif (is_array($access)) - { - $access = ArrayHelper::toInteger($access); - $query->whereIn($db->quoteName('a.access'), $access); - } - - // Filter by featured. - $featured = (string) $this->getState('filter.featured'); - - if (\in_array($featured, ['0','1'])) - { - $featured = (int) $featured; - $query->where($db->quoteName('a.featured') . ' = :featured') - ->bind(':featured', $featured, ParameterType::INTEGER); - } - - // Filter by access level on categories. - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - $query->whereIn($db->quoteName('c.access'), $groups); - } - - // Filter by published state - $workflowStage = (string) $this->getState('filter.stage'); - - if ($params->get('workflow_enabled') && is_numeric($workflowStage)) - { - $workflowStage = (int) $workflowStage; - $query->where($db->quoteName('wa.stage_id') . ' = :stage') - ->bind(':stage', $workflowStage, ParameterType::INTEGER); - } - - $published = (string) $this->getState('filter.published'); - - if ($published !== '*') - { - if (is_numeric($published)) - { - $state = (int) $published; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif (!is_numeric($workflowStage)) - { - $query->whereIn( - $db->quoteName('a.state'), - [ - ContentComponent::CONDITION_PUBLISHED, - ContentComponent::CONDITION_UNPUBLISHED, - ] - ); - } - } - - // Filter by categories and by level - $categoryId = $this->getState('filter.category_id', array()); - $level = (int) $this->getState('filter.level'); - - if (!is_array($categoryId)) - { - $categoryId = $categoryId ? array($categoryId) : array(); - } - - // Case: Using both categories filter and by level filter - if (count($categoryId)) - { - $categoryId = ArrayHelper::toInteger($categoryId); - $categoryTable = Table::getInstance('Category', 'JTable'); - $subCatItemsWhere = array(); - - foreach ($categoryId as $key => $filter_catid) - { - $categoryTable->load($filter_catid); - - // Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting. - $valuesToBind = [$categoryTable->lft, $categoryTable->rgt]; - - if ($level) - { - $valuesToBind[] = $level + $categoryTable->level - 1; - } - - // Bind values and get parameter names. - $bounded = $query->bindArray($valuesToBind); - - $categoryWhere = $db->quoteName('c.lft') . ' >= ' . $bounded[0] . ' AND ' . $db->quoteName('c.rgt') . ' <= ' . $bounded[1]; - - if ($level) - { - $categoryWhere .= ' AND ' . $db->quoteName('c.level') . ' <= ' . $bounded[2]; - } - - $subCatItemsWhere[] = '(' . $categoryWhere . ')'; - } - - $query->where('(' . implode(' OR ', $subCatItemsWhere) . ')'); - } - - // Case: Using only the by level filter - elseif ($level = (int) $level) - { - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by author - $authorId = $this->getState('filter.author_id'); - - if (is_numeric($authorId)) - { - $authorId = (int) $authorId; - $type = $this->getState('filter.author_id.include', true) ? ' = ' : ' <> '; - $query->where($db->quoteName('a.created_by') . $type . ':authorId') - ->bind(':authorId', $authorId, ParameterType::INTEGER); - } - elseif (is_array($authorId)) - { - // Check to see if by_me is in the array - if (\in_array('by_me', $authorId)) - - // Replace by_me with the current user id in the array - { - $authorId['by_me'] = $user->id; - } - - $authorId = ArrayHelper::toInteger($authorId); - $query->whereIn($db->quoteName('a.created_by'), $authorId); - } - - // Filter by search in title. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - elseif (stripos($search, 'author:') === 0) - { - $search = '%' . substr($search, 7) . '%'; - $query->where('(' . $db->quoteName('ua.name') . ' LIKE :search1 OR ' . $db->quoteName('ua.username') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - elseif (stripos($search, 'content:') === 0) - { - $search = '%' . substr($search, 8) . '%'; - $query->where('(' . $db->quoteName('a.introtext') . ' LIKE :search1 OR ' . $db->quoteName('a.fulltext') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where( - '(' . $db->quoteName('a.title') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2' - . ' OR ' . $db->quoteName('a.note') . ' LIKE :search3)' - ) - ->bind([':search1', ':search2', ':search3'], $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Filter by a single or group of tags. - $tag = $this->getState('filter.tag'); - - // Run simplified query when filtering by one tag. - if (\is_array($tag) && \count($tag) === 1) - { - $tag = $tag[0]; - } - - if ($tag && \is_array($tag)) - { - $tag = ArrayHelper::toInteger($tag); - - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', - $db->quoteName('type_alias') . ' = ' . $db->quote('com_content.article'), - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ); - } - elseif ($tag = (int) $tag) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->where( - [ - $db->quoteName('tagmap.tag_id') . ' = :tag', - $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_content.article'), - ] - ) - ->bind(':tag', $tag, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'a.id'); - $orderDirn = $this->state->get('list.direction', 'DESC'); - - if ($orderCol === 'a.ordering' || $orderCol === 'category_title') - { - $ordering = [ - $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), - $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), - ]; - } - else - { - $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); - } - - $query->order($ordering); - - return $query; - } - - /** - * Method to get all transitions at once for all articles - * - * @return array|boolean - * - * @since 4.0.0 - */ - public function getTransitions() - { - // Get a storage key. - $store = $this->getStoreId('getTransitions'); - - // Try to load the data from internal storage. - if (isset($this->cache[$store])) - { - return $this->cache[$store]; - } - - $db = $this->getDbo(); - $user = Factory::getUser(); - - $items = $this->getItems(); - - if ($items === false) - { - return false; - } - - $stage_ids = ArrayHelper::getColumn($items, 'stage_id'); - $stage_ids = ArrayHelper::toInteger($stage_ids); - $stage_ids = array_values(array_unique(array_filter($stage_ids))); - - $workflow_ids = ArrayHelper::getColumn($items, 'workflow_id'); - $workflow_ids = ArrayHelper::toInteger($workflow_ids); - $workflow_ids = array_values(array_unique(array_filter($workflow_ids))); - - $this->cache[$store] = array(); - - try - { - if (count($stage_ids) || count($workflow_ids)) - { - Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); - - $query = $db->getQuery(true); - - $query ->select( - [ - $db->quoteName('t.id', 'value'), - $db->quoteName('t.title', 'text'), - $db->quoteName('t.from_stage_id'), - $db->quoteName('t.to_stage_id'), - $db->quoteName('s.id', 'stage_id'), - $db->quoteName('s.title', 'stage_title'), - $db->quoteName('t.workflow_id'), - ] - ) - ->from($db->quoteName('#__workflow_transitions', 't')) - ->innerJoin( - $db->quoteName('#__workflow_stages', 's'), - $db->quoteName('t.to_stage_id') . ' = ' . $db->quoteName('s.id') - ) - ->where( - [ - $db->quoteName('t.published') . ' = 1', - $db->quoteName('s.published') . ' = 1', - ] - ) - ->order($db->quoteName('t.ordering')); - - $where = []; - - if (count($stage_ids)) - { - $where[] = $db->quoteName('t.from_stage_id') . ' IN (' . implode(',', $query->bindArray($stage_ids)) . ')'; - } - - if (count($workflow_ids)) - { - $where[] = '(' . $db->quoteName('t.from_stage_id') . ' = -1 AND ' . $db->quoteName('t.workflow_id') . ' IN (' . implode(',', $query->bindArray($workflow_ids)) . '))'; - } - - $query->where('((' . implode(') OR (', $where) . '))'); - - $transitions = $db->setQuery($query)->loadAssocList(); - - foreach ($transitions as $key => $transition) - { - if (!$user->authorise('core.execute.transition', 'com_content.transition.' . (int) $transition['value'])) - { - unset($transitions[$key]); - } - - $transitions[$key]['text'] = Text::_($transition['text']); - } - - $this->cache[$store] = $transitions; - } - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $this->cache[$store]; - } - - /** - * Method to get a list of articles. - * Overridden to add item type alias. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - - foreach ($items as $item) - { - $item->typeAlias = 'com_content.article'; - - if (isset($item->metadata)) - { - $registry = new Registry($item->metadata); - $item->metadata = $registry->toArray(); - } - } - - return $items; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + * @see \Joomla\CMS\MVC\Controller\BaseController + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_title', + 'state', 'a.state', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'modified', 'a.modified', + 'created_by', 'a.created_by', + 'created_by_alias', 'a.created_by_alias', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'featured_up', 'fp.featured_up', + 'featured_down', 'fp.featured_down', + 'language', 'a.language', + 'hits', 'a.hits', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'published', 'a.published', + 'author_id', + 'category_id', + 'level', + 'tag', + 'rating_count', 'rating', + 'stage', 'wa.stage_id', + 'ws.title' + ); + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config); + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \Joomla\CMS\Form\Form|null The Form object or null if the form can't be found + * + * @since 3.2 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + $params = ComponentHelper::getParams('com_content'); + + if (!$params->get('workflow_enabled')) { + $form->removeField('stage', 'filter'); + } else { + $ordering = $form->getField('fullordering', 'list'); + + $ordering->addOption('JSTAGE_ASC', ['value' => 'ws.title ASC']); + $ordering->addOption('JSTAGE_DESC', ['value' => 'ws.title DESC']); + } + + return $form; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); + $this->setState('filter.search', $search); + + $featured = $this->getUserStateFromRequest($this->context . '.filter.featured', 'filter_featured', ''); + $this->setState('filter.featured', $featured); + + $published = $this->getUserStateFromRequest($this->context . '.filter.published', 'filter_published', ''); + $this->setState('filter.published', $published); + + $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); + $this->setState('filter.level', $level); + + $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); + $this->setState('filter.language', $language); + + $formSubmitted = $app->input->post->get('form_submitted'); + + // Gets the value of a user state variable and sets it in the session + $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); + $this->getUserStateFromRequest($this->context . '.filter.author_id', 'filter_author_id'); + $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id'); + $this->getUserStateFromRequest($this->context . '.filter.tag', 'filter_tag', ''); + + if ($formSubmitted) { + $access = $app->input->post->get('access'); + $this->setState('filter.access', $access); + + $authorId = $app->input->post->get('author_id'); + $this->setState('filter.author_id', $authorId); + + $categoryId = $app->input->post->get('category_id'); + $this->setState('filter.category_id', $categoryId); + + $tag = $app->input->post->get('tag'); + $this->setState('filter.tag', $tag); + } + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + $this->setState('filter.forcedLanguage', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . serialize($this->getState('filter.access')); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . serialize($this->getState('filter.author_id')); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + $params = ComponentHelper::getParams('com_content'); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.asset_id'), + $db->quoteName('a.title'), + $db->quoteName('a.alias'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.catid'), + $db->quoteName('a.state'), + $db->quoteName('a.access'), + $db->quoteName('a.created'), + $db->quoteName('a.created_by'), + $db->quoteName('a.created_by_alias'), + $db->quoteName('a.modified'), + $db->quoteName('a.ordering'), + $db->quoteName('a.featured'), + $db->quoteName('a.language'), + $db->quoteName('a.hits'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + $db->quoteName('a.introtext'), + $db->quoteName('a.fulltext'), + $db->quoteName('a.note'), + $db->quoteName('a.images'), + $db->quoteName('a.metakey'), + $db->quoteName('a.metadesc'), + $db->quoteName('a.metadata'), + $db->quoteName('a.version'), + ] + ) + ) + ->select( + [ + $db->quoteName('fp.featured_up'), + $db->quoteName('fp.featured_down'), + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('uc.name', 'editor'), + $db->quoteName('ag.title', 'access_level'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('c.created_user_id', 'category_uid'), + $db->quoteName('c.level', 'category_level'), + $db->quoteName('c.published', 'category_published'), + $db->quoteName('parent.title', 'parent_category_title'), + $db->quoteName('parent.id', 'parent_category_id'), + $db->quoteName('parent.created_user_id', 'parent_category_uid'), + $db->quoteName('parent.level', 'parent_category_level'), + $db->quoteName('ua.name', 'author_name'), + $db->quoteName('wa.stage_id', 'stage_id'), + $db->quoteName('ws.title', 'stage_title'), + $db->quoteName('ws.workflow_id', 'workflow_id'), + $db->quoteName('w.title', 'workflow_title'), + ] + ) + ->from($db->quoteName('#__content', 'a')) + ->where($db->quoteName('wa.extension') . ' = ' . $db->quote('com_content.article')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) + ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) + ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')) + ->join('INNER', $db->quoteName('#__workflow_associations', 'wa'), $db->quoteName('wa.item_id') . ' = ' . $db->quoteName('a.id')) + ->join('INNER', $db->quoteName('#__workflow_stages', 'ws'), $db->quoteName('ws.id') . ' = ' . $db->quoteName('wa.stage_id')) + ->join('INNER', $db->quoteName('#__workflows', 'w'), $db->quoteName('w.id') . ' = ' . $db->quoteName('ws.workflow_id')); + + if (PluginHelper::isEnabled('content', 'vote')) { + $query->select( + [ + 'COALESCE(NULLIF(ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 0), 0), 0)' + . ' AS ' . $db->quoteName('rating'), + 'COALESCE(NULLIF(' . $db->quoteName('v.rating_count') . ', 0), 0) AS ' . $db->quoteName('rating_count'), + ] + ) + ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')); + } + + // Join over the associations. + if (Associations::isEnabled()) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') + ->from($db->quoteName('#__associations', 'asso1')) + ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) + ->where( + [ + $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('asso1.context') . ' = ' . $db->quote('com_content.item'), + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); + } + + // Filter by access level. + $access = $this->getState('filter.access'); + + if (is_numeric($access)) { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } elseif (is_array($access)) { + $access = ArrayHelper::toInteger($access); + $query->whereIn($db->quoteName('a.access'), $access); + } + + // Filter by featured. + $featured = (string) $this->getState('filter.featured'); + + if (\in_array($featured, ['0','1'])) { + $featured = (int) $featured; + $query->where($db->quoteName('a.featured') . ' = :featured') + ->bind(':featured', $featured, ParameterType::INTEGER); + } + + // Filter by access level on categories. + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + $query->whereIn($db->quoteName('c.access'), $groups); + } + + // Filter by published state + $workflowStage = (string) $this->getState('filter.stage'); + + if ($params->get('workflow_enabled') && is_numeric($workflowStage)) { + $workflowStage = (int) $workflowStage; + $query->where($db->quoteName('wa.stage_id') . ' = :stage') + ->bind(':stage', $workflowStage, ParameterType::INTEGER); + } + + $published = (string) $this->getState('filter.published'); + + if ($published !== '*') { + if (is_numeric($published)) { + $state = (int) $published; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif (!is_numeric($workflowStage)) { + $query->whereIn( + $db->quoteName('a.state'), + [ + ContentComponent::CONDITION_PUBLISHED, + ContentComponent::CONDITION_UNPUBLISHED, + ] + ); + } + } + + // Filter by categories and by level + $categoryId = $this->getState('filter.category_id', array()); + $level = (int) $this->getState('filter.level'); + + if (!is_array($categoryId)) { + $categoryId = $categoryId ? array($categoryId) : array(); + } + + // Case: Using both categories filter and by level filter + if (count($categoryId)) { + $categoryId = ArrayHelper::toInteger($categoryId); + $categoryTable = Table::getInstance('Category', 'JTable'); + $subCatItemsWhere = array(); + + foreach ($categoryId as $key => $filter_catid) { + $categoryTable->load($filter_catid); + + // Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting. + $valuesToBind = [$categoryTable->lft, $categoryTable->rgt]; + + if ($level) { + $valuesToBind[] = $level + $categoryTable->level - 1; + } + + // Bind values and get parameter names. + $bounded = $query->bindArray($valuesToBind); + + $categoryWhere = $db->quoteName('c.lft') . ' >= ' . $bounded[0] . ' AND ' . $db->quoteName('c.rgt') . ' <= ' . $bounded[1]; + + if ($level) { + $categoryWhere .= ' AND ' . $db->quoteName('c.level') . ' <= ' . $bounded[2]; + } + + $subCatItemsWhere[] = '(' . $categoryWhere . ')'; + } + + $query->where('(' . implode(' OR ', $subCatItemsWhere) . ')'); + } elseif ($level = (int) $level) { + // Case: Using only the by level filter + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by author + $authorId = $this->getState('filter.author_id'); + + if (is_numeric($authorId)) { + $authorId = (int) $authorId; + $type = $this->getState('filter.author_id.include', true) ? ' = ' : ' <> '; + $query->where($db->quoteName('a.created_by') . $type . ':authorId') + ->bind(':authorId', $authorId, ParameterType::INTEGER); + } elseif (is_array($authorId)) { + // Check to see if by_me is in the array + if (\in_array('by_me', $authorId)) { + // Replace by_me with the current user id in the array + $authorId['by_me'] = $user->id; + } + + $authorId = ArrayHelper::toInteger($authorId); + $query->whereIn($db->quoteName('a.created_by'), $authorId); + } + + // Filter by search in title. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } elseif (stripos($search, 'author:') === 0) { + $search = '%' . substr($search, 7) . '%'; + $query->where('(' . $db->quoteName('ua.name') . ' LIKE :search1 OR ' . $db->quoteName('ua.username') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } elseif (stripos($search, 'content:') === 0) { + $search = '%' . substr($search, 8) . '%'; + $query->where('(' . $db->quoteName('a.introtext') . ' LIKE :search1 OR ' . $db->quoteName('a.fulltext') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where( + '(' . $db->quoteName('a.title') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2' + . ' OR ' . $db->quoteName('a.note') . ' LIKE :search3)' + ) + ->bind([':search1', ':search2', ':search3'], $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Filter by a single or group of tags. + $tag = $this->getState('filter.tag'); + + // Run simplified query when filtering by one tag. + if (\is_array($tag) && \count($tag) === 1) { + $tag = $tag[0]; + } + + if ($tag && \is_array($tag)) { + $tag = ArrayHelper::toInteger($tag); + + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', + $db->quoteName('type_alias') . ' = ' . $db->quote('com_content.article'), + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ); + } elseif ($tag = (int) $tag) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->where( + [ + $db->quoteName('tagmap.tag_id') . ' = :tag', + $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_content.article'), + ] + ) + ->bind(':tag', $tag, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'a.id'); + $orderDirn = $this->state->get('list.direction', 'DESC'); + + if ($orderCol === 'a.ordering' || $orderCol === 'category_title') { + $ordering = [ + $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), + $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), + ]; + } else { + $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); + } + + $query->order($ordering); + + return $query; + } + + /** + * Method to get all transitions at once for all articles + * + * @return array|boolean + * + * @since 4.0.0 + */ + public function getTransitions() + { + // Get a storage key. + $store = $this->getStoreId('getTransitions'); + + // Try to load the data from internal storage. + if (isset($this->cache[$store])) { + return $this->cache[$store]; + } + + $db = $this->getDatabase(); + $user = Factory::getUser(); + + $items = $this->getItems(); + + if ($items === false) { + return false; + } + + $stage_ids = ArrayHelper::getColumn($items, 'stage_id'); + $stage_ids = ArrayHelper::toInteger($stage_ids); + $stage_ids = array_values(array_unique(array_filter($stage_ids))); + + $workflow_ids = ArrayHelper::getColumn($items, 'workflow_id'); + $workflow_ids = ArrayHelper::toInteger($workflow_ids); + $workflow_ids = array_values(array_unique(array_filter($workflow_ids))); + + $this->cache[$store] = array(); + + try { + if (count($stage_ids) || count($workflow_ids)) { + Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); + + $query = $db->getQuery(true); + + $query ->select( + [ + $db->quoteName('t.id', 'value'), + $db->quoteName('t.title', 'text'), + $db->quoteName('t.from_stage_id'), + $db->quoteName('t.to_stage_id'), + $db->quoteName('s.id', 'stage_id'), + $db->quoteName('s.title', 'stage_title'), + $db->quoteName('t.workflow_id'), + ] + ) + ->from($db->quoteName('#__workflow_transitions', 't')) + ->innerJoin( + $db->quoteName('#__workflow_stages', 's'), + $db->quoteName('t.to_stage_id') . ' = ' . $db->quoteName('s.id') + ) + ->where( + [ + $db->quoteName('t.published') . ' = 1', + $db->quoteName('s.published') . ' = 1', + ] + ) + ->order($db->quoteName('t.ordering')); + + $where = []; + + if (count($stage_ids)) { + $where[] = $db->quoteName('t.from_stage_id') . ' IN (' . implode(',', $query->bindArray($stage_ids)) . ')'; + } + + if (count($workflow_ids)) { + $where[] = '(' . $db->quoteName('t.from_stage_id') . ' = -1 AND ' . $db->quoteName('t.workflow_id') . ' IN (' . implode(',', $query->bindArray($workflow_ids)) . '))'; + } + + $query->where('((' . implode(') OR (', $where) . '))'); + + $transitions = $db->setQuery($query)->loadAssocList(); + + foreach ($transitions as $key => $transition) { + if (!$user->authorise('core.execute.transition', 'com_content.transition.' . (int) $transition['value'])) { + unset($transitions[$key]); + } + + $transitions[$key]['text'] = Text::_($transition['text']); + } + + $this->cache[$store] = $transitions; + } + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $this->cache[$store]; + } + + /** + * Method to get a list of articles. + * Overridden to add item type alias. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + foreach ($items as $item) { + $item->typeAlias = 'com_content.article'; + + if (isset($item->metadata)) { + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + } + } + + return $items; + } } diff --git a/code/administrator/components/com_content/src/Model/FeatureModel.php b/code/administrator/components/com_content/src/Model/FeatureModel.php index 8eebd6ae..250b5e28 100644 --- a/code/administrator/components/com_content/src/Model/FeatureModel.php +++ b/code/administrator/components/com_content/src/Model/FeatureModel.php @@ -1,4 +1,5 @@ setState('filter.featured', 1); - } + // Filter by featured articles. + $this->setState('filter.featured', 1); + } - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 4.0.0 - */ - protected function getListQuery() - { - $query = parent::getListQuery(); + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 4.0.0 + */ + protected function getListQuery() + { + $query = parent::getListQuery(); - $query->select($this->getDbo()->quoteName('fp.ordering')); + $query->select($this->getDatabase()->quoteName('fp.ordering')); - return $query; - } + return $query; + } } diff --git a/code/administrator/components/com_content/src/Service/HTML/AdministratorService.php b/code/administrator/components/com_content/src/Service/HTML/AdministratorService.php index 82691d82..a432a1df 100644 --- a/code/administrator/components/com_content/src/Service/HTML/AdministratorService.php +++ b/code/administrator/components/com_content/src/Service/HTML/AdministratorService.php @@ -1,4 +1,5 @@ $associated) - { - $associations[$tag] = (int) $associated->id; - } + // Get the associations + if ($associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $articleid)) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } - // Get the associated menu items - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - 'c.*', - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('l.lang_code'), - $db->quoteName('cat.title', 'category_title'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__content', 'c')) - ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) - ->whereIn($db->quoteName('c.id'), array_values($associations)) - ->where($db->quoteName('c.id') . ' != :articleId') - ->bind(':articleId', $articleid, ParameterType::INTEGER); + // Get the associated menu items + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + 'c.*', + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('l.lang_code'), + $db->quoteName('cat.title', 'category_title'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__content', 'c')) + ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) + ->whereIn($db->quoteName('c.id'), array_values($associations)) + ->where($db->quoteName('c.id') . ' != :articleId') + ->bind(':articleId', $articleid, ParameterType::INTEGER); - $db->setQuery($query); + $db->setQuery($query); - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500, $e); - } + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500, $e); + } - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_content&task=article.edit&id=' . (int) $item->id); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); - $classes = 'badge bg-secondary'; + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_content&task=article.edit&id=' . (int) $item->id); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); + $classes = 'badge bg-secondary'; - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } - $html = LayoutHelper::render('joomla.content.associations', $items); - } + $html = LayoutHelper::render('joomla.content.associations', $items); + } - return $html; - } + return $html; + } } diff --git a/code/administrator/components/com_content/src/Service/HTML/Icon.php b/code/administrator/components/com_content/src/Service/HTML/Icon.php index b70cbaf2..e39da57d 100644 --- a/code/administrator/components/com_content/src/Service/HTML/Icon.php +++ b/code/administrator/components/com_content/src/Service/HTML/Icon.php @@ -1,4 +1,5 @@ id; - - $text = ''; - - if ($params->get('show_icons')) - { - $text .= ''; - } - - $text .= Text::_('COM_CONTENT_NEW_ARTICLE'); - - // Add the button classes to the attribs array - if (isset($attribs['class'])) - { - $attribs['class'] .= ' btn btn-primary'; - } - else - { - $attribs['class'] = 'btn btn-primary'; - } - - $button = HTMLHelper::_('link', Route::_($url), $text, $attribs); - - return $button; - } - - /** - * Display an edit icon for the article. - * - * This icon will not display in a popup window, nor if the article is trashed. - * Edit access checks must be performed in the calling code. - * - * @param object $article The article information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML for the article edit icon. - * - * @since 4.0.0 - */ - public function edit($article, $params, $attribs = array(), $legacy = false) - { - $user = Factory::getUser(); - $uri = Uri::getInstance(); - - // Ignore if in a popup window. - if ($params && $params->get('popup')) - { - return ''; - } - - // Ignore if the state is negative (trashed). - if (!in_array($article->state, [Workflow::CONDITION_UNPUBLISHED, Workflow::CONDITION_PUBLISHED])) - { - return ''; - } - - // Show checked_out icon if the article is checked out by a different user - if (property_exists($article, 'checked_out') - && property_exists($article, 'checked_out_time') - && !is_null($article->checked_out) - && $article->checked_out != $user->get('id')) - { - $checkoutUser = Factory::getUser($article->checked_out); - $date = HTMLHelper::_('date', $article->checked_out_time); - $tooltip = Text::sprintf('COM_CONTENT_CHECKED_OUT_BY', $checkoutUser->name) - . '
' . $date; - - $text = LayoutHelper::render('joomla.content.icons.edit_lock', array('article' => $article, 'tooltip' => $tooltip, 'legacy' => $legacy)); - - $attribs['aria-describedby'] = 'editarticle-' . (int) $article->id; - $output = HTMLHelper::_('link', '#', $text, $attribs); - - return $output; - } - - $contentUrl = RouteHelper::getArticleRoute($article->slug, $article->catid, $article->language); - $url = $contentUrl . '&task=article.edit&a_id=' . $article->id . '&return=' . base64_encode($uri); - - if ($article->state == Workflow::CONDITION_UNPUBLISHED) - { - $tooltip = Text::_('COM_CONTENT_EDIT_UNPUBLISHED_ARTICLE'); - } - else - { - $tooltip = Text::_('COM_CONTENT_EDIT_PUBLISHED_ARTICLE'); - } - - $text = LayoutHelper::render('joomla.content.icons.edit', array('article' => $article, 'tooltip' => $tooltip, 'legacy' => $legacy)); - - $attribs['aria-describedby'] = 'editarticle-' . (int) $article->id; - $output = HTMLHelper::_('link', Route::_($url), $text, $attribs); - - return $output; - } - - /** - * Method to generate a link to print an article - * - * @param Registry $params The item parameters - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML markup for the popup link - * - * @since 4.0.0 - */ - public function print_screen($params, $legacy = false) - { - $text = LayoutHelper::render('joomla.content.icons.print_screen', array('params' => $params, 'legacy' => $legacy)); - - return ''; - } + /** + * Method to generate a link to the create item page for the given category + * + * @param object $category The category information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the create item link + * + * @since 4.0.0 + */ + public function create($category, $params, $attribs = array(), $legacy = false) + { + $uri = Uri::getInstance(); + + $url = 'index.php?option=com_content&task=article.add&return=' . base64_encode($uri) . '&a_id=0&catid=' . $category->id; + + $text = ''; + + if ($params->get('show_icons')) { + $text .= ''; + } + + $text .= Text::_('COM_CONTENT_NEW_ARTICLE'); + + // Add the button classes to the attribs array + if (isset($attribs['class'])) { + $attribs['class'] .= ' btn btn-primary'; + } else { + $attribs['class'] = 'btn btn-primary'; + } + + $button = HTMLHelper::_('link', Route::_($url), $text, $attribs); + + return $button; + } + + /** + * Display an edit icon for the article. + * + * This icon will not display in a popup window, nor if the article is trashed. + * Edit access checks must be performed in the calling code. + * + * @param object $article The article information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML for the article edit icon. + * + * @since 4.0.0 + */ + public function edit($article, $params, $attribs = array(), $legacy = false) + { + $user = Factory::getUser(); + $uri = Uri::getInstance(); + + // Ignore if in a popup window. + if ($params && $params->get('popup')) { + return ''; + } + + // Ignore if the state is negative (trashed). + if (!in_array($article->state, [Workflow::CONDITION_UNPUBLISHED, Workflow::CONDITION_PUBLISHED])) { + return ''; + } + + // Show checked_out icon if the article is checked out by a different user + if ( + property_exists($article, 'checked_out') + && property_exists($article, 'checked_out_time') + && !is_null($article->checked_out) + && $article->checked_out != $user->get('id') + ) { + $checkoutUser = Factory::getUser($article->checked_out); + $date = HTMLHelper::_('date', $article->checked_out_time); + $tooltip = Text::sprintf('COM_CONTENT_CHECKED_OUT_BY', $checkoutUser->name) + . '
' . $date; + + $text = LayoutHelper::render('joomla.content.icons.edit_lock', array('article' => $article, 'tooltip' => $tooltip, 'legacy' => $legacy)); + + $attribs['aria-describedby'] = 'editarticle-' . (int) $article->id; + $output = HTMLHelper::_('link', '#', $text, $attribs); + + return $output; + } + + $contentUrl = RouteHelper::getArticleRoute($article->slug, $article->catid, $article->language); + $url = $contentUrl . '&task=article.edit&a_id=' . $article->id . '&return=' . base64_encode($uri); + + if ($article->state == Workflow::CONDITION_UNPUBLISHED) { + $tooltip = Text::_('COM_CONTENT_EDIT_UNPUBLISHED_ARTICLE'); + } else { + $tooltip = Text::_('COM_CONTENT_EDIT_PUBLISHED_ARTICLE'); + } + + $text = LayoutHelper::render('joomla.content.icons.edit', array('article' => $article, 'tooltip' => $tooltip, 'legacy' => $legacy)); + + $attribs['aria-describedby'] = 'editarticle-' . (int) $article->id; + $output = HTMLHelper::_('link', Route::_($url), $text, $attribs); + + return $output; + } + + /** + * Method to generate a link to print an article + * + * @param Registry $params The item parameters + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the popup link + * + * @since 4.0.0 + */ + public function print_screen($params, $legacy = false) + { + $text = LayoutHelper::render('joomla.content.icons.print_screen', array('params' => $params, 'legacy' => $legacy)); + + return ''; + } } diff --git a/code/administrator/components/com_content/src/Table/ArticleTable.php b/code/administrator/components/com_content/src/Table/ArticleTable.php index 35d8f589..69f6c9de 100644 --- a/code/administrator/components/com_content/src/Table/ArticleTable.php +++ b/code/administrator/components/com_content/src/Table/ArticleTable.php @@ -1,4 +1,5 @@ getLayout() == 'pagebreak') - { - parent::display($tpl); - - return; - } - - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - $this->canDo = ContentHelper::getActions('com_content', 'article', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); - - // Only allow to select tags with All language or with the forced language. - $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - $user = Factory::getUser(); - $userId = $user->id; - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Built the actions for new and existing records. - $canDo = $this->canDo; - - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title( - Text::_('COM_CONTENT_PAGE_' . ($checkedOut ? 'VIEW_ARTICLE' : ($isNew ? 'ADD_ARTICLE' : 'EDIT_ARTICLE'))), - 'pencil-alt article-add' - ); - - // For new records, check the create permission. - if ($isNew && (count($user->getAuthorisedCategories('com_content', 'core.create')) > 0)) - { - $toolbar->apply('article.apply'); - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) use ($user) - { - $childBar->save('article.save'); - - if ($user->authorise('core.create', 'com_menus.menu')) - { - $childBar->save('article.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); - } - - $childBar->save2new('article.save2new'); - } - ); - - $toolbar->cancel('article.cancel', 'JTOOLBAR_CANCEL'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - if (!$checkedOut && $itemEditable) - { - $toolbar->apply('article.apply'); - } - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) - { - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - $childBar->save('article.save'); - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $childBar->save2new('article.save2new'); - } - } - - // If checked out, we can still save2menu - if ($user->authorise('core.create', 'com_menus.menu')) - { - $childBar->save('article.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); - } - - // If checked out, we can still save - if ($canDo->get('core.create')) - { - $childBar->save2copy('article.save2copy'); - } - } - ); - - $toolbar->cancel('article.cancel', 'JTOOLBAR_CLOSE'); - - if (!$isNew) - { - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) - { - $toolbar->versions('com_content.article', $this->item->id); - } - - $url = RouteHelper::getArticleRoute($this->item->id . ':' . $this->item->alias, $this->item->catid, $this->item->language); - - $toolbar->preview(Route::link('site', $url, true), 'JGLOBAL_PREVIEW') - ->bodyHeight(80) - ->modalWidth(90); - - if (PluginHelper::isEnabled('system', 'jooa11y')) - { - $toolbar->jooa11y(Route::link('site', $url . '&jooa11y=1', true), 'JGLOBAL_JOOA11Y') - ->bodyHeight(80) - ->modalWidth(90); - } - - if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) - { - $toolbar->standardButton('contract') - ->text('JTOOLBAR_ASSOCIATIONS') - ->task('article.editAssociations'); - } - } - } - - $toolbar->divider(); - - ToolbarHelper::inlinehelp(); - - $toolbar->help('Articles:_Edit'); - } + /** + * The \JForm object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var object + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $canDo; + + /** + * Pagebreak TOC alias + * + * @var string + */ + protected $eName; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws \Exception + */ + public function display($tpl = null) + { + if ($this->getLayout() == 'pagebreak') { + parent::display($tpl); + + return; + } + + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->canDo = ContentHelper::getActions('com_content', 'article', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Built the actions for new and existing records. + $canDo = $this->canDo; + + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title( + Text::_('COM_CONTENT_PAGE_' . ($checkedOut ? 'VIEW_ARTICLE' : ($isNew ? 'ADD_ARTICLE' : 'EDIT_ARTICLE'))), + 'pencil-alt article-add' + ); + + // For new records, check the create permission. + if ($isNew && (count($user->getAuthorisedCategories('com_content', 'core.create')) > 0)) { + $toolbar->apply('article.apply'); + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) use ($user) { + $childBar->save('article.save'); + + if ($user->authorise('core.create', 'com_menus.menu')) { + $childBar->save('article.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); + } + + $childBar->save2new('article.save2new'); + } + ); + + $toolbar->cancel('article.cancel', 'JTOOLBAR_CANCEL'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + if (!$checkedOut && $itemEditable) { + $toolbar->apply('article.apply'); + } + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) { + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + $childBar->save('article.save'); + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $childBar->save2new('article.save2new'); + } + } + + // If checked out, we can still save2menu + if ($user->authorise('core.create', 'com_menus.menu')) { + $childBar->save('article.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); + } + + // If checked out, we can still save + if ($canDo->get('core.create')) { + $childBar->save2copy('article.save2copy'); + } + } + ); + + $toolbar->cancel('article.cancel', 'JTOOLBAR_CLOSE'); + + if (!$isNew) { + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) { + $toolbar->versions('com_content.article', $this->item->id); + } + + $url = RouteHelper::getArticleRoute($this->item->id . ':' . $this->item->alias, $this->item->catid, $this->item->language); + + $toolbar->preview(Route::link('site', $url, true), 'JGLOBAL_PREVIEW') + ->bodyHeight(80) + ->modalWidth(90); + + if (PluginHelper::isEnabled('system', 'jooa11y')) { + $toolbar->jooa11y(Route::link('site', $url . '&jooa11y=1', true), 'JGLOBAL_JOOA11Y') + ->bodyHeight(80) + ->modalWidth(90); + } + + if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + $toolbar->standardButton('contract') + ->text('JTOOLBAR_ASSOCIATIONS') + ->task('article.editAssociations'); + } + } + } + + $toolbar->divider(); + + ToolbarHelper::inlinehelp(); + + $toolbar->help('Articles:_Edit'); + } } diff --git a/code/administrator/components/com_content/src/View/Articles/HtmlView.php b/code/administrator/components/com_content/src/View/Articles/HtmlView.php index deb7a7c8..e4565910 100644 --- a/code/administrator/components/com_content/src/View/Articles/HtmlView.php +++ b/code/administrator/components/com_content/src/View/Articles/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->vote = PluginHelper::isEnabled('content', 'vote'); - $this->hits = ComponentHelper::getParams('com_content')->get('record_hits', 1); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) - { - PluginHelper::importPlugin('workflow'); - - $this->transitions = $this->get('Transitions'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors')) || $this->transitions === false) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In article associations modal we need to remove language filter if forcing a language. - // We also need to change the category filter to show show categories with All or the forced language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - - // One last changes needed is to change the category filter to just show categories with All language or with the forced language. - $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_CONTENT_ARTICLES_TITLE'), 'copy article'); - - if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) - { - $toolbar->addNew('article.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || \count($this->transitions))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if (\count($this->transitions)) - { - $childBar->separatorButton('transition-headline') - ->text('COM_CONTENT_RUN_TRANSITIONS') - ->buttonClass('text-center py-2 h3'); - - $cmd = "Joomla.submitbutton('articles.runTransition');"; - $messages = "{error: [Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST')]}"; - $alert = 'Joomla.renderMessages(' . $messages . ')'; - $cmd = 'if (document.adminForm.boxchecked.value == 0) { ' . $alert . ' } else { ' . $cmd . ' }'; - - foreach ($this->transitions as $transition) - { - $childBar->standardButton('transition') - ->text($transition['text']) - ->buttonClass('transition-' . (int) $transition['value']) - ->icon('icon-project-diagram') - ->onclick('document.adminForm.transition_id.value=' . (int) $transition['value'] . ';' . $cmd); - } - - $childBar->separatorButton('transition-separator'); - } - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('articles.publish')->listCheck(true); - - $childBar->unpublish('articles.unpublish')->listCheck(true); - - $childBar->standardButton('featured') - ->text('JFEATURE') - ->task('articles.featured') - ->listCheck(true); - - $childBar->standardButton('unfeatured') - ->text('JUNFEATURE') - ->task('articles.unfeatured') - ->listCheck(true); - - $childBar->archive('articles.archive')->listCheck(true); - - $childBar->checkin('articles.checkin')->listCheck(true); - - if ($this->state->get('filter.published') != ContentComponent::CONDITION_TRASHED) - { - $childBar->trash('articles.trash')->listCheck(true); - } - } - - // Add a batch button - if ($user->authorise('core.create', 'com_content') - && $user->authorise('core.edit', 'com_content') - && $user->authorise('core.execute.transition', 'com_content')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) - { - $toolbar->delete('articles.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) - { - $toolbar->preferences('com_content'); - } - - $toolbar->help('Articles'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * All transition, which can be executed of one if the items + * + * @var array + */ + protected $transitions = []; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->vote = PluginHelper::isEnabled('content', 'vote'); + $this->hits = ComponentHelper::getParams('com_content')->get('record_hits', 1); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) { + PluginHelper::importPlugin('workflow'); + + $this->transitions = $this->get('Transitions'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors')) || $this->transitions === false) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In article associations modal we need to remove language filter if forcing a language. + // We also need to change the category filter to show show categories with All or the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + + // One last changes needed is to change the category filter to just show categories with All language or with the forced language. + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); + $user = $this->getCurrentUser(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_CONTENT_ARTICLES_TITLE'), 'copy article'); + + if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $toolbar->addNew('article.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || \count($this->transitions))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if (\count($this->transitions)) { + $childBar->separatorButton('transition-headline') + ->text('COM_CONTENT_RUN_TRANSITIONS') + ->buttonClass('text-center py-2 h3'); + + $cmd = "Joomla.submitbutton('articles.runTransition');"; + $messages = "{error: [Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST')]}"; + $alert = 'Joomla.renderMessages(' . $messages . ')'; + $cmd = 'if (document.adminForm.boxchecked.value == 0) { ' . $alert . ' } else { ' . $cmd . ' }'; + + foreach ($this->transitions as $transition) { + $childBar->standardButton('transition') + ->text($transition['text']) + ->buttonClass('transition-' . (int) $transition['value']) + ->icon('icon-project-diagram') + ->onclick('document.adminForm.transition_id.value=' . (int) $transition['value'] . ';' . $cmd); + } + + $childBar->separatorButton('transition-separator'); + } + + if ($canDo->get('core.edit.state')) { + $childBar->publish('articles.publish')->listCheck(true); + + $childBar->unpublish('articles.unpublish')->listCheck(true); + + $childBar->standardButton('featured') + ->text('JFEATURE') + ->task('articles.featured') + ->listCheck(true); + + $childBar->standardButton('unfeatured') + ->text('JUNFEATURE') + ->task('articles.unfeatured') + ->listCheck(true); + + $childBar->archive('articles.archive')->listCheck(true); + + $childBar->checkin('articles.checkin')->listCheck(true); + + if ($this->state->get('filter.published') != ContentComponent::CONDITION_TRASHED) { + $childBar->trash('articles.trash')->listCheck(true); + } + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_content') + && $user->authorise('core.edit', 'com_content') + && $user->authorise('core.execute.transition', 'com_content') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) { + $toolbar->delete('articles.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) { + $toolbar->preferences('com_content'); + } + + $toolbar->help('Articles'); + } } diff --git a/code/administrator/components/com_content/src/View/Featured/HtmlView.php b/code/administrator/components/com_content/src/View/Featured/HtmlView.php index ca034b37..413505cf 100644 --- a/code/administrator/components/com_content/src/View/Featured/HtmlView.php +++ b/code/administrator/components/com_content/src/View/Featured/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->vote = PluginHelper::isEnabled('content', 'vote'); - $this->hits = ComponentHelper::getParams('com_content')->get('record_hits', 1); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) - { - PluginHelper::importPlugin('workflow'); - - $this->transitions = $this->get('Transitions'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_CONTENT_FEATURED_TITLE'), 'star featured'); - - if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) - { - $toolbar->addNew('article.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || \count($this->transitions))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if (\count($this->transitions)) - { - $childBar->separatorButton('transition-headline') - ->text('COM_CONTENT_RUN_TRANSITIONS') - ->buttonClass('text-center py-2 h3'); - - $cmd = "Joomla.submitbutton('articles.runTransition');"; - $messages = "{error: [Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST')]}"; - $alert = 'Joomla.renderMessages(' . $messages . ')'; - $cmd = 'if (document.adminForm.boxchecked.value == 0) { ' . $alert . ' } else { ' . $cmd . ' }'; - - foreach ($this->transitions as $transition) - { - $childBar->standardButton('transition') - ->text($transition['text']) - ->buttonClass('transition-' . (int) $transition['value']) - ->icon('icon-project-diagram') - ->onclick('document.adminForm.transition_id.value=' . (int) $transition['value'] . ';' . $cmd); - } - - $childBar->separatorButton('transition-separator'); - } - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('articles.publish')->listCheck(true); - - $childBar->unpublish('articles.unpublish')->listCheck(true); - - $childBar->standardButton('unfeatured') - ->text('JUNFEATURE') - ->task('articles.unfeatured') - ->listCheck(true); - - $childBar->archive('articles.archive')->listCheck(true); - - $childBar->checkin('articles.checkin')->listCheck(true); - - if (!$this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED) - { - $childBar->trash('articles.trash')->listCheck(true); - } - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) - { - $toolbar->delete('articles.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) - { - $toolbar->preferences('com_content'); - } - - ToolbarHelper::help('Articles:_Featured'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * All transition, which can be executed of one if the items + * + * @var array + */ + protected $transitions = []; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->vote = PluginHelper::isEnabled('content', 'vote'); + $this->hits = ComponentHelper::getParams('com_content')->get('record_hits', 1); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) { + PluginHelper::importPlugin('workflow'); + + $this->transitions = $this->get('Transitions'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_CONTENT_FEATURED_TITLE'), 'star featured'); + + if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $toolbar->addNew('article.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || \count($this->transitions))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if (\count($this->transitions)) { + $childBar->separatorButton('transition-headline') + ->text('COM_CONTENT_RUN_TRANSITIONS') + ->buttonClass('text-center py-2 h3'); + + $cmd = "Joomla.submitbutton('articles.runTransition');"; + $messages = "{error: [Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST')]}"; + $alert = 'Joomla.renderMessages(' . $messages . ')'; + $cmd = 'if (document.adminForm.boxchecked.value == 0) { ' . $alert . ' } else { ' . $cmd . ' }'; + + foreach ($this->transitions as $transition) { + $childBar->standardButton('transition') + ->text($transition['text']) + ->buttonClass('transition-' . (int) $transition['value']) + ->icon('icon-project-diagram') + ->onclick('document.adminForm.transition_id.value=' . (int) $transition['value'] . ';' . $cmd); + } + + $childBar->separatorButton('transition-separator'); + } + + if ($canDo->get('core.edit.state')) { + $childBar->publish('articles.publish')->listCheck(true); + + $childBar->unpublish('articles.unpublish')->listCheck(true); + + $childBar->standardButton('unfeatured') + ->text('JUNFEATURE') + ->task('articles.unfeatured') + ->listCheck(true); + + $childBar->archive('articles.archive')->listCheck(true); + + $childBar->checkin('articles.checkin')->listCheck(true); + + if (!$this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED) { + $childBar->trash('articles.trash')->listCheck(true); + } + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) { + $toolbar->delete('articles.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) { + $toolbar->preferences('com_content'); + } + + ToolbarHelper::help('Articles:_Featured'); + } } diff --git a/code/administrator/components/com_content/tmpl/article/edit.php b/code/administrator/components/com_content/tmpl/article/edit.php index 29ce01e1..969455f1 100644 --- a/code/administrator/components/com_content/tmpl/article/edit.php +++ b/code/administrator/components/com_content/tmpl/article/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->getRegistry()->addExtensionRegistryFile('com_contenthistory'); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_contenthistory.admin-history-versions'); + ->useScript('form.validate') + ->useScript('com_contenthistory.admin-history-versions'); $this->configFieldsets = array('editorConfig'); $this->hiddenFieldsets = array('basic-limited'); @@ -42,15 +43,13 @@ $assoc = Associations::isEnabled(); $showArticleOptions = $params->get('show_article_options', 1); -if (!$assoc || !$showArticleOptions) -{ - $this->ignore_fieldsets[] = 'frontendassociations'; +if (!$assoc || !$showArticleOptions) { + $this->ignore_fieldsets[] = 'frontendassociations'; } -if (!$showArticleOptions) -{ - // Ignore fieldsets inside Options tab - $this->ignore_fieldsets = array_merge($this->ignore_fieldsets, ['attribs', 'basic', 'category', 'author', 'date', 'other']); +if (!$showArticleOptions) { + // Ignore fieldsets inside Options tab + $this->ignore_fieldsets = array_merge($this->ignore_fieldsets, ['attribs', 'basic', 'category', 'author', 'date', 'other']); } // In case of modal @@ -59,129 +58,129 @@ $tmpl = $isModal || $input->get('tmpl', '', 'cmd') === 'component' ? '&tmpl=component' : ''; ?> - - -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> - - -
-
-
-
- form->getLabel('articletext'); ?> - form->getInput('articletext'); ?> -
-
-
-
- -
-
- - - - - get('show_urls_images_backend') == 1) : ?> - -
-
- -
- form->getFieldsets()[$fieldset]->label); ?> -
- form->renderFieldset($fieldset); ?> -
-
- -
-
- -
- form->getFieldsets()[$fieldset]->label); ?> -
- form->renderFieldset($fieldset); ?> -
-
- -
-
- - - - - - - - get('show_publishing_options', 1) == 1) : ?> - -
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- - - - get('show_associations_edit', 1) == 1) : ?> - -
- -
- -
-
- - - - - - canDo->get('core.admin') && $params->get('show_configure_edit_options', 1) == 1) : ?> - -
- -
- form->renderFieldset('editorConfig'); ?> -
-
- - - - canDo->get('core.admin') && $params->get('show_permissions', 1) == 1) : ?> - -
- -
- form->getInput('rules'); ?> -
-
- - - - - - - get('show_publishing_options', 1) == 0) : ?> - form->getInput('id'); ?> - - - - - - - -
+ + +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+
+
+
+ form->getLabel('articletext'); ?> + form->getInput('articletext'); ?> +
+
+
+
+ +
+
+ + + + + get('show_urls_images_backend') == 1) : ?> + +
+
+ +
+ form->getFieldsets()[$fieldset]->label); ?> +
+ form->renderFieldset($fieldset); ?> +
+
+ +
+
+ +
+ form->getFieldsets()[$fieldset]->label); ?> +
+ form->renderFieldset($fieldset); ?> +
+
+ +
+
+ + + + + + + + get('show_publishing_options', 1) == 1) : ?> + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + + + get('show_associations_edit', 1) == 1) : ?> + +
+ +
+ +
+
+ + + + + + canDo->get('core.admin') && $params->get('show_configure_edit_options', 1) == 1) : ?> + +
+ +
+ form->renderFieldset('editorConfig'); ?> +
+
+ + + + canDo->get('core.admin') && $params->get('show_permissions', 1) == 1) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + + + + get('show_publishing_options', 1) == 0) : ?> + form->getInput('id'); ?> + + + + + + + +
diff --git a/code/administrator/components/com_content/tmpl/article/modal.php b/code/administrator/components/com_content/tmpl/article/modal.php index 9147d5b5..f6d9da1f 100644 --- a/code/administrator/components/com_content/tmpl/article/modal.php +++ b/code/administrator/components/com_content/tmpl/article/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/code/administrator/components/com_content/tmpl/article/pagebreak.php b/code/administrator/components/com_content/tmpl/article/pagebreak.php index 53a8211f..4612825d 100644 --- a/code/administrator/components/com_content/tmpl/article/pagebreak.php +++ b/code/administrator/components/com_content/tmpl/article/pagebreak.php @@ -1,4 +1,5 @@
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
- - - -
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ + + +
diff --git a/code/administrator/components/com_content/tmpl/articles/default.php b/code/administrator/components/com_content/tmpl/articles/default.php index 65bc6b43..3e260a1d 100644 --- a/code/administrator/components/com_content/tmpl/articles/default.php +++ b/code/administrator/components/com_content/tmpl/articles/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); +$wa->useScript('table.columns') + ->useScript('multiselect'); + $app = Factory::getApplication(); $user = Factory::getUser(); $userId = $user->get('id'); @@ -31,390 +37,364 @@ $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'a.ordering'; -if (strpos($listOrder, 'publish_up') !== false) -{ - $orderingColumn = 'publish_up'; -} -elseif (strpos($listOrder, 'publish_down') !== false) -{ - $orderingColumn = 'publish_down'; -} -elseif (strpos($listOrder, 'modified') !== false) -{ - $orderingColumn = 'modified'; -} -else -{ - $orderingColumn = 'created'; +if (strpos($listOrder, 'publish_up') !== false) { + $orderingColumn = 'publish_up'; +} elseif (strpos($listOrder, 'publish_down') !== false) { + $orderingColumn = 'publish_down'; +} elseif (strpos($listOrder, 'modified') !== false) { + $orderingColumn = 'modified'; +} else { + $orderingColumn = 'created'; } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_content&task=articles.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_content&task=articles.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } -/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ -$wa = $this->document->getWebAssetManager(); -$wa->useScript('multiselect'); - $workflow_enabled = ComponentHelper::getParams('com_content')->get('workflow_enabled'); $workflow_state = false; $workflow_featured = false; if ($workflow_enabled) : + $wa->getRegistry()->addExtensionRegistryFile('com_workflow'); + $wa->useScript('com_workflow.admin-items-workflow-buttons') + ->useScript('com_content.articles-status'); -// @todo move the script to a file -$js = <<getRegistry()->addExtensionRegistryFile('com_workflow'); -$wa->useScript('com_workflow.admin-items-workflow-buttons') - ->addInlineScript($js, [], ['type' => 'module']); - -$workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); -$workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); - + $workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); + $workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); endif; $assoc = Associations::isEnabled(); ?>
-
-
-
- $this)); - ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - - - - hits) : ?> - - - vote) : ?> - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $item->max_ordering = 0; - $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', 'com_content.article.' . $item->id) && $item->created_by == $userId; - $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; - $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); - $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; - $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); - $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; +
+
+
+ $this)); + ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + + + + + hits) : ?> + + + vote) : ?> + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $item->max_ordering = 0; + $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', 'com_content.article.' . $item->id) && $item->created_by == $userId; + $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; + $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); + $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; + $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); + $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; - $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); + $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); - $transition_ids = ArrayHelper::getColumn($transitions, 'value'); - $transition_ids = ArrayHelper::toInteger($transition_ids); + $transition_ids = ArrayHelper::getColumn($transitions, 'value'); + $transition_ids = ArrayHelper::toInteger($transition_ids); - ?> - - - - - + + + + - - + + - + - - - - - - - - - - - hits) : ?> - - - vote) : ?> - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - $transitions, - 'title' => Text::_($item->stage_title), - 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), - 'id' => 'workflow-' . $item->id, - 'task' => 'articles.runTransition' - ]; + ?> +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + $transitions, + 'title' => Text::_($item->stage_title), + 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), + 'id' => 'workflow-' . $item->id, + 'task' => 'articles.runTransition' + ]; - echo (new TransitionButton($options)) - ->render(0, $i); - ?> -
- stage_title); ?> -
-
- 'articles.', - 'disabled' => $workflow_featured || !$canChange, - 'id' => 'featured-' . $item->id - ]; + echo (new TransitionButton($options)) + ->render(0, $i); + ?> +
+ stage_title); ?> +
+
+ 'articles.', + 'disabled' => $workflow_featured || !$canChange, + 'id' => 'featured-' . $item->id + ]; - echo (new FeaturedButton) - ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); - ?> - - 'articles.', - 'disabled' => $workflow_state || !$canChange, - 'id' => 'state-' . $item->id - ]; + echo (new FeaturedButton()) + ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); + ?> + + 'articles.', + 'disabled' => $workflow_state || !$canChange, + 'id' => 'state-' . $item->id, + 'category_published' => $item->category_published + ]; - echo (new PublishedButton)->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); - ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'articles.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
-
- parent_category_id . '&extension=com_content'); - $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); - $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); - echo Text::_('JCATEGORY') . ': '; - if ($item->category_level != '1') : - if ($item->parent_category_level != '1') : - echo ' » '; - endif; - endif; - if (Factory::getLanguage()->isRtl()) - { - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - if ($item->category_level != '1') : - echo ' « '; - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - endif; - } - else - { - if ($item->category_level != '1') : - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo ' » '; - endif; - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - } - ?> -
-
-
- escape($item->access_level); ?> - - created_by != 0) : ?> - - escape($item->author_name); ?> - - - - - created_by_alias) : ?> -
escape($item->created_by_alias)); ?>
- -
- association) : ?> - id); ?> - - - - - {$orderingColumn}; - echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; - ?> - - - hits; ?> - - - - rating_count; ?> - - - - rating; ?> - - - id; ?> -
+ echo (new PublishedButton())->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); + ?> + + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'articles.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
+
+ parent_category_id . '&extension=com_content'); + $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); + $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); + echo Text::_('JCATEGORY') . ': '; + if ($item->category_level != '1') : + if ($item->parent_category_level != '1') : + echo ' » '; + endif; + endif; + if (Factory::getLanguage()->isRtl()) { + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + if ($item->category_level != '1') : + echo ' « '; + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + endif; + } else { + if ($item->category_level != '1') : + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo ' » '; + endif; + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + } + if ($item->category_published < '1') : + echo $item->category_published == '0' ? ' (' . Text::_('JUNPUBLISHED') . ')' : ' (' . Text::_('JTRASHED') . ')'; + endif; + ?> +
+
+ + + escape($item->access_level); ?> + + + created_by != 0) : ?> + + escape($item->author_name); ?> + + + + + created_by_alias) : ?> +
escape($item->created_by_alias)); ?>
+ + + + + association) : ?> + id); ?> + + + + + + + + + + {$orderingColumn}; + echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; + ?> + + hits) : ?> + + + hits; ?> + + + + vote) : ?> + + + rating_count; ?> + + + + + rating; ?> + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_content') - && $user->authorise('core.edit', 'com_content') - && $user->authorise('core.edit.state', 'com_content')) : ?> - Text::_('COM_CONTENT_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_content') + && $user->authorise('core.edit', 'com_content') + && $user->authorise('core.edit.state', 'com_content') + ) : ?> + Text::_('COM_CONTENT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - + + + - - - -
-
-
+ + + + + +
diff --git a/code/administrator/components/com_content/tmpl/articles/default_batch_body.php b/code/administrator/components/com_content/tmpl/articles/default_batch_body.php index eaa3e08b..0f838acc 100644 --- a/code/administrator/components/com_content/tmpl/articles/default_batch_body.php +++ b/code/administrator/components/com_content/tmpl/articles/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Component\ComponentHelper; @@ -21,39 +23,39 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
-
- = 0) : ?> -
-
- 'com_content']); ?> -
-
- -
-
- -
-
- authorise('core.admin', 'com_content') && $params->get('workflow_enabled')) : ?> -
-
- 'com_content']); ?> -
-
- -
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ 'com_content']); ?> +
+
+ +
+
+ +
+
+ authorise('core.admin', 'com_content') && $params->get('workflow_enabled')) : ?> +
+
+ 'com_content']); ?> +
+
+ +
diff --git a/code/administrator/components/com_content/tmpl/articles/default_batch_footer.php b/code/administrator/components/com_content/tmpl/articles/default_batch_footer.php index 3fe481e8..fd221e57 100644 --- a/code/administrator/components/com_content/tmpl/articles/default_batch_footer.php +++ b/code/administrator/components/com_content/tmpl/articles/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -16,8 +18,8 @@ ?> diff --git a/code/administrator/components/com_content/tmpl/articles/emptystate.php b/code/administrator/components/com_content/tmpl/articles/emptystate.php index 222805bc..331fd161 100644 --- a/code/administrator/components/com_content/tmpl/articles/emptystate.php +++ b/code/administrator/components/com_content/tmpl/articles/emptystate.php @@ -1,4 +1,5 @@ 'COM_CONTENT', - 'formURL' => 'index.php?option=com_content&view=articles', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Adding_a_new_article', - 'icon' => 'icon-copy article', + 'textPrefix' => 'COM_CONTENT', + 'formURL' => 'index.php?option=com_content&view=articles', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Adding_a_new_article', + 'icon' => 'icon-copy article', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_content&task=article.add'; +if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_content&task=article.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_content/tmpl/articles/modal.php b/code/administrator/components/com_content/tmpl/articles/modal.php index 2ed1cdc8..d99d647c 100644 --- a/code/administrator/components/com_content/tmpl/articles/modal.php +++ b/code/administrator/components/com_content/tmpl/articles/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('core') - ->useScript('multiselect') - ->useScript('com_content.admin-articles-modal'); + ->useScript('multiselect') + ->useScript('com_content.admin-articles-modal'); $function = $app->input->getCmd('function', 'jSelectArticle'); $editor = $app->input->getCmd('editor', ''); @@ -38,140 +38,132 @@ $onclick = $this->escape($function); $multilang = Multilanguage::isEnabled(); -if (!empty($editor)) -{ - // This view is used also in com_menus. Load the xtd script only if the editor is set! - $this->document->addScriptOptions('xtd-articles', array('editor' => $editor)); - $onclick = "jSelectArticle"; +if (!empty($editor)) { + // This view is used also in com_menus. Load the xtd script only if the editor is set! + $this->document->addScriptOptions('xtd-articles', array('editor' => $editor)); + $onclick = "jSelectArticle"; } ?>
-
+ - $this)); ?> + $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - ); - ?> - items as $i => $item) : ?> - language && $multilang) - { - $tag = strlen($item->language); - if ($tag == 5) - { - $lang = substr($item->language, 0, 2); - } - elseif ($tag == 6) - { - $lang = substr($item->language, 0, 3); - } - else { - $lang = ''; - } - } - elseif (!$multilang) - { - $lang = ''; - } - ?> - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - -
- - - - - escape($onclick) . '"' - . ' data-id="' . $item->id . '"' - . ' data-title="' . $this->escape($item->title) . '"' - . ' data-cat-id="' . $this->escape($item->catid) . '"' - . ' data-uri="' . $this->escape(RouteHelper::getArticleRoute($item->id, $item->catid, $item->language)) . '"' - . ' data-language="' . $this->escape($lang) . '"'; - ?> - > - escape($item->title); ?> - -
- note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
-
- escape($item->category_title); ?> -
-
- escape($item->access_level); ?> - - - - created, Text::_('DATE_FORMAT_LC4')); ?> - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + ); + ?> + items as $i => $item) : ?> + language && $multilang) { + $tag = strlen($item->language); + if ($tag == 5) { + $lang = substr($item->language, 0, 2); + } elseif ($tag == 6) { + $lang = substr($item->language, 0, 3); + } else { + $lang = ''; + } + } elseif (!$multilang) { + $lang = ''; + } + ?> + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+ + + + + escape($onclick) . '"' + . ' data-id="' . $item->id . '"' + . ' data-title="' . $this->escape($item->title) . '"' + . ' data-cat-id="' . $this->escape($item->catid) . '"' + . ' data-uri="' . $this->escape(RouteHelper::getArticleRoute($item->id, $item->catid, $item->language)) . '"' + . ' data-language="' . $this->escape($lang) . '"'; + ?> + > + escape($item->title); ?> + +
+ note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
+
+ escape($item->category_title); ?> +
+
+ escape($item->access_level); ?> + + + + created, Text::_('DATE_FORMAT_LC4')); ?> + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - + + + + -
+
diff --git a/code/administrator/components/com_content/tmpl/featured/default.php b/code/administrator/components/com_content/tmpl/featured/default.php index 8c773b89..8aff2eb6 100644 --- a/code/administrator/components/com_content/tmpl/featured/default.php +++ b/code/administrator/components/com_content/tmpl/featured/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); +$wa->useScript('table.columns') + ->useScript('multiselect'); + $app = Factory::getApplication(); $user = Factory::getUser(); $userId = $user->get('id'); @@ -31,62 +37,33 @@ $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'fp.ordering'; -if (strpos($listOrder, 'publish_up') !== false) -{ - $orderingColumn = 'publish_up'; -} -elseif (strpos($listOrder, 'publish_down') !== false) -{ - $orderingColumn = 'publish_down'; -} -elseif (strpos($listOrder, 'modified') !== false) -{ - $orderingColumn = 'modified'; -} -else -{ - $orderingColumn = 'created'; +if (strpos($listOrder, 'publish_up') !== false) { + $orderingColumn = 'publish_up'; +} elseif (strpos($listOrder, 'publish_down') !== false) { + $orderingColumn = 'publish_down'; +} elseif (strpos($listOrder, 'modified') !== false) { + $orderingColumn = 'modified'; +} else { + $orderingColumn = 'created'; } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_content&task=featured.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_content&task=featured.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } -/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ -$wa = $this->document->getWebAssetManager(); -$wa->useScript('multiselect'); - $workflow_enabled = ComponentHelper::getParams('com_content')->get('workflow_enabled'); $workflow_state = false; $workflow_featured = false; if ($workflow_enabled) : + $wa->getRegistry()->addExtensionRegistryFile('com_workflow'); + $wa->useScript('com_workflow.admin-items-workflow-buttons') + ->useScript('com_content.articles-status'); -// @todo move the script to a file -$js = <<getRegistry()->addExtensionRegistryFile('com_workflow'); -$wa->useScript('com_workflow.admin-items-workflow-buttons') - ->addInlineScript($js, [], ['type' => 'module']); - -$workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); -$workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); - + $workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); + $workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); endif; $assoc = Associations::isEnabled(); @@ -94,328 +71,328 @@ ?>
-
-
-
- $this)); - ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - - - - hits) : ?> - - - vote) : ?> - - - - - - - class="js-draggable" data-url="" data-direction=""> - items); ?> - items as $i => $item) : - $item->max_ordering = 0; - $ordering = ($listOrder == 'fp.ordering'); - $assetId = 'com_content.article.' . $item->id; - $canCreate = $user->authorise('core.create', 'com_content.category.' . $item->catid); - $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; - $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); - $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; - $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); - $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; +
+
+
+ $this)); + ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + + + + + hits) : ?> + + + vote) : ?> + + + + + + + class="js-draggable" data-url="" data-direction=""> + items); ?> + items as $i => $item) : + $item->max_ordering = 0; + $ordering = ($listOrder == 'fp.ordering'); + $assetId = 'com_content.article.' . $item->id; + $canCreate = $user->authorise('core.create', 'com_content.category.' . $item->catid); + $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; + $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); + $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; + $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); + $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; - $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); + $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); - $transition_ids = ArrayHelper::getColumn($transitions, 'value'); - $transition_ids = ArrayHelper::toInteger($transition_ids); + $transition_ids = ArrayHelper::getColumn($transitions, 'value'); + $transition_ids = ArrayHelper::toInteger($transition_ids); - ?> - - - + + - - + + - - + + - + - - - - - - - - - - - hits) : ?> - - - vote) : ?> - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- id, false, 'cid', 'cb', $item->title); ?> - - +
+ id, false, 'cid', 'cb', $item->title); ?> + + - - - - - - - - $transitions, - 'title' => Text::_($item->stage_title), - 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), - 'id' => 'workflow-' . $item->id, - 'task' => 'articles.runTransitions' - ]; + if (!$canChange) { + $iconClass = ' inactive'; + } elseif (!$saveOrder) { + $iconClass = ' inactive" title="' . Text::_('JORDERINGDISABLED'); + } + ?> + + + + + + + + $transitions, + 'title' => Text::_($item->stage_title), + 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), + 'id' => 'workflow-' . $item->id, + 'task' => 'articles.runTransitions' + ]; - echo (new TransitionButton($options)) - ->render(0, $i); - ?> -
- stage_title); ?> -
-
- 'articles.', - 'disabled' => $workflow_featured || !$canChange, - 'id' => 'featured-' . $item->id - ]; + echo (new TransitionButton($options)) + ->render(0, $i); + ?> +
+ stage_title); ?> +
+
+ 'articles.', + 'disabled' => $workflow_featured || !$canChange, + 'id' => 'featured-' . $item->id + ]; - echo (new FeaturedButton) - ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); - ?> - - 'articles.', - 'disabled' => $workflow_state || !$canChange, - 'id' => 'state-' . $item->id - ]; + echo (new FeaturedButton()) + ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); + ?> + + 'articles.', + 'disabled' => $workflow_state || !$canChange, + 'id' => 'state-' . $item->id, + 'category_published' => $item->category_published + ]; - echo (new PublishedButton)->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); - ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'articles.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
-
- parent_category_id . '&extension=com_content'); - $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); - $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); - echo Text::_('JCATEGORY') . ': '; - if ($item->category_level != '1') : - if ($item->parent_category_level != '1') : - echo ' » '; - endif; - endif; - if (Factory::getLanguage()->isRtl()) - { - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - if ($item->category_level != '1') : - echo ' « '; - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - endif; - } - else - { - if ($item->category_level != '1') : - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo ' » '; - endif; - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - } - ?> -
-
-
- escape($item->access_level); ?> - - created_by != 0) : ?> - - escape($item->author_name); ?> - - - - - created_by_alias) : ?> -
escape($item->created_by_alias)); ?>
- -
- association) : ?> - id); ?> - - - - - {$orderingColumn}; - echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; - ?> - - - hits; ?> - - - - rating_count; ?> - - - - rating; ?> - - - id; ?> -
+ echo (new PublishedButton())->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); + ?> + + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'articles.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
+
+ parent_category_id . '&extension=com_content'); + $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); + $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); + echo Text::_('JCATEGORY') . ': '; + if ($item->category_level != '1') : + if ($item->parent_category_level != '1') : + echo ' » '; + endif; + endif; + if (Factory::getLanguage()->isRtl()) { + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + if ($item->category_level != '1') : + echo ' « '; + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + endif; + } else { + if ($item->category_level != '1') : + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo ' » '; + endif; + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + } + if ($item->category_published < '1') : + echo $item->category_published == '0' ? ' (' . Text::_('JUNPUBLISHED') . ')' : ' (' . Text::_('JTRASHED') . ')'; + endif; + ?> +
+
+ + + escape($item->access_level); ?> + + + created_by != 0) : ?> + + escape($item->author_name); ?> + + + + + created_by_alias) : ?> +
escape($item->created_by_alias)); ?>
+ + + + + association) : ?> + id); ?> + + + + + + + + + + {$orderingColumn}; + echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; + ?> + + hits) : ?> + + + hits; ?> + + + + vote) : ?> + + + rating_count; ?> + + + + + rating; ?> + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - Text::_('JTOOLBAR_CHANGE_STATUS'), - 'footer' => $this->loadTemplate('stage_footer'), - ), - $this->loadTemplate('stage_body') - ); ?> + Text::_('JTOOLBAR_CHANGE_STATUS'), + 'footer' => $this->loadTemplate('stage_footer'), + ), + $this->loadTemplate('stage_body') + ); ?> - + - - - + + + - - - - -
-
-
+ + + + + + +
diff --git a/code/administrator/components/com_content/tmpl/featured/default_stage_body.php b/code/administrator/components/com_content/tmpl/featured/default_stage_body.php index 3dadb990..d5f510e4 100644 --- a/code/administrator/components/com_content/tmpl/featured/default_stage_body.php +++ b/code/administrator/components/com_content/tmpl/featured/default_stage_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -13,11 +15,11 @@ ?>
-
-
-

-
-
-
-
+
+
+

+
+
+
+
diff --git a/code/administrator/components/com_content/tmpl/featured/default_stage_footer.php b/code/administrator/components/com_content/tmpl/featured/default_stage_footer.php index 88653817..32ec9abc 100644 --- a/code/administrator/components/com_content/tmpl/featured/default_stage_footer.php +++ b/code/administrator/components/com_content/tmpl/featured/default_stage_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -16,8 +18,8 @@ ?> diff --git a/code/administrator/components/com_content/tmpl/featured/emptystate.php b/code/administrator/components/com_content/tmpl/featured/emptystate.php index 99bc733f..83748e92 100644 --- a/code/administrator/components/com_content/tmpl/featured/emptystate.php +++ b/code/administrator/components/com_content/tmpl/featured/emptystate.php @@ -1,4 +1,5 @@ 'COM_CONTENT', - 'formURL' => 'index.php?option=com_content&view=featured', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Adding_a_new_article', + 'textPrefix' => 'COM_CONTENT', + 'formURL' => 'index.php?option=com_content&view=featured', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Adding_a_new_article', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_content&task=article.add'; +if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_content&task=article.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_contenthistory/contenthistory.xml b/code/administrator/components/com_contenthistory/contenthistory.xml index a89352af..4891d084 100644 --- a/code/administrator/components/com_contenthistory/contenthistory.xml +++ b/code/administrator/components/com_contenthistory/contenthistory.xml @@ -2,7 +2,7 @@ com_contenthistory Joomla! Project - May 2013 + 2013-05 (C) 2013 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_contenthistory/helpers/contenthistory.php b/code/administrator/components/com_contenthistory/helpers/contenthistory.php index 1c4b548e..d7cdc231 100644 --- a/code/administrator/components/com_contenthistory/helpers/contenthistory.php +++ b/code/administrator/components/com_contenthistory/helpers/contenthistory.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Categories helper. diff --git a/code/administrator/components/com_contenthistory/services/provider.php b/code/administrator/components/com_contenthistory/services/provider.php index 5840a2fc..b3c3ca13 100644 --- a/code/administrator/components/com_contenthistory/services/provider.php +++ b/code/administrator/components/com_contenthistory/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Contenthistory')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Contenthistory')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Contenthistory')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Contenthistory')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_contenthistory/src/Controller/DisplayController.php b/code/administrator/components/com_contenthistory/src/Controller/DisplayController.php index a7fbefd5..bbe9c128 100644 --- a/code/administrator/components/com_contenthistory/src/Controller/DisplayController.php +++ b/code/administrator/components/com_contenthistory/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The name of the model + * @param string $prefix The prefix for the model + * @param array $config An additional array of parameters + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model + * + * @since 3.2 + */ + public function getModel($name = 'History', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } - /** - * Toggles the keep forever value for one or more history rows. If it was Yes, changes to No. If No, changes to Yes. - * - * @return void - * - * @since 3.2 - */ - public function keep() - { - $this->checkToken(); + /** + * Toggles the keep forever value for one or more history rows. If it was Yes, changes to No. If No, changes to Yes. + * + * @return void + * + * @since 3.2 + */ + public function keep() + { + $this->checkToken(); - // Get items to toggle keep forever from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); + // Get items to toggle keep forever from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); - // Remove zero values resulting from input filter - $cid = array_filter($cid); + // Remove zero values resulting from input filter + $cid = array_filter($cid); - if (empty($cid)) - { - $this->app->enqueueMessage(Text::_('COM_CONTENTHISTORY_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel(); + if (empty($cid)) { + $this->app->enqueueMessage(Text::_('COM_CONTENTHISTORY_NO_ITEM_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel(); - // Toggle keep forever status of the selected items. - if ($model->keep($cid)) - { - $this->setMessage(Text::plural('COM_CONTENTHISTORY_N_ITEMS_KEEP_TOGGLE', count($cid))); - } - else - { - $this->setMessage($model->getError(), 'error'); - } - } + // Toggle keep forever status of the selected items. + if ($model->keep($cid)) { + $this->setMessage(Text::plural('COM_CONTENTHISTORY_N_ITEMS_KEEP_TOGGLE', count($cid))); + } else { + $this->setMessage($model->getError(), 'error'); + } + } - $this->setRedirect( - Route::_( - 'index.php?option=com_contenthistory&view=history&layout=modal&tmpl=component&item_id=' - . $this->input->getCmd('item_id') . '&' . Session::getFormToken() . '=1', false - ) - ); - } + $this->setRedirect( + Route::_( + 'index.php?option=com_contenthistory&view=history&layout=modal&tmpl=component&item_id=' + . $this->input->getCmd('item_id') . '&' . Session::getFormToken() . '=1', + false + ) + ); + } - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - return '&layout=modal&tmpl=component&item_id=' . $this->input->get('item_id') . '&' . Session::getFormToken() . '=1'; - } + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + return '&layout=modal&tmpl=component&item_id=' . $this->input->get('item_id') . '&' . Session::getFormToken() . '=1'; + } } diff --git a/code/administrator/components/com_contenthistory/src/Controller/PreviewController.php b/code/administrator/components/com_contenthistory/src/Controller/PreviewController.php index 9c9b14b3..dfd3cdb0 100644 --- a/code/administrator/components/com_contenthistory/src/Controller/PreviewController.php +++ b/code/administrator/components/com_contenthistory/src/Controller/PreviewController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The name of the model + * @param string $prefix The prefix for the model + * @param array $config An additional array of parameters + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model + * + * @since 3.2 + */ + public function getModel($name = 'Preview', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_contenthistory/src/Dispatcher/Dispatcher.php b/code/administrator/components/com_contenthistory/src/Dispatcher/Dispatcher.php index 61d79a98..0e0ceb8b 100644 --- a/code/administrator/components/com_contenthistory/src/Dispatcher/Dispatcher.php +++ b/code/administrator/components/com_contenthistory/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getIdentity()->guest) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + * + * @throws \Exception|NotAllowed + */ + protected function checkAccess() + { + // Check the user has permission to access this component if in the backend + if ($this->app->getIdentity()->guest) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/code/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php b/code/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php index 18ab0f48..4d32cd60 100644 --- a/code/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php +++ b/code/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php @@ -1,4 +1,5 @@ $value) - { - $result[$name] = $value; - - if (is_object($value)) - { - foreach ($value as $subName => $subValue) - { - $result[$subName] = $subValue; - } - } - } - - return $result; - } - - /** - * Method to decode JSON-encoded fields in a standard object. Used to unpack JSON strings in the content history data column. - * - * @param string $jsonString JSON String to convert to an object. - * - * @return \stdClass Object with any JSON-encoded fields unpacked. - * - * @since 3.2 - */ - public static function decodeFields($jsonString) - { - $object = json_decode($jsonString); - - if (is_object($object)) - { - foreach ($object as $name => $value) - { - if ($subObject = json_decode($value)) - { - $object->$name = $subObject; - } - } - } - - return $object; - } - - /** - * Method to get field labels for the fields in the JSON-encoded object. - * First we see if we can find translatable labels for the fields in the object. - * We translate any we can find and return an array in the format object->name => label. - * - * @param \stdClass $object Standard class object in the format name->value. - * @param ContentType $typesTable Table object with content history options. - * - * @return \stdClass Contains two associative arrays. - * $formValues->labels in the format name => label (for example, 'id' => 'Article ID'). - * $formValues->values in the format name => value (for example, 'state' => 'Published'. - * This translates the text from the selected option in the form. - * - * @since 3.2 - */ - public static function getFormValues($object, ContentType $typesTable) - { - $labels = array(); - $values = array(); - $expandedObjectArray = static::createObjectArray($object); - static::loadLanguageFiles($typesTable->type_alias); - - if ($formFile = static::getFormFile($typesTable)) - { - if ($xml = simplexml_load_file($formFile)) - { - // Now we need to get all of the labels from the form - $fieldArray = $xml->xpath('//field'); - $fieldArray = array_merge($fieldArray, $xml->xpath('//fields')); - - foreach ($fieldArray as $field) - { - if ($label = (string) $field->attributes()->label) - { - $labels[(string) $field->attributes()->name] = Text::_($label); - } - } - - // Get values for any list type fields - $listFieldArray = $xml->xpath('//field[@type="list" or @type="radio"]'); - - foreach ($listFieldArray as $field) - { - $name = (string) $field->attributes()->name; - - if (isset($expandedObjectArray[$name])) - { - $optionFieldArray = $field->xpath('option[@value="' . $expandedObjectArray[$name] . '"]'); - - $valueText = null; - - if (is_array($optionFieldArray) && count($optionFieldArray)) - { - $valueText = trim((string) $optionFieldArray[0]); - } - - $values[(string) $field->attributes()->name] = Text::_($valueText); - } - } - } - } - - $result = new \stdClass; - $result->labels = $labels; - $result->values = $values; - - return $result; - } - - /** - * Method to get the XML form file for this component. Used to get translated field names for history preview. - * - * @param ContentType $typesTable Table object with content history options. - * - * @return mixed \JModel object if successful, false if no model found. - * - * @since 3.2 - */ - public static function getFormFile(ContentType $typesTable) - { - // First, see if we have a file name in the $typesTable - $options = json_decode($typesTable->content_history_options); - - if (is_object($options) && isset($options->formFile) && File::exists(JPATH_ROOT . '/' . $options->formFile)) - { - $result = JPATH_ROOT . '/' . $options->formFile; - } - else - { - $aliasArray = explode('.', $typesTable->type_alias); - $component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0]; - $path = Folder::makeSafe(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/'); - array_shift($aliasArray); - $file = File::makeSafe(implode('.', $aliasArray) . '.xml'); - $result = File::exists($path . $file) ? $path . $file : false; - } - - return $result; - } - - /** - * Method to query the database using values from lookup objects. - * - * @param \stdClass $lookup The std object with the values needed to do the query. - * @param mixed $value The value used to find the matching title or name. Typically the id. - * - * @return mixed Value from database (for example, name or title) on success, false on failure. - * - * @since 3.2 - */ - public static function getLookupValue($lookup, $value) - { - $result = false; - - if (isset($lookup->sourceColumn) && isset($lookup->targetTable) && isset($lookup->targetColumn) && isset($lookup->displayColumn)) - { - $db = Factory::getDbo(); - $value = (int) $value; - $query = $db->getQuery(true); - $query->select($db->quoteName($lookup->displayColumn)) - ->from($db->quoteName($lookup->targetTable)) - ->where($db->quoteName($lookup->targetColumn) . ' = :value') - ->bind(':value', $value, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $result = $db->loadResult(); - } - catch (\Exception $e) - { - // Ignore any errors and just return false - return false; - } - } - - return $result; - } - - /** - * Method to remove fields from the object based on values entered in the #__content_types table. - * - * @param \stdClass $object Object to be passed to view layout file. - * @param ContentType $typeTable Table object with content history options. - * - * @return \stdClass object with hidden fields removed. - * - * @since 3.2 - */ - public static function hideFields($object, ContentType $typeTable) - { - if ($options = json_decode($typeTable->content_history_options)) - { - if (isset($options->hideFields) && is_array($options->hideFields)) - { - foreach ($options->hideFields as $field) - { - unset($object->$field); - } - } - } - - return $object; - } - - /** - * Method to load the language files for the component whose history is being viewed. - * - * @param string $typeAlias The type alias, for example 'com_content.article'. - * - * @return void - * - * @since 3.2 - */ - public static function loadLanguageFiles($typeAlias) - { - $aliasArray = explode('.', $typeAlias); - - if (is_array($aliasArray) && count($aliasArray) == 2) - { - $component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0]; - $lang = Factory::getLanguage(); - - /** - * Loading language file from the administrator/language directory then - * loading language file from the administrator/components/extension/language directory - */ - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - // Force loading of backend global language file - $lang->load('joomla', Path::clean(JPATH_ADMINISTRATOR)); - } - } - - /** - * Method to create object to pass to the layout. Format is as follows: - * field is std object with name, value. - * - * Value can be a std object with name, value pairs. - * - * @param \stdClass $object The std object from the JSON string. Can be nested 1 level deep. - * @param \stdClass $formValues Standard class of label and value in an associative array. - * - * @return \stdClass Object with translated labels where available - * - * @since 3.2 - */ - public static function mergeLabels($object, $formValues) - { - $result = new \stdClass; - - if ($object === null) - { - return $result; - } - - $labelsArray = $formValues->labels; - $valuesArray = $formValues->values; - - foreach ($object as $name => $value) - { - $result->$name = new \stdClass; - $result->$name->name = $name; - $result->$name->value = $valuesArray[$name] ?? $value; - $result->$name->label = $labelsArray[$name] ?? $name; - - if (is_object($value)) - { - $subObject = new \stdClass; - - foreach ($value as $subName => $subValue) - { - $subObject->$subName = new \stdClass; - $subObject->$subName->name = $subName; - $subObject->$subName->value = $valuesArray[$subName] ?? $subValue; - $subObject->$subName->label = $labelsArray[$subName] ?? $subName; - $result->$name->value = $subObject; - } - } - } - - return $result; - } - - /** - * Method to prepare the object for the preview and compare views. - * - * @param ContentHistory $table Table object loaded with data. - * - * @return \stdClass Object ready for the views. - * - * @since 3.2 - */ - public static function prepareData(ContentHistory $table) - { - $object = static::decodeFields($table->version_data); - $typesTable = Table::getInstance('ContentType', 'Joomla\\CMS\\Table\\'); - $typeAlias = explode('.', $table->item_id); - array_pop($typeAlias); - $typesTable->load(array('type_alias' => implode('.', $typeAlias))); - $formValues = static::getFormValues($object, $typesTable); - $object = static::mergeLabels($object, $formValues); - $object = static::hideFields($object, $typesTable); - $object = static::processLookupFields($object, $typesTable); - - return $object; - } - - /** - * Method to process any lookup values found in the content_history_options column for this table. - * This allows category title and user name to be displayed instead of the id column. - * - * @param \stdClass $object The std object from the JSON string. Can be nested 1 level deep. - * @param ContentType $typesTable Table object loaded with data. - * - * @return \stdClass Object with lookup values inserted. - * - * @since 3.2 - */ - public static function processLookupFields($object, ContentType $typesTable) - { - if ($options = json_decode($typesTable->content_history_options)) - { - if (isset($options->displayLookup) && is_array($options->displayLookup)) - { - foreach ($options->displayLookup as $lookup) - { - $sourceColumn = $lookup->sourceColumn ?? false; - $sourceValue = $object->$sourceColumn->value ?? false; - - if ($sourceColumn && $sourceValue && ($lookupValue = static::getLookupValue($lookup, $sourceValue))) - { - $object->$sourceColumn->value = $lookupValue; - } - } - } - } - - return $object; - } + /** + * Method to put all field names, including nested ones, in a single array for easy lookup. + * + * @param \stdClass $object Standard class object that may contain one level of nested objects. + * + * @return array Associative array of all field names, including ones in a nested object. + * + * @since 3.2 + */ + public static function createObjectArray($object) + { + $result = array(); + + if ($object === null) { + return $result; + } + + foreach ($object as $name => $value) { + $result[$name] = $value; + + if (is_object($value)) { + foreach ($value as $subName => $subValue) { + $result[$subName] = $subValue; + } + } + } + + return $result; + } + + /** + * Method to decode JSON-encoded fields in a standard object. Used to unpack JSON strings in the content history data column. + * + * @param string $jsonString JSON String to convert to an object. + * + * @return \stdClass Object with any JSON-encoded fields unpacked. + * + * @since 3.2 + */ + public static function decodeFields($jsonString) + { + $object = json_decode($jsonString); + + if (is_object($object)) { + foreach ($object as $name => $value) { + if ($subObject = json_decode($value)) { + $object->$name = $subObject; + } + } + } + + return $object; + } + + /** + * Method to get field labels for the fields in the JSON-encoded object. + * First we see if we can find translatable labels for the fields in the object. + * We translate any we can find and return an array in the format object->name => label. + * + * @param \stdClass $object Standard class object in the format name->value. + * @param ContentType $typesTable Table object with content history options. + * + * @return \stdClass Contains two associative arrays. + * $formValues->labels in the format name => label (for example, 'id' => 'Article ID'). + * $formValues->values in the format name => value (for example, 'state' => 'Published'. + * This translates the text from the selected option in the form. + * + * @since 3.2 + */ + public static function getFormValues($object, ContentType $typesTable) + { + $labels = array(); + $values = array(); + $expandedObjectArray = static::createObjectArray($object); + static::loadLanguageFiles($typesTable->type_alias); + + if ($formFile = static::getFormFile($typesTable)) { + if ($xml = simplexml_load_file($formFile)) { + // Now we need to get all of the labels from the form + $fieldArray = $xml->xpath('//field'); + $fieldArray = array_merge($fieldArray, $xml->xpath('//fields')); + + foreach ($fieldArray as $field) { + if ($label = (string) $field->attributes()->label) { + $labels[(string) $field->attributes()->name] = Text::_($label); + } + } + + // Get values for any list type fields + $listFieldArray = $xml->xpath('//field[@type="list" or @type="radio"]'); + + foreach ($listFieldArray as $field) { + $name = (string) $field->attributes()->name; + + if (isset($expandedObjectArray[$name])) { + $optionFieldArray = $field->xpath('option[@value="' . $expandedObjectArray[$name] . '"]'); + + $valueText = null; + + if (is_array($optionFieldArray) && count($optionFieldArray)) { + $valueText = trim((string) $optionFieldArray[0]); + } + + $values[(string) $field->attributes()->name] = Text::_($valueText); + } + } + } + } + + $result = new \stdClass(); + $result->labels = $labels; + $result->values = $values; + + return $result; + } + + /** + * Method to get the XML form file for this component. Used to get translated field names for history preview. + * + * @param ContentType $typesTable Table object with content history options. + * + * @return mixed \JModel object if successful, false if no model found. + * + * @since 3.2 + */ + public static function getFormFile(ContentType $typesTable) + { + // First, see if we have a file name in the $typesTable + $options = json_decode($typesTable->content_history_options); + + if (is_object($options) && isset($options->formFile) && File::exists(JPATH_ROOT . '/' . $options->formFile)) { + $result = JPATH_ROOT . '/' . $options->formFile; + } else { + $aliasArray = explode('.', $typesTable->type_alias); + $component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0]; + $path = Folder::makeSafe(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/'); + array_shift($aliasArray); + $file = File::makeSafe(implode('.', $aliasArray) . '.xml'); + $result = File::exists($path . $file) ? $path . $file : false; + } + + return $result; + } + + /** + * Method to query the database using values from lookup objects. + * + * @param \stdClass $lookup The std object with the values needed to do the query. + * @param mixed $value The value used to find the matching title or name. Typically the id. + * + * @return mixed Value from database (for example, name or title) on success, false on failure. + * + * @since 3.2 + */ + public static function getLookupValue($lookup, $value) + { + $result = false; + + if (isset($lookup->sourceColumn) && isset($lookup->targetTable) && isset($lookup->targetColumn) && isset($lookup->displayColumn)) { + $db = Factory::getDbo(); + $value = (int) $value; + $query = $db->getQuery(true); + $query->select($db->quoteName($lookup->displayColumn)) + ->from($db->quoteName($lookup->targetTable)) + ->where($db->quoteName($lookup->targetColumn) . ' = :value') + ->bind(':value', $value, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $result = $db->loadResult(); + } catch (\Exception $e) { + // Ignore any errors and just return false + return false; + } + } + + return $result; + } + + /** + * Method to remove fields from the object based on values entered in the #__content_types table. + * + * @param \stdClass $object Object to be passed to view layout file. + * @param ContentType $typeTable Table object with content history options. + * + * @return \stdClass object with hidden fields removed. + * + * @since 3.2 + */ + public static function hideFields($object, ContentType $typeTable) + { + if ($options = json_decode($typeTable->content_history_options)) { + if (isset($options->hideFields) && is_array($options->hideFields)) { + foreach ($options->hideFields as $field) { + unset($object->$field); + } + } + } + + return $object; + } + + /** + * Method to load the language files for the component whose history is being viewed. + * + * @param string $typeAlias The type alias, for example 'com_content.article'. + * + * @return void + * + * @since 3.2 + */ + public static function loadLanguageFiles($typeAlias) + { + $aliasArray = explode('.', $typeAlias); + + if (is_array($aliasArray) && count($aliasArray) == 2) { + $component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0]; + $lang = Factory::getLanguage(); + + /** + * Loading language file from the administrator/language directory then + * loading language file from the administrator/components/extension/language directory + */ + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + // Force loading of backend global language file + $lang->load('joomla', Path::clean(JPATH_ADMINISTRATOR)); + } + } + + /** + * Method to create object to pass to the layout. Format is as follows: + * field is std object with name, value. + * + * Value can be a std object with name, value pairs. + * + * @param \stdClass $object The std object from the JSON string. Can be nested 1 level deep. + * @param \stdClass $formValues Standard class of label and value in an associative array. + * + * @return \stdClass Object with translated labels where available + * + * @since 3.2 + */ + public static function mergeLabels($object, $formValues) + { + $result = new \stdClass(); + + if ($object === null) { + return $result; + } + + $labelsArray = $formValues->labels; + $valuesArray = $formValues->values; + + foreach ($object as $name => $value) { + $result->$name = new \stdClass(); + $result->$name->name = $name; + $result->$name->value = $valuesArray[$name] ?? $value; + $result->$name->label = $labelsArray[$name] ?? $name; + + if (is_object($value)) { + $subObject = new \stdClass(); + + foreach ($value as $subName => $subValue) { + $subObject->$subName = new \stdClass(); + $subObject->$subName->name = $subName; + $subObject->$subName->value = $valuesArray[$subName] ?? $subValue; + $subObject->$subName->label = $labelsArray[$subName] ?? $subName; + $result->$name->value = $subObject; + } + } + } + + return $result; + } + + /** + * Method to prepare the object for the preview and compare views. + * + * @param ContentHistory $table Table object loaded with data. + * + * @return \stdClass Object ready for the views. + * + * @since 3.2 + */ + public static function prepareData(ContentHistory $table) + { + $object = static::decodeFields($table->version_data); + $typesTable = Table::getInstance('ContentType', 'Joomla\\CMS\\Table\\'); + $typeAlias = explode('.', $table->item_id); + array_pop($typeAlias); + $typesTable->load(array('type_alias' => implode('.', $typeAlias))); + $formValues = static::getFormValues($object, $typesTable); + $object = static::mergeLabels($object, $formValues); + $object = static::hideFields($object, $typesTable); + $object = static::processLookupFields($object, $typesTable); + + return $object; + } + + /** + * Method to process any lookup values found in the content_history_options column for this table. + * This allows category title and user name to be displayed instead of the id column. + * + * @param \stdClass $object The std object from the JSON string. Can be nested 1 level deep. + * @param ContentType $typesTable Table object loaded with data. + * + * @return \stdClass Object with lookup values inserted. + * + * @since 3.2 + */ + public static function processLookupFields($object, ContentType $typesTable) + { + if ($options = json_decode($typesTable->content_history_options)) { + if (isset($options->displayLookup) && is_array($options->displayLookup)) { + foreach ($options->displayLookup as $lookup) { + $sourceColumn = $lookup->sourceColumn ?? false; + $sourceValue = $object->$sourceColumn->value ?? false; + + if ($sourceColumn && $sourceValue && ($lookupValue = static::getLookupValue($lookup, $sourceValue))) { + $object->$sourceColumn->value = $lookupValue; + } + } + } + } + + return $object; + } } diff --git a/code/administrator/components/com_contenthistory/src/Model/CompareModel.php b/code/administrator/components/com_contenthistory/src/Model/CompareModel.php index 7c0805a5..b95a7549 100644 --- a/code/administrator/components/com_contenthistory/src/Model/CompareModel.php +++ b/code/administrator/components/com_contenthistory/src/Model/CompareModel.php @@ -1,4 +1,5 @@ input; - - /** @var ContentHistory $table1 */ - $table1 = $this->getTable('ContentHistory'); - - /** @var ContentHistory $table2 */ - $table2 = $this->getTable('ContentHistory'); - - $id1 = $input->getInt('id1'); - $id2 = $input->getInt('id2'); - - if (!$id1 || \is_array($id1) || !$id2 || \is_array($id2)) - { - $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_INVALID_ID')); - - return false; - } - - $result = array(); - - if (!$table1->load($id1) || !$table2->load($id2)) - { - $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_VERSION_NOT_FOUND')); - - // Assume a failure to load the content means broken data, abort mission - return false; - } - - // Get the first history record's content type record so we can check ACL - /** @var ContentType $contentTypeTable */ - $contentTypeTable = $this->getTable('ContentType'); - $typeAlias = explode('.', $table1->item_id); - array_pop($typeAlias); - $typeAlias = implode('.', $typeAlias); - - if (!$contentTypeTable->load(array('type_alias' => $typeAlias))) - { - $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_FAILED_LOADING_CONTENT_TYPE')); - - // Assume a failure to load the content type means broken data, abort mission - return false; - } - - $user = Factory::getUser(); - - // Access check - if (!$user->authorise('core.edit', $table1->item_id) && !$this->canEdit($table1)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $nullDate = $this->getDbo()->getNullDate(); - - foreach (array($table1, $table2) as $table) - { - $object = new \stdClass; - $object->data = ContenthistoryHelper::prepareData($table); - $object->version_note = $table->version_note; - - // Let's use custom calendars when present - $object->save_date = HTMLHelper::_('date', $table->save_date, Text::_('DATE_FORMAT_LC6')); - - $dateProperties = array ( - 'modified_time', - 'created_time', - 'modified', - 'created', - 'checked_out_time', - 'publish_up', - 'publish_down', - ); - - foreach ($dateProperties as $dateProperty) - { - if (property_exists($object->data, $dateProperty) - && $object->data->$dateProperty->value !== null - && $object->data->$dateProperty->value !== $nullDate) - { - $object->data->$dateProperty->value = HTMLHelper::_( - 'date', - $object->data->$dateProperty->value, - Text::_('DATE_FORMAT_LC6') - ); - } - } - - $result[] = $object; - } - - return $result; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.2 - */ - public function getTable($type = 'Contenthistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Method to test whether a record is editable - * - * @param ContentHistory $record A Table object. - * - * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. - * - * @since 3.6 - */ - protected function canEdit($record) - { - $result = false; - - if (!empty($record->item_id)) - { - /** - * Make sure user has edit privileges for this content item. Note that we use edit permissions - * for the content item, not delete permissions for the content history row. - */ - $user = Factory::getUser(); - $result = $user->authorise('core.edit', $record->item_id); - - // Finally try session (this catches edit.own case too) - if (!$result) - { - /** @var ContentType $contentTypeTable */ - $contentTypeTable = $this->getTable('ContentType'); - - $typeAlias = explode('.', $record->item_id); - $id = array_pop($typeAlias); - $typeAlias = implode('.', $typeAlias); - $contentTypeTable->load(array('type_alias' => $typeAlias)); - $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); - $result = in_array((int) $id, $typeEditables); - } - } - - return $result; - } + /** + * Method to get a version history row. + * + * @return array|boolean On success, array of populated tables. False on failure. + * + * @since 3.2 + * + * @throws NotAllowed Thrown if not authorised to edit an item + */ + public function getItems() + { + $input = Factory::getApplication()->input; + + /** @var ContentHistory $table1 */ + $table1 = $this->getTable('ContentHistory'); + + /** @var ContentHistory $table2 */ + $table2 = $this->getTable('ContentHistory'); + + $id1 = $input->getInt('id1'); + $id2 = $input->getInt('id2'); + + if (!$id1 || \is_array($id1) || !$id2 || \is_array($id2)) { + $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_INVALID_ID')); + + return false; + } + + $result = array(); + + if (!$table1->load($id1) || !$table2->load($id2)) { + $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_VERSION_NOT_FOUND')); + + // Assume a failure to load the content means broken data, abort mission + return false; + } + + // Get the first history record's content type record so we can check ACL + /** @var ContentType $contentTypeTable */ + $contentTypeTable = $this->getTable('ContentType'); + $typeAlias = explode('.', $table1->item_id); + array_pop($typeAlias); + $typeAlias = implode('.', $typeAlias); + + if (!$contentTypeTable->load(array('type_alias' => $typeAlias))) { + $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_FAILED_LOADING_CONTENT_TYPE')); + + // Assume a failure to load the content type means broken data, abort mission + return false; + } + + $user = Factory::getUser(); + + // Access check + if (!$user->authorise('core.edit', $table1->item_id) && !$this->canEdit($table1)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $nullDate = $this->getDatabase()->getNullDate(); + + foreach (array($table1, $table2) as $table) { + $object = new \stdClass(); + $object->data = ContenthistoryHelper::prepareData($table); + $object->version_note = $table->version_note; + + // Let's use custom calendars when present + $object->save_date = HTMLHelper::_('date', $table->save_date, Text::_('DATE_FORMAT_LC6')); + + $dateProperties = array ( + 'modified_time', + 'created_time', + 'modified', + 'created', + 'checked_out_time', + 'publish_up', + 'publish_down', + ); + + foreach ($dateProperties as $dateProperty) { + if ( + property_exists($object->data, $dateProperty) + && $object->data->$dateProperty->value !== null + && $object->data->$dateProperty->value !== $nullDate + ) { + $object->data->$dateProperty->value = HTMLHelper::_( + 'date', + $object->data->$dateProperty->value, + Text::_('DATE_FORMAT_LC6') + ); + } + } + + $result[] = $object; + } + + return $result; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.2 + */ + public function getTable($type = 'Contenthistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Method to test whether a record is editable + * + * @param ContentHistory $record A Table object. + * + * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. + * + * @since 3.6 + */ + protected function canEdit($record) + { + $result = false; + + if (!empty($record->item_id)) { + /** + * Make sure user has edit privileges for this content item. Note that we use edit permissions + * for the content item, not delete permissions for the content history row. + */ + $user = Factory::getUser(); + $result = $user->authorise('core.edit', $record->item_id); + + // Finally try session (this catches edit.own case too) + if (!$result) { + /** @var ContentType $contentTypeTable */ + $contentTypeTable = $this->getTable('ContentType'); + + $typeAlias = explode('.', $record->item_id); + $id = array_pop($typeAlias); + $typeAlias = implode('.', $typeAlias); + $contentTypeTable->load(array('type_alias' => $typeAlias)); + $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); + $result = in_array((int) $id, $typeEditables); + } + } + + return $result; + } } diff --git a/code/administrator/components/com_contenthistory/src/Model/HistoryModel.php b/code/administrator/components/com_contenthistory/src/Model/HistoryModel.php index 9109ffd8..eef600e1 100644 --- a/code/administrator/components/com_contenthistory/src/Model/HistoryModel.php +++ b/code/administrator/components/com_contenthistory/src/Model/HistoryModel.php @@ -1,4 +1,5 @@ item_id)) - { - return false; - } - - /** - * Make sure user has edit privileges for this content item. Note that we use edit permissions - * for the content item, not delete permissions for the content history row. - */ - $user = Factory::getUser(); - - if ($user->authorise('core.edit', $record->item_id)) - { - return true; - } - - // Finally try session (this catches edit.own case too) - /** @var ContentType $contentTypeTable */ - $contentTypeTable = $this->getTable('ContentType'); - - $typeAlias = explode('.', $record->item_id); - $id = array_pop($typeAlias); - $typeAlias = implode('.', $typeAlias); - $contentTypeTable->load(array('type_alias' => $typeAlias)); - $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); - $result = in_array((int) $id, $typeEditables); - - return $result; - } - - /** - * Method to test whether a history record can be deleted. Note that we check whether we have edit permissions - * for the content item row. - * - * @param ContentHistory $record A Table object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 3.6 - */ - protected function canDelete($record) - { - return $this->canEdit($record); - } - - /** - * Method to delete one or more records from content history table. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 3.2 - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ((int) $table->keep_forever === 1) - { - unset($pks[$i]); - continue; - } - - if ($this->canEdit($table)) - { - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - $error = $this->getError(); - - if ($error) - { - try - { - Log::add($error, Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage($error, 'warning'); - } - - return false; - } - else - { - try - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'warning'); - } - - return false; - } - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.4.5 - * - * @throws NotAllowed Thrown if not authorised to edit an item - */ - public function getItems() - { - $items = parent::getItems(); - $user = Factory::getUser(); - - if ($items === false) - { - return false; - } - - // This should be an array with at least one element - if (!is_array($items) || !isset($items[0])) - { - return $items; - } - - // Access check - if (!$user->authorise('core.edit', $items[0]->item_id) && !$this->canEdit($items[0])) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - return $items; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.2 - */ - public function getTable($type = 'ContentHistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - /** - * Method to toggle on and off the keep forever value for one or more records from content history table. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 3.2 - */ - public function keep(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ($this->canEdit($table)) - { - $table->keep_forever = $table->keep_forever ? 0 : 1; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - $error = $this->getError(); - - if ($error) - { - try - { - Log::add($error, Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage($error, 'warning'); - } - - return false; - } - else - { - try - { - Log::add(Text::_('COM_CONTENTHISTORY_ERROR_KEEP_NOT_PERMITTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONTENTHISTORY_ERROR_KEEP_NOT_PERMITTED'), 'warning'); - } - - return false; - } - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.2 - */ - protected function populateState($ordering = 'h.save_date', $direction = 'DESC') - { - $input = Factory::getApplication()->input; - $itemId = $input->get('item_id', '', 'string'); - - $this->setState('item_id', $itemId); - $this->setState('sha1_hash', $this->getSha1Hash()); - - // Load the parameters. - $params = ComponentHelper::getParams('com_contenthistory'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 3.2 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - $itemId = $this->getState('item_id'); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('h.version_id'), - $db->quoteName('h.item_id'), - $db->quoteName('h.version_note'), - $db->quoteName('h.save_date'), - $db->quoteName('h.editor_user_id'), - $db->quoteName('h.character_count'), - $db->quoteName('h.sha1_hash'), - $db->quoteName('h.version_data'), - $db->quoteName('h.keep_forever'), - ] - ) - ) - ->from($db->quoteName('#__history', 'h')) - ->where($db->quoteName('h.item_id') . ' = :itemid') - ->bind(':itemid', $itemId, ParameterType::STRING) - - // Join over the users for the editor - ->select($db->quoteName('uc.name', 'editor')) - ->join('LEFT', - $db->quoteName('#__users', 'uc'), - $db->quoteName('uc.id') . ' = ' . $db->quoteName('h.editor_user_id') - ); - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering'); - $orderDirn = $this->state->get('list.direction'); - $query->order($db->quoteName($orderCol) . $orderDirn); - - return $query; - } - - /** - * Get the sha1 hash value for the current item being edited. - * - * @return string sha1 hash of row data - * - * @since 3.2 - */ - protected function getSha1Hash() - { - $result = false; - $item_id = Factory::getApplication()->input->getCmd('item_id', ''); - $typeAlias = explode('.', $item_id); - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/' . $typeAlias[0] . '/tables'); - $typeTable = $this->getTable('ContentType'); - $typeTable->load(['type_alias' => $typeAlias[0] . '.' . $typeAlias[1]]); - $contentTable = $typeTable->getContentTable(); - - if ($contentTable && $contentTable->load($typeAlias[2])) - { - $helper = new CMSHelper; - - $dataObject = $helper->getDataObject($contentTable); - $result = $this->getTable('ContentHistory')->getSha1(json_encode($dataObject), $typeTable); - } - - return $result; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'version_id', + 'h.version_id', + 'version_note', + 'h.version_note', + 'save_date', + 'h.save_date', + 'editor_user_id', + 'h.editor_user_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to test whether a record is editable + * + * @param ContentHistory $record A Table object. + * + * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. + * + * @since 3.2 + */ + protected function canEdit($record) + { + if (empty($record->item_id)) { + return false; + } + + /** + * Make sure user has edit privileges for this content item. Note that we use edit permissions + * for the content item, not delete permissions for the content history row. + */ + $user = Factory::getUser(); + + if ($user->authorise('core.edit', $record->item_id)) { + return true; + } + + // Finally try session (this catches edit.own case too) + /** @var ContentType $contentTypeTable */ + $contentTypeTable = $this->getTable('ContentType'); + + $typeAlias = explode('.', $record->item_id); + $id = array_pop($typeAlias); + $typeAlias = implode('.', $typeAlias); + $contentTypeTable->load(array('type_alias' => $typeAlias)); + $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); + $result = in_array((int) $id, $typeEditables); + + return $result; + } + + /** + * Method to test whether a history record can be deleted. Note that we check whether we have edit permissions + * for the content item row. + * + * @param ContentHistory $record A Table object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 3.6 + */ + protected function canDelete($record) + { + return $this->canEdit($record); + } + + /** + * Method to delete one or more records from content history table. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 3.2 + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ((int) $table->keep_forever === 1) { + unset($pks[$i]); + continue; + } + + if ($this->canEdit($table)) { + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + $error = $this->getError(); + + if ($error) { + try { + Log::add($error, Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage($error, 'warning'); + } + + return false; + } else { + try { + Log::add(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'warning'); + } + + return false; + } + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.4.5 + * + * @throws NotAllowed Thrown if not authorised to edit an item + */ + public function getItems() + { + $items = parent::getItems(); + $user = Factory::getUser(); + + if ($items === false) { + return false; + } + + // This should be an array with at least one element + if (!is_array($items) || !isset($items[0])) { + return $items; + } + + // Access check + if (!$user->authorise('core.edit', $items[0]->item_id) && !$this->canEdit($items[0])) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + return $items; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.2 + */ + public function getTable($type = 'ContentHistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + /** + * Method to toggle on and off the keep forever value for one or more records from content history table. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 3.2 + */ + public function keep(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ($this->canEdit($table)) { + $table->keep_forever = $table->keep_forever ? 0 : 1; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + $error = $this->getError(); + + if ($error) { + try { + Log::add($error, Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage($error, 'warning'); + } + + return false; + } else { + try { + Log::add(Text::_('COM_CONTENTHISTORY_ERROR_KEEP_NOT_PERMITTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONTENTHISTORY_ERROR_KEEP_NOT_PERMITTED'), 'warning'); + } + + return false; + } + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.2 + */ + protected function populateState($ordering = 'h.save_date', $direction = 'DESC') + { + $input = Factory::getApplication()->input; + $itemId = $input->get('item_id', '', 'string'); + + $this->setState('item_id', $itemId); + $this->setState('sha1_hash', $this->getSha1Hash()); + + // Load the parameters. + $params = ComponentHelper::getParams('com_contenthistory'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 3.2 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $itemId = $this->getState('item_id'); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('h.version_id'), + $db->quoteName('h.item_id'), + $db->quoteName('h.version_note'), + $db->quoteName('h.save_date'), + $db->quoteName('h.editor_user_id'), + $db->quoteName('h.character_count'), + $db->quoteName('h.sha1_hash'), + $db->quoteName('h.version_data'), + $db->quoteName('h.keep_forever'), + ] + ) + ) + ->from($db->quoteName('#__history', 'h')) + ->where($db->quoteName('h.item_id') . ' = :itemid') + ->bind(':itemid', $itemId, ParameterType::STRING) + + // Join over the users for the editor + ->select($db->quoteName('uc.name', 'editor')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'uc'), + $db->quoteName('uc.id') . ' = ' . $db->quoteName('h.editor_user_id') + ); + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering'); + $orderDirn = $this->state->get('list.direction'); + $query->order($db->quoteName($orderCol) . $orderDirn); + + return $query; + } + + /** + * Get the sha1 hash value for the current item being edited. + * + * @return string sha1 hash of row data + * + * @since 3.2 + */ + protected function getSha1Hash() + { + $result = false; + $item_id = Factory::getApplication()->input->getCmd('item_id', ''); + $typeAlias = explode('.', $item_id); + Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/' . $typeAlias[0] . '/tables'); + $typeTable = $this->getTable('ContentType'); + $typeTable->load(['type_alias' => $typeAlias[0] . '.' . $typeAlias[1]]); + $contentTable = $typeTable->getContentTable(); + + if ($contentTable && $contentTable->load($typeAlias[2])) { + $helper = new CMSHelper(); + + $dataObject = $helper->getDataObject($contentTable); + $result = $this->getTable('ContentHistory')->getSha1(json_encode($dataObject), $typeTable); + } + + return $result; + } } diff --git a/code/administrator/components/com_contenthistory/src/Model/PreviewModel.php b/code/administrator/components/com_contenthistory/src/Model/PreviewModel.php index b3b0898a..4cb79936 100644 --- a/code/administrator/components/com_contenthistory/src/Model/PreviewModel.php +++ b/code/administrator/components/com_contenthistory/src/Model/PreviewModel.php @@ -1,4 +1,5 @@ getTable('ContentHistory'); - $versionId = Factory::getApplication()->input->getInt('version_id'); - - if (!$versionId || \is_array($versionId) || !$table->load($versionId)) - { - return false; - } - - $user = Factory::getUser(); - - // Access check - if (!$user->authorise('core.edit', $table->item_id) && !$this->canEdit($table)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $result = new \stdClass; - $result->version_note = $table->version_note; - $result->data = ContenthistoryHelper::prepareData($table); - - // Let's use custom calendars when present - $result->save_date = HTMLHelper::_('date', $table->save_date, Text::_('DATE_FORMAT_LC6')); - - $dateProperties = array ( - 'modified_time', - 'created_time', - 'modified', - 'created', - 'checked_out_time', - 'publish_up', - 'publish_down', - ); - - $nullDate = $this->getDbo()->getNullDate(); - - foreach ($dateProperties as $dateProperty) - { - if (property_exists($result->data, $dateProperty) - && $result->data->$dateProperty->value !== null - && $result->data->$dateProperty->value !== $nullDate) - { - $result->data->$dateProperty->value = HTMLHelper::_( - 'date', - $result->data->$dateProperty->value, - Text::_('DATE_FORMAT_LC6') - ); - } - } - - return $result; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.2 - */ - public function getTable($type = 'ContentHistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Method to test whether a record is editable - * - * @param ContentHistory $record A Table object. - * - * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. - * - * @since 3.6 - */ - protected function canEdit($record) - { - $result = false; - - if (!empty($record->item_id)) - { - /** - * Make sure user has edit privileges for this content item. Note that we use edit permissions - * for the content item, not delete permissions for the content history row. - */ - $user = Factory::getUser(); - $result = $user->authorise('core.edit', $record->item_id); - - // Finally try session (this catches edit.own case too) - if (!$result) - { - /** @var ContentType $contentTypeTable */ - $contentTypeTable = $this->getTable('ContentType'); - - $typeAlias = explode('.', $record->item_id); - $id = array_pop($typeAlias); - $typeAlias = implode('.', $typeAlias); - $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); - $result = in_array((int) $id, $typeEditables); - } - } - - return $result; - } + /** + * Method to get a version history row. + * + * @param integer $pk The id of the item + * + * @return \stdClass|boolean On success, standard object with row data. False on failure. + * + * @since 3.2 + * + * @throws NotAllowed Thrown if not authorised to edit an item + */ + public function getItem($pk = null) + { + /** @var ContentHistory $table */ + $table = $this->getTable('ContentHistory'); + $versionId = Factory::getApplication()->input->getInt('version_id'); + + if (!$versionId || \is_array($versionId) || !$table->load($versionId)) { + return false; + } + + $user = Factory::getUser(); + + // Access check + if (!$user->authorise('core.edit', $table->item_id) && !$this->canEdit($table)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $result = new \stdClass(); + $result->version_note = $table->version_note; + $result->data = ContenthistoryHelper::prepareData($table); + + // Let's use custom calendars when present + $result->save_date = HTMLHelper::_('date', $table->save_date, Text::_('DATE_FORMAT_LC6')); + + $dateProperties = array ( + 'modified_time', + 'created_time', + 'modified', + 'created', + 'checked_out_time', + 'publish_up', + 'publish_down', + ); + + $nullDate = $this->getDatabase()->getNullDate(); + + foreach ($dateProperties as $dateProperty) { + if ( + property_exists($result->data, $dateProperty) + && $result->data->$dateProperty->value !== null + && $result->data->$dateProperty->value !== $nullDate + ) { + $result->data->$dateProperty->value = HTMLHelper::_( + 'date', + $result->data->$dateProperty->value, + Text::_('DATE_FORMAT_LC6') + ); + } + } + + return $result; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.2 + */ + public function getTable($type = 'ContentHistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Method to test whether a record is editable + * + * @param ContentHistory $record A Table object. + * + * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. + * + * @since 3.6 + */ + protected function canEdit($record) + { + $result = false; + + if (!empty($record->item_id)) { + /** + * Make sure user has edit privileges for this content item. Note that we use edit permissions + * for the content item, not delete permissions for the content history row. + */ + $user = Factory::getUser(); + $result = $user->authorise('core.edit', $record->item_id); + + // Finally try session (this catches edit.own case too) + if (!$result) { + /** @var ContentType $contentTypeTable */ + $contentTypeTable = $this->getTable('ContentType'); + + $typeAlias = explode('.', $record->item_id); + $id = array_pop($typeAlias); + $typeAlias = implode('.', $typeAlias); + $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); + $result = in_array((int) $id, $typeEditables); + } + } + + return $result; + } } diff --git a/code/administrator/components/com_contenthistory/src/View/Compare/HtmlView.php b/code/administrator/components/com_contenthistory/src/View/Compare/HtmlView.php index 8759e635..220cd463 100644 --- a/code/administrator/components/com_contenthistory/src/View/Compare/HtmlView.php +++ b/code/administrator/components/com_contenthistory/src/View/Compare/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + } } diff --git a/code/administrator/components/com_contenthistory/src/View/History/HtmlView.php b/code/administrator/components/com_contenthistory/src/View/History/HtmlView.php index 77bbc7eb..a9aac479 100644 --- a/code/administrator/components/com_contenthistory/src/View/History/HtmlView.php +++ b/code/administrator/components/com_contenthistory/src/View/History/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->toolbar = $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page toolbar. - * - * @return Toolbar - * - * @since 4.0.0 - */ - protected function addToolbar(): Toolbar - { - /** @var Toolbar $toolbar */ - $toolbar = Factory::getContainer()->get(ToolbarFactoryInterface::class)->createToolbar('toolbar'); - - // Cache a session token for reuse throughout. - $token = Session::getFormToken(); - - // Clean up input to ensure a clean url. - $aliasArray = explode('.', $this->state->item_id); - $option = $aliasArray[1] == 'category' - ? 'com_categories&extension=' . implode('.', array_slice($aliasArray, 0, count($aliasArray) - 2)) - : $aliasArray[0]; - $filter = InputFilter::getInstance(); - $task = $filter->clean($aliasArray[1], 'cmd') . '.loadhistory'; - - // Build the final urls. - $loadUrl = Route::_('index.php?option=' . $filter->clean($option, 'cmd') . '&task=' . $task . '&' . $token . '=1'); - $previewUrl = Route::_('index.php?option=com_contenthistory&view=preview&layout=preview&tmpl=component&' . $token . '=1'); - $compareUrl = Route::_('index.php?option=com_contenthistory&view=compare&layout=compare&tmpl=component&' . $token . '=1'); - - $toolbar->basicButton('load') - ->attributes(['data-url' => $loadUrl]) - ->icon('icon-upload') - ->buttonClass('btn btn-success') - ->text('COM_CONTENTHISTORY_BUTTON_LOAD') - ->listCheck(true); - - $toolbar->basicButton('preview') - ->attributes(['data-url' => $previewUrl]) - ->icon('icon-search') - ->text('COM_CONTENTHISTORY_BUTTON_PREVIEW') - ->listCheck(true); - - $toolbar->basicButton('compare') - ->attributes(['data-url' => $compareUrl]) - ->icon('icon-search-plus') - ->text('COM_CONTENTHISTORY_BUTTON_COMPARE') - ->listCheck(true); - - $toolbar->basicButton('keep') - ->task('history.keep') - ->buttonClass('btn btn-inverse') - ->icon('icon-lock') - ->text('COM_CONTENTHISTORY_BUTTON_KEEP') - ->listCheck(true); - - $toolbar->basicButton('delete') - ->task('history.delete') - ->buttonClass('btn btn-danger') - ->icon('icon-times') - ->text('COM_CONTENTHISTORY_BUTTON_DELETE') - ->listCheck(true); - - return $toolbar; - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The model state + * + * @var Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->toolbar = $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page toolbar. + * + * @return Toolbar + * + * @since 4.0.0 + */ + protected function addToolbar(): Toolbar + { + /** @var Toolbar $toolbar */ + $toolbar = Factory::getContainer()->get(ToolbarFactoryInterface::class)->createToolbar('toolbar'); + + // Cache a session token for reuse throughout. + $token = Session::getFormToken(); + + // Clean up input to ensure a clean url. + $aliasArray = explode('.', $this->state->item_id); + $option = $aliasArray[1] == 'category' + ? 'com_categories&extension=' . implode('.', array_slice($aliasArray, 0, count($aliasArray) - 2)) + : $aliasArray[0]; + $filter = InputFilter::getInstance(); + $task = $filter->clean($aliasArray[1], 'cmd') . '.loadhistory'; + + // Build the final urls. + $loadUrl = Route::_('index.php?option=' . $filter->clean($option, 'cmd') . '&task=' . $task . '&' . $token . '=1'); + $previewUrl = Route::_('index.php?option=com_contenthistory&view=preview&layout=preview&tmpl=component&' . $token . '=1'); + $compareUrl = Route::_('index.php?option=com_contenthistory&view=compare&layout=compare&tmpl=component&' . $token . '=1'); + + $toolbar->basicButton('load') + ->attributes(['data-url' => $loadUrl]) + ->icon('icon-upload') + ->buttonClass('btn btn-success') + ->text('COM_CONTENTHISTORY_BUTTON_LOAD') + ->listCheck(true); + + $toolbar->basicButton('preview') + ->attributes(['data-url' => $previewUrl]) + ->icon('icon-search') + ->text('COM_CONTENTHISTORY_BUTTON_PREVIEW') + ->listCheck(true); + + $toolbar->basicButton('compare') + ->attributes(['data-url' => $compareUrl]) + ->icon('icon-search-plus') + ->text('COM_CONTENTHISTORY_BUTTON_COMPARE') + ->listCheck(true); + + $toolbar->basicButton('keep') + ->task('history.keep') + ->buttonClass('btn btn-inverse') + ->icon('icon-lock') + ->text('COM_CONTENTHISTORY_BUTTON_KEEP') + ->listCheck(true); + + $toolbar->basicButton('delete') + ->task('history.delete') + ->buttonClass('btn btn-danger') + ->icon('icon-times') + ->text('COM_CONTENTHISTORY_BUTTON_DELETE') + ->listCheck(true); + + return $toolbar; + } } diff --git a/code/administrator/components/com_contenthistory/src/View/Preview/HtmlView.php b/code/administrator/components/com_contenthistory/src/View/Preview/HtmlView.php index df61b6c2..b53a9f8c 100644 --- a/code/administrator/components/com_contenthistory/src/View/Preview/HtmlView.php +++ b/code/administrator/components/com_contenthistory/src/View/Preview/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); - if (false === $this->item) - { - Factory::getLanguage()->load('com_content', JPATH_SITE, null, true); + if (false === $this->item) { + Factory::getLanguage()->load('com_content', JPATH_SITE, null, true); - throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); - } + throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); + } - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - parent::display($tpl); - } + parent::display($tpl); + } } diff --git a/code/administrator/components/com_contenthistory/tmpl/compare/compare.php b/code/administrator/components/com_contenthistory/tmpl/compare/compare.php index 78e0a3fc..c269fdde 100644 --- a/code/administrator/components/com_contenthistory/tmpl/compare/compare.php +++ b/code/administrator/components/com_contenthistory/tmpl/compare/compare.php @@ -1,4 +1,5 @@
-

+

- - - - - - - - - - - - $value) : ?> - value) && isset($object2->$name->value) && $value->value != $object2->$name->value) : ?> - value)) : ?> - - - - value as $subName => $subValue) : ?> - $name->value->$subName->value ?? ''; ?> - value || $newSubValue) : ?> - value != $newSubValue) : ?> - - - - - - - - - - - - - - $name->value = is_object($object2->$name->value) ? json_encode($object2->$name->value) : $object2->$name->value; ?> - - - - - - - -
- -
- label; ?> -
  label; ?>value, ENT_COMPAT, 'UTF-8'); ?> 
- label; ?> - value); ?>$name->value, ENT_COMPAT, 'UTF-8'); ?> 
+ + + + + + + + + + + + $value) : ?> + value) && isset($object2->$name->value) && $value->value != $object2->$name->value) : ?> + value)) : ?> + + + + value as $subName => $subValue) : ?> + $name->value->$subName->value ?? ''; ?> + value || $newSubValue) : ?> + value != $newSubValue) : ?> + + + + + + + + + + + + + + $name->value = is_object($object2->$name->value) ? json_encode($object2->$name->value) : $object2->$name->value; ?> + + + + + + + +
+ +
+ label; ?> +
  label; ?>value, ENT_COMPAT, 'UTF-8'); ?> 
+ label; ?> + value); ?>$name->value, ENT_COMPAT, 'UTF-8'); ?> 
diff --git a/code/administrator/components/com_contenthistory/tmpl/history/modal.php b/code/administrator/components/com_contenthistory/tmpl/history/modal.php index 3f6f7cbf..00dd1848 100644 --- a/code/administrator/components/com_contenthistory/tmpl/history/modal.php +++ b/code/administrator/components/com_contenthistory/tmpl/history/modal.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('multiselect') - ->useScript('com_contenthistory.admin-history-modal'); + ->useScript('com_contenthistory.admin-history-modal'); ?>
-
- toolbar->render(); ?> -
-
- - - - - - - - - - - - - - - items as $item) : ?> - - - - - - - - - - - -
- -
- - - - - - - - - - - -
- version_id, false, 'cid', 'cb', $item->save_date); ?> - - - save_date, Text::_('DATE_FORMAT_LC6')); ?> - - sha1_hash == $hash) : ?> - - - - version_note); ?> - - keep_forever) : ?> - - - - - - editor); ?> - - character_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> -
+
+ toolbar->render(); ?> +
+ + + + + + + + + + + + + + + + items as $item) : ?> + + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+ version_id, false, 'cid', 'cb', $item->save_date); ?> + + + save_date, Text::_('DATE_FORMAT_LC6')); ?> + + sha1_hash == $hash) : ?> + + + + version_note); ?> + + keep_forever) : ?> + + + + + + editor); ?> + + character_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - + + + -
+
diff --git a/code/administrator/components/com_contenthistory/tmpl/preview/preview.php b/code/administrator/components/com_contenthistory/tmpl/preview/preview.php index a97b9f04..a1e96ee4 100644 --- a/code/administrator/components/com_contenthistory/tmpl/preview/preview.php +++ b/code/administrator/components/com_contenthistory/tmpl/preview/preview.php @@ -1,4 +1,5 @@
-

- item->save_date); ?> -

- item->version_note) : ?> -

- item->version_note); ?> -

- +

+ item->save_date); ?> +

+ item->version_note) : ?> +

+ item->version_note); ?> +

+ - - - - - - - - - - item->data as $name => $value) : ?> - value)) : ?> - - - - value as $subName => $subValue) : ?> - - - - - - - - - - - - - - - -
- -
- label; ?> -
  label; ?>value; ?>
label; ?>value; ?>
+ + + + + + + + + + item->data as $name => $value) : ?> + value)) : ?> + + + + value as $subName => $subValue) : ?> + value)) : ?> + value = (\is_object($subValue->value) || \is_array($subValue->value)) ? \json_encode($subValue->value, \JSON_UNESCAPED_UNICODE) : $subValue->value; ?> + + + + + + + + + + + + + + +
+ +
+ label; ?> +
  label; ?>value; ?>
label; ?>value; ?>
diff --git a/code/administrator/components/com_cpanel/cpanel.xml b/code/administrator/components/com_cpanel/cpanel.xml index f69b3aec..f0e23c34 100644 --- a/code/administrator/components/com_cpanel/cpanel.xml +++ b/code/administrator/components/com_cpanel/cpanel.xml @@ -2,7 +2,7 @@ com_cpanel Joomla! Project - Jun 2007 + 2007-06 (C) 2007 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_cpanel/services/provider.php b/code/administrator/components/com_cpanel/services/provider.php index 0b42c914..064bc2bd 100644 --- a/code/administrator/components/com_cpanel/services/provider.php +++ b/code/administrator/components/com_cpanel/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Cpanel')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Cpanel')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Cpanel')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Cpanel')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_cpanel/src/Controller/DisplayController.php b/code/administrator/components/com_cpanel/src/Controller/DisplayController.php index d49c4635..1671a942 100644 --- a/code/administrator/components/com_cpanel/src/Controller/DisplayController.php +++ b/code/administrator/components/com_cpanel/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->set('tmpl', 'cpanel'); - /** - * Typical view method for MVC based architecture - * - * This function is provide as a default implementation, in most cases - * you will need to override it in your own controllers. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe url parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. - * - * @return static An instance of the current object to support chaining. - * - * @since 3.0 - */ - public function display($cachable = false, $urlparams = array()) - { - /* - * Set the template - this will display cpanel.php - * from the selected admin template. - */ - $this->input->set('tmpl', 'cpanel'); + return parent::display($cachable, $urlparams); + } - return parent::display($cachable, $urlparams); - } + /** + * Method to add a module to a dashboard + * + * @since 4.0.0 + * + * @return void + */ + public function addModule() + { + $position = $this->input->get('position', 'cpanel'); + $function = $this->input->get('function'); - /** - * Method to add a module to a dashboard - * - * @since 4.0.0 - * - * @return void - */ - public function addModule() - { - $position = $this->input->get('position', 'cpanel'); - $function = $this->input->get('function'); + $appendLink = ''; - $appendLink = ''; + if ($function) { + $appendLink .= '&function=' . $function; + } - if ($function) - { - $appendLink .= '&function=' . $function; - } + if (substr($position, 0, 6) != 'cpanel') { + $position = 'cpanel'; + } - if (substr($position, 0, 6) != 'cpanel') - { - $position = 'cpanel'; - } + // Administrator + $clientId = (\Joomla\CMS\Application\ApplicationHelper::getClientInfo('administrator', true))->id; - Factory::getApplication()->setUserState('com_modules.modules.filter.position', $position); - Factory::getApplication()->setUserState('com_modules.modules.client_id', '1'); + $this->app->setUserState('com_modules.modules.' . $clientId . '.filter.position', $position); + $this->app->setUserState('com_modules.modules.client_id', (string) $clientId); - $this->setRedirect(Route::_('index.php?option=com_modules&view=select&tmpl=component&layout=modal' . $appendLink, false)); - } + $this->setRedirect(Route::_('index.php?option=com_modules&view=select&tmpl=component&layout=modal' . $appendLink, false)); + } } diff --git a/code/administrator/components/com_cpanel/src/Dispatcher/Dispatcher.php b/code/administrator/components/com_cpanel/src/Dispatcher/Dispatcher.php index 2dd0c8c7..833eb607 100644 --- a/code/administrator/components/com_cpanel/src/Dispatcher/Dispatcher.php +++ b/code/administrator/components/com_cpanel/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->getCmd('dashboard', ''); - - $position = ApplicationHelper::stringURLSafe($dashboard); - - // Generate a title for the view cpanel - if (!empty($dashboard)) - { - $parts = explode('.', $dashboard); - $component = $parts[0]; - - if (strpos($component, 'com_') === false) - { - $component = 'com_' . $component; - } - - // Need to load the language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE) - || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); - $lang->load($component); - - // Lookup dashboard attributes from component manifest file - $manifestFile = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'; - - if (is_file($manifestFile)) - { - $manifest = simplexml_load_file($manifestFile); - - if ($dashboardManifests = $manifest->dashboards) - { - foreach ($dashboardManifests->children() as $dashboardManifest) - { - if ((string) $dashboardManifest === $dashboard) - { - $title = Text::_((string) $dashboardManifest->attributes()->title); - $icon = (string) $dashboardManifest->attributes()->icon; - - break; - } - } - } - } - - if (empty($title)) - { - // Try building a title - $prefix = strtoupper($component) . '_DASHBOARD'; - - $sectionkey = !empty($parts[1]) ? '_' . strtoupper($parts[1]) : ''; - $key = $prefix . $sectionkey . '_TITLE'; - $keyIcon = $prefix . $sectionkey . '_ICON'; - - // Search for a component title - if ($lang->hasKey($key)) - { - $title = Text::_($key); - } - else - { - // Try with a string from CPanel - $key = 'COM_CPANEL_DASHBOARD_' . $parts[0] . '_TITLE'; - - if ($lang->hasKey($key)) - { - $title = Text::_($key); - } - else - { - $title = Text::_('COM_CPANEL_DASHBOARD_BASE_TITLE'); - } - } - - // Define the icon - if (empty($parts[1])) - { - // Default core icons. - if ($parts[0] === 'components') - { - $icon = 'icon-puzzle-piece'; - } - elseif ($parts[0] === 'system') - { - $icon = 'icon-wrench'; - } - elseif ($parts[0] === 'help') - { - $icon = 'icon-info-circle'; - } - elseif ($lang->hasKey($keyIcon)) - { - $icon = Text::_($keyIcon); - } - else - { - $icon = 'icon-home'; - } - } - elseif ($lang->hasKey($keyIcon)) - { - $icon = Text::_($keyIcon); - } - } - } - else - { - // Home Dashboard - $title = Text::_('COM_CPANEL_DASHBOARD_BASE_TITLE'); - $icon = 'icon-home'; - } - - // Set toolbar items for the page - ToolbarHelper::title($title, $icon . ' cpanel'); - ToolbarHelper::help('screen.cpanel'); - - // Display the cpanel modules - $this->position = $position ? 'cpanel-' . $position : 'cpanel'; - $this->modules = ModuleHelper::getModules($this->position); - - $quickicons = $position ? 'icon-' . $position : 'icon'; - $this->quickicons = ModuleHelper::getModules($quickicons); - - parent::display($tpl); - } + /** + * Array of cpanel modules + * + * @var array + */ + protected $modules = null; + + /** + * Array of cpanel modules + * + * @var array + */ + protected $quickicons = null; + + /** + * Moduleposition to load + * + * @var string + */ + protected $position = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $dashboard = $app->input->getCmd('dashboard', ''); + + $position = ApplicationHelper::stringURLSafe($dashboard); + + // Generate a title for the view cpanel + if (!empty($dashboard)) { + $parts = explode('.', $dashboard); + $component = $parts[0]; + + if (strpos($component, 'com_') === false) { + $component = 'com_' . $component; + } + + // Need to load the language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE) + || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); + $lang->load($component); + + // Lookup dashboard attributes from component manifest file + $manifestFile = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'; + + if (is_file($manifestFile)) { + $manifest = simplexml_load_file($manifestFile); + + if ($dashboardManifests = $manifest->dashboards) { + foreach ($dashboardManifests->children() as $dashboardManifest) { + if ((string) $dashboardManifest === $dashboard) { + $title = Text::_((string) $dashboardManifest->attributes()->title); + $icon = (string) $dashboardManifest->attributes()->icon; + + break; + } + } + } + } + + if (empty($title)) { + // Try building a title + $prefix = strtoupper($component) . '_DASHBOARD'; + + $sectionkey = !empty($parts[1]) ? '_' . strtoupper($parts[1]) : ''; + $key = $prefix . $sectionkey . '_TITLE'; + $keyIcon = $prefix . $sectionkey . '_ICON'; + + // Search for a component title + if ($lang->hasKey($key)) { + $title = Text::_($key); + } else { + // Try with a string from CPanel + $key = 'COM_CPANEL_DASHBOARD_' . $parts[0] . '_TITLE'; + + if ($lang->hasKey($key)) { + $title = Text::_($key); + } else { + $title = Text::_('COM_CPANEL_DASHBOARD_BASE_TITLE'); + } + } + + // Define the icon + if (empty($parts[1])) { + // Default core icons. + if ($parts[0] === 'components') { + $icon = 'icon-puzzle-piece'; + } elseif ($parts[0] === 'system') { + $icon = 'icon-wrench'; + } elseif ($parts[0] === 'help') { + $icon = 'icon-info-circle'; + } elseif ($lang->hasKey($keyIcon)) { + $icon = Text::_($keyIcon); + } else { + $icon = 'icon-home'; + } + } elseif ($lang->hasKey($keyIcon)) { + $icon = Text::_($keyIcon); + } + } + } else { + // Home Dashboard + $title = Text::_('COM_CPANEL_DASHBOARD_BASE_TITLE'); + $icon = 'icon-home'; + } + + // Set toolbar items for the page + ToolbarHelper::title($title, $icon . ' cpanel'); + ToolbarHelper::help('screen.cpanel'); + + // Display the cpanel modules + $this->position = $position ? 'cpanel-' . $position : 'cpanel'; + $this->modules = ModuleHelper::getModules($this->position); + + $quickicons = $position ? 'icon-' . $position : 'icon'; + $this->quickicons = ModuleHelper::getModules($quickicons); + + parent::display($tpl); + } } diff --git a/code/administrator/components/com_cpanel/tmpl/cpanel/default.php b/code/administrator/components/com_cpanel/tmpl/cpanel/default.php index b11b265e..20d1219d 100644 --- a/code/administrator/components/com_cpanel/tmpl/cpanel/default.php +++ b/code/administrator/components/com_cpanel/tmpl/cpanel/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_cpanel.admin-cpanel') - ->useScript('com_cpanel.admin-addmodule'); + ->useScript('com_cpanel.admin-addmodule'); $user = Factory::getUser(); // Set up the bootstrap modal that will be used for all module editors echo HTMLHelper::_( - 'bootstrap.renderModal', - 'moduleDashboardAddModal', - array( - 'title' => Text::_('COM_CPANEL_ADD_MODULE_MODAL_TITLE'), - 'backdrop' => 'static', - 'url' => Route::_('index.php?option=com_cpanel&task=addModule&function=jSelectModuleType&position=' . $this->escape($this->position)), - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'footer' => '' - . '', - ) + 'bootstrap.renderModal', + 'moduleDashboardAddModal', + array( + 'title' => Text::_('COM_CPANEL_ADD_MODULE_MODAL_TITLE'), + 'backdrop' => 'static', + 'url' => Route::_('index.php?option=com_cpanel&task=addModule&function=jSelectModuleType&position=' . $this->escape($this->position)), + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'footer' => '' + . '', + ) ); ?>
-
-
- quickicons) : - foreach ($this->quickicons as $iconmodule) - { - echo ModuleHelper::renderModule($iconmodule, array('style' => 'well')); - } - endif; - foreach ($this->modules as $module) - { - echo ModuleHelper::renderModule($module, array('style' => 'well')); - } - ?> - authorise('core.create', 'com_modules')) : ?> -
-
- -
-
- -
-
+
+
+ quickicons) : + foreach ($this->quickicons as $iconmodule) { + echo ModuleHelper::renderModule($iconmodule, array('style' => 'well')); + } + endif; + foreach ($this->modules as $module) { + echo ModuleHelper::renderModule($module, array('style' => 'well')); + } + ?> + authorise('core.create', 'com_modules')) : ?> +
+
+ +
+
+ +
+
diff --git a/code/administrator/components/com_fields/fields.xml b/code/administrator/components/com_fields/fields.xml index 4cd5a798..1ecf2885 100644 --- a/code/administrator/components/com_fields/fields.xml +++ b/code/administrator/components/com_fields/fields.xml @@ -2,7 +2,7 @@ com_fields Joomla! Project - March 2016 + 2016-03 (C) 2016 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_fields/helpers/fields.php b/code/administrator/components/com_fields/helpers/fields.php index 81d9091d..2b250c4f 100644 --- a/code/administrator/components/com_fields/helpers/fields.php +++ b/code/administrator/components/com_fields/helpers/fields.php @@ -1,12 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * FieldsHelper diff --git a/code/administrator/components/com_fields/services/provider.php b/code/administrator/components/com_fields/services/provider.php index 16284025..bb7fc5dc 100644 --- a/code/administrator/components/com_fields/services/provider.php +++ b/code/administrator/components/com_fields/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Fields')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Fields')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Fields')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Fields')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Fields')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Fields')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new FieldsComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new FieldsComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_fields/src/Controller/DisplayController.php b/code/administrator/components/com_fields/src/Controller/DisplayController.php index 97099ec7..95be7bf0 100644 --- a/code/administrator/components/com_fields/src/Controller/DisplayController.php +++ b/code/administrator/components/com_fields/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'fields'); - $id = $this->input->getInt('id'); + /** + * Typical view method for MVC based architecture + * + * This function is provide as a default implementation, in most cases + * you will need to override it in your own controllers. + * + * @param boolean $cachable If true, the view output will be cached + * @param array|bool $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()} + * + * @return BaseController|boolean A Controller object to support chaining. + * + * @since 3.7.0 + */ + public function display($cachable = false, $urlparams = false) + { + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'fields'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($vName == 'field' && !$this->checkEditId('com_fields.edit.field', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($vName == 'field' && !$this->checkEditId('com_fields.edit.field', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_fields&view=fields&context=' . $this->input->get('context'), false)); + $this->setRedirect(Route::_('index.php?option=com_fields&view=fields&context=' . $this->input->get('context'), false)); - return false; - } + return false; + } - return parent::display($cachable, $urlparams); - } + return parent::display($cachable, $urlparams); + } } diff --git a/code/administrator/components/com_fields/src/Controller/FieldController.php b/code/administrator/components/com_fields/src/Controller/FieldController.php index b0681ddb..646ddda7 100644 --- a/code/administrator/components/com_fields/src/Controller/FieldController.php +++ b/code/administrator/components/com_fields/src/Controller/FieldController.php @@ -1,4 +1,5 @@ internalContext = $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'); - $parts = FieldsHelper::extract($this->internalContext); - $this->component = $parts ? $parts[0] : null; - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 3.7.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->component); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.7.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Zero record (id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->component . '.field.' . $recordId)) - { - return true; - } - - // Check edit own on the record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->component . '.field.' . $recordId)) - { - // Existing record already has an owner, get it - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - // Grant if current user is owner of the record - return $user->id == $record->created_user_id; - } - - return false; - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 3.7.0 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Field'); - - // Preset the redirect - $this->setRedirect('index.php?option=com_fields&view=fields&context=' . $this->internalContext); - - return parent::batch($model); - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 3.7.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - return parent::getRedirectToItemAppend($recordId) . '&context=' . $this->internalContext; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 3.7.0 - */ - protected function getRedirectToListAppend() - { - return parent::getRedirectToListAppend() . '&context=' . $this->internalContext; - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.7.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $item = $model->getItem(); - - if (isset($item->params) && is_array($item->params)) - { - $registry = new Registry; - $registry->loadArray($item->params); - $item->params = (string) $registry; - } - } + /** + * @var string + */ + private $internalContext; + + /** + * @var string + */ + private $component; + + /** + * The prefix to use with controller messages. + * + * @var string + + * @since 3.7.0 + */ + protected $text_prefix = 'COM_FIELDS_FIELD'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.7.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->internalContext = $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'); + $parts = FieldsHelper::extract($this->internalContext); + $this->component = $parts ? $parts[0] : null; + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 3.7.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->component); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.7.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Zero record (id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->component . '.field.' . $recordId)) { + return true; + } + + // Check edit own on the record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->component . '.field.' . $recordId)) { + // Existing record already has an owner, get it + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + // Grant if current user is owner of the record + return $user->id == $record->created_user_id; + } + + return false; + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 3.7.0 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Field'); + + // Preset the redirect + $this->setRedirect('index.php?option=com_fields&view=fields&context=' . $this->internalContext); + + return parent::batch($model); + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 3.7.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + return parent::getRedirectToItemAppend($recordId) . '&context=' . $this->internalContext; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 3.7.0 + */ + protected function getRedirectToListAppend() + { + return parent::getRedirectToListAppend() . '&context=' . $this->internalContext; + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.7.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $item = $model->getItem(); + + if (isset($item->params) && is_array($item->params)) { + $registry = new Registry(); + $registry->loadArray($item->params); + $item->params = (string) $registry; + } + } } diff --git a/code/administrator/components/com_fields/src/Controller/FieldsController.php b/code/administrator/components/com_fields/src/Controller/FieldsController.php index 9d7ad0c8..088bff84 100644 --- a/code/administrator/components/com_fields/src/Controller/FieldsController.php +++ b/code/administrator/components/com_fields/src/Controller/FieldsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 3.7.0 + */ + public function getModel($name = 'Field', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_fields/src/Controller/GroupController.php b/code/administrator/components/com_fields/src/Controller/GroupController.php index 1c644954..7016ed2f 100644 --- a/code/administrator/components/com_fields/src/Controller/GroupController.php +++ b/code/administrator/components/com_fields/src/Controller/GroupController.php @@ -1,4 +1,5 @@ input->getCmd('context')); - - if ($parts) - { - $this->component = $parts[0]; - } - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 3.7.0 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Group'); - - // Preset the redirect - $this->setRedirect('index.php?option=com_fields&view=groups'); - - return parent::batch($model); - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 3.7.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->component); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.7.0 - */ - protected function allowEdit($data = array(), $key = 'parent_id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Zero record (parent_id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->component . '.fieldgroup.' . $recordId)) - { - return true; - } - - // Check edit own on the record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->component . '.fieldgroup.' . $recordId) || $user->authorise('core.edit.own', $this->component)) - { - // Existing record already has an owner, get it - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - // Grant if current user is owner of the record - return $user->id == $record->created_by; - } - - return false; - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.7.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $item = $model->getItem(); - - if (isset($item->params) && is_array($item->params)) - { - $registry = new Registry; - $registry->loadArray($item->params); - $item->params = (string) $registry; - } - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - $append .= '&context=' . $this->input->get('context'); - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&context=' . $this->input->get('context'); - - return $append; - } + /** + * The prefix to use with controller messages. + * + * @var string + + * @since 3.7.0 + */ + protected $text_prefix = 'COM_FIELDS_GROUP'; + + /** + * The component for which the group applies. + * + * @var string + * @since 3.7.0 + */ + private $component = ''; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 3.7.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $parts = FieldsHelper::extract($this->input->getCmd('context')); + + if ($parts) { + $this->component = $parts[0]; + } + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 3.7.0 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Group'); + + // Preset the redirect + $this->setRedirect('index.php?option=com_fields&view=groups'); + + return parent::batch($model); + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 3.7.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->component); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.7.0 + */ + protected function allowEdit($data = array(), $key = 'parent_id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Zero record (parent_id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->component . '.fieldgroup.' . $recordId)) { + return true; + } + + // Check edit own on the record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->component . '.fieldgroup.' . $recordId) || $user->authorise('core.edit.own', $this->component)) { + // Existing record already has an owner, get it + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + // Grant if current user is owner of the record + return $user->id == $record->created_by; + } + + return false; + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.7.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $item = $model->getItem(); + + if (isset($item->params) && is_array($item->params)) { + $registry = new Registry(); + $registry->loadArray($item->params); + $item->params = (string) $registry; + } + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $append .= '&context=' . $this->input->get('context'); + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&context=' . $this->input->get('context'); + + return $append; + } } diff --git a/code/administrator/components/com_fields/src/Controller/GroupsController.php b/code/administrator/components/com_fields/src/Controller/GroupsController.php index 247217bb..7568ded1 100644 --- a/code/administrator/components/com_fields/src/Controller/GroupsController.php +++ b/code/administrator/components/com_fields/src/Controller/GroupsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 3.7.0 + */ + public function getModel($name = 'Group', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_fields/src/Dispatcher/Dispatcher.php b/code/administrator/components/com_fields/src/Dispatcher/Dispatcher.php index ba4e8515..69b7513a 100644 --- a/code/administrator/components/com_fields/src/Dispatcher/Dispatcher.php +++ b/code/administrator/components/com_fields/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getUserStateFromRequest( - 'com_fields.groups.context', - 'context', - $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'), - 'CMD' - ); + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + */ + protected function checkAccess() + { + $context = $this->app->getUserStateFromRequest( + 'com_fields.groups.context', + 'context', + $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'), + 'CMD' + ); - $parts = FieldsHelper::extract($context); + $parts = FieldsHelper::extract($context); - if (!$parts || !$this->app->getIdentity()->authorise('core.manage', $parts[0])) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + if (!$parts || !$this->app->getIdentity()->authorise('core.manage', $parts[0])) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/code/administrator/components/com_fields/src/Extension/FieldsComponent.php b/code/administrator/components/com_fields/src/Extension/FieldsComponent.php index 68fa6b3b..5027972e 100644 --- a/code/administrator/components/com_fields/src/Extension/FieldsComponent.php +++ b/code/administrator/components/com_fields/src/Extension/FieldsComponent.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT a.name AS text, a.element AS value') - ->from('#__extensions as a') - ->where('a.enabled >= 1') - ->where('a.type =' . $db->quote('component')); - - $items = $db->setQuery($query)->loadObjectList(); - - $options = []; - - if (count($items)) - { - $lang = Factory::getLanguage(); - - $components = []; - - // Search for components supporting Fieldgroups - suppose that these components support fields as well - foreach ($items as &$item) - { - $availableActions = Access::getActionsFromFile( - JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', - "/access/section[@name='fieldgroup']/" - ); - - if (!empty($availableActions)) - { - // Load language - $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; - $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) - || $lang->load($item->value . 'sys', $source); - - // Translate component name - $item->text = Text::_($item->text); - - $components[] = $item; - } - } - - if (empty($components)) - { - return []; - } - - foreach ($components as $component) - { - // Search for different contexts - $c = Factory::getApplication()->bootComponent($component->value); - - if ($c instanceof FieldsServiceInterface) - { - $contexts = $c->getContexts(); - - foreach ($contexts as $context) - { - $newOption = new \stdClass; - $newOption->value = strtolower($component->value . '.' . $context); - $newOption->text = $component->text . ' - ' . Text::_($context); - $options[] = $newOption; - } - } - else - { - $options[] = $component; - } - } - - // Sort by name - $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $items); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'ComponentsFieldgroup'; + + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + // Initialise variable. + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('DISTINCT a.name AS text, a.element AS value') + ->from('#__extensions as a') + ->where('a.enabled >= 1') + ->where('a.type =' . $db->quote('component')); + + $items = $db->setQuery($query)->loadObjectList(); + + $options = []; + + if (count($items)) { + $lang = Factory::getLanguage(); + + $components = []; + + // Search for components supporting Fieldgroups - suppose that these components support fields as well + foreach ($items as &$item) { + $availableActions = Access::getActionsFromFile( + JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', + "/access/section[@name='fieldgroup']/" + ); + + if (!empty($availableActions)) { + // Load language + $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; + $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) + || $lang->load($item->value . 'sys', $source); + + // Translate component name + $item->text = Text::_($item->text); + + $components[] = $item; + } + } + + if (empty($components)) { + return []; + } + + foreach ($components as $component) { + // Search for different contexts + $c = Factory::getApplication()->bootComponent($component->value); + + if ($c instanceof FieldsServiceInterface) { + $contexts = $c->getContexts(); + + foreach ($contexts as $context) { + $newOption = new \stdClass(); + $newOption->value = strtolower($component->value . '.' . $context); + $newOption->text = $component->text . ' - ' . Text::_($context); + $options[] = $newOption; + } + } else { + $options[] = $component; + } + } + + // Sort by name + $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $items); + + return $options; + } } diff --git a/code/administrator/components/com_fields/src/Field/ComponentsFieldsField.php b/code/administrator/components/com_fields/src/Field/ComponentsFieldsField.php index 47f5b9ef..f0620c25 100644 --- a/code/administrator/components/com_fields/src/Field/ComponentsFieldsField.php +++ b/code/administrator/components/com_fields/src/Field/ComponentsFieldsField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT a.name AS text, a.element AS value') - ->from('#__extensions as a') - ->where('a.enabled >= 1') - ->where('a.type =' . $db->quote('component')); - - $items = $db->setQuery($query)->loadObjectList(); - - $options = []; - - if (count($items)) - { - $lang = Factory::getLanguage(); - - $components = []; - - // Search for components supporting Fieldgroups - suppose that these components support fields as well - foreach ($items as &$item) - { - $availableActions = Access::getActionsFromFile( - JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', - "/access/section[@name='fieldgroup']/" - ); - - if (!empty($availableActions)) - { - // Load language - $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; - $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) - || $lang->load($item->value . 'sys', $source); - - // Translate component name - $item->text = Text::_($item->text); - - $components[] = $item; - } - } - - if (empty($components)) - { - return []; - } - - foreach ($components as $component) - { - // Search for different contexts - $c = Factory::getApplication()->bootComponent($component->value); - - if ($c instanceof FieldsServiceInterface) - { - $contexts = $c->getContexts(); - - foreach ($contexts as $context) - { - $newOption = new \stdClass; - $newOption->value = strtolower($component->value . '.' . $context); - $newOption->text = $component->text . ' - ' . Text::_($context); - $options[] = $newOption; - } - } - else - { - $options[] = $component; - } - } - - // Sort by name - $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $items); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'ComponentsFields'; + + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + // Initialise variable. + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('DISTINCT a.name AS text, a.element AS value') + ->from('#__extensions as a') + ->where('a.enabled >= 1') + ->where('a.type =' . $db->quote('component')); + + $items = $db->setQuery($query)->loadObjectList(); + + $options = []; + + if (count($items)) { + $lang = Factory::getLanguage(); + + $components = []; + + // Search for components supporting Fieldgroups - suppose that these components support fields as well + foreach ($items as &$item) { + $availableActions = Access::getActionsFromFile( + JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', + "/access/section[@name='fieldgroup']/" + ); + + if (!empty($availableActions)) { + // Load language + $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; + $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) + || $lang->load($item->value . 'sys', $source); + + // Translate component name + $item->text = Text::_($item->text); + + $components[] = $item; + } + } + + if (empty($components)) { + return []; + } + + foreach ($components as $component) { + // Search for different contexts + $c = Factory::getApplication()->bootComponent($component->value); + + if ($c instanceof FieldsServiceInterface) { + $contexts = $c->getContexts(); + + foreach ($contexts as $context) { + $newOption = new \stdClass(); + $newOption->value = strtolower($component->value . '.' . $context); + $newOption->text = $component->text . ' - ' . Text::_($context); + $options[] = $newOption; + } + } else { + $options[] = $component; + } + } + + // Sort by name + $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $items); + + return $options; + } } diff --git a/code/administrator/components/com_fields/src/Field/FieldLayoutField.php b/code/administrator/components/com_fields/src/Field/FieldLayoutField.php index 2ea362c8..5be257ca 100644 --- a/code/administrator/components/com_fields/src/Field/FieldLayoutField.php +++ b/code/administrator/components/com_fields/src/Field/FieldLayoutField.php @@ -1,4 +1,5 @@ form->getValue('context')); - $extension = $extension[0]; - - if ($extension) - { - // Get the database object and a new query object. - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - // Build the query. - $query->select('element, name') - ->from('#__extensions') - ->where('client_id = 0') - ->where('type = ' . $db->quote('template')) - ->where('enabled = 1'); - - // Set the query and load the templates. - $db->setQuery($query); - $templates = $db->loadObjectList('element'); - - // Build the search paths for component layouts. - $component_path = Path::clean(JPATH_SITE . '/components/' . $extension . '/layouts/field'); - - // Prepare array of component layouts - $component_layouts = array(); - - // Prepare the grouped list - $groups = array(); - - // Add "Use Default" - $groups[]['items'][] = HTMLHelper::_('select.option', '', Text::_('JOPTION_USE_DEFAULT')); - - // Add the layout options from the component path. - if (is_dir($component_path) && ($component_layouts = Folder::files($component_path, '^[^_]*\.php$', false, true))) - { - // Create the group for the component - $groups['_'] = array(); - $groups['_']['id'] = $this->id . '__'; - $groups['_']['text'] = Text::sprintf('JOPTION_FROM_COMPONENT'); - $groups['_']['items'] = array(); - - foreach ($component_layouts as $i => $file) - { - // Add an option to the component group - $value = basename($file, '.php'); - $component_layouts[$i] = $value; - - if ($value === 'render') - { - continue; - } - - $groups['_']['items'][] = HTMLHelper::_('select.option', $value, $value); - } - } - - // Loop on all templates - if ($templates) - { - foreach ($templates as $template) - { - $files = array(); - $template_paths = array( - Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/' . $extension . '/field'), - Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/com_fields/field'), - Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/field'), - ); - - // Add the layout options from the template paths. - foreach ($template_paths as $template_path) - { - if (is_dir($template_path)) - { - $files = array_merge($files, Folder::files($template_path, '^[^_]*\.php$', false, true)); - } - } - - foreach ($files as $i => $file) - { - $value = basename($file, '.php'); - - // Remove the default "render.php" or layout files that exist in the component folder - if ($value === 'render' || in_array($value, $component_layouts)) - { - unset($files[$i]); - } - } - - if (count($files)) - { - // Create the group for the template - $groups[$template->name] = array(); - $groups[$template->name]['id'] = $this->id . '_' . $template->element; - $groups[$template->name]['text'] = Text::sprintf('JOPTION_FROM_TEMPLATE', $template->name); - $groups[$template->name]['items'] = array(); - - foreach ($files as $file) - { - // Add an option to the template group - $value = basename($file, '.php'); - $groups[$template->name]['items'][] = HTMLHelper::_('select.option', $value, $value); - } - } - } - } - - // Compute attributes for the grouped list - $attr = $this->element['size'] ? ' size="' . (int) $this->element['size'] . '"' : ''; - $attr .= $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; - - // Prepare HTML code - $html = array(); - - // Compute the current selected values - $selected = array($this->value); - - // Add a grouped list - $html[] = HTMLHelper::_( - 'select.groupedlist', $groups, $this->name, - array('id' => $this->id, 'group.id' => 'id', 'list.attr' => $attr, 'list.select' => $selected) - ); - - return implode($html); - } - - return ''; - } + /** + * The form field type. + * + * @var string + * @since 3.9.0 + */ + protected $type = 'FieldLayout'; + + /** + * Method to get the field input for a field layout field. + * + * @return string The field input. + * + * @since 3.9.0 + */ + protected function getInput() + { + $extension = explode('.', $this->form->getValue('context')); + $extension = $extension[0]; + + if ($extension) { + // Get the database object and a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Build the query. + $query->select('element, name') + ->from('#__extensions') + ->where('client_id = 0') + ->where('type = ' . $db->quote('template')) + ->where('enabled = 1'); + + // Set the query and load the templates. + $db->setQuery($query); + $templates = $db->loadObjectList('element'); + + // Build the search paths for component layouts. + $component_path = Path::clean(JPATH_SITE . '/components/' . $extension . '/layouts/field'); + + // Prepare array of component layouts + $component_layouts = array(); + + // Prepare the grouped list + $groups = array(); + + // Add "Use Default" + $groups[]['items'][] = HTMLHelper::_('select.option', '', Text::_('JOPTION_USE_DEFAULT')); + + // Add the layout options from the component path. + if (is_dir($component_path) && ($component_layouts = Folder::files($component_path, '^[^_]*\.php$', false, true))) { + // Create the group for the component + $groups['_'] = array(); + $groups['_']['id'] = $this->id . '__'; + $groups['_']['text'] = Text::sprintf('JOPTION_FROM_COMPONENT'); + $groups['_']['items'] = array(); + + foreach ($component_layouts as $i => $file) { + // Add an option to the component group + $value = basename($file, '.php'); + $component_layouts[$i] = $value; + + if ($value === 'render') { + continue; + } + + $groups['_']['items'][] = HTMLHelper::_('select.option', $value, $value); + } + } + + // Loop on all templates + if ($templates) { + foreach ($templates as $template) { + $files = array(); + $template_paths = array( + Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/' . $extension . '/field'), + Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/com_fields/field'), + Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/field'), + ); + + // Add the layout options from the template paths. + foreach ($template_paths as $template_path) { + if (is_dir($template_path)) { + $files = array_merge($files, Folder::files($template_path, '^[^_]*\.php$', false, true)); + } + } + + foreach ($files as $i => $file) { + $value = basename($file, '.php'); + + // Remove the default "render.php" or layout files that exist in the component folder + if ($value === 'render' || in_array($value, $component_layouts)) { + unset($files[$i]); + } + } + + if (count($files)) { + // Create the group for the template + $groups[$template->name] = array(); + $groups[$template->name]['id'] = $this->id . '_' . $template->element; + $groups[$template->name]['text'] = Text::sprintf('JOPTION_FROM_TEMPLATE', $template->name); + $groups[$template->name]['items'] = array(); + + foreach ($files as $file) { + // Add an option to the template group + $value = basename($file, '.php'); + $groups[$template->name]['items'][] = HTMLHelper::_('select.option', $value, $value); + } + } + } + } + + // Compute attributes for the grouped list + $attr = $this->element['size'] ? ' size="' . (int) $this->element['size'] . '"' : ''; + $attr .= $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + // Prepare HTML code + $html = array(); + + // Compute the current selected values + $selected = array($this->value); + + // Add a grouped list + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + $this->name, + array('id' => $this->id, 'group.id' => 'id', 'list.attr' => $attr, 'list.select' => $selected) + ); + + return implode($html); + } + + return ''; + } } diff --git a/code/administrator/components/com_fields/src/Field/FieldcontextsField.php b/code/administrator/components/com_fields/src/Field/FieldcontextsField.php index 3f223313..4d049efc 100644 --- a/code/administrator/components/com_fields/src/Field/FieldcontextsField.php +++ b/code/administrator/components/com_fields/src/Field/FieldcontextsField.php @@ -1,4 +1,5 @@ getOptions() ? parent::getInput() : ''; - } + /** + * Method to get the field input markup for a generic list. + * Use the multiple attribute to enable multiselect. + * + * @return string The field input markup. + * + * @since 3.7.0 + */ + protected function getInput() + { + return $this->getOptions() ? parent::getInput() : ''; + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 3.7.0 - */ - protected function getOptions() - { - $parts = explode('.', $this->value); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $parts = explode('.', $this->value); - $component = Factory::getApplication()->bootComponent($parts[0]); + $component = Factory::getApplication()->bootComponent($parts[0]); - if ($component instanceof FieldsServiceInterface) - { - return $component->getContexts(); - } + if ($component instanceof FieldsServiceInterface) { + return $component->getContexts(); + } - return []; - } + return []; + } } diff --git a/code/administrator/components/com_fields/src/Field/FieldgroupsField.php b/code/administrator/components/com_fields/src/Field/FieldgroupsField.php index 5475eb42..7b6c01c4 100644 --- a/code/administrator/components/com_fields/src/Field/FieldgroupsField.php +++ b/code/administrator/components/com_fields/src/Field/FieldgroupsField.php @@ -1,4 +1,5 @@ element['context']; - $states = $this->element['state'] ?: '0,1'; - $states = ArrayHelper::toInteger(explode(',', $states)); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $context = (string) $this->element['context']; + $states = $this->element['state'] ?: '0,1'; + $states = ArrayHelper::toInteger(explode(',', $states)); - $user = Factory::getUser(); - $viewlevels = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); + $user = Factory::getUser(); + $viewlevels = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('title', 'text'), - $db->quoteName('id', 'value'), - $db->quoteName('state'), - ] - ); - $query->from($db->quoteName('#__fields_groups')); - $query->whereIn($db->quoteName('state'), $states); - $query->where($db->quoteName('context') . ' = :context'); - $query->whereIn($db->quoteName('access'), $viewlevels); - $query->order('ordering asc, id asc'); - $query->bind(':context', $context); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('title', 'text'), + $db->quoteName('id', 'value'), + $db->quoteName('state'), + ] + ); + $query->from($db->quoteName('#__fields_groups')); + $query->whereIn($db->quoteName('state'), $states); + $query->where($db->quoteName('context') . ' = :context'); + $query->whereIn($db->quoteName('access'), $viewlevels); + $query->order('ordering asc, id asc'); + $query->bind(':context', $context); - $db->setQuery($query); - $options = $db->loadObjectList(); + $db->setQuery($query); + $options = $db->loadObjectList(); - foreach ($options AS $option) - { - if ($option->state == 0) - { - $option->text = '[' . $option->text . ']'; - } + foreach ($options as $option) { + if ($option->state == 0) { + $option->text = '[' . $option->text . ']'; + } - if ($option->state == 2) - { - $option->text = '{' . $option->text . '}'; - } - } + if ($option->state == 2) { + $option->text = '{' . $option->text . '}'; + } + } - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/code/administrator/components/com_fields/src/Field/SectionField.php b/code/administrator/components/com_fields/src/Field/SectionField.php index 2c3549c7..b331e9d1 100644 --- a/code/administrator/components/com_fields/src/Field/SectionField.php +++ b/code/administrator/components/com_fields/src/Field/SectionField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 3.7.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 3.7.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); - // Onchange must always be the change context function - $this->onchange = 'Joomla.fieldsChangeContext(this.value);'; + // Onchange must always be the change context function + $this->onchange = 'Joomla.fieldsChangeContext(this.value);'; - return $return; - } + return $return; + } - /** - * Method to get the field input markup for a generic list. - * Use the multiple attribute to enable multiselect. - * - * @return string The field input markup. - * - * @since 3.7.0 - */ - protected function getInput() - { - Factory::getApplication()->getDocument()->getWebAssetManager() - ->useScript('com_fields.admin-field-changecontext'); + /** + * Method to get the field input markup for a generic list. + * Use the multiple attribute to enable multiselect. + * + * @return string The field input markup. + * + * @since 3.7.0 + */ + protected function getInput() + { + Factory::getApplication()->getDocument()->getWebAssetManager() + ->useScript('com_fields.admin-field-changecontext'); - return parent::getInput(); - } + return parent::getInput(); + } } diff --git a/code/administrator/components/com_fields/src/Field/SubfieldsField.php b/code/administrator/components/com_fields/src/Field/SubfieldsField.php index 1640d6cb..940c07b1 100644 --- a/code/administrator/components/com_fields/src/Field/SubfieldsField.php +++ b/code/administrator/components/com_fields/src/Field/SubfieldsField.php @@ -1,4 +1,5 @@ context])) - { - static::$customFieldsCache[$this->context] = FieldsHelper::getFields($this->context, null, false, null, true); - } - - // Iterate over the custom fields for this context - foreach (static::$customFieldsCache[$this->context] as $customField) - { - // Skip our own subform type. We won't have subform in subform. - if ($customField->type == 'subform') - { - continue; - } - - $options[] = HTMLHelper::_( - 'select.option', - $customField->id, - ($customField->title . ' (' . $customField->type . ')') - ); - } - - // Sorting the fields based on the text which is displayed - usort( - $options, - function ($a, $b) - { - return strcmp($a->text, $b->text); - } - ); - - if (count($options) == 0) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_FIELDS_NO_FIELDS_TO_CREATE_SUBFORM_FIELD_WARNING'), 'warning'); - } - - return $options; - } - - /** - * Method to attach a JForm object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); - - if ($return) - { - $this->context = (string) $this->element['context']; - } - - return $return; - } + /** + * The name of this Field type. + * + * @var string + * + * @since 4.0.0 + */ + public $type = 'Subfields'; + + /** + * Configuration option for this field type to could filter the displayed custom field instances + * by a given context. Default value empty string. If given empty string, displays all custom fields. + * + * @var string + * + * @since 4.0.0 + */ + protected $context = ''; + + /** + * Array to do a fast in-memory caching of all custom field items. Used to not bother the + * FieldsHelper with a call every time this field is being rendered. + * + * @var array + * + * @since 4.0.0 + */ + protected static $customFieldsCache = array(); + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 4.0.0 + */ + protected function getOptions() + { + $options = parent::getOptions(); + + // Check whether we have a result for this context yet + if (!isset(static::$customFieldsCache[$this->context])) { + static::$customFieldsCache[$this->context] = FieldsHelper::getFields($this->context, null, false, null, true); + } + + // Iterate over the custom fields for this context + foreach (static::$customFieldsCache[$this->context] as $customField) { + // Skip our own subform type. We won't have subform in subform. + if ($customField->type == 'subform') { + continue; + } + + $options[] = HTMLHelper::_( + 'select.option', + $customField->id, + ($customField->title . ' (' . $customField->type . ')') + ); + } + + // Sorting the fields based on the text which is displayed + usort( + $options, + function ($a, $b) { + return strcmp($a->text, $b->text); + } + ); + + if (count($options) == 0) { + Factory::getApplication()->enqueueMessage(Text::_('COM_FIELDS_NO_FIELDS_TO_CREATE_SUBFORM_FIELD_WARNING'), 'warning'); + } + + return $options; + } + + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); + + if ($return) { + $this->context = (string) $this->element['context']; + } + + return $return; + } } diff --git a/code/administrator/components/com_fields/src/Field/TypeField.php b/code/administrator/components/com_fields/src/Field/TypeField.php index d48b1aef..1e01dd68 100644 --- a/code/administrator/components/com_fields/src/Field/TypeField.php +++ b/code/administrator/components/com_fields/src/Field/TypeField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 3.7.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 3.7.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); - $this->onchange = 'Joomla.typeHasChanged(this);'; + $this->onchange = 'Joomla.typeHasChanged(this);'; - return $return; - } + return $return; + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 3.7.0 - */ - protected function getOptions() - { - $options = parent::getOptions(); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $options = parent::getOptions(); - $fieldTypes = FieldsHelper::getFieldTypes(); + $fieldTypes = FieldsHelper::getFieldTypes(); - foreach ($fieldTypes as $fieldType) - { - $options[] = HTMLHelper::_('select.option', $fieldType['type'], $fieldType['label']); - } + foreach ($fieldTypes as $fieldType) { + $options[] = HTMLHelper::_('select.option', $fieldType['type'], $fieldType['label']); + } - // Sorting the fields based on the text which is displayed - usort( - $options, - function ($a, $b) - { - return strcmp($a->text, $b->text); - } - ); + // Sorting the fields based on the text which is displayed + usort( + $options, + function ($a, $b) { + return strcmp($a->text, $b->text); + } + ); - // Load scripts - Factory::getApplication()->getDocument()->getWebAssetManager() - ->useScript('com_fields.admin-field-typehaschanged') - ->useScript('webcomponent.core-loader'); + // Load scripts + Factory::getApplication()->getDocument()->getWebAssetManager() + ->useScript('com_fields.admin-field-typehaschanged') + ->useScript('webcomponent.core-loader'); - return $options; - } + return $options; + } } diff --git a/code/administrator/components/com_fields/src/Helper/FieldsHelper.php b/code/administrator/components/com_fields/src/Helper/FieldsHelper.php index e2f39866..ecfdefdd 100644 --- a/code/administrator/components/com_fields/src/Helper/FieldsHelper.php +++ b/code/administrator/components/com_fields/src/Helper/FieldsHelper.php @@ -1,4 +1,5 @@ bootComponent($parts[0]); - - if ($component instanceof FieldsServiceInterface) - { - $newSection = $component->validateSection($parts[1], $item); - } - - if ($newSection) - { - $parts[1] = $newSection; - } - - return $parts; - } - - /** - * Returns the fields for the given context. - * If the item is an object the returned fields do have an additional field - * "value" which represents the value for the given item. If the item has an - * assigned_cat_ids field, then additionally fields which belong to that - * category will be returned. - * Should the value being prepared to be shown in an HTML context then - * prepareValue must be set to true. No further escaping needs to be done. - * The values of the fields can be overridden by an associative array where the keys - * have to be a name and its corresponding value. - * - * @param string $context The context of the content passed to the helper - * @param null $item The item being edited in the form - * @param int|bool $prepareValue (if int is display event): 1 - AfterTitle, 2 - BeforeDisplay, 3 - AfterDisplay, 0 - OFF - * @param array|null $valuesToOverride The values to override - * @param bool $includeSubformFields Should I include fields marked as Only Use In Subform? - * - * @return array - * - * @throws \Exception - * @since 3.7.0 - */ - public static function getFields( - $context, $item = null, $prepareValue = false, array $valuesToOverride = null, bool $includeSubformFields = false - ) - { - if (self::$fieldsCache === null) - { - // Load the model - self::$fieldsCache = Factory::getApplication()->bootComponent('com_fields') - ->getMVCFactory()->createModel('Fields', 'Administrator', ['ignore_request' => true]); - - self::$fieldsCache->setState('filter.state', 1); - self::$fieldsCache->setState('list.limit', 0); - } - - if ($includeSubformFields) - { - self::$fieldsCache->setState('filter.only_use_in_subform', ''); - } - else - { - self::$fieldsCache->setState('filter.only_use_in_subform', 0); - } - - if (is_array($item)) - { - $item = (object) $item; - } - - if (Multilanguage::isEnabled() && isset($item->language) && $item->language != '*') - { - self::$fieldsCache->setState('filter.language', array('*', $item->language)); - } - - self::$fieldsCache->setState('filter.context', $context); - self::$fieldsCache->setState('filter.assigned_cat_ids', array()); - - /* - * If item has assigned_cat_ids parameter display only fields which - * belong to the category - */ - if ($item && (isset($item->catid) || isset($item->fieldscatid))) - { - $assignedCatIds = $item->catid ?? $item->fieldscatid; - - if (!is_array($assignedCatIds)) - { - $assignedCatIds = explode(',', $assignedCatIds); - } - - // Fields without any category assigned should show as well - $assignedCatIds[] = 0; - - self::$fieldsCache->setState('filter.assigned_cat_ids', $assignedCatIds); - } - - $fields = self::$fieldsCache->getItems(); - - if ($fields === false) - { - return array(); - } - - if ($item && isset($item->id)) - { - if (self::$fieldCache === null) - { - self::$fieldCache = Factory::getApplication()->bootComponent('com_fields') - ->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); - } - - $fieldIds = array_map( - function ($f) - { - return $f->id; - }, - $fields - ); - - $fieldValues = self::$fieldCache->getFieldValues($fieldIds, $item->id); - - $new = array(); - - foreach ($fields as $key => $original) - { - /* - * Doing a clone, otherwise fields for different items will - * always reference to the same object - */ - $field = clone $original; - - if ($valuesToOverride && array_key_exists($field->name, $valuesToOverride)) - { - $field->value = $valuesToOverride[$field->name]; - } - elseif ($valuesToOverride && array_key_exists($field->id, $valuesToOverride)) - { - $field->value = $valuesToOverride[$field->id]; - } - elseif (array_key_exists($field->id, $fieldValues)) - { - $field->value = $fieldValues[$field->id]; - } - - if (!isset($field->value) || $field->value === '') - { - $field->value = $field->default_value; - } - - $field->rawvalue = $field->value; - - // If boolean prepare, if int, it is the event type: 1 - After Title, 2 - Before Display Content, 3 - After Display Content, 0 - Do not prepare - if ($prepareValue && (is_bool($prepareValue) || $prepareValue === (int) $field->params->get('display', '2'))) - { - PluginHelper::importPlugin('fields'); - - /* - * On before field prepare - * Event allow plugins to modify the output of the field before it is prepared - */ - Factory::getApplication()->triggerEvent('onCustomFieldsBeforePrepareField', array($context, $item, &$field)); - - // Gathering the value for the field - $value = Factory::getApplication()->triggerEvent('onCustomFieldsPrepareField', array($context, $item, &$field)); - - if (is_array($value)) - { - $value = implode(' ', $value); - } - - /* - * On after field render - * Event allows plugins to modify the output of the prepared field - */ - Factory::getApplication()->triggerEvent('onCustomFieldsAfterPrepareField', array($context, $item, $field, &$value)); - - // Assign the value - $field->value = $value; - } - - $new[$key] = $field; - } - - $fields = $new; - } - - return $fields; - } - - /** - * Renders the layout file and data on the context and does a fall back to - * Fields afterwards. - * - * @param string $context The context of the content passed to the helper - * @param string $layoutFile layoutFile - * @param array $displayData displayData - * - * @return NULL|string - * - * @since 3.7.0 - */ - public static function render($context, $layoutFile, $displayData) - { - $value = ''; - - /* - * Because the layout refreshes the paths before the render function is - * called, so there is no way to load the layout overrides in the order - * template -> context -> fields. - * If there is no override in the context then we need to call the - * layout from Fields. - */ - if ($parts = self::extract($context)) - { - // Trying to render the layout on the component from the context - $value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => $parts[0], 'client' => 0)); - } - - if ($value == '') - { - // Trying to render the layout on Fields itself - $value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => 'com_fields','client' => 0)); - } - - return $value; - } - - /** - * PrepareForm - * - * @param string $context The context of the content passed to the helper - * @param Form $form form - * @param object $data data. - * - * @return boolean - * - * @since 3.7.0 - */ - public static function prepareForm($context, Form $form, $data) - { - // Extracting the component and section - $parts = self::extract($context); - - if (! $parts) - { - return true; - } - - $context = $parts[0] . '.' . $parts[1]; - - // When no fields available return here - $fields = self::getFields($parts[0] . '.' . $parts[1], new CMSObject); - - if (! $fields) - { - return true; - } - - $component = $parts[0]; - $section = $parts[1]; - - $assignedCatids = $data->catid ?? $data->fieldscatid ?? $form->getValue('catid'); - - // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category - $assignedCatids = is_array($assignedCatids) - ? (int) reset($assignedCatids) - : (int) $assignedCatids; - - if (!$assignedCatids && $formField = $form->getField('catid')) - { - $assignedCatids = $formField->getAttribute('default', null); - - if (!$assignedCatids) - { - // Choose the first category available - $catOptions = $formField->options; - - if ($catOptions && !empty($catOptions[0]->value)) - { - $assignedCatids = (int) $catOptions[0]->value; - } - } - - $data->fieldscatid = $assignedCatids; - } - - /* - * If there is a catid field we need to reload the page when the catid - * is changed - */ - if ($form->getField('catid') && $parts[0] != 'com_fields') - { - /* - * Setting some parameters for the category field - */ - $form->setFieldAttribute('catid', 'refresh-enabled', true); - $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); - $form->setFieldAttribute('catid', 'refresh-section', $section); - } - - // Getting the fields - $fields = self::getFields($parts[0] . '.' . $parts[1], $data); - - if (!$fields) - { - return true; - } - - $fieldTypes = self::getFieldTypes(); - - // Creating the dom - $xml = new \DOMDocument('1.0', 'UTF-8'); - $fieldsNode = $xml->appendChild(new \DOMElement('form'))->appendChild(new \DOMElement('fields')); - $fieldsNode->setAttribute('name', 'com_fields'); - - // Organizing the fields according to their group - $fieldsPerGroup = array(0 => array()); - - foreach ($fields as $field) - { - if (!array_key_exists($field->type, $fieldTypes)) - { - // Field type is not available - continue; - } - - if (!array_key_exists($field->group_id, $fieldsPerGroup)) - { - $fieldsPerGroup[$field->group_id] = array(); - } - - if ($path = $fieldTypes[$field->type]['path']) - { - // Add the lookup path for the field - FormHelper::addFieldPath($path); - } - - if ($path = $fieldTypes[$field->type]['rules']) - { - // Add the lookup path for the rule - FormHelper::addRulePath($path); - } - - $fieldsPerGroup[$field->group_id][] = $field; - } - - $model = Factory::getApplication()->bootComponent('com_fields') - ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]); - $model->setState('filter.context', $context); - - /** - * $model->getItems() would only return existing groups, but we also - * have the 'default' group with id 0 which is not in the database, - * so we create it virtually here. - */ - $defaultGroup = new \stdClass; - $defaultGroup->id = 0; - $defaultGroup->title = ''; - $defaultGroup->description = ''; - $iterateGroups = array_merge(array($defaultGroup), $model->getItems()); - - // Looping through the groups - foreach ($iterateGroups as $group) - { - if (empty($fieldsPerGroup[$group->id])) - { - continue; - } - - // Defining the field set - /** @var \DOMElement $fieldset */ - $fieldset = $fieldsNode->appendChild(new \DOMElement('fieldset')); - $fieldset->setAttribute('name', 'fields-' . $group->id); - $fieldset->setAttribute('addfieldpath', '/administrator/components/' . $component . '/models/fields'); - $fieldset->setAttribute('addrulepath', '/administrator/components/' . $component . '/models/rules'); - - $label = $group->title; - $description = $group->description; - - if (!$label) - { - $key = strtoupper($component . '_FIELDS_' . $section . '_LABEL'); - - if (!Factory::getLanguage()->hasKey($key)) - { - $key = 'JGLOBAL_FIELDS'; - } - - $label = $key; - } - - if (!$description) - { - $key = strtoupper($component . '_FIELDS_' . $section . '_DESC'); - - if (Factory::getLanguage()->hasKey($key)) - { - $description = $key; - } - } - - $fieldset->setAttribute('label', $label); - $fieldset->setAttribute('description', strip_tags($description)); - - // Looping through the fields for that context - foreach ($fieldsPerGroup[$group->id] as $field) - { - try - { - Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($field, $fieldset, $form)); - - /* - * If the field belongs to an assigned_cat_id but the assigned_cat_ids in the data - * is not known, set the required flag to false on any circumstance. - */ - if (!$assignedCatids && !empty($field->assigned_cat_ids) && $form->getField($field->name)) - { - $form->setFieldAttribute($field->name, 'required', 'false'); - } - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - // When the field set is empty, then remove it - if (!$fieldset->hasChildNodes()) - { - $fieldsNode->removeChild($fieldset); - } - } - - // Loading the XML fields string into the form - $form->load($xml->saveXML()); - - $model = Factory::getApplication()->bootComponent('com_fields') - ->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); - - if ((!isset($data->id) || !$data->id) && Factory::getApplication()->input->getCmd('controller') == 'modules' - && Factory::getApplication()->isClient('site')) - { - // Modules on front end editing don't have data and an id set - $data->id = Factory::getApplication()->input->getInt('id'); - } - - // Looping through the fields again to set the value - if (!isset($data->id) || !$data->id) - { - return true; - } - - foreach ($fields as $field) - { - $value = $model->getFieldValue($field->id, $data->id); - - if ($value === null) - { - continue; - } - - if (!is_array($value) && $value !== '') - { - // Function getField doesn't cache the fields, so we try to do it only when necessary - $formField = $form->getField($field->name, 'com_fields'); - - if ($formField && $formField->forceMultiple) - { - $value = (array) $value; - } - } - - // Setting the value on the field - $form->setValue($field->name, 'com_fields', $value); - } - - return true; - } - - /** - * Return a boolean if the actual logged in user can edit the given field value. - * - * @param \stdClass $field The field - * - * @return boolean - * - * @since 3.7.0 - */ - public static function canEditFieldValue($field) - { - $parts = self::extract($field->context); - - return Factory::getUser()->authorise('core.edit.value', $parts[0] . '.field.' . (int) $field->id); - } - - /** - * Return a boolean based on field (and field group) display / show_on settings - * - * @param \stdClass $field The field - * - * @return boolean - * - * @since 3.8.7 - */ - public static function displayFieldOnForm($field) - { - $app = Factory::getApplication(); - - // Detect if the field should be shown at all - if ($field->params->get('show_on') == 1 && $app->isClient('administrator')) - { - return false; - } - elseif ($field->params->get('show_on') == 2 && $app->isClient('site')) - { - return false; - } - - if (!self::canEditFieldValue($field)) - { - $fieldDisplayReadOnly = $field->params->get('display_readonly', '2'); - - if ($fieldDisplayReadOnly == '2') - { - // Inherit from field group display read-only setting - $groupModel = $app->bootComponent('com_fields') - ->getMVCFactory()->createModel('Group', 'Administrator', ['ignore_request' => true]); - $groupDisplayReadOnly = $groupModel->getItem($field->group_id)->params->get('display_readonly', '1'); - $fieldDisplayReadOnly = $groupDisplayReadOnly; - } - - if ($fieldDisplayReadOnly == '0') - { - // Do not display field on form when field is read-only - return false; - } - } - - // Display field on form - return true; - } - - /** - * Gets assigned categories ids for a field - * - * @param \stdClass[] $fieldId The field ID - * - * @return array Array with the assigned category ids - * - * @since 4.0.0 - */ - public static function getAssignedCategoriesIds($fieldId) - { - $fieldId = (int) $fieldId; - - if (!$fieldId) - { - return array(); - } - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - $query->select($db->quoteName('a.category_id')) - ->from($db->quoteName('#__fields_categories', 'a')) - ->where('a.field_id = ' . $fieldId); - - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Gets assigned categories titles for a field - * - * @param \stdClass[] $fieldId The field ID - * - * @return array Array with the assigned categories - * - * @since 3.7.0 - */ - public static function getAssignedCategoriesTitles($fieldId) - { - $fieldId = (int) $fieldId; - - if (!$fieldId) - { - return []; - } - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - $query->select($db->quoteName('c.title')) - ->from($db->quoteName('#__fields_categories', 'a')) - ->join('INNER', $db->quoteName('#__categories', 'c') . ' ON a.category_id = c.id') - ->where($db->quoteName('field_id') . ' = :fieldid') - ->bind(':fieldid', $fieldId, ParameterType::INTEGER); - - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Gets the fields system plugin extension id. - * - * @return integer The fields system plugin extension id. - * - * @since 3.7.0 - */ - public static function getFieldsPluginId() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('fields')); - $db->setQuery($query); - - try - { - $result = (int) $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - $result = 0; - } - - return $result; - } - - /** - * Loads the fields plugins and returns an array of field types from the plugins. - * - * The returned array contains arrays with the following keys: - * - label: The label of the field - * - type: The type of the field - * - path: The path of the folder where the field can be found - * - * @return array - * - * @since 3.7.0 - */ - public static function getFieldTypes() - { - PluginHelper::importPlugin('fields'); - $eventData = Factory::getApplication()->triggerEvent('onCustomFieldsGetTypes'); - - $data = array(); - - foreach ($eventData as $fields) - { - foreach ($fields as $fieldDescription) - { - if (!array_key_exists('path', $fieldDescription)) - { - $fieldDescription['path'] = null; - } - - if (!array_key_exists('rules', $fieldDescription)) - { - $fieldDescription['rules'] = null; - } - - $data[$fieldDescription['type']] = $fieldDescription; - } - } - - return $data; - } - - /** - * Clears the internal cache for the custom fields. - * - * @return void - * - * @since 3.8.0 - */ - public static function clearFieldsCache() - { - self::$fieldCache = null; - self::$fieldsCache = null; - } + /** + * @var FieldsModel + */ + private static $fieldsCache = null; + + /** + * @var FieldsModel + */ + private static $fieldCache = null; + + /** + * Extracts the component and section from the context string which has to + * be in the format component.context. + * + * @param string $contextString contextString + * @param object $item optional item object + * + * @return array|null + * + * @since 3.7.0 + */ + public static function extract($contextString, $item = null) + { + $parts = explode('.', $contextString, 2); + + if (count($parts) < 2) { + return null; + } + + $newSection = ''; + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof FieldsServiceInterface) { + $newSection = $component->validateSection($parts[1], $item); + } + + if ($newSection) { + $parts[1] = $newSection; + } + + return $parts; + } + + /** + * Returns the fields for the given context. + * If the item is an object the returned fields do have an additional field + * "value" which represents the value for the given item. If the item has an + * assigned_cat_ids field, then additionally fields which belong to that + * category will be returned. + * Should the value being prepared to be shown in an HTML context then + * prepareValue must be set to true. No further escaping needs to be done. + * The values of the fields can be overridden by an associative array where the keys + * have to be a name and its corresponding value. + * + * @param string $context The context of the content passed to the helper + * @param null $item The item being edited in the form + * @param int|bool $prepareValue (if int is display event): 1 - AfterTitle, 2 - BeforeDisplay, 3 - AfterDisplay, 0 - OFF + * @param array|null $valuesToOverride The values to override + * @param bool $includeSubformFields Should I include fields marked as Only Use In Subform? + * + * @return array + * + * @throws \Exception + * @since 3.7.0 + */ + public static function getFields( + $context, + $item = null, + $prepareValue = false, + array $valuesToOverride = null, + bool $includeSubformFields = false + ) { + if (self::$fieldsCache === null) { + // Load the model + self::$fieldsCache = Factory::getApplication()->bootComponent('com_fields') + ->getMVCFactory()->createModel('Fields', 'Administrator', ['ignore_request' => true]); + + self::$fieldsCache->setState('filter.state', 1); + self::$fieldsCache->setState('list.limit', 0); + } + + if ($includeSubformFields) { + self::$fieldsCache->setState('filter.only_use_in_subform', ''); + } else { + self::$fieldsCache->setState('filter.only_use_in_subform', 0); + } + + if (is_array($item)) { + $item = (object) $item; + } + + if (Multilanguage::isEnabled() && isset($item->language) && $item->language != '*') { + self::$fieldsCache->setState('filter.language', array('*', $item->language)); + } + + self::$fieldsCache->setState('filter.context', $context); + self::$fieldsCache->setState('filter.assigned_cat_ids', array()); + + /* + * If item has assigned_cat_ids parameter display only fields which + * belong to the category + */ + if ($item && (isset($item->catid) || isset($item->fieldscatid))) { + $assignedCatIds = $item->catid ?? $item->fieldscatid; + + if (!is_array($assignedCatIds)) { + $assignedCatIds = explode(',', $assignedCatIds); + } + + // Fields without any category assigned should show as well + $assignedCatIds[] = 0; + + self::$fieldsCache->setState('filter.assigned_cat_ids', $assignedCatIds); + } + + $fields = self::$fieldsCache->getItems(); + + if ($fields === false) { + return array(); + } + + if ($item && isset($item->id)) { + if (self::$fieldCache === null) { + self::$fieldCache = Factory::getApplication()->bootComponent('com_fields') + ->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); + } + + $fieldIds = array_map( + function ($f) { + return $f->id; + }, + $fields + ); + + $fieldValues = self::$fieldCache->getFieldValues($fieldIds, $item->id); + + $new = array(); + + foreach ($fields as $key => $original) { + /* + * Doing a clone, otherwise fields for different items will + * always reference to the same object + */ + $field = clone $original; + + if ($valuesToOverride && array_key_exists($field->name, $valuesToOverride)) { + $field->value = $valuesToOverride[$field->name]; + } elseif ($valuesToOverride && array_key_exists($field->id, $valuesToOverride)) { + $field->value = $valuesToOverride[$field->id]; + } elseif (array_key_exists($field->id, $fieldValues)) { + $field->value = $fieldValues[$field->id]; + } + + if (!isset($field->value) || $field->value === '') { + $field->value = $field->default_value; + } + + $field->rawvalue = $field->value; + + // If boolean prepare, if int, it is the event type: 1 - After Title, 2 - Before Display Content, 3 - After Display Content, 0 - Do not prepare + if ($prepareValue && (is_bool($prepareValue) || $prepareValue === (int) $field->params->get('display', '2'))) { + PluginHelper::importPlugin('fields'); + + /* + * On before field prepare + * Event allow plugins to modify the output of the field before it is prepared + */ + Factory::getApplication()->triggerEvent('onCustomFieldsBeforePrepareField', array($context, $item, &$field)); + + // Gathering the value for the field + $value = Factory::getApplication()->triggerEvent('onCustomFieldsPrepareField', array($context, $item, &$field)); + + if (is_array($value)) { + $value = implode(' ', $value); + } + + /* + * On after field render + * Event allows plugins to modify the output of the prepared field + */ + Factory::getApplication()->triggerEvent('onCustomFieldsAfterPrepareField', array($context, $item, $field, &$value)); + + // Assign the value + $field->value = $value; + } + + $new[$key] = $field; + } + + $fields = $new; + } + + return $fields; + } + + /** + * Renders the layout file and data on the context and does a fall back to + * Fields afterwards. + * + * @param string $context The context of the content passed to the helper + * @param string $layoutFile layoutFile + * @param array $displayData displayData + * + * @return NULL|string + * + * @since 3.7.0 + */ + public static function render($context, $layoutFile, $displayData) + { + $value = ''; + + /* + * Because the layout refreshes the paths before the render function is + * called, so there is no way to load the layout overrides in the order + * template -> context -> fields. + * If there is no override in the context then we need to call the + * layout from Fields. + */ + if ($parts = self::extract($context)) { + // Trying to render the layout on the component from the context + $value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => $parts[0], 'client' => 0)); + } + + if ($value == '') { + // Trying to render the layout on Fields itself + $value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => 'com_fields','client' => 0)); + } + + return $value; + } + + /** + * PrepareForm + * + * @param string $context The context of the content passed to the helper + * @param Form $form form + * @param object $data data. + * + * @return boolean + * + * @since 3.7.0 + */ + public static function prepareForm($context, Form $form, $data) + { + // Extracting the component and section + $parts = self::extract($context); + + if (! $parts) { + return true; + } + + $context = $parts[0] . '.' . $parts[1]; + + // When no fields available return here + $fields = self::getFields($parts[0] . '.' . $parts[1], new CMSObject()); + + if (! $fields) { + return true; + } + + $component = $parts[0]; + $section = $parts[1]; + + $assignedCatids = $data->catid ?? $data->fieldscatid ?? $form->getValue('catid'); + + // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category + $assignedCatids = is_array($assignedCatids) + ? (int) reset($assignedCatids) + : (int) $assignedCatids; + + if (!$assignedCatids && $formField = $form->getField('catid')) { + $assignedCatids = $formField->getAttribute('default', null); + + if (!$assignedCatids) { + // Choose the first category available + $catOptions = $formField->options; + + if ($catOptions && !empty($catOptions[0]->value)) { + $assignedCatids = (int) $catOptions[0]->value; + } + } + + $data->fieldscatid = $assignedCatids; + } + + /* + * If there is a catid field we need to reload the page when the catid + * is changed + */ + if ($form->getField('catid') && $parts[0] != 'com_fields') { + /* + * Setting some parameters for the category field + */ + $form->setFieldAttribute('catid', 'refresh-enabled', true); + $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); + $form->setFieldAttribute('catid', 'refresh-section', $section); + } + + // Getting the fields + $fields = self::getFields($parts[0] . '.' . $parts[1], $data); + + if (!$fields) { + return true; + } + + $fieldTypes = self::getFieldTypes(); + + // Creating the dom + $xml = new \DOMDocument('1.0', 'UTF-8'); + $fieldsNode = $xml->appendChild(new \DOMElement('form'))->appendChild(new \DOMElement('fields')); + $fieldsNode->setAttribute('name', 'com_fields'); + + // Organizing the fields according to their group + $fieldsPerGroup = array(0 => array()); + + foreach ($fields as $field) { + if (!array_key_exists($field->type, $fieldTypes)) { + // Field type is not available + continue; + } + + if (!array_key_exists($field->group_id, $fieldsPerGroup)) { + $fieldsPerGroup[$field->group_id] = array(); + } + + if ($path = $fieldTypes[$field->type]['path']) { + // Add the lookup path for the field + FormHelper::addFieldPath($path); + } + + if ($path = $fieldTypes[$field->type]['rules']) { + // Add the lookup path for the rule + FormHelper::addRulePath($path); + } + + $fieldsPerGroup[$field->group_id][] = $field; + } + + $model = Factory::getApplication()->bootComponent('com_fields') + ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]); + $model->setState('filter.context', $context); + + /** + * $model->getItems() would only return existing groups, but we also + * have the 'default' group with id 0 which is not in the database, + * so we create it virtually here. + */ + $defaultGroup = new \stdClass(); + $defaultGroup->id = 0; + $defaultGroup->title = ''; + $defaultGroup->description = ''; + $iterateGroups = array_merge(array($defaultGroup), $model->getItems()); + + // Looping through the groups + foreach ($iterateGroups as $group) { + if (empty($fieldsPerGroup[$group->id])) { + continue; + } + + // Defining the field set + /** @var \DOMElement $fieldset */ + $fieldset = $fieldsNode->appendChild(new \DOMElement('fieldset')); + $fieldset->setAttribute('name', 'fields-' . $group->id); + $fieldset->setAttribute('addfieldpath', '/administrator/components/' . $component . '/models/fields'); + $fieldset->setAttribute('addrulepath', '/administrator/components/' . $component . '/models/rules'); + + $label = $group->title; + $description = $group->description; + + if (!$label) { + $key = strtoupper($component . '_FIELDS_' . $section . '_LABEL'); + + if (!Factory::getLanguage()->hasKey($key)) { + $key = 'JGLOBAL_FIELDS'; + } + + $label = $key; + } + + if (!$description) { + $key = strtoupper($component . '_FIELDS_' . $section . '_DESC'); + + if (Factory::getLanguage()->hasKey($key)) { + $description = $key; + } + } + + $fieldset->setAttribute('label', $label); + $fieldset->setAttribute('description', strip_tags($description)); + + // Looping through the fields for that context + foreach ($fieldsPerGroup[$group->id] as $field) { + try { + Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($field, $fieldset, $form)); + + /* + * If the field belongs to an assigned_cat_id but the assigned_cat_ids in the data + * is not known, set the required flag to false on any circumstance. + */ + if (!$assignedCatids && !empty($field->assigned_cat_ids) && $form->getField($field->name)) { + $form->setFieldAttribute($field->name, 'required', 'false'); + } + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + // When the field set is empty, then remove it + if (!$fieldset->hasChildNodes()) { + $fieldsNode->removeChild($fieldset); + } + } + + // Loading the XML fields string into the form + $form->load($xml->saveXML()); + + $model = Factory::getApplication()->bootComponent('com_fields') + ->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); + + if ( + (!isset($data->id) || !$data->id) && Factory::getApplication()->input->getCmd('controller') == 'modules' + && Factory::getApplication()->isClient('site') + ) { + // Modules on front end editing don't have data and an id set + $data->id = Factory::getApplication()->input->getInt('id'); + } + + // Looping through the fields again to set the value + if (!isset($data->id) || !$data->id) { + return true; + } + + foreach ($fields as $field) { + $value = $model->getFieldValue($field->id, $data->id); + + if ($value === null) { + continue; + } + + if (!is_array($value) && $value !== '') { + // Function getField doesn't cache the fields, so we try to do it only when necessary + $formField = $form->getField($field->name, 'com_fields'); + + if ($formField && $formField->forceMultiple) { + $value = (array) $value; + } + } + + // Setting the value on the field + $form->setValue($field->name, 'com_fields', $value); + } + + return true; + } + + /** + * Return a boolean if the actual logged in user can edit the given field value. + * + * @param \stdClass $field The field + * + * @return boolean + * + * @since 3.7.0 + */ + public static function canEditFieldValue($field) + { + $parts = self::extract($field->context); + + return Factory::getUser()->authorise('core.edit.value', $parts[0] . '.field.' . (int) $field->id); + } + + /** + * Return a boolean based on field (and field group) display / show_on settings + * + * @param \stdClass $field The field + * + * @return boolean + * + * @since 3.8.7 + */ + public static function displayFieldOnForm($field) + { + $app = Factory::getApplication(); + + // Detect if the field should be shown at all + if ($field->params->get('show_on') == 1 && $app->isClient('administrator')) { + return false; + } elseif ($field->params->get('show_on') == 2 && $app->isClient('site')) { + return false; + } + + if (!self::canEditFieldValue($field)) { + $fieldDisplayReadOnly = $field->params->get('display_readonly', '2'); + + if ($fieldDisplayReadOnly == '2') { + // Inherit from field group display read-only setting + $groupModel = $app->bootComponent('com_fields') + ->getMVCFactory()->createModel('Group', 'Administrator', ['ignore_request' => true]); + $groupDisplayReadOnly = $groupModel->getItem($field->group_id)->params->get('display_readonly', '1'); + $fieldDisplayReadOnly = $groupDisplayReadOnly; + } + + if ($fieldDisplayReadOnly == '0') { + // Do not display field on form when field is read-only + return false; + } + } + + // Display field on form + return true; + } + + /** + * Gets assigned categories ids for a field + * + * @param \stdClass[] $fieldId The field ID + * + * @return array Array with the assigned category ids + * + * @since 4.0.0 + */ + public static function getAssignedCategoriesIds($fieldId) + { + $fieldId = (int) $fieldId; + + if (!$fieldId) { + return array(); + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('a.category_id')) + ->from($db->quoteName('#__fields_categories', 'a')) + ->where('a.field_id = ' . $fieldId); + + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Gets assigned categories titles for a field + * + * @param \stdClass[] $fieldId The field ID + * + * @return array Array with the assigned categories + * + * @since 3.7.0 + */ + public static function getAssignedCategoriesTitles($fieldId) + { + $fieldId = (int) $fieldId; + + if (!$fieldId) { + return []; + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('c.title')) + ->from($db->quoteName('#__fields_categories', 'a')) + ->join('INNER', $db->quoteName('#__categories', 'c') . ' ON a.category_id = c.id') + ->where($db->quoteName('field_id') . ' = :fieldid') + ->bind(':fieldid', $fieldId, ParameterType::INTEGER); + + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Gets the fields system plugin extension id. + * + * @return integer The fields system plugin extension id. + * + * @since 3.7.0 + */ + public static function getFieldsPluginId() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('fields')); + $db->setQuery($query); + + try { + $result = (int) $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + $result = 0; + } + + return $result; + } + + /** + * Loads the fields plugins and returns an array of field types from the plugins. + * + * The returned array contains arrays with the following keys: + * - label: The label of the field + * - type: The type of the field + * - path: The path of the folder where the field can be found + * + * @return array + * + * @since 3.7.0 + */ + public static function getFieldTypes() + { + PluginHelper::importPlugin('fields'); + $eventData = Factory::getApplication()->triggerEvent('onCustomFieldsGetTypes'); + + $data = array(); + + foreach ($eventData as $fields) { + foreach ($fields as $fieldDescription) { + if (!array_key_exists('path', $fieldDescription)) { + $fieldDescription['path'] = null; + } + + if (!array_key_exists('rules', $fieldDescription)) { + $fieldDescription['rules'] = null; + } + + $data[$fieldDescription['type']] = $fieldDescription; + } + } + + return $data; + } + + /** + * Clears the internal cache for the custom fields. + * + * @return void + * + * @since 3.8.0 + */ + public static function clearFieldsCache() + { + self::$fieldCache = null; + self::$fieldsCache = null; + } } diff --git a/code/administrator/components/com_fields/src/Model/FieldModel.php b/code/administrator/components/com_fields/src/Model/FieldModel.php index 53633531..2e8c4244 100644 --- a/code/administrator/components/com_fields/src/Model/FieldModel.php +++ b/code/administrator/components/com_fields/src/Model/FieldModel.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage' - ); - - /** - * @var array - * - * @since 3.7.0 - */ - private $valueCache = array(); - - /** - * Constructor - * - * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). - * @param MVCFactoryInterface $factory The factory. - * - * @since 3.7.0 - * @throws \Exception - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null) - { - parent::__construct($config, $factory); - - $this->typeAlias = Factory::getApplication()->input->getCmd('context', 'com_content.article') . '.field'; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 3.7.0 - */ - public function save($data) - { - $field = null; - - if (isset($data['id']) && $data['id']) - { - $field = $this->getItem($data['id']); - } - - if (!isset($data['label']) && isset($data['params']['label'])) - { - $data['label'] = $data['params']['label']; - - unset($data['params']['label']); - } - - // Alter the title for save as copy - $input = Factory::getApplication()->input; - - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['title'] == $origTable->title) - { - list($title, $name) = $this->generateNewTitle($data['group_id'], $data['name'], $data['title']); - $data['title'] = $title; - $data['label'] = $title; - $data['name'] = $name; - } - else - { - if ($data['name'] == $origTable->name) - { - $data['name'] = ''; - } - } - - $data['state'] = 0; - } - - // Load the fields plugins, perhaps they want to do something - PluginHelper::importPlugin('fields'); - - $message = $this->checkDefaultValue($data); - - if ($message !== true) - { - $this->setError($message); - - return false; - } - - if (!parent::save($data)) - { - return false; - } - - // Save the assigned categories into #__fields_categories - $db = $this->getDbo(); - $id = (int) $this->getState('field.id'); - - /** - * If the field is only used in subform, set Category to None automatically so that it will only be displayed - * as part of SubForm on add/edit item screen - */ - if (!empty($data['only_use_in_subform'])) - { - $cats = [-1]; - } - else - { - $cats = isset($data['assigned_cat_ids']) ? (array) $data['assigned_cat_ids'] : array(); - $cats = ArrayHelper::toInteger($cats); - } - - $assignedCatIds = array(); - - foreach ($cats as $cat) - { - // If we have found the 'JNONE' category, remove all other from the result and break. - if ($cat == '-1') - { - $assignedCatIds = array('-1'); - break; - } - - if ($cat) - { - $assignedCatIds[] = $cat; - } - } - - // First delete all assigned categories - $query = $db->getQuery(true); - $query->delete('#__fields_categories') - ->where($db->quoteName('field_id') . ' = :fieldid') - ->bind(':fieldid', $id, ParameterType::INTEGER); - - $db->setQuery($query); - $db->execute(); - - // Inset new assigned categories - $tupel = new \stdClass; - $tupel->field_id = $id; - - foreach ($assignedCatIds as $catId) - { - $tupel->category_id = $catId; - $db->insertObject('#__fields_categories', $tupel); - } - - /** - * If the options have changed, delete the values. This should only apply for list, checkboxes and radio - * custom field types, because when their options are being changed, their values might get invalid, because - * e.g. there is a value selected from a list, which is not part of the list anymore. Hence we need to delete - * all values that are not part of the options anymore. Note: The only field types with fieldparams+options - * are those above listed plus the subfields type. And we do explicitly not want the values to be deleted - * when the options of a subfields field are getting changed. - */ - if ($field && in_array($field->type, array('list', 'checkboxes', 'radio'), true) - && isset($data['fieldparams']['options']) && isset($field->fieldparams['options'])) - { - $oldParams = $this->getParams($field->fieldparams['options']); - $newParams = $this->getParams($data['fieldparams']['options']); - - if (is_object($oldParams) && is_object($newParams) && $oldParams != $newParams) - { - // Get new values. - $names = array_column((array) $newParams, 'value'); - - $fieldId = (int) $field->id; - $query = $db->getQuery(true); - $query->delete($db->quoteName('#__fields_values')) - ->where($db->quoteName('field_id') . ' = :fieldid') - ->bind(':fieldid', $fieldId, ParameterType::INTEGER); - - // If new values are set, delete only old values. Otherwise delete all values. - if ($names) - { - $query->whereNotIn($db->quoteName('value'), $names, ParameterType::STRING); - } - - $db->setQuery($query); - $db->execute(); - } - } - - FieldsHelper::clearFieldsCache(); - - return true; - } - - - /** - * Checks if the default value is valid for the given data. If a string is returned then - * it can be assumed that the default value is invalid. - * - * @param array $data The data. - * - * @return true|string true if valid, a string containing the exception message when not. - * - * @since 3.7.0 - */ - private function checkDefaultValue($data) - { - // Empty default values are correct - if (empty($data['default_value']) && $data['default_value'] !== '0') - { - return true; - } - - $types = FieldsHelper::getFieldTypes(); - - // Check if type exists - if (!array_key_exists($data['type'], $types)) - { - return true; - } - - $path = $types[$data['type']]['rules']; - - // Add the path for the rules of the plugin when available - if ($path) - { - // Add the lookup path for the rule - FormHelper::addRulePath($path); - } - - // Create the fields object - $obj = (object) $data; - $obj->params = new Registry($obj->params); - $obj->fieldparams = new Registry(!empty($obj->fieldparams) ? $obj->fieldparams : array()); - - // Prepare the dom - $dom = new \DOMDocument; - $node = $dom->appendChild(new \DOMElement('form')); - - // Trigger the event to create the field dom node - Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($obj, $node, new Form($data['context']))); - - // Check if a node is created - if (!$node->firstChild) - { - return true; - } - - // Define the type either from the field or from the data - $type = $node->firstChild->getAttribute('validate') ? : $data['type']; - - // Load the rule - $rule = FormHelper::loadRuleType($type); - - // When no rule exists, we allow the default value - if (!$rule) - { - return true; - } - - try - { - // Perform the check - $result = $rule->test(simplexml_import_dom($node->firstChild), $data['default_value']); - - // Check if the test succeeded - return $result === true ? : Text::_('COM_FIELDS_FIELD_INVALID_DEFAULT_VALUE'); - } - catch (\UnexpectedValueException $e) - { - return $e->getMessage(); - } - } - - /** - * Converts the unknown params into an object. - * - * @param mixed $params The params. - * - * @return \stdClass Object on success, false on failure. - * - * @since 3.7.0 - */ - private function getParams($params) - { - if (is_string($params)) - { - $params = json_decode($params); - } - - if (is_array($params)) - { - $params = (object) $params; - } - - return $params; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 3.7.0 - */ - public function getItem($pk = null) - { - $result = parent::getItem($pk); - - if ($result) - { - // Prime required properties. - if (empty($result->id)) - { - $result->context = Factory::getApplication()->input->getCmd('context', $this->getState('field.context')); - } - - if (property_exists($result, 'fieldparams') && $result->fieldparams !== null) - { - $registry = new Registry; - - if ($result->fieldparams) - { - $registry->loadString($result->fieldparams); - } - - $result->fieldparams = $registry->toArray(); - } - - $db = $this->getDbo(); - $query = $db->getQuery(true); - $fieldId = (int) $result->id; - $query->select($db->quoteName('category_id')) - ->from($db->quoteName('#__fields_categories')) - ->where($db->quoteName('field_id') . ' = :fieldid') - ->bind(':fieldid', $fieldId, ParameterType::INTEGER); - - $db->setQuery($query); - $result->assigned_cat_ids = $db->loadColumn() ?: array(0); - } - - return $result; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.7.0 - * @throws \Exception - */ - public function getTable($name = 'Field', $prefix = 'Administrator', $options = array()) - { - // Default to text type - $table = parent::getTable($name, $prefix, $options); - $table->type = 'text'; - - return $table; - } - - /** - * Method to change the title & name. - * - * @param integer $categoryId The id of the category. - * @param string $name The name. - * @param string $title The title. - * - * @return array Contains the modified title and name. - * - * @since 3.7.0 - */ - protected function generateNewTitle($categoryId, $name, $title) - { - // Alter the title & name - $table = $this->getTable(); - - while ($table->load(array('name' => $name))) - { - $title = StringHelper::increment($title); - $name = StringHelper::increment($name, 'dash'); - } - - return array( - $title, - $name, - ); - } - - /** - * Method to delete one or more records. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 3.7.0 - */ - public function delete(&$pks) - { - $success = parent::delete($pks); - - if ($success) - { - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - $pks = array_filter($pks); - - if (!empty($pks)) - { - // Delete Values - $query = $this->getDbo()->getQuery(true); - - $query->delete($query->quoteName('#__fields_values')) - ->whereIn($query->quoteName('field_id'), $pks); - - $this->getDbo()->setQuery($query)->execute(); - - // Delete Assigned Categories - $query = $this->getDbo()->getQuery(true); - - $query->delete($query->quoteName('#__fields_categories')) - ->whereIn($query->quoteName('field_id'), $pks); - - $this->getDbo()->setQuery($query)->execute(); - } - } - - return $success; - } - - /** - * Abstract method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 3.7.0 - */ - public function getForm($data = array(), $loadData = true) - { - $context = $this->getState('field.context'); - $jinput = Factory::getApplication()->input; - - // A workaround to get the context into the model for save requests. - if (empty($context) && isset($data['context'])) - { - $context = $data['context']; - $parts = FieldsHelper::extract($context); - - $this->setState('field.context', $context); - - if ($parts) - { - $this->setState('field.component', $parts[0]); - $this->setState('field.section', $parts[1]); - } - } - - if (isset($data['type'])) - { - // This is needed that the plugins can determine the type - $this->setState('field.type', $data['type']); - } - - // Load the fields plugin that they can add additional parameters to the form - PluginHelper::importPlugin('fields'); - - // Get the form. - $form = $this->loadForm( - 'com_fields.field.' . $context, 'field', - array( - 'control' => 'jform', - 'load_data' => true, - ) - ); - - if (empty($form)) - { - return false; - } - - // Modify the form based on Edit State access controls. - if (empty($data['context'])) - { - $data['context'] = $context; - } - - $fieldId = $jinput->get('id'); - $assetKey = $this->state->get('field.component') . '.field.' . $fieldId; - - if (!Factory::getUser()->authorise('core.edit.state', $assetKey)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('state', 'disabled', 'true'); - - // Disable fields while saving. The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('state', 'filter', 'unset'); - } - - // Don't allow to change the created_user_id user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_user_id', 'filter', 'unset'); - } - - // In case we are editing a field, field type cannot be changed, so some extra handling below is needed - if ($fieldId) - { - $fieldType = $form->getField('type'); - - if ($fieldType->value == 'subform') - { - // Only Use In subform should not be available for subform field type, so we remove it - $form->removeField('only_use_in_subform'); - } - else - { - // Field type could not be changed, so remove showon attribute to avoid js errors - $form->setFieldAttribute('only_use_in_subform', 'showon', ''); - } - } - - return $form; - } - - /** - * Setting the value for the given field id, context and item id. - * - * @param string $fieldId The field ID. - * @param string $itemId The ID of the item. - * @param string $value The value. - * - * @return boolean - * - * @since 3.7.0 - */ - public function setFieldValue($fieldId, $itemId, $value) - { - $field = $this->getItem($fieldId); - $params = $field->params; - - if (is_array($params)) - { - $params = new Registry($params); - } - - // Don't save the value when the user is not authorized to change it - if (!$field || !FieldsHelper::canEditFieldValue($field)) - { - return false; - } - - $needsDelete = false; - $needsInsert = false; - $needsUpdate = false; - - $oldValue = $this->getFieldValue($fieldId, $itemId); - $value = (array) $value; - - if ($oldValue === null) - { - // No records available, doing normal insert - $needsInsert = true; - } - elseif (count($value) == 1 && count((array) $oldValue) == 1) - { - // Only a single row value update can be done when not empty - $needsUpdate = is_array($value[0]) ? count($value[0]) : strlen($value[0]); - $needsDelete = !$needsUpdate; - } - else - { - // Multiple values, we need to purge the data and do a new - // insert - $needsDelete = true; - $needsInsert = true; - } - - if ($needsDelete) - { - $fieldId = (int) $fieldId; - - // Deleting the existing record as it is a reset - $query = $this->getDbo()->getQuery(true); - - $query->delete($query->quoteName('#__fields_values')) - ->where($query->quoteName('field_id') . ' = :fieldid') - ->where($query->quoteName('item_id') . ' = :itemid') - ->bind(':fieldid', $fieldId, ParameterType::INTEGER) - ->bind(':itemid', $itemId); - - $this->getDbo()->setQuery($query)->execute(); - } - - if ($needsInsert) - { - $newObj = new \stdClass; - - $newObj->field_id = (int) $fieldId; - $newObj->item_id = $itemId; - - foreach ($value as $v) - { - $newObj->value = $v; - - $this->getDbo()->insertObject('#__fields_values', $newObj); - } - } - - if ($needsUpdate) - { - $updateObj = new \stdClass; - - $updateObj->field_id = (int) $fieldId; - $updateObj->item_id = $itemId; - $updateObj->value = reset($value); - - $this->getDbo()->updateObject('#__fields_values', $updateObj, array('field_id', 'item_id')); - } - - $this->valueCache = array(); - FieldsHelper::clearFieldsCache(); - - return true; - } - - /** - * Returning the value for the given field id, context and item id. - * - * @param string $fieldId The field ID. - * @param string $itemId The ID of the item. - * - * @return NULL|string - * - * @since 3.7.0 - */ - public function getFieldValue($fieldId, $itemId) - { - $values = $this->getFieldValues(array($fieldId), $itemId); - - if (array_key_exists($fieldId, $values)) - { - return $values[$fieldId]; - } - - return null; - } - - /** - * Returning the values for the given field ids, context and item id. - * - * @param array $fieldIds The field Ids. - * @param string $itemId The ID of the item. - * - * @return NULL|array - * - * @since 3.7.0 - */ - public function getFieldValues(array $fieldIds, $itemId) - { - if (!$fieldIds) - { - return array(); - } - - // Create a unique key for the cache - $key = md5(serialize($fieldIds) . $itemId); - - // Fill the cache when it doesn't exist - if (!array_key_exists($key, $this->valueCache)) - { - // Create the query - $query = $this->getDbo()->getQuery(true); - - $query->select($query->quoteName(['field_id', 'value'])) - ->from($query->quoteName('#__fields_values')) - ->whereIn($query->quoteName('field_id'), ArrayHelper::toInteger($fieldIds)) - ->where($query->quoteName('item_id') . ' = :itemid') - ->bind(':itemid', $itemId); - - // Fetch the row from the database - $rows = $this->getDbo()->setQuery($query)->loadObjectList(); - - $data = array(); - - // Fill the data container from the database rows - foreach ($rows as $row) - { - // If there are multiple values for a field, create an array - if (array_key_exists($row->field_id, $data)) - { - // Transform it to an array - if (!is_array($data[$row->field_id])) - { - $data[$row->field_id] = array($data[$row->field_id]); - } - - // Set the value in the array - $data[$row->field_id][] = $row->value; - - // Go to the next row, otherwise the value gets overwritten in the data container - continue; - } - - // Set the value - $data[$row->field_id] = $row->value; - } - - // Assign it to the internal cache - $this->valueCache[$key] = $data; - } - - // Return the value from the cache - return $this->valueCache[$key]; - } - - /** - * Cleaning up the values for the given item on the context. - * - * @param string $context The context. - * @param string $itemId The Item ID. - * - * @return void - * - * @since 3.7.0 - */ - public function cleanupValues($context, $itemId) - { - // Delete with inner join is not possible so we need to do a subquery - $fieldsQuery = $this->getDbo()->getQuery(true); - $fieldsQuery->select($fieldsQuery->quoteName('id')) - ->from($fieldsQuery->quoteName('#__fields')) - ->where($fieldsQuery->quoteName('context') . ' = :context'); - - $query = $this->getDbo()->getQuery(true); - - $query->delete($query->quoteName('#__fields_values')) - ->where($query->quoteName('field_id') . ' IN (' . $fieldsQuery . ')') - ->where($query->quoteName('item_id') . ' = :itemid') - ->bind(':itemid', $itemId) - ->bind(':context', $context); - - $this->getDbo()->setQuery($query)->execute(); - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 3.7.0 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->state != -2) - { - return false; - } - - $parts = FieldsHelper::extract($record->context); - - return Factory::getUser()->authorise('core.delete', $parts[0] . '.field.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the - * component. - * - * @since 3.7.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - $parts = FieldsHelper::extract($record->context); - - // Check for existing field. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $parts[0] . '.field.' . (int) $record->id); - } - - return $user->authorise('core.edit.state', $parts[0]); - } - - /** - * Stock method to auto-populate the model state. - * - * @return void - * - * @since 3.7.0 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState($this->getName() . '.id', $pk); - - $context = $app->input->get('context', 'com_content.article'); - $this->setState('field.context', $context); - $parts = FieldsHelper::extract($context); - - // Extract the component name - $this->setState('field.component', $parts[0]); - - // Extract the optional section name - $this->setState('field.section', (count($parts) > 1) ? $parts[1] : null); - - // Load the parameters. - $params = ComponentHelper::getParams('com_fields'); - $this->setState('params', $params); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param Table $table A Table object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 3.7.0 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('context') . ' = ' . $this->_db->quote($table->context), - ]; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 3.7.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_fields.edit.field.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Language, Access) in edit form - // if those have been selected in Category Manager - if (!$data->id) - { - // Check for which context the Category Manager is used and - // get selected fields - $filters = (array) $app->getUserState('com_fields.fields.filter'); - - $data->set('state', $app->input->getInt('state', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null))); - $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); - $data->set('group_id', $app->input->getString('group_id', (!empty($filters['group_id']) ? $filters['group_id'] : null))); - $data->set( - 'access', - $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) - ); - - // Set the type if available from the request - $data->set('type', $app->input->getWord('type', $this->state->get('field.type', $data->get('type')))); - } - - if ($data->label && !isset($data->params['label'])) - { - $data->params['label'] = $data->label; - } - } - - $this->preprocessData('com_fields.field', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see JFormRule - * @see JFilterInput - * @since 3.9.23 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_fields')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 3.7.0 - * - * @throws \Exception if there is an error in the form event. - * - * @see \Joomla\CMS\Form\FormField - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $component = $this->state->get('field.component'); - $section = $this->state->get('field.section'); - $dataObject = $data; - - if (is_array($dataObject)) - { - $dataObject = (object) $dataObject; - } - - if (isset($dataObject->type)) - { - $form->setFieldAttribute('type', 'component', $component); - - // Not allowed to change the type of an existing record - if ($dataObject->id) - { - $form->setFieldAttribute('type', 'readonly', 'true'); - } - - // Allow to override the default value label and description through the plugin - $key = 'PLG_FIELDS_' . strtoupper($dataObject->type) . '_DEFAULT_VALUE_LABEL'; - - if (Factory::getLanguage()->hasKey($key)) - { - $form->setFieldAttribute('default_value', 'label', $key); - } - - $key = 'PLG_FIELDS_' . strtoupper($dataObject->type) . '_DEFAULT_VALUE_DESC'; - - if (Factory::getLanguage()->hasKey($key)) - { - $form->setFieldAttribute('default_value', 'description', $key); - } - - // Remove placeholder field on list fields - if ($dataObject->type == 'list') - { - $form->removeField('hint', 'params'); - } - } - - // Get the categories for this component (and optionally this section, if available) - $cat = ( - function () use ($component, $section) { - // Get the CategoryService for this component - $componentObject = $this->bootComponent($component); - - if (!$componentObject instanceof CategoryServiceInterface) - { - // No CategoryService -> no categories - return null; - } - - $cat = null; - - // Try to get the categories for this component and section - try - { - $cat = $componentObject->getCategory([], $section ?: ''); - } - catch (SectionNotFoundException $e) - { - // Not found for component and section -> Now try once more without the section, so only component - try - { - $cat = $componentObject->getCategory(); - } - catch (SectionNotFoundException $e) - { - // If we haven't found it now, return (no categories available for this component) - return null; - } - } - - // So we found categories for at least the component, return them - return $cat; - } - )(); - - // If we found categories, and if the root category has children, set them in the form - if ($cat && $cat->get('root')->hasChildren()) - { - $form->setFieldAttribute('assigned_cat_ids', 'extension', $cat->getExtension()); - } - else - { - // Else remove the field from the form - $form->removeField('assigned_cat_ids'); - } - - $form->setFieldAttribute('type', 'component', $component); - $form->setFieldAttribute('group_id', 'context', $this->state->get('field.context')); - $form->setFieldAttribute('rules', 'component', $component); - - // Looking in the component forms folder for a specific section forms file - $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/forms/fields/' . $section . '.xml'); - - if (!file_exists($path)) - { - // Looking in the component models/forms folder for a specific section forms file - $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/fields/' . $section . '.xml'); - } - - if (file_exists($path)) - { - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE); - $lang->load($component, JPATH_BASE . '/components/' . $component); - - if (!$form->loadFile($path, false)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Clean the cache - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 3.7.0 - */ - protected function cleanCache($group = null, $clientId = 0) - { - $context = Factory::getApplication()->input->get('context'); - - switch ($context) - { - case 'com_content': - parent::cleanCache('com_content'); - parent::cleanCache('mod_articles_archive'); - parent::cleanCache('mod_articles_categories'); - parent::cleanCache('mod_articles_category'); - parent::cleanCache('mod_articles_latest'); - parent::cleanCache('mod_articles_news'); - parent::cleanCache('mod_articles_popular'); - break; - default: - parent::cleanCache($context); - break; - } - } - - /** - * Batch copy fields to a new group. - * - * @param integer $value The new value matching a fields group. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return array|boolean new IDs if successful, false otherwise and internal error is set. - * - * @since 3.7.0 - */ - protected function batchCopy($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - $table = $this->getTable(); - $newIds = array(); - $component = $this->state->get('filter.component'); - $value = (int) $value; - - foreach ($pks as $pk) - { - if ($user->authorise('core.create', $component . '.fieldgroup.' . $value)) - { - $table->reset(); - $table->load($pk); - - $table->group_id = $value; - - // Reset the ID because we are making a copy - $table->id = 0; - - // Unpublish the new field - $table->state = 0; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Get the new item ID - $newId = $table->get('id'); - - // Add the new ID to the array - $newIds[$pk] = $newId; - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return $newIds; - } - - /** - * Batch move fields to a new group. - * - * @param integer $value The new value matching a fields group. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 3.7.0 - */ - protected function batchMove($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - $table = $this->getTable(); - $context = explode('.', Factory::getApplication()->getUserState('com_fields.fields.context')); - $value = (int) $value; - - foreach ($pks as $pk) - { - if ($user->authorise('core.edit', $context[0] . '.fieldgroup.' . $value)) - { - $table->reset(); - $table->load($pk); - - $table->group_id = $value; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } + /** + * @var null|string + * + * @since 3.7.0 + */ + public $typeAlias = null; + + /** + * @var string + * + * @since 3.7.0 + */ + protected $text_prefix = 'COM_FIELDS'; + + /** + * Batch copy/move command. If set to false, + * the batch copy/move command is not supported + * + * @var string + * @since 3.4 + */ + protected $batch_copymove = 'group_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage' + ); + + /** + * @var array + * + * @since 3.7.0 + */ + private $valueCache = array(); + + /** + * Constructor + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * + * @since 3.7.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + parent::__construct($config, $factory); + + $this->typeAlias = Factory::getApplication()->input->getCmd('context', 'com_content.article') . '.field'; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 3.7.0 + */ + public function save($data) + { + $field = null; + + if (isset($data['id']) && $data['id']) { + $field = $this->getItem($data['id']); + } + + if (!isset($data['label']) && isset($data['params']['label'])) { + $data['label'] = $data['params']['label']; + + unset($data['params']['label']); + } + + // Alter the title for save as copy + $input = Factory::getApplication()->input; + + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['title'] == $origTable->title) { + list($title, $name) = $this->generateNewTitle($data['group_id'], $data['name'], $data['title']); + $data['title'] = $title; + $data['label'] = $title; + $data['name'] = $name; + } else { + if ($data['name'] == $origTable->name) { + $data['name'] = ''; + } + } + + $data['state'] = 0; + } + + // Load the fields plugins, perhaps they want to do something + PluginHelper::importPlugin('fields'); + + $message = $this->checkDefaultValue($data); + + if ($message !== true) { + $this->setError($message); + + return false; + } + + if (!parent::save($data)) { + return false; + } + + // Save the assigned categories into #__fields_categories + $db = $this->getDatabase(); + $id = (int) $this->getState('field.id'); + + /** + * If the field is only used in subform, set Category to None automatically so that it will only be displayed + * as part of SubForm on add/edit item screen + */ + if (!empty($data['only_use_in_subform'])) { + $cats = [-1]; + } else { + $cats = isset($data['assigned_cat_ids']) ? (array) $data['assigned_cat_ids'] : array(); + $cats = ArrayHelper::toInteger($cats); + } + + $assignedCatIds = array(); + + foreach ($cats as $cat) { + // If we have found the 'JNONE' category, remove all other from the result and break. + if ($cat == '-1') { + $assignedCatIds = array('-1'); + break; + } + + if ($cat) { + $assignedCatIds[] = $cat; + } + } + + // First delete all assigned categories + $query = $db->getQuery(true); + $query->delete('#__fields_categories') + ->where($db->quoteName('field_id') . ' = :fieldid') + ->bind(':fieldid', $id, ParameterType::INTEGER); + + $db->setQuery($query); + $db->execute(); + + // Inset new assigned categories + $tuple = new \stdClass(); + $tuple->field_id = $id; + + foreach ($assignedCatIds as $catId) { + $tuple->category_id = $catId; + $db->insertObject('#__fields_categories', $tuple); + } + + /** + * If the options have changed, delete the values. This should only apply for list, checkboxes and radio + * custom field types, because when their options are being changed, their values might get invalid, because + * e.g. there is a value selected from a list, which is not part of the list anymore. Hence we need to delete + * all values that are not part of the options anymore. Note: The only field types with fieldparams+options + * are those above listed plus the subfields type. And we do explicitly not want the values to be deleted + * when the options of a subfields field are getting changed. + */ + if ( + $field && in_array($field->type, array('list', 'checkboxes', 'radio'), true) + && isset($data['fieldparams']['options']) && isset($field->fieldparams['options']) + ) { + $oldParams = $this->getParams($field->fieldparams['options']); + $newParams = $this->getParams($data['fieldparams']['options']); + + if (is_object($oldParams) && is_object($newParams) && $oldParams != $newParams) { + // Get new values. + $names = array_column((array) $newParams, 'value'); + + $fieldId = (int) $field->id; + $query = $db->getQuery(true); + $query->delete($db->quoteName('#__fields_values')) + ->where($db->quoteName('field_id') . ' = :fieldid') + ->bind(':fieldid', $fieldId, ParameterType::INTEGER); + + // If new values are set, delete only old values. Otherwise delete all values. + if ($names) { + $query->whereNotIn($db->quoteName('value'), $names, ParameterType::STRING); + } + + $db->setQuery($query); + $db->execute(); + } + } + + FieldsHelper::clearFieldsCache(); + + return true; + } + + + /** + * Checks if the default value is valid for the given data. If a string is returned then + * it can be assumed that the default value is invalid. + * + * @param array $data The data. + * + * @return true|string true if valid, a string containing the exception message when not. + * + * @since 3.7.0 + */ + private function checkDefaultValue($data) + { + // Empty default values are correct + if (empty($data['default_value']) && $data['default_value'] !== '0') { + return true; + } + + $types = FieldsHelper::getFieldTypes(); + + // Check if type exists + if (!array_key_exists($data['type'], $types)) { + return true; + } + + $path = $types[$data['type']]['rules']; + + // Add the path for the rules of the plugin when available + if ($path) { + // Add the lookup path for the rule + FormHelper::addRulePath($path); + } + + // Create the fields object + $obj = (object) $data; + $obj->params = new Registry($obj->params); + $obj->fieldparams = new Registry(!empty($obj->fieldparams) ? $obj->fieldparams : array()); + + // Prepare the dom + $dom = new \DOMDocument(); + $node = $dom->appendChild(new \DOMElement('form')); + + // Trigger the event to create the field dom node + $form = new Form($data['context']); + $form->setDatabase($this->getDatabase()); + Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($obj, $node, $form)); + + // Check if a node is created + if (!$node->firstChild) { + return true; + } + + // Define the type either from the field or from the data + $type = $node->firstChild->getAttribute('validate') ? : $data['type']; + + // Load the rule + $rule = FormHelper::loadRuleType($type); + + // When no rule exists, we allow the default value + if (!$rule) { + return true; + } + + if ($rule instanceof DatabaseAwareInterface) { + try { + $rule->setDatabase($this->getDatabase()); + } catch (DatabaseNotFoundException $e) { + @trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED); + $rule->setDatabase(Factory::getContainer()->get(DatabaseInterface::class)); + } + } + + try { + // Perform the check + $result = $rule->test(simplexml_import_dom($node->firstChild), $data['default_value']); + + // Check if the test succeeded + return $result === true ? : Text::_('COM_FIELDS_FIELD_INVALID_DEFAULT_VALUE'); + } catch (\UnexpectedValueException $e) { + return $e->getMessage(); + } + } + + /** + * Converts the unknown params into an object. + * + * @param mixed $params The params. + * + * @return \stdClass Object on success, false on failure. + * + * @since 3.7.0 + */ + private function getParams($params) + { + if (is_string($params)) { + $params = json_decode($params); + } + + if (is_array($params)) { + $params = (object) $params; + } + + return $params; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 3.7.0 + */ + public function getItem($pk = null) + { + $result = parent::getItem($pk); + + if ($result) { + // Prime required properties. + if (empty($result->id)) { + $result->context = Factory::getApplication()->input->getCmd('context', $this->getState('field.context')); + } + + if (property_exists($result, 'fieldparams') && $result->fieldparams !== null) { + $registry = new Registry(); + + if ($result->fieldparams) { + $registry->loadString($result->fieldparams); + } + + $result->fieldparams = $registry->toArray(); + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $fieldId = (int) $result->id; + $query->select($db->quoteName('category_id')) + ->from($db->quoteName('#__fields_categories')) + ->where($db->quoteName('field_id') . ' = :fieldid') + ->bind(':fieldid', $fieldId, ParameterType::INTEGER); + + $db->setQuery($query); + $result->assigned_cat_ids = $db->loadColumn() ?: array(0); + } + + return $result; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.7.0 + * @throws \Exception + */ + public function getTable($name = 'Field', $prefix = 'Administrator', $options = array()) + { + // Default to text type + $table = parent::getTable($name, $prefix, $options); + $table->type = 'text'; + + return $table; + } + + /** + * Method to change the title & name. + * + * @param integer $categoryId The id of the category. + * @param string $name The name. + * @param string $title The title. + * + * @return array Contains the modified title and name. + * + * @since 3.7.0 + */ + protected function generateNewTitle($categoryId, $name, $title) + { + // Alter the title & name + $table = $this->getTable(); + + while ($table->load(array('name' => $name))) { + $title = StringHelper::increment($title); + $name = StringHelper::increment($name, 'dash'); + } + + return array( + $title, + $name, + ); + } + + /** + * Method to delete one or more records. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 3.7.0 + */ + public function delete(&$pks) + { + $success = parent::delete($pks); + + if ($success) { + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + $pks = array_filter($pks); + + if (!empty($pks)) { + // Delete Values + $query = $this->getDatabase()->getQuery(true); + + $query->delete($query->quoteName('#__fields_values')) + ->whereIn($query->quoteName('field_id'), $pks); + + $this->getDatabase()->setQuery($query)->execute(); + + // Delete Assigned Categories + $query = $this->getDatabase()->getQuery(true); + + $query->delete($query->quoteName('#__fields_categories')) + ->whereIn($query->quoteName('field_id'), $pks); + + $this->getDatabase()->setQuery($query)->execute(); + } + } + + return $success; + } + + /** + * Abstract method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 3.7.0 + */ + public function getForm($data = array(), $loadData = true) + { + $context = $this->getState('field.context'); + $jinput = Factory::getApplication()->input; + + // A workaround to get the context into the model for save requests. + if (empty($context) && isset($data['context'])) { + $context = $data['context']; + $parts = FieldsHelper::extract($context); + + $this->setState('field.context', $context); + + if ($parts) { + $this->setState('field.component', $parts[0]); + $this->setState('field.section', $parts[1]); + } + } + + if (isset($data['type'])) { + // This is needed that the plugins can determine the type + $this->setState('field.type', $data['type']); + } + + // Load the fields plugin that they can add additional parameters to the form + PluginHelper::importPlugin('fields'); + + // Get the form. + $form = $this->loadForm( + 'com_fields.field.' . $context, + 'field', + array( + 'control' => 'jform', + 'load_data' => true, + ) + ); + + if (empty($form)) { + return false; + } + + // Modify the form based on Edit State access controls. + if (empty($data['context'])) { + $data['context'] = $context; + } + + $fieldId = $jinput->get('id'); + $assetKey = $this->state->get('field.component') . '.field.' . $fieldId; + + if (!Factory::getUser()->authorise('core.edit.state', $assetKey)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + + // Disable fields while saving. The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + // Don't allow to change the created_user_id user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_user_id', 'filter', 'unset'); + } + + // In case we are editing a field, field type cannot be changed, so some extra handling below is needed + if ($fieldId) { + $fieldType = $form->getField('type'); + + if ($fieldType->value == 'subform') { + // Only Use In subform should not be available for subform field type, so we remove it + $form->removeField('only_use_in_subform'); + } else { + // Field type could not be changed, so remove showon attribute to avoid js errors + $form->setFieldAttribute('only_use_in_subform', 'showon', ''); + } + } + + return $form; + } + + /** + * Setting the value for the given field id, context and item id. + * + * @param string $fieldId The field ID. + * @param string $itemId The ID of the item. + * @param string $value The value. + * + * @return boolean + * + * @since 3.7.0 + */ + public function setFieldValue($fieldId, $itemId, $value) + { + $field = $this->getItem($fieldId); + $params = $field->params; + + if (is_array($params)) { + $params = new Registry($params); + } + + // Don't save the value when the user is not authorized to change it + if (!$field || !FieldsHelper::canEditFieldValue($field)) { + return false; + } + + $needsDelete = false; + $needsInsert = false; + $needsUpdate = false; + + $oldValue = $this->getFieldValue($fieldId, $itemId); + $value = (array) $value; + + if ($oldValue === null) { + // No records available, doing normal insert + $needsInsert = true; + } elseif (count($value) == 1 && count((array) $oldValue) == 1) { + // Only a single row value update can be done when not empty + $needsUpdate = is_array($value[0]) ? count($value[0]) : strlen($value[0]); + $needsDelete = !$needsUpdate; + } else { + // Multiple values, we need to purge the data and do a new + // insert + $needsDelete = true; + $needsInsert = true; + } + + if ($needsDelete) { + $fieldId = (int) $fieldId; + + // Deleting the existing record as it is a reset + $query = $this->getDatabase()->getQuery(true); + + $query->delete($query->quoteName('#__fields_values')) + ->where($query->quoteName('field_id') . ' = :fieldid') + ->where($query->quoteName('item_id') . ' = :itemid') + ->bind(':fieldid', $fieldId, ParameterType::INTEGER) + ->bind(':itemid', $itemId); + + $this->getDatabase()->setQuery($query)->execute(); + } + + if ($needsInsert) { + $newObj = new \stdClass(); + + $newObj->field_id = (int) $fieldId; + $newObj->item_id = $itemId; + + foreach ($value as $v) { + $newObj->value = $v; + + $this->getDatabase()->insertObject('#__fields_values', $newObj); + } + } + + if ($needsUpdate) { + $updateObj = new \stdClass(); + + $updateObj->field_id = (int) $fieldId; + $updateObj->item_id = $itemId; + $updateObj->value = reset($value); + + $this->getDatabase()->updateObject('#__fields_values', $updateObj, array('field_id', 'item_id')); + } + + $this->valueCache = array(); + FieldsHelper::clearFieldsCache(); + + return true; + } + + /** + * Returning the value for the given field id, context and item id. + * + * @param string $fieldId The field ID. + * @param string $itemId The ID of the item. + * + * @return NULL|string + * + * @since 3.7.0 + */ + public function getFieldValue($fieldId, $itemId) + { + $values = $this->getFieldValues(array($fieldId), $itemId); + + if (array_key_exists($fieldId, $values)) { + return $values[$fieldId]; + } + + return null; + } + + /** + * Returning the values for the given field ids, context and item id. + * + * @param array $fieldIds The field Ids. + * @param string $itemId The ID of the item. + * + * @return NULL|array + * + * @since 3.7.0 + */ + public function getFieldValues(array $fieldIds, $itemId) + { + if (!$fieldIds) { + return array(); + } + + // Create a unique key for the cache + $key = md5(serialize($fieldIds) . $itemId); + + // Fill the cache when it doesn't exist + if (!array_key_exists($key, $this->valueCache)) { + // Create the query + $query = $this->getDatabase()->getQuery(true); + + $query->select($query->quoteName(['field_id', 'value'])) + ->from($query->quoteName('#__fields_values')) + ->whereIn($query->quoteName('field_id'), ArrayHelper::toInteger($fieldIds)) + ->where($query->quoteName('item_id') . ' = :itemid') + ->bind(':itemid', $itemId); + + // Fetch the row from the database + $rows = $this->getDatabase()->setQuery($query)->loadObjectList(); + + $data = array(); + + // Fill the data container from the database rows + foreach ($rows as $row) { + // If there are multiple values for a field, create an array + if (array_key_exists($row->field_id, $data)) { + // Transform it to an array + if (!is_array($data[$row->field_id])) { + $data[$row->field_id] = array($data[$row->field_id]); + } + + // Set the value in the array + $data[$row->field_id][] = $row->value; + + // Go to the next row, otherwise the value gets overwritten in the data container + continue; + } + + // Set the value + $data[$row->field_id] = $row->value; + } + + // Assign it to the internal cache + $this->valueCache[$key] = $data; + } + + // Return the value from the cache + return $this->valueCache[$key]; + } + + /** + * Cleaning up the values for the given item on the context. + * + * @param string $context The context. + * @param string $itemId The Item ID. + * + * @return void + * + * @since 3.7.0 + */ + public function cleanupValues($context, $itemId) + { + // Delete with inner join is not possible so we need to do a subquery + $fieldsQuery = $this->getDatabase()->getQuery(true); + $fieldsQuery->select($fieldsQuery->quoteName('id')) + ->from($fieldsQuery->quoteName('#__fields')) + ->where($fieldsQuery->quoteName('context') . ' = :context'); + + $query = $this->getDatabase()->getQuery(true); + + $query->delete($query->quoteName('#__fields_values')) + ->where($query->quoteName('field_id') . ' IN (' . $fieldsQuery . ')') + ->where($query->quoteName('item_id') . ' = :itemid') + ->bind(':itemid', $itemId) + ->bind(':context', $context); + + $this->getDatabase()->setQuery($query)->execute(); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 3.7.0 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->state != -2) { + return false; + } + + $parts = FieldsHelper::extract($record->context); + + return Factory::getUser()->authorise('core.delete', $parts[0] . '.field.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the + * component. + * + * @since 3.7.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + $parts = FieldsHelper::extract($record->context); + + // Check for existing field. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $parts[0] . '.field.' . (int) $record->id); + } + + return $user->authorise('core.edit.state', $parts[0]); + } + + /** + * Stock method to auto-populate the model state. + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState($this->getName() . '.id', $pk); + + $context = $app->input->get('context', 'com_content.article'); + $this->setState('field.context', $context); + $parts = FieldsHelper::extract($context); + + // Extract the component name + $this->setState('field.component', $parts[0]); + + // Extract the optional section name + $this->setState('field.section', (count($parts) > 1) ? $parts[1] : null); + + // Load the parameters. + $params = ComponentHelper::getParams('com_fields'); + $this->setState('params', $params); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param Table $table A Table object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 3.7.0 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('context') . ' = ' . $db->quote($table->context), + ]; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 3.7.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_fields.edit.field.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Language, Access) in edit form + // if those have been selected in Category Manager + if (!$data->id) { + // Check for which context the Category Manager is used and + // get selected fields + $filters = (array) $app->getUserState('com_fields.fields.filter'); + + $data->set('state', $app->input->getInt('state', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null))); + $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); + $data->set('group_id', $app->input->getString('group_id', (!empty($filters['group_id']) ? $filters['group_id'] : null))); + $data->set( + 'access', + $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) + ); + + // Set the type if available from the request + $data->set('type', $app->input->getWord('type', $this->state->get('field.type', $data->get('type')))); + } + + if ($data->label && !isset($data->params['label'])) { + $data->params['label'] = $data->label; + } + } + + $this->preprocessData('com_fields.field', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see JFormRule + * @see JFilterInput + * @since 3.9.23 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_fields')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 3.7.0 + * + * @throws \Exception if there is an error in the form event. + * + * @see \Joomla\CMS\Form\FormField + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $component = $this->state->get('field.component'); + $section = $this->state->get('field.section'); + $dataObject = $data; + + if (is_array($dataObject)) { + $dataObject = (object) $dataObject; + } + + if (isset($dataObject->type)) { + $form->setFieldAttribute('type', 'component', $component); + + // Not allowed to change the type of an existing record + if ($dataObject->id) { + $form->setFieldAttribute('type', 'readonly', 'true'); + } + + // Allow to override the default value label and description through the plugin + $key = 'PLG_FIELDS_' . strtoupper($dataObject->type) . '_DEFAULT_VALUE_LABEL'; + + if (Factory::getLanguage()->hasKey($key)) { + $form->setFieldAttribute('default_value', 'label', $key); + } + + $key = 'PLG_FIELDS_' . strtoupper($dataObject->type) . '_DEFAULT_VALUE_DESC'; + + if (Factory::getLanguage()->hasKey($key)) { + $form->setFieldAttribute('default_value', 'description', $key); + } + + // Remove placeholder field on list fields + if ($dataObject->type == 'list') { + $form->removeField('hint', 'params'); + } + } + + // Get the categories for this component (and optionally this section, if available) + $cat = ( + function () use ($component, $section) { + // Get the CategoryService for this component + $componentObject = $this->bootComponent($component); + + if (!$componentObject instanceof CategoryServiceInterface) { + // No CategoryService -> no categories + return null; + } + + $cat = null; + + // Try to get the categories for this component and section + try { + $cat = $componentObject->getCategory([], $section ?: ''); + } catch (SectionNotFoundException $e) { + // Not found for component and section -> Now try once more without the section, so only component + try { + $cat = $componentObject->getCategory(); + } catch (SectionNotFoundException $e) { + // If we haven't found it now, return (no categories available for this component) + return null; + } + } + + // So we found categories for at least the component, return them + return $cat; + } + )(); + + // If we found categories, and if the root category has children, set them in the form + if ($cat && $cat->get('root')->hasChildren()) { + $form->setFieldAttribute('assigned_cat_ids', 'extension', $cat->getExtension()); + } else { + // Else remove the field from the form + $form->removeField('assigned_cat_ids'); + } + + $form->setFieldAttribute('type', 'component', $component); + $form->setFieldAttribute('group_id', 'context', $this->state->get('field.context')); + $form->setFieldAttribute('rules', 'component', $component); + + // Looking in the component forms folder for a specific section forms file + $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/forms/fields/' . $section . '.xml'); + + if (!file_exists($path)) { + // Looking in the component models/forms folder for a specific section forms file + $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/fields/' . $section . '.xml'); + } + + if (file_exists($path)) { + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE); + $lang->load($component, JPATH_BASE . '/components/' . $component); + + if (!$form->loadFile($path, false)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Clean the cache + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 3.7.0 + */ + protected function cleanCache($group = null, $clientId = 0) + { + $context = Factory::getApplication()->input->get('context'); + + switch ($context) { + case 'com_content': + parent::cleanCache('com_content'); + parent::cleanCache('mod_articles_archive'); + parent::cleanCache('mod_articles_categories'); + parent::cleanCache('mod_articles_category'); + parent::cleanCache('mod_articles_latest'); + parent::cleanCache('mod_articles_news'); + parent::cleanCache('mod_articles_popular'); + break; + default: + parent::cleanCache($context); + break; + } + } + + /** + * Batch copy fields to a new group. + * + * @param integer $value The new value matching a fields group. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return array|boolean new IDs if successful, false otherwise and internal error is set. + * + * @since 3.7.0 + */ + protected function batchCopy($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + $table = $this->getTable(); + $newIds = array(); + $component = $this->state->get('filter.component'); + $value = (int) $value; + + foreach ($pks as $pk) { + if ($user->authorise('core.create', $component . '.fieldgroup.' . $value)) { + $table->reset(); + $table->load($pk); + + $table->group_id = $value; + + // Reset the ID because we are making a copy + $table->id = 0; + + // Alter the title if necessary + $data = $this->generateNewTitle(0, $table->name, $table->title); + $table->title = $data['0']; + $table->name = $data['1']; + $table->label = $data['0']; + + // Unpublish the new field + $table->state = 0; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Get the new item ID + $newId = $table->get('id'); + + // Add the new ID to the array + $newIds[$pk] = $newId; + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return $newIds; + } + + /** + * Batch move fields to a new group. + * + * @param integer $value The new value matching a fields group. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 3.7.0 + */ + protected function batchMove($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + $table = $this->getTable(); + $context = explode('.', Factory::getApplication()->getUserState('com_fields.fields.context')); + $value = (int) $value; + + foreach ($pks as $pk) { + if ($user->authorise('core.edit', $context[0] . '.fieldgroup.' . $value)) { + $table->reset(); + $table->load($pk); + + $table->group_id = $value; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } } diff --git a/code/administrator/components/com_fields/src/Model/FieldsModel.php b/code/administrator/components/com_fields/src/Model/FieldsModel.php index b1ff50ae..a895dfcb 100644 --- a/code/administrator/components/com_fields/src/Model/FieldsModel.php +++ b/code/administrator/components/com_fields/src/Model/FieldsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.context', 'context', 'com_content.article', 'CMD'); - $this->setState('filter.context', $context); - - // Split context into component and optional section - $parts = FieldsHelper::extract($context); - - if ($parts) - { - $this->setState('filter.component', $parts[0]); - $this->setState('filter.section', $parts[1]); - } - } - - /** - * Method to get a store id based on the model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. - * - * @return string A store id. - * - * @since 3.7.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.context'); - $id .= ':' . serialize($this->getState('filter.assigned_cat_ids')); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.group_id'); - $id .= ':' . serialize($this->getState('filter.language')); - $id .= ':' . $this->getState('filter.only_use_in_subform'); - - return parent::getStoreId($id); - } - - /** - * Method to get a DatabaseQuery object for retrieving the data set from a database. - * - * @return \Joomla\Database\DatabaseQuery A DatabaseQuery object to retrieve the data set. - * - * @since 3.7.0 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - $app = Factory::getApplication(); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'DISTINCT a.id, a.title, a.name, a.checked_out, a.checked_out_time, a.note' . - ', a.state, a.access, a.created_time, a.created_user_id, a.ordering, a.language' . - ', a.fieldparams, a.params, a.type, a.default_value, a.context, a.group_id' . - ', a.label, a.description, a.required, a.only_use_in_subform' - ) - ); - $query->from('#__fields AS a'); - - // Join over the language - $query->select('l.title AS language_title, l.image AS language_image') - ->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language'); - - // Join over the users for the checked out user. - $query->select('uc.name AS editor')->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); - - // Join over the asset groups. - $query->select('ag.title AS access_level')->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); - - // Join over the users for the author. - $query->select('ua.name AS author_name')->join('LEFT', '#__users AS ua ON ua.id = a.created_user_id'); - - // Join over the field groups. - $query->select('g.title AS group_title, g.access as group_access, g.state AS group_state, g.note as group_note'); - $query->join('LEFT', '#__fields_groups AS g ON g.id = a.group_id'); - - // Filter by context - if ($context = $this->getState('filter.context')) - { - $query->where($db->quoteName('a.context') . ' = :context') - ->bind(':context', $context); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - if (is_array($access)) - { - $access = ArrayHelper::toInteger($access); - $query->whereIn($db->quoteName('a.access'), $access); - } - else - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - } - - if (($categories = $this->getState('filter.assigned_cat_ids')) && $context) - { - $categories = (array) $categories; - $categories = ArrayHelper::toInteger($categories); - $parts = FieldsHelper::extract($context); - - if ($parts) - { - // Get the categories for this component (and optionally this section, if available) - $cat = ( - function () use ($parts) { - // Get the CategoryService for this component - $componentObject = $this->bootComponent($parts[0]); - - if (!$componentObject instanceof CategoryServiceInterface) - { - // No CategoryService -> no categories - return null; - } - - $cat = null; - - // Try to get the categories for this component and section - try - { - $cat = $componentObject->getCategory([], $parts[1] ?: ''); - } - catch (SectionNotFoundException $e) - { - // Not found for component and section -> Now try once more without the section, so only component - try - { - $cat = $componentObject->getCategory(); - } - catch (SectionNotFoundException $e) - { - // If we haven't found it now, return (no categories available for this component) - return null; - } - } - - // So we found categories for at least the component, return them - return $cat; - } - )(); - - if ($cat) - { - foreach ($categories as $assignedCatIds) - { - // Check if we have the actual category - $parent = $cat->get($assignedCatIds); - - if ($parent) - { - $categories[] = (int) $parent->id; - - // Traverse the tree up to get all the fields which are attached to a parent - while ($parent->getParent() && $parent->getParent()->id != 'root') - { - $parent = $parent->getParent(); - $categories[] = (int) $parent->id; - } - } - } - } - } - - $categories = array_unique($categories); - - // Join over the assigned categories - $query->join('LEFT', $db->quoteName('#__fields_categories') . ' AS fc ON fc.field_id = a.id'); - - if (in_array('0', $categories)) - { - $query->where( - '(' . - $db->quoteName('fc.category_id') . ' IS NULL OR ' . - $db->quoteName('fc.category_id') . ' IN (' . implode(',', $query->bindArray(array_values($categories), ParameterType::INTEGER)) . ')' . - ')' - ); - } - else - { - $query->whereIn($db->quoteName('fc.category_id'), $categories); - } - } - - // Implement View Level Access - if (!$app->isClient('administrator') || !$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.group_id') . ' = 0', - $db->quoteName('g.access') . ' IN (' . implode(',', $query->bindArray($groups, ParameterType::INTEGER)) . ')' - ], - 'OR' - ); - } - - // Filter by state - $state = $this->getState('filter.state'); - - // Include group state only when not on on back end list - $includeGroupState = !$app->isClient('administrator') || - $app->input->get('option') != 'com_fields' || - $app->input->get('view') != 'fields'; - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - - if ($includeGroupState) - { - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.group_id') . ' = 0', - $db->quoteName('g.state') . ' = :gstate', - ], - 'OR' - ) - ->bind(':gstate', $state, ParameterType::INTEGER); - } - } - elseif (!$state) - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - - if ($includeGroupState) - { - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.group_id') . ' = 0', - $db->quoteName('g.state') . ' IN (' . implode(',', $query->bindArray([0, 1], ParameterType::INTEGER)) . ')' - ], - 'OR' - ); - } - } - - $groupId = $this->getState('filter.group_id'); - - if (is_numeric($groupId)) - { - $groupId = (int) $groupId; - $query->where($db->quoteName('a.group_id') . ' = :groupid') - ->bind(':groupid', $groupId, ParameterType::INTEGER); - } - - $onlyUseInSubForm = $this->getState('filter.only_use_in_subform'); - - if (is_numeric($onlyUseInSubForm)) - { - $onlyUseInSubForm = (int) $onlyUseInSubForm; - $query->where($db->quoteName('a.only_use_in_subform') . ' = :only_use_in_subform') - ->bind(':only_use_in_subform', $onlyUseInSubForm, ParameterType::INTEGER); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $search, ParameterType::INTEGER); - } - elseif (stripos($search, 'author:') === 0) - { - $search = '%' . substr($search, 7) . '%'; - $query->where( - '(' . - $db->quoteName('ua.name') . ' LIKE :name OR ' . - $db->quoteName('ua.username') . ' LIKE :username' . - ')' - ) - ->bind(':name', $search) - ->bind(':username', $search); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where( - '(' . - $db->quoteName('a.title') . ' LIKE :title OR ' . - $db->quoteName('a.name') . ' LIKE :sname OR ' . - $db->quoteName('a.note') . ' LIKE :note' . - ')' - ) - ->bind(':title', $search) - ->bind(':sname', $search) - ->bind(':note', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $language = (array) $language; - - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - - // Add the list ordering clause - $listOrdering = $this->state->get('list.ordering', 'a.ordering'); - $orderDirn = $this->state->get('list.direction', 'ASC'); - - $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); - - return $query; - } - - /** - * Gets an array of objects from the results of database query. - * - * @param string $query The query. - * @param integer $limitstart Offset. - * @param integer $limit The number of records. - * - * @return array An array of results. - * - * @since 3.7.0 - * @throws \RuntimeException - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $result = parent::_getList($query, $limitstart, $limit); - - if (is_array($result)) - { - foreach ($result as $field) - { - $field->fieldparams = new Registry($field->fieldparams); - $field->params = new Registry($field->params); - } - } - - return $result; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return \Joomla\CMS\Form\Form|bool the Form object or false - * - * @since 3.7.0 - */ - public function getFilterForm($data = array(), $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - - if ($form) - { - $form->setValue('context', null, $this->getState('filter.context')); - $form->setFieldAttribute('group_id', 'context', $this->getState('filter.context'), 'filter'); - $form->setFieldAttribute('assigned_cat_ids', 'extension', $this->state->get('filter.component'), 'filter'); - } - - return $form; - } - - /** - * Get the groups for the batch method - * - * @return array An array of groups - * - * @since 3.7.0 - */ - public function getGroups() - { - $user = Factory::getUser(); - $viewlevels = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); - $context = $this->state->get('filter.context'); - - $db = $this->getDbo(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('title', 'text'), - $db->quoteName('id', 'value'), - $db->quoteName('state'), - ] - ); - $query->from($db->quoteName('#__fields_groups')); - $query->whereIn($db->quoteName('state'), [0, 1]); - $query->where($db->quoteName('context') . ' = :context'); - $query->whereIn($db->quoteName('access'), $viewlevels); - $query->bind(':context', $context); - - $db->setQuery($query); - - return $db->loadObjectList(); - } + /** + * Constructor + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * + * @since 3.7.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'type', 'a.type', + 'name', 'a.name', + 'state', 'a.state', + 'access', 'a.access', + 'access_level', + 'only_use_in_subform', + 'language', 'a.language', + 'ordering', 'a.ordering', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'created_time', 'a.created_time', + 'created_user_id', 'a.created_user_id', + 'group_title', 'g.title', + 'category_id', 'a.category_id', + 'group_id', 'a.group_id', + 'assigned_cat_ids' + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState($ordering = null, $direction = null) + { + // List state information. + parent::populateState('a.ordering', 'asc'); + + $context = $this->getUserStateFromRequest($this->context . '.context', 'context', 'com_content.article', 'CMD'); + $this->setState('filter.context', $context); + + // Split context into component and optional section + $parts = FieldsHelper::extract($context); + + if ($parts) { + $this->setState('filter.component', $parts[0]); + $this->setState('filter.section', $parts[1]); + } + } + + /** + * Method to get a store id based on the model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. + * + * @return string A store id. + * + * @since 3.7.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.context'); + $id .= ':' . serialize($this->getState('filter.assigned_cat_ids')); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.group_id'); + $id .= ':' . serialize($this->getState('filter.language')); + $id .= ':' . $this->getState('filter.only_use_in_subform'); + + return parent::getStoreId($id); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return \Joomla\Database\DatabaseQuery A DatabaseQuery object to retrieve the data set. + * + * @since 3.7.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + $app = Factory::getApplication(); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'DISTINCT a.id, a.title, a.name, a.checked_out, a.checked_out_time, a.note' . + ', a.state, a.access, a.created_time, a.created_user_id, a.ordering, a.language' . + ', a.fieldparams, a.params, a.type, a.default_value, a.context, a.group_id' . + ', a.label, a.description, a.required, a.only_use_in_subform' + ) + ); + $query->from('#__fields AS a'); + + // Join over the language + $query->select('l.title AS language_title, l.image AS language_image') + ->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language'); + + // Join over the users for the checked out user. + $query->select('uc.name AS editor')->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); + + // Join over the asset groups. + $query->select('ag.title AS access_level')->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); + + // Join over the users for the author. + $query->select('ua.name AS author_name')->join('LEFT', '#__users AS ua ON ua.id = a.created_user_id'); + + // Join over the field groups. + $query->select('g.title AS group_title, g.access as group_access, g.state AS group_state, g.note as group_note'); + $query->join('LEFT', '#__fields_groups AS g ON g.id = a.group_id'); + + // Filter by context + if ($context = $this->getState('filter.context')) { + $query->where($db->quoteName('a.context') . ' = :context') + ->bind(':context', $context); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + if (is_array($access)) { + $access = ArrayHelper::toInteger($access); + $query->whereIn($db->quoteName('a.access'), $access); + } else { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + } + + if (($categories = $this->getState('filter.assigned_cat_ids')) && $context) { + $categories = (array) $categories; + $categories = ArrayHelper::toInteger($categories); + $parts = FieldsHelper::extract($context); + + if ($parts) { + // Get the categories for this component (and optionally this section, if available) + $cat = ( + function () use ($parts) { + // Get the CategoryService for this component + $componentObject = $this->bootComponent($parts[0]); + + if (!$componentObject instanceof CategoryServiceInterface) { + // No CategoryService -> no categories + return null; + } + + $cat = null; + + // Try to get the categories for this component and section + try { + $cat = $componentObject->getCategory([], $parts[1] ?: ''); + } catch (SectionNotFoundException $e) { + // Not found for component and section -> Now try once more without the section, so only component + try { + $cat = $componentObject->getCategory(); + } catch (SectionNotFoundException $e) { + // If we haven't found it now, return (no categories available for this component) + return null; + } + } + + // So we found categories for at least the component, return them + return $cat; + } + )(); + + if ($cat) { + foreach ($categories as $assignedCatIds) { + // Check if we have the actual category + $parent = $cat->get($assignedCatIds); + + if ($parent) { + $categories[] = (int) $parent->id; + + // Traverse the tree up to get all the fields which are attached to a parent + while ($parent->getParent() && $parent->getParent()->id != 'root') { + $parent = $parent->getParent(); + $categories[] = (int) $parent->id; + } + } + } + } + } + + $categories = array_unique($categories); + + // Join over the assigned categories + $query->join('LEFT', $db->quoteName('#__fields_categories') . ' AS fc ON fc.field_id = a.id'); + + if (in_array('0', $categories)) { + $query->where( + '(' . + $db->quoteName('fc.category_id') . ' IS NULL OR ' . + $db->quoteName('fc.category_id') . ' IN (' . implode(',', $query->bindArray(array_values($categories), ParameterType::INTEGER)) . ')' . + ')' + ); + } else { + $query->whereIn($db->quoteName('fc.category_id'), $categories); + } + } + + // Implement View Level Access + if (!$app->isClient('administrator') || !$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.group_id') . ' = 0', + $db->quoteName('g.access') . ' IN (' . implode(',', $query->bindArray($groups, ParameterType::INTEGER)) . ')' + ], + 'OR' + ); + } + + // Filter by state + $state = $this->getState('filter.state'); + + // Include group state only when not on on back end list + $includeGroupState = !$app->isClient('administrator') || + $app->input->get('option') != 'com_fields' || + $app->input->get('view') != 'fields'; + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + + if ($includeGroupState) { + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.group_id') . ' = 0', + $db->quoteName('g.state') . ' = :gstate', + ], + 'OR' + ) + ->bind(':gstate', $state, ParameterType::INTEGER); + } + } elseif (!$state) { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + + if ($includeGroupState) { + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.group_id') . ' = 0', + $db->quoteName('g.state') . ' IN (' . implode(',', $query->bindArray([0, 1], ParameterType::INTEGER)) . ')' + ], + 'OR' + ); + } + } + + $groupId = $this->getState('filter.group_id'); + + if (is_numeric($groupId)) { + $groupId = (int) $groupId; + $query->where($db->quoteName('a.group_id') . ' = :groupid') + ->bind(':groupid', $groupId, ParameterType::INTEGER); + } + + $onlyUseInSubForm = $this->getState('filter.only_use_in_subform'); + + if (is_numeric($onlyUseInSubForm)) { + $onlyUseInSubForm = (int) $onlyUseInSubForm; + $query->where($db->quoteName('a.only_use_in_subform') . ' = :only_use_in_subform') + ->bind(':only_use_in_subform', $onlyUseInSubForm, ParameterType::INTEGER); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $search, ParameterType::INTEGER); + } elseif (stripos($search, 'author:') === 0) { + $search = '%' . substr($search, 7) . '%'; + $query->where( + '(' . + $db->quoteName('ua.name') . ' LIKE :name OR ' . + $db->quoteName('ua.username') . ' LIKE :username' . + ')' + ) + ->bind(':name', $search) + ->bind(':username', $search); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where( + '(' . + $db->quoteName('a.title') . ' LIKE :title OR ' . + $db->quoteName('a.name') . ' LIKE :sname OR ' . + $db->quoteName('a.note') . ' LIKE :note' . + ')' + ) + ->bind(':title', $search) + ->bind(':sname', $search) + ->bind(':note', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $language = (array) $language; + + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } + + // Add the list ordering clause + $listOrdering = $this->state->get('list.ordering', 'a.ordering'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + + $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); + + return $query; + } + + /** + * Gets an array of objects from the results of database query. + * + * @param string $query The query. + * @param integer $limitstart Offset. + * @param integer $limit The number of records. + * + * @return array An array of results. + * + * @since 3.7.0 + * @throws \RuntimeException + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $result = parent::_getList($query, $limitstart, $limit); + + if (is_array($result)) { + foreach ($result as $field) { + $field->fieldparams = new Registry($field->fieldparams); + $field->params = new Registry($field->params); + } + } + + return $result; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \Joomla\CMS\Form\Form|bool the Form object or false + * + * @since 3.7.0 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + if ($form) { + $form->setValue('context', null, $this->getState('filter.context')); + $form->setFieldAttribute('group_id', 'context', $this->getState('filter.context'), 'filter'); + $form->setFieldAttribute('assigned_cat_ids', 'extension', $this->state->get('filter.component'), 'filter'); + } + + return $form; + } + + /** + * Get the groups for the batch method + * + * @return array An array of groups + * + * @since 3.7.0 + */ + public function getGroups() + { + $user = Factory::getUser(); + $viewlevels = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); + $context = $this->state->get('filter.context'); + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('title', 'text'), + $db->quoteName('id', 'value'), + $db->quoteName('state'), + ] + ); + $query->from($db->quoteName('#__fields_groups')); + $query->whereIn($db->quoteName('state'), [0, 1]); + $query->where($db->quoteName('context') . ' = :context'); + $query->whereIn($db->quoteName('access'), $viewlevels); + $query->bind(':context', $context); + + $db->setQuery($query); + + return $db->loadObjectList(); + } } diff --git a/code/administrator/components/com_fields/src/Model/GroupModel.php b/code/administrator/components/com_fields/src/Model/GroupModel.php index d87bef73..29e77d79 100644 --- a/code/administrator/components/com_fields/src/Model/GroupModel.php +++ b/code/administrator/components/com_fields/src/Model/GroupModel.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage' - ); - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 3.7.0 - */ - public function save($data) - { - // Alter the title for save as copy - $input = Factory::getApplication()->input; - - // Save new group as unpublished - if ($input->get('task') == 'save2copy') - { - $data['state'] = 0; - } - - return parent::save($data); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.7.0 - * @throws \Exception - */ - public function getTable($name = 'Group', $prefix = 'Administrator', $options = array()) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Abstract method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return mixed A Form object on success, false on failure - * - * @since 3.7.0 - */ - public function getForm($data = array(), $loadData = true) - { - $context = $this->getState('filter.context'); - $jinput = Factory::getApplication()->input; - - if (empty($context) && isset($data['context'])) - { - $context = $data['context']; - $this->setState('filter.context', $context); - } - - // Get the form. - $form = $this->loadForm( - 'com_fields.group.' . $context, 'group', - array( - 'control' => 'jform', - 'load_data' => $loadData, - ) - ); - - if (empty($form)) - { - return false; - } - - // Modify the form based on Edit State access controls. - if (empty($data['context'])) - { - $data['context'] = $context; - } - - $user = Factory::getUser(); - - if (!$user->authorise('core.edit.state', $context . '.fieldgroup.' . $jinput->get('id'))) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('state', 'disabled', 'true'); - - // Disable fields while saving. The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('state', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!$user->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 3.7.0 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->state != -2) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', $record->context . '.fieldgroup.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the - * component. - * - * @since 3.7.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - // Check for existing fieldgroup. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $record->context . '.fieldgroup.' . (int) $record->id); - } - - // Default to component settings. - return $user->authorise('core.edit.state', $record->context); - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.7.0 - */ - protected function populateState() - { - parent::populateState(); - - $context = Factory::getApplication()->getUserStateFromRequest('com_fields.groups.context', 'context', 'com_fields', 'CMD'); - $this->setState('filter.context', $context); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param Table $table A Table object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 3.7.0 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('context') . ' = ' . $this->_db->quote($table->context), - ]; - } - - /** - * Method to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @see \Joomla\CMS\Form\FormField - * @since 3.7.0 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - parent::preprocessForm($form, $data, $group); - - $parts = FieldsHelper::extract($this->state->get('filter.context')); - - // Extract the component name - $component = $parts[0]; - - // Extract the optional section name - $section = (count($parts) > 1) ? $parts[1] : null; - - if ($parts) - { - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $component); - } - - if ($section !== null) - { - // Looking first in the component models/forms folder - $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/fieldgroup/' . $section . '.xml'); - - if (file_exists($path)) - { - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE); - $lang->load($component, JPATH_BASE . '/components/' . $component); - - if (!$form->loadFile($path, false)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - } - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see JFormRule - * @see JFilterInput - * @since 3.9.23 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_fields')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 3.7.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_fields.edit.group.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Language, Access) in edit form if those have been selected in Field Group Manager - if (!$data->id) - { - // Check for which context the Field Group Manager is used and get selected fields - $context = substr($app->getUserState('com_fields.groups.filter.context', ''), 4); - $filters = (array) $app->getUserState('com_fields.groups.' . $context . '.filter'); - - $data->set( - 'state', - $app->input->getInt('state', (!empty($filters['state']) ? $filters['state'] : null)) - ); - $data->set( - 'language', - $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null)) - ); - $data->set( - 'access', - $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) - ); - } - } - - $this->preprocessData('com_fields.group', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 3.7.0 - */ - public function getItem($pk = null) - { - if ($item = parent::getItem($pk)) - { - // Prime required properties. - if (empty($item->id)) - { - $item->context = $this->getState('filter.context'); - } - - if (property_exists($item, 'params')) - { - $item->params = new Registry($item->params); - } - } - - return $item; - } - - /** - * Clean the cache - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 3.7.0 - */ - protected function cleanCache($group = null, $clientId = 0) - { - $context = Factory::getApplication()->input->get('context'); - - parent::cleanCache($context); - } + /** + * @var null|string + * + * @since 3.7.0 + */ + public $typeAlias = null; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage' + ); + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 3.7.0 + */ + public function save($data) + { + // Alter the title for save as copy + $input = Factory::getApplication()->input; + + // Save new group as unpublished + if ($input->get('task') == 'save2copy') { + $data['state'] = 0; + } + + return parent::save($data); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.7.0 + * @throws \Exception + */ + public function getTable($name = 'Group', $prefix = 'Administrator', $options = array()) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Abstract method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return mixed A Form object on success, false on failure + * + * @since 3.7.0 + */ + public function getForm($data = array(), $loadData = true) + { + $context = $this->getState('filter.context'); + $jinput = Factory::getApplication()->input; + + if (empty($context) && isset($data['context'])) { + $context = $data['context']; + $this->setState('filter.context', $context); + } + + // Get the form. + $form = $this->loadForm( + 'com_fields.group.' . $context, + 'group', + array( + 'control' => 'jform', + 'load_data' => $loadData, + ) + ); + + if (empty($form)) { + return false; + } + + // Modify the form based on Edit State access controls. + if (empty($data['context'])) { + $data['context'] = $context; + } + + $user = Factory::getUser(); + + if (!$user->authorise('core.edit.state', $context . '.fieldgroup.' . $jinput->get('id'))) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + + // Disable fields while saving. The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!$user->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 3.7.0 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->state != -2) { + return false; + } + + return Factory::getUser()->authorise('core.delete', $record->context . '.fieldgroup.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the + * component. + * + * @since 3.7.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + // Check for existing fieldgroup. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $record->context . '.fieldgroup.' . (int) $record->id); + } + + // Default to component settings. + return $user->authorise('core.edit.state', $record->context); + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState() + { + parent::populateState(); + + $context = Factory::getApplication()->getUserStateFromRequest('com_fields.groups.context', 'context', 'com_fields', 'CMD'); + $this->setState('filter.context', $context); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param Table $table A Table object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 3.7.0 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('context') . ' = ' . $db->quote($table->context), + ]; + } + + /** + * Method to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @see \Joomla\CMS\Form\FormField + * @since 3.7.0 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + parent::preprocessForm($form, $data, $group); + + $parts = FieldsHelper::extract($this->state->get('filter.context')); + + // Extract the component name + $component = $parts[0]; + + // Extract the optional section name + $section = (count($parts) > 1) ? $parts[1] : null; + + if ($parts) { + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $component); + } + + if ($section !== null) { + // Looking first in the component models/forms folder + $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/fieldgroup/' . $section . '.xml'); + + if (file_exists($path)) { + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE); + $lang->load($component, JPATH_BASE . '/components/' . $component); + + if (!$form->loadFile($path, false)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + } + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see JFormRule + * @see JFilterInput + * @since 3.9.23 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_fields')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 3.7.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_fields.edit.group.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Language, Access) in edit form if those have been selected in Field Group Manager + if (!$data->id) { + // Check for which context the Field Group Manager is used and get selected fields + $context = substr($app->getUserState('com_fields.groups.filter.context', ''), 4); + $filters = (array) $app->getUserState('com_fields.groups.' . $context . '.filter'); + + $data->set( + 'state', + $app->input->getInt('state', (!empty($filters['state']) ? $filters['state'] : null)) + ); + $data->set( + 'language', + $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null)) + ); + $data->set( + 'access', + $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) + ); + } + } + + $this->preprocessData('com_fields.group', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 3.7.0 + */ + public function getItem($pk = null) + { + if ($item = parent::getItem($pk)) { + // Prime required properties. + if (empty($item->id)) { + $item->context = $this->getState('filter.context'); + } + + if (property_exists($item, 'params')) { + $item->params = new Registry($item->params); + } + } + + return $item; + } + + /** + * Clean the cache + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 3.7.0 + */ + protected function cleanCache($group = null, $clientId = 0) + { + $context = Factory::getApplication()->input->get('context'); + + parent::cleanCache($context); + } } diff --git a/code/administrator/components/com_fields/src/Model/GroupsModel.php b/code/administrator/components/com_fields/src/Model/GroupsModel.php index 914441fb..98e78e35 100644 --- a/code/administrator/components/com_fields/src/Model/GroupsModel.php +++ b/code/administrator/components/com_fields/src/Model/GroupsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.context', 'context', 'com_content', 'CMD'); - $this->setState('filter.context', $context); - } - - /** - * Method to get a store id based on the model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. - * - * @return string A store id. - * - * @since 3.7.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.context'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . print_r($this->getState('filter.language'), true); - - return parent::getStoreId($id); - } - - /** - * Method to get a DatabaseQuery object for retrieving the data set from a database. - * - * @return \Joomla\Database\DatabaseQuery A DatabaseQuery object to retrieve the data set. - * - * @since 3.7.0 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select($this->getState('list.select', 'a.*')); - $query->from('#__fields_groups AS a'); - - // Join over the language - $query->select('l.title AS language_title, l.image AS language_image') - ->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language'); - - // Join over the users for the checked out user. - $query->select('uc.name AS editor')->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); - - // Join over the asset groups. - $query->select('ag.title AS access_level')->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); - - // Join over the users for the author. - $query->select('ua.name AS author_name')->join('LEFT', '#__users AS ua ON ua.id = a.created_by'); - - // Filter by context - if ($context = $this->getState('filter.context', 'com_fields')) - { - $query->where($db->quoteName('a.context') . ' = :context') - ->bind(':context', $context); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - if (is_array($access)) - { - $access = ArrayHelper::toInteger($access); - $query->whereIn($db->quoteName('a.access'), $access); - } - else - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - // Filter by published state - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif (!$state) - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':id', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where($db->quoteName('a.title') . ' LIKE :search') - ->bind(':search', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $language = (array) $language; - - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - - // Add the list ordering clause - $listOrdering = $this->getState('list.ordering', 'a.ordering'); - $listDirn = $db->escape($this->getState('list.direction', 'ASC')); - - $query->order($db->escape($listOrdering) . ' ' . $listDirn); - - return $query; - } - - /** - * Gets an array of objects from the results of database query. - * - * @param string $query The query. - * @param integer $limitstart Offset. - * @param integer $limit The number of records. - * - * @return array An array of results. - * - * @since 3.8.7 - * @throws \RuntimeException - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $result = parent::_getList($query, $limitstart, $limit); - - if (is_array($result)) - { - foreach ($result as $group) - { - $group->params = new Registry($group->params); - } - } - - return $result; - } + /** + * Context string for the model type. This is used to handle uniqueness + * when dealing with the getStoreId() method and caching data structures. + * + * @var string + * @since 3.7.0 + */ + protected $context = 'com_fields.groups'; + + /** + * Constructor + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * + * @since 3.7.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'type', 'a.type', + 'state', 'a.state', + 'access', 'a.access', + 'access_level', + 'language', 'a.language', + 'ordering', 'a.ordering', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'created', 'a.created', + 'created_by', 'a.created_by', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState($ordering = null, $direction = null) + { + // List state information. + parent::populateState('a.ordering', 'asc'); + + $context = $this->getUserStateFromRequest($this->context . '.context', 'context', 'com_content', 'CMD'); + $this->setState('filter.context', $context); + } + + /** + * Method to get a store id based on the model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. + * + * @return string A store id. + * + * @since 3.7.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.context'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . print_r($this->getState('filter.language'), true); + + return parent::getStoreId($id); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return \Joomla\Database\DatabaseQuery A DatabaseQuery object to retrieve the data set. + * + * @since 3.7.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select($this->getState('list.select', 'a.*')); + $query->from('#__fields_groups AS a'); + + // Join over the language + $query->select('l.title AS language_title, l.image AS language_image') + ->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language'); + + // Join over the users for the checked out user. + $query->select('uc.name AS editor')->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); + + // Join over the asset groups. + $query->select('ag.title AS access_level')->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); + + // Join over the users for the author. + $query->select('ua.name AS author_name')->join('LEFT', '#__users AS ua ON ua.id = a.created_by'); + + // Filter by context + if ($context = $this->getState('filter.context', 'com_fields')) { + $query->where($db->quoteName('a.context') . ' = :context') + ->bind(':context', $context); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + if (is_array($access)) { + $access = ArrayHelper::toInteger($access); + $query->whereIn($db->quoteName('a.access'), $access); + } else { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + // Filter by published state + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif (!$state) { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':id', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where($db->quoteName('a.title') . ' LIKE :search') + ->bind(':search', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $language = (array) $language; + + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } + + // Add the list ordering clause + $listOrdering = $this->getState('list.ordering', 'a.ordering'); + $listDirn = $db->escape($this->getState('list.direction', 'ASC')); + + $query->order($db->escape($listOrdering) . ' ' . $listDirn); + + return $query; + } + + /** + * Gets an array of objects from the results of database query. + * + * @param string $query The query. + * @param integer $limitstart Offset. + * @param integer $limit The number of records. + * + * @return array An array of results. + * + * @since 3.8.7 + * @throws \RuntimeException + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $result = parent::_getList($query, $limitstart, $limit); + + if (is_array($result)) { + foreach ($result as $group) { + $group->params = new Registry($group->params); + } + } + + return $result; + } } diff --git a/code/administrator/components/com_fields/src/Plugin/FieldsListPlugin.php b/code/administrator/components/com_fields/src/Plugin/FieldsListPlugin.php index 4d112e36..ce93f75f 100644 --- a/code/administrator/components/com_fields/src/Plugin/FieldsListPlugin.php +++ b/code/administrator/components/com_fields/src/Plugin/FieldsListPlugin.php @@ -1,4 +1,5 @@ setAttribute('validate', 'options'); + $fieldNode->setAttribute('validate', 'options'); - foreach ($this->getOptionsFromField($field) as $value => $name) - { - $option = new \DOMElement('option', htmlspecialchars($value, ENT_COMPAT, 'UTF-8')); - $option->textContent = htmlspecialchars(Text::_($name), ENT_COMPAT, 'UTF-8'); + foreach ($this->getOptionsFromField($field) as $value => $name) { + $option = new \DOMElement('option', htmlspecialchars($value, ENT_COMPAT, 'UTF-8')); + $option->textContent = htmlspecialchars(Text::_($name), ENT_COMPAT, 'UTF-8'); - $element = $fieldNode->appendChild($option); - $element->setAttribute('value', $value); - } + $element = $fieldNode->appendChild($option); + $element->setAttribute('value', $value); + } - return $fieldNode; - } + return $fieldNode; + } - /** - * Returns an array of key values to put in a list from the given field. - * - * @param \stdClass $field The field. - * - * @return array - * - * @since 3.7.0 - */ - public function getOptionsFromField($field) - { - $data = array(); + /** + * Returns an array of key values to put in a list from the given field. + * + * @param \stdClass $field The field. + * + * @return array + * + * @since 3.7.0 + */ + public function getOptionsFromField($field) + { + $data = array(); - // Fetch the options from the plugin - $params = clone $this->params; - $params->merge($field->fieldparams); + // Fetch the options from the plugin + $params = clone $this->params; + $params->merge($field->fieldparams); - foreach ($params->get('options', array()) as $option) - { - $op = (object) $option; - $data[$op->value] = $op->name; - } + foreach ($params->get('options', array()) as $option) { + $op = (object) $option; + $data[$op->value] = $op->name; + } - return $data; - } + return $data; + } } diff --git a/code/administrator/components/com_fields/src/Plugin/FieldsPlugin.php b/code/administrator/components/com_fields/src/Plugin/FieldsPlugin.php index 5d2554d0..51a4cde3 100644 --- a/code/administrator/components/com_fields/src/Plugin/FieldsPlugin.php +++ b/code/administrator/components/com_fields/src/Plugin/FieldsPlugin.php @@ -1,4 +1,5 @@ _type . $this->_name])) - { - return $types_cache[$this->_type . $this->_name]; - } - - $types = array(); - - // The root of the plugin - $root = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name; - - foreach (Folder::files($root . '/tmpl', '.php') as $layout) - { - // Strip the extension - $layout = str_replace('.php', '', $layout); - - // The data array - $data = array(); - - // The language key - $key = strtoupper($layout); - - if ($key != strtoupper($this->_name)) - { - $key = strtoupper($this->_name) . '_' . $layout; - } - - // Needed attributes - $data['type'] = $layout; - - if ($this->app->getLanguage()->hasKey('PLG_FIELDS_' . $key . '_LABEL')) - { - $data['label'] = Text::sprintf('PLG_FIELDS_' . $key . '_LABEL', strtolower($key)); - - // Fix wrongly set parentheses in RTL languages - if ($this->app->getLanguage()->isRtl()) - { - $data['label'] = $data['label'] . '‎'; - } - } - else - { - $data['label'] = $key; - } - - $path = $root . '/fields'; - - // Add the path when it exists - if (file_exists($path)) - { - $data['path'] = $path; - } - - $path = $root . '/rules'; - - // Add the path when it exists - if (file_exists($path)) - { - $data['rules'] = $path; - } - - $types[] = $data; - } - - // Add to cache and return the data - $types_cache[$this->_type . $this->_name] = $types; - - return $types; - } - - /** - * Prepares the field value. - * - * @param string $context The context. - * @param \stdclass $item The item. - * @param \stdclass $field The field. - * - * @return string - * - * @since 3.7.0 - */ - public function onCustomFieldsPrepareField($context, $item, $field) - { - // Check if the field should be processed by us - if (!$this->isTypeSupported($field->type)) - { - return; - } - - // Merge the params from the plugin and field which has precedence - $fieldParams = clone $this->params; - $fieldParams->merge($field->fieldparams); - - // Get the path for the layout file - $path = PluginHelper::getLayoutPath('fields', $this->_name, $field->type); - - // Render the layout - ob_start(); - include $path; - $output = ob_get_clean(); - - // Return the output - return $output; - } - - /** - * Transforms the field into a DOM XML element and appends it as a child on the given parent. - * - * @param \stdClass $field The field. - * @param \DOMElement $parent The field node parent. - * @param Form $form The form. - * - * @return \DOMElement - * - * @since 3.7.0 - */ - public function onCustomFieldsPrepareDom($field, \DOMElement $parent, Form $form) - { - // Check if the field should be processed by us - if (!$this->isTypeSupported($field->type)) - { - return null; - } - - // Detect if the field is configured to be displayed on the form - if (!FieldsHelper::displayFieldOnForm($field)) - { - return null; - } - - // Create the node - $node = $parent->appendChild(new \DOMElement('field')); - - // Set the attributes - $node->setAttribute('name', $field->name); - $node->setAttribute('type', $field->type); - $node->setAttribute('label', $field->label); - $node->setAttribute('labelclass', $field->params->get('label_class', '')); - $node->setAttribute('description', $field->description); - $node->setAttribute('class', $field->params->get('class', '')); - $node->setAttribute('hint', $field->params->get('hint', '')); - $node->setAttribute('required', $field->required ? 'true' : 'false'); - - if ($layout = $field->params->get('form_layout')) - { - $node->setAttribute('layout', $layout); - } - - if ($field->default_value !== '') - { - $defaultNode = $node->appendChild(new \DOMElement('default')); - $defaultNode->appendChild(new \DOMCdataSection($field->default_value)); - } - - // Combine the two params - $params = clone $this->params; - $params->merge($field->fieldparams); - - // Set the specific field parameters - foreach ($params->toArray() as $key => $param) - { - if (is_array($param)) - { - // Multidimensional arrays (eg. list options) can't be transformed properly - $param = count($param) == count($param, COUNT_RECURSIVE) ? implode(',', $param) : ''; - } - - if ($param === '' || (!is_string($param) && !is_numeric($param))) - { - continue; - } - - $node->setAttribute($key, $param); - } - - // Check if it is allowed to edit the field - if (!FieldsHelper::canEditFieldValue($field)) - { - $node->setAttribute('disabled', 'true'); - } - - // Return the node - return $node; - } - - /** - * The form event. Load additional parameters when available into the field form. - * Only when the type of the form is of interest. - * - * @param Form $form The form - * @param \stdClass $data The data - * - * @return void - * - * @since 3.7.0 - */ - public function onContentPrepareForm(Form $form, $data) - { - $path = $this->getFormPath($form, $data); - - if ($path === null) - { - return; - } - - // Load the specific plugin parameters - $form->load(file_get_contents($path), true, '/form/*'); - } - - /** - * Returns the path of the XML definition file for the field parameters - * - * @param Form $form The form - * @param \stdClass $data The data - * - * @return string - * - * @since 4.0.0 - */ - protected function getFormPath(Form $form, $data) - { - // Check if the field form is calling us - if (strpos($form->getName(), 'com_fields.field') !== 0) - { - return null; - } - - // Ensure it is an object - $formData = (object) $data; - - // Gather the type - $type = $form->getValue('type'); - - if (!empty($formData->type)) - { - $type = $formData->type; - } - - // Not us - if (!$this->isTypeSupported($type)) - { - return null; - } - - $path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/params/' . $type . '.xml'; - - // Check if params file exists - if (!file_exists($path)) - { - return null; - } - - return $path; - } - - /** - * Returns true if the given type is supported by the plugin. - * - * @param string $type The type - * - * @return boolean - * - * @since 3.7.0 - */ - protected function isTypeSupported($type) - { - foreach ($this->onCustomFieldsGetTypes() as $typeSpecification) - { - if ($type == $typeSpecification['type']) - { - return true; - } - } - - return false; - } + /** + * Affects constructor behavior. If true, language files will be loaded automatically. + * + * @var boolean + * @since 3.7.0 + */ + protected $autoloadLanguage = true; + + /** + * Application object. + * + * @var \Joomla\CMS\Application\CMSApplication + * @since 4.0.0 + */ + protected $app; + + /** + * Returns the custom fields types. + * + * @return string[][] + * + * @since 3.7.0 + */ + public function onCustomFieldsGetTypes() + { + // Cache filesystem access / checks + static $types_cache = array(); + + if (isset($types_cache[$this->_type . $this->_name])) { + return $types_cache[$this->_type . $this->_name]; + } + + $types = array(); + + // The root of the plugin + $root = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name; + + foreach (Folder::files($root . '/tmpl', '.php') as $layout) { + // Strip the extension + $layout = str_replace('.php', '', $layout); + + // The data array + $data = array(); + + // The language key + $key = strtoupper($layout); + + if ($key != strtoupper($this->_name)) { + $key = strtoupper($this->_name) . '_' . $layout; + } + + // Needed attributes + $data['type'] = $layout; + + if ($this->app->getLanguage()->hasKey('PLG_FIELDS_' . $key . '_LABEL')) { + $data['label'] = Text::sprintf('PLG_FIELDS_' . $key . '_LABEL', strtolower($key)); + + // Fix wrongly set parentheses in RTL languages + if ($this->app->getLanguage()->isRtl()) { + $data['label'] = $data['label'] . '‎'; + } + } else { + $data['label'] = $key; + } + + $path = $root . '/fields'; + + // Add the path when it exists + if (file_exists($path)) { + $data['path'] = $path; + } + + $path = $root . '/rules'; + + // Add the path when it exists + if (file_exists($path)) { + $data['rules'] = $path; + } + + $types[] = $data; + } + + // Add to cache and return the data + $types_cache[$this->_type . $this->_name] = $types; + + return $types; + } + + /** + * Prepares the field value. + * + * @param string $context The context. + * @param \stdclass $item The item. + * @param \stdclass $field The field. + * + * @return string + * + * @since 3.7.0 + */ + public function onCustomFieldsPrepareField($context, $item, $field) + { + // Check if the field should be processed by us + if (!$this->isTypeSupported($field->type)) { + return; + } + + // Merge the params from the plugin and field which has precedence + $fieldParams = clone $this->params; + $fieldParams->merge($field->fieldparams); + + // Get the path for the layout file + $path = PluginHelper::getLayoutPath('fields', $this->_name, $field->type); + + // Render the layout + ob_start(); + include $path; + $output = ob_get_clean(); + + // Return the output + return $output; + } + + /** + * Transforms the field into a DOM XML element and appends it as a child on the given parent. + * + * @param \stdClass $field The field. + * @param \DOMElement $parent The field node parent. + * @param Form $form The form. + * + * @return \DOMElement + * + * @since 3.7.0 + */ + public function onCustomFieldsPrepareDom($field, \DOMElement $parent, Form $form) + { + // Check if the field should be processed by us + if (!$this->isTypeSupported($field->type)) { + return null; + } + + // Detect if the field is configured to be displayed on the form + if (!FieldsHelper::displayFieldOnForm($field)) { + return null; + } + + // Create the node + $node = $parent->appendChild(new \DOMElement('field')); + + // Set the attributes + $node->setAttribute('name', $field->name); + $node->setAttribute('type', $field->type); + $node->setAttribute('label', $field->label); + $node->setAttribute('labelclass', $field->params->get('label_class', '')); + $node->setAttribute('description', $field->description); + $node->setAttribute('class', $field->params->get('class', '')); + $node->setAttribute('hint', $field->params->get('hint', '')); + $node->setAttribute('required', $field->required ? 'true' : 'false'); + + if ($layout = $field->params->get('form_layout')) { + $node->setAttribute('layout', $layout); + } + + if ($field->default_value !== '') { + $defaultNode = $node->appendChild(new \DOMElement('default')); + $defaultNode->appendChild(new \DOMCdataSection($field->default_value)); + } + + // Combine the two params + $params = clone $this->params; + $params->merge($field->fieldparams); + + // Set the specific field parameters + foreach ($params->toArray() as $key => $param) { + if (is_array($param)) { + // Multidimensional arrays (eg. list options) can't be transformed properly + $param = count($param) == count($param, COUNT_RECURSIVE) ? implode(',', $param) : ''; + } + + if ($param === '' || (!is_string($param) && !is_numeric($param))) { + continue; + } + + $node->setAttribute($key, $param); + } + + // Check if it is allowed to edit the field + if (!FieldsHelper::canEditFieldValue($field)) { + $node->setAttribute('disabled', 'true'); + } + + // Return the node + return $node; + } + + /** + * The form event. Load additional parameters when available into the field form. + * Only when the type of the form is of interest. + * + * @param Form $form The form + * @param \stdClass $data The data + * + * @return void + * + * @since 3.7.0 + */ + public function onContentPrepareForm(Form $form, $data) + { + $path = $this->getFormPath($form, $data); + + if ($path === null) { + return; + } + + // Load the specific plugin parameters + $form->load(file_get_contents($path), true, '/form/*'); + } + + /** + * Returns the path of the XML definition file for the field parameters + * + * @param Form $form The form + * @param \stdClass $data The data + * + * @return string + * + * @since 4.0.0 + */ + protected function getFormPath(Form $form, $data) + { + // Check if the field form is calling us + if (strpos($form->getName(), 'com_fields.field') !== 0) { + return null; + } + + // Ensure it is an object + $formData = (object) $data; + + // Gather the type + $type = $form->getValue('type'); + + if (!empty($formData->type)) { + $type = $formData->type; + } + + // Not us + if (!$this->isTypeSupported($type)) { + return null; + } + + $path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/params/' . $type . '.xml'; + + // Check if params file exists + if (!file_exists($path)) { + return null; + } + + return $path; + } + + /** + * Returns true if the given type is supported by the plugin. + * + * @param string $type The type + * + * @return boolean + * + * @since 3.7.0 + */ + protected function isTypeSupported($type) + { + foreach ($this->onCustomFieldsGetTypes() as $typeSpecification) { + if ($type == $typeSpecification['type']) { + return true; + } + } + + return false; + } } diff --git a/code/administrator/components/com_fields/src/Table/FieldTable.php b/code/administrator/components/com_fields/src/Table/FieldTable.php index 20953310..5555e94e 100644 --- a/code/administrator/components/com_fields/src/Table/FieldTable.php +++ b/code/administrator/components/com_fields/src/Table/FieldTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - } - - /** - * Method to bind an associative array or object to the JTable instance.This - * method only binds properties that are publicly accessible and optionally - * takes an array of properties to ignore when binding. - * - * @param mixed $src An associative array or object to bind to the JTable instance. - * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success. - * - * @since 3.7.0 - * @throws \InvalidArgumentException - */ - public function bind($src, $ignore = '') - { - if (isset($src['params']) && is_array($src['params'])) - { - $registry = new Registry; - $registry->loadArray($src['params']); - $src['params'] = (string) $registry; - } - - if (isset($src['fieldparams']) && is_array($src['fieldparams'])) - { - // Make sure $registry->options contains no duplicates when the field type is subform - if (isset($src['type']) && $src['type'] == 'subform' && isset($src['fieldparams']['options'])) - { - // Fast lookup map to check which custom field ids we have already seen - $seen_customfields = array(); - - // Container for the new $src['fieldparams']['options'] - $options = array(); - - // Iterate through the old options - $i = 0; - - foreach ($src['fieldparams']['options'] as $option) - { - // Check whether we have not yet seen this custom field id - if (!isset($seen_customfields[$option['customfield']])) - { - // We haven't, so add it to the final options - $seen_customfields[$option['customfield']] = true; - $options['option' . $i] = $option; - $i++; - } - } - - // And replace the options with the deduplicated ones. - $src['fieldparams']['options'] = $options; - } - - $registry = new Registry; - $registry->loadArray($src['fieldparams']); - $src['fieldparams'] = (string) $registry; - } - - // Bind the rules. - if (isset($src['rules']) && is_array($src['rules'])) - { - $rules = new Rules($src['rules']); - $this->setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to perform sanity checks on the JTable instance properties to ensure - * they are safe to store in the database. Child classes should override this - * method to make sure the data they are storing in the database is safe and - * as expected before storage. - * - * @return boolean True if the instance is sane and able to be stored in the database. - * - * @link https://docs.joomla.org/Special:MyLanguage/JTable/check - * @since 3.7.0 - */ - public function check() - { - // Check for valid name - if (trim($this->title) == '') - { - $this->setError(Text::_('COM_FIELDS_MUSTCONTAIN_A_TITLE_FIELD')); - - return false; - } - - if (empty($this->name)) - { - $this->name = $this->title; - } - - $this->name = ApplicationHelper::stringURLSafe($this->name, $this->language); - - if (trim(str_replace('-', '', $this->name)) == '') - { - $this->name = StringHelper::increment($this->name, 'dash'); - } - - $this->name = str_replace(',', '-', $this->name); - - // Verify that the name is unique - $table = new static($this->_db); - - if ($table->load(array('name' => $this->name)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_FIELDS_ERROR_UNIQUE_NAME')); - - return false; - } - - $this->name = str_replace(',', '-', $this->name); - - if (empty($this->type)) - { - $this->type = 'text'; - } - - if (empty($this->fieldparams)) - { - $this->fieldparams = '{}'; - } - - $date = Factory::getDate()->toSql(); - $user = Factory::getUser(); - - // Set created date if not set. - if (!(int) $this->created_time) - { - $this->created_time = $date; - } - - if ($this->id) - { - // Existing item - $this->modified_time = $date; - $this->modified_by = $user->get('id'); - } - else - { - if (!(int) $this->modified_time) - { - $this->modified_time = $this->created_time; - } - - if (empty($this->created_user_id)) - { - $this->created_user_id = $user->get('id'); - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_user_id; - } - } - - if (empty($this->group_id)) - { - $this->group_id = 0; - } - - return true; - } - - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - return parent::store($updateNulls); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 3.7.0 - */ - protected function _getAssetName() - { - $contextArray = explode('.', $this->context); - - return $contextArray[0] . '.field.' . (int) $this->id; - } - - /** - * Method to return the title to use for the asset table. In - * tracking the assets a title is kept for each asset so that there is some - * context available in a unified access manager. Usually this would just - * return $this->title or $this->name or whatever is being used for the - * primary name of the row. If this method is not overridden, the asset name is used. - * - * @return string The string to use as the title in the asset table. - * - * @link https://docs.joomla.org/Special:MyLanguage/JTable/getAssetTitle - * @since 3.7.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Method to get the parent asset under which to register this one. - * By default, all assets are registered to the ROOT node with ID, - * which will default to 1 if none exists. - * The extended class can define a table and id to lookup. If the - * asset does not exist it will be created. - * - * @param Table $table A Table object for the asset parent. - * @param integer $id Id to look up - * - * @return integer - * - * @since 3.7.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $contextArray = explode('.', $this->context); - $component = $contextArray[0]; - - if ($this->group_id) - { - $assetId = $this->getAssetId($component . '.fieldgroup.' . (int) $this->group_id); - - if ($assetId) - { - return $assetId; - } - } - else - { - $assetId = $this->getAssetId($component); - - if ($assetId) - { - return $assetId; - } - } - - return parent::_getAssetParentId($table, $id); - } - - /** - * Returns an asset id for the given name or false. - * - * @param string $name The asset name - * - * @return number|boolean - * - * @since 3.7.0 - */ - private function getAssetId($name) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('name') . ' = :name') - ->bind(':name', $name); - - // Get the asset id from the database. - $db->setQuery($query); - - $assetId = null; - - if ($result = $db->loadResult()) - { - $assetId = (int) $result; - - if ($assetId) - { - return $assetId; - } - } - - return false; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Class constructor. + * + * @param DatabaseDriver $db DatabaseDriver object. + * + * @since 3.7.0 + */ + public function __construct($db = null) + { + parent::__construct('#__fields', 'id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Method to bind an associative array or object to the JTable instance.This + * method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param mixed $src An associative array or object to bind to the JTable instance. + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 3.7.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = '') + { + if (isset($src['params']) && is_array($src['params'])) { + $registry = new Registry(); + $registry->loadArray($src['params']); + $src['params'] = (string) $registry; + } + + if (isset($src['fieldparams']) && is_array($src['fieldparams'])) { + // Make sure $registry->options contains no duplicates when the field type is subform + if (isset($src['type']) && $src['type'] == 'subform' && isset($src['fieldparams']['options'])) { + // Fast lookup map to check which custom field ids we have already seen + $seen_customfields = array(); + + // Container for the new $src['fieldparams']['options'] + $options = array(); + + // Iterate through the old options + $i = 0; + + foreach ($src['fieldparams']['options'] as $option) { + // Check whether we have not yet seen this custom field id + if (!isset($seen_customfields[$option['customfield']])) { + // We haven't, so add it to the final options + $seen_customfields[$option['customfield']] = true; + $options['option' . $i] = $option; + $i++; + } + } + + // And replace the options with the deduplicated ones. + $src['fieldparams']['options'] = $options; + } + + $registry = new Registry(); + $registry->loadArray($src['fieldparams']); + $src['fieldparams'] = (string) $registry; + } + + // Bind the rules. + if (isset($src['rules']) && is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to perform sanity checks on the JTable instance properties to ensure + * they are safe to store in the database. Child classes should override this + * method to make sure the data they are storing in the database is safe and + * as expected before storage. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @link https://docs.joomla.org/Special:MyLanguage/JTable/check + * @since 3.7.0 + */ + public function check() + { + // Check for valid name + if (trim($this->title) == '') { + $this->setError(Text::_('COM_FIELDS_MUSTCONTAIN_A_TITLE_FIELD')); + + return false; + } + + if (empty($this->name)) { + $this->name = $this->title; + } + + $this->name = ApplicationHelper::stringURLSafe($this->name, $this->language); + + if (trim(str_replace('-', '', $this->name)) == '') { + $this->name = StringHelper::increment($this->name, 'dash'); + } + + $this->name = str_replace(',', '-', $this->name); + + // Verify that the name is unique + $table = new static($this->_db); + + if ($table->load(array('name' => $this->name)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_FIELDS_ERROR_UNIQUE_NAME')); + + return false; + } + + $this->name = str_replace(',', '-', $this->name); + + if (empty($this->type)) { + $this->type = 'text'; + } + + if (empty($this->fieldparams)) { + $this->fieldparams = '{}'; + } + + $date = Factory::getDate()->toSql(); + $user = Factory::getUser(); + + // Set created date if not set. + if (!(int) $this->created_time) { + $this->created_time = $date; + } + + if ($this->id) { + // Existing item + $this->modified_time = $date; + $this->modified_by = $user->get('id'); + } else { + if (!(int) $this->modified_time) { + $this->modified_time = $this->created_time; + } + + if (empty($this->created_user_id)) { + $this->created_user_id = $user->get('id'); + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_user_id; + } + } + + if (empty($this->group_id)) { + $this->group_id = 0; + } + + return true; + } + + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + return parent::store($updateNulls); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 3.7.0 + */ + protected function _getAssetName() + { + $contextArray = explode('.', $this->context); + + return $contextArray[0] . '.field.' . (int) $this->id; + } + + /** + * Method to return the title to use for the asset table. In + * tracking the assets a title is kept for each asset so that there is some + * context available in a unified access manager. Usually this would just + * return $this->title or $this->name or whatever is being used for the + * primary name of the row. If this method is not overridden, the asset name is used. + * + * @return string The string to use as the title in the asset table. + * + * @link https://docs.joomla.org/Special:MyLanguage/JTable/getAssetTitle + * @since 3.7.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Method to get the parent asset under which to register this one. + * By default, all assets are registered to the ROOT node with ID, + * which will default to 1 if none exists. + * The extended class can define a table and id to lookup. If the + * asset does not exist it will be created. + * + * @param Table $table A Table object for the asset parent. + * @param integer $id Id to look up + * + * @return integer + * + * @since 3.7.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $contextArray = explode('.', $this->context); + $component = $contextArray[0]; + + if ($this->group_id) { + $assetId = $this->getAssetId($component . '.fieldgroup.' . (int) $this->group_id); + + if ($assetId) { + return $assetId; + } + } else { + $assetId = $this->getAssetId($component); + + if ($assetId) { + return $assetId; + } + } + + return parent::_getAssetParentId($table, $id); + } + + /** + * Returns an asset id for the given name or false. + * + * @param string $name The asset name + * + * @return number|boolean + * + * @since 3.7.0 + */ + private function getAssetId($name) + { + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = :name') + ->bind(':name', $name); + + // Get the asset id from the database. + $db->setQuery($query); + + $assetId = null; + + if ($result = $db->loadResult()) { + $assetId = (int) $result; + + if ($assetId) { + return $assetId; + } + } + + return false; + } } diff --git a/code/administrator/components/com_fields/src/Table/GroupTable.php b/code/administrator/components/com_fields/src/Table/GroupTable.php index 47457223..61fa03e7 100644 --- a/code/administrator/components/com_fields/src/Table/GroupTable.php +++ b/code/administrator/components/com_fields/src/Table/GroupTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - } - - /** - * Method to bind an associative array or object to the JTable instance.This - * method only binds properties that are publicly accessible and optionally - * takes an array of properties to ignore when binding. - * - * @param mixed $src An associative array or object to bind to the JTable instance. - * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success. - * - * @since 3.7.0 - * @throws \InvalidArgumentException - */ - public function bind($src, $ignore = '') - { - if (isset($src['params']) && is_array($src['params'])) - { - $registry = new Registry; - $registry->loadArray($src['params']); - $src['params'] = (string) $registry; - } - - // Bind the rules. - if (isset($src['rules']) && is_array($src['rules'])) - { - $rules = new Rules($src['rules']); - $this->setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to perform sanity checks on the JTable instance properties to ensure - * they are safe to store in the database. Child classes should override this - * method to make sure the data they are storing in the database is safe and - * as expected before storage. - * - * @return boolean True if the instance is sane and able to be stored in the database. - * - * @link https://docs.joomla.org/Special:MyLanguage/JTable/check - * @since 3.7.0 - */ - public function check() - { - // Check for a title. - if (trim($this->title) == '') - { - $this->setError(Text::_('COM_FIELDS_MUSTCONTAIN_A_TITLE_GROUP')); - - return false; - } - - $date = Factory::getDate()->toSql(); - $user = Factory::getUser(); - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = $date; - } - - if ($this->id) - { - $this->modified = $date; - $this->modified_by = $user->get('id'); - } - else - { - if (!(int) $this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->created_by)) - { - $this->created_by = $user->get('id'); - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - } - - if ($this->params === null) - { - $this->params = '{}'; - } - - return true; - } - - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - return parent::store($updateNulls); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 3.7.0 - */ - protected function _getAssetName() - { - $component = explode('.', $this->context); - - return $component[0] . '.fieldgroup.' . (int) $this->id; - } - - /** - * Method to return the title to use for the asset table. In - * tracking the assets a title is kept for each asset so that there is some - * context available in a unified access manager. Usually this would just - * return $this->title or $this->name or whatever is being used for the - * primary name of the row. If this method is not overridden, the asset name is used. - * - * @return string The string to use as the title in the asset table. - * - * @link https://docs.joomla.org/Special:MyLanguage/JTable/getAssetTitle - * @since 3.7.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Method to get the parent asset under which to register this one. - * By default, all assets are registered to the ROOT node with ID, - * which will default to 1 if none exists. - * The extended class can define a table and id to lookup. If the - * asset does not exist it will be created. - * - * @param Table $table A Table object for the asset parent. - * @param integer $id Id to look up - * - * @return integer - * - * @since 3.7.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $component = explode('.', $this->context); - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('name') . ' = :name') - ->bind(':name', $component[0]); - $db->setQuery($query); - - if ($assetId = (int) $db->loadResult()) - { - return $assetId; - } - - return parent::_getAssetParentId($table, $id); - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Class constructor. + * + * @param DatabaseDriver $db DatabaseDriver object. + * + * @since 3.7.0 + */ + public function __construct($db = null) + { + parent::__construct('#__fields_groups', 'id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Method to bind an associative array or object to the JTable instance.This + * method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param mixed $src An associative array or object to bind to the JTable instance. + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 3.7.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = '') + { + if (isset($src['params']) && is_array($src['params'])) { + $registry = new Registry(); + $registry->loadArray($src['params']); + $src['params'] = (string) $registry; + } + + // Bind the rules. + if (isset($src['rules']) && is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to perform sanity checks on the JTable instance properties to ensure + * they are safe to store in the database. Child classes should override this + * method to make sure the data they are storing in the database is safe and + * as expected before storage. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @link https://docs.joomla.org/Special:MyLanguage/JTable/check + * @since 3.7.0 + */ + public function check() + { + // Check for a title. + if (trim($this->title) == '') { + $this->setError(Text::_('COM_FIELDS_MUSTCONTAIN_A_TITLE_GROUP')); + + return false; + } + + $date = Factory::getDate()->toSql(); + $user = Factory::getUser(); + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = $date; + } + + if ($this->id) { + $this->modified = $date; + $this->modified_by = $user->get('id'); + } else { + if (!(int) $this->modified) { + $this->modified = $this->created; + } + + if (empty($this->created_by)) { + $this->created_by = $user->get('id'); + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + } + + if ($this->params === null) { + $this->params = '{}'; + } + + return true; + } + + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + return parent::store($updateNulls); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 3.7.0 + */ + protected function _getAssetName() + { + $component = explode('.', $this->context); + + return $component[0] . '.fieldgroup.' . (int) $this->id; + } + + /** + * Method to return the title to use for the asset table. In + * tracking the assets a title is kept for each asset so that there is some + * context available in a unified access manager. Usually this would just + * return $this->title or $this->name or whatever is being used for the + * primary name of the row. If this method is not overridden, the asset name is used. + * + * @return string The string to use as the title in the asset table. + * + * @link https://docs.joomla.org/Special:MyLanguage/JTable/getAssetTitle + * @since 3.7.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Method to get the parent asset under which to register this one. + * By default, all assets are registered to the ROOT node with ID, + * which will default to 1 if none exists. + * The extended class can define a table and id to lookup. If the + * asset does not exist it will be created. + * + * @param Table $table A Table object for the asset parent. + * @param integer $id Id to look up + * + * @return integer + * + * @since 3.7.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $component = explode('.', $this->context); + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = :name') + ->bind(':name', $component[0]); + $db->setQuery($query); + + if ($assetId = (int) $db->loadResult()) { + return $assetId; + } + + return parent::_getAssetParentId($table, $id); + } } diff --git a/code/administrator/components/com_fields/src/View/Field/HtmlView.php b/code/administrator/components/com_fields/src/View/Field/HtmlView.php index 8159b9ac..5eafa567 100644 --- a/code/administrator/components/com_fields/src/View/Field/HtmlView.php +++ b/code/administrator/components/com_fields/src/View/Field/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - $this->canDo = ContentHelper::getActions($this->state->get('field.component'), 'field', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - Factory::getApplication()->input->set('hidemainmenu', true); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Adds the toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $component = $this->state->get('field.component'); - $section = $this->state->get('field.section'); - $userId = Factory::getUser()->get('id'); - $canDo = $this->canDo; - - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Avoid nonsense situation. - if ($component == 'com_fields') - { - return; - } - - // Load component language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - $title = Text::sprintf('COM_FIELDS_VIEW_FIELD_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE', Text::_(strtoupper($component))); - - // Prepare the toolbar. - ToolbarHelper::title( - $title, - 'puzzle field-' . ($isNew ? 'add' : 'edit') . ' ' . substr($component, 4) . ($section ? "-$section" : '') . '-field-' . - ($isNew ? 'add' : 'edit') - ); - - // For new records, check the create permission. - if ($isNew) - { - ToolbarHelper::apply('field.apply'); - - ToolbarHelper::saveGroup( - [ - ['save', 'field.save'], - ['save2new', 'field.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('field.cancel'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - ToolbarHelper::apply('field.apply'); - - $toolbarButtons[] = ['save', 'field.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'field.save2new']; - } - } - - // If an existing item, can save to a copy. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'field.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('field.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::help('Component:_New_or_Edit_Field'); - } + /** + * @var \Joomla\CMS\Form\Form + * + * @since 3.7.0 + */ + protected $form; + + /** + * @var CMSObject + * + * @since 3.7.0 + */ + protected $item; + + /** + * @var CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see HtmlView::loadTemplate() + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + $this->canDo = ContentHelper::getActions($this->state->get('field.component'), 'field', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + Factory::getApplication()->input->set('hidemainmenu', true); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Adds the toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $component = $this->state->get('field.component'); + $section = $this->state->get('field.section'); + $userId = $this->getCurrentUser()->get('id'); + $canDo = $this->canDo; + + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Avoid nonsense situation. + if ($component == 'com_fields') { + return; + } + + // Load component language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + $title = Text::sprintf('COM_FIELDS_VIEW_FIELD_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE', Text::_(strtoupper($component))); + + // Prepare the toolbar. + ToolbarHelper::title( + $title, + 'puzzle field-' . ($isNew ? 'add' : 'edit') . ' ' . substr($component, 4) . ($section ? "-$section" : '') . '-field-' . + ($isNew ? 'add' : 'edit') + ); + + // For new records, check the create permission. + if ($isNew) { + ToolbarHelper::apply('field.apply'); + + ToolbarHelper::saveGroup( + [ + ['save', 'field.save'], + ['save2new', 'field.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('field.cancel'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + ToolbarHelper::apply('field.apply'); + + $toolbarButtons[] = ['save', 'field.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'field.save2new']; + } + } + + // If an existing item, can save to a copy. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'field.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('field.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::help('Component:_New_or_Edit_Field'); + } } diff --git a/code/administrator/components/com_fields/src/View/Fields/HtmlView.php b/code/administrator/components/com_fields/src/View/Fields/HtmlView.php index ac30ba0f..02b4f257 100644 --- a/code/administrator/components/com_fields/src/View/Fields/HtmlView.php +++ b/code/administrator/components/com_fields/src/View/Fields/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Display a warning if the fields system plugin is disabled - if (!PluginHelper::isEnabled('system', 'fields')) - { - $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . FieldsHelper::getFieldsPluginId()); - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_FIELDS_SYSTEM_PLUGIN_NOT_ENABLED', $link), 'warning'); - } - - // Only add toolbar when not in modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Adds the toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $fieldId = $this->state->get('filter.field_id'); - $component = $this->state->get('filter.component'); - $section = $this->state->get('filter.section'); - $canDo = ContentHelper::getActions($component, 'field', $fieldId); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - // Avoid nonsense situation. - if ($component == 'com_fields') - { - return; - } - - // Load extension language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - $title = Text::sprintf('COM_FIELDS_VIEW_FIELDS_TITLE', Text::_(strtoupper($component))); - - // Prepare the toolbar. - ToolbarHelper::title($title, 'puzzle-piece fields ' . substr($component, 4) . ($section ? "-$section" : '') . '-fields'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('field.add'); - } - - if ($canDo->get('core.edit.state') || Factory::getUser()->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('fields.publish')->listCheck(true); - - $childBar->unpublish('fields.unpublish')->listCheck(true); - - $childBar->archive('fields.archive')->listCheck(true); - } - - if (Factory::getUser()->authorise('core.admin')) - { - $childBar->checkin('fields.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && !$this->state->get('filter.state') == -2) - { - $childBar->trash('fields.trash')->listCheck(true); - } - - // Add a batch button - if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete', $component)) - { - $toolbar->delete('fields.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences($component); - } - - $toolbar->help('Component:_Fields'); - } + /** + * @var \Joomla\CMS\Form\Form + * + * @since 3.7.0 + */ + public $filterForm; + + /** + * @var array + * + * @since 3.7.0 + */ + public $activeFilters; + + /** + * @var array + * + * @since 3.7.0 + */ + protected $items; + + /** + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.7.0 + */ + protected $pagination; + + /** + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see \Joomla\CMS\MVC\View\HtmlView::loadTemplate() + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Display a warning if the fields system plugin is disabled + if (!PluginHelper::isEnabled('system', 'fields')) { + $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . FieldsHelper::getFieldsPluginId()); + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_FIELDS_SYSTEM_PLUGIN_NOT_ENABLED', $link), 'warning'); + } + + // Only add toolbar when not in modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Adds the toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $fieldId = $this->state->get('filter.field_id'); + $component = $this->state->get('filter.component'); + $section = $this->state->get('filter.section'); + $canDo = ContentHelper::getActions($component, 'field', $fieldId); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + // Avoid nonsense situation. + if ($component == 'com_fields') { + return; + } + + // Load extension language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + $title = Text::sprintf('COM_FIELDS_VIEW_FIELDS_TITLE', Text::_(strtoupper($component))); + + // Prepare the toolbar. + ToolbarHelper::title($title, 'puzzle-piece fields ' . substr($component, 4) . ($section ? "-$section" : '') . '-fields'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('field.add'); + } + + if ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('fields.publish')->listCheck(true); + + $childBar->unpublish('fields.unpublish')->listCheck(true); + + $childBar->archive('fields.archive')->listCheck(true); + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + $childBar->checkin('fields.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && !$this->state->get('filter.state') == -2) { + $childBar->trash('fields.trash')->listCheck(true); + } + + // Add a batch button + if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete', $component)) { + $toolbar->delete('fields.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences($component); + } + + $toolbar->help('Component:_Fields'); + } } diff --git a/code/administrator/components/com_fields/src/View/Group/HtmlView.php b/code/administrator/components/com_fields/src/View/Group/HtmlView.php index 0a10b060..cf3e8caa 100644 --- a/code/administrator/components/com_fields/src/View/Group/HtmlView.php +++ b/code/administrator/components/com_fields/src/View/Group/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - $component = ''; - $parts = FieldsHelper::extract($this->state->get('filter.context')); - - if ($parts) - { - $component = $parts[0]; - } - - $this->canDo = ContentHelper::getActions($component, 'fieldgroup', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - Factory::getApplication()->input->set('hidemainmenu', true); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Adds the toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $component = ''; - $parts = FieldsHelper::extract($this->state->get('filter.context')); - - if ($parts) - { - $component = $parts[0]; - } - - $userId = Factory::getUser()->get('id'); - $canDo = $this->canDo; - - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Avoid nonsense situation. - if ($component == 'com_fields') - { - return; - } - - // Load component language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - $title = Text::sprintf('COM_FIELDS_VIEW_GROUP_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE', Text::_(strtoupper($component))); - - // Prepare the toolbar. - ToolbarHelper::title( - $title, - 'puzzle-piece field-' . ($isNew ? 'add' : 'edit') . ' ' . substr($component, 4) . '-group-' . - ($isNew ? 'add' : 'edit') - ); - - $toolbarButtons = []; - - // For new records, check the create permission. - if ($isNew) - { - ToolbarHelper::apply('group.apply'); - - ToolbarHelper::saveGroup( - [ - ['save', 'group.save'], - ['save2new', 'group.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('group.cancel'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - ToolbarHelper::apply('group.apply'); - - $toolbarButtons[] = ['save', 'group.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'group.save2new']; - } - } - - // If an existing item, can save to a copy. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'group.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::help('Component:_New_or_Edit_Field_Group'); - } + /** + * @var \Joomla\CMS\Form\Form + * + * @since 3.7.0 + */ + protected $form; + + /** + * @var CMSObject + * + * @since 3.7.0 + */ + protected $item; + + /** + * @var CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * + * @since 3.7.0 + */ + protected $canDo; + + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see JViewLegacy::loadTemplate() + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + $component = ''; + $parts = FieldsHelper::extract($this->state->get('filter.context')); + + if ($parts) { + $component = $parts[0]; + } + + $this->canDo = ContentHelper::getActions($component, 'fieldgroup', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + Factory::getApplication()->input->set('hidemainmenu', true); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Adds the toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $component = ''; + $parts = FieldsHelper::extract($this->state->get('filter.context')); + + if ($parts) { + $component = $parts[0]; + } + + $userId = $this->getCurrentUser()->get('id'); + $canDo = $this->canDo; + + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Avoid nonsense situation. + if ($component == 'com_fields') { + return; + } + + // Load component language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + $title = Text::sprintf('COM_FIELDS_VIEW_GROUP_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE', Text::_(strtoupper($component))); + + // Prepare the toolbar. + ToolbarHelper::title( + $title, + 'puzzle-piece field-' . ($isNew ? 'add' : 'edit') . ' ' . substr($component, 4) . '-group-' . + ($isNew ? 'add' : 'edit') + ); + + $toolbarButtons = []; + + // For new records, check the create permission. + if ($isNew) { + ToolbarHelper::apply('group.apply'); + + ToolbarHelper::saveGroup( + [ + ['save', 'group.save'], + ['save2new', 'group.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('group.cancel'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + ToolbarHelper::apply('group.apply'); + + $toolbarButtons[] = ['save', 'group.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'group.save2new']; + } + } + + // If an existing item, can save to a copy. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'group.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::help('Component:_New_or_Edit_Field_Group'); + } } diff --git a/code/administrator/components/com_fields/src/View/Groups/HtmlView.php b/code/administrator/components/com_fields/src/View/Groups/HtmlView.php index 107bc27b..4b36150b 100644 --- a/code/administrator/components/com_fields/src/View/Groups/HtmlView.php +++ b/code/administrator/components/com_fields/src/View/Groups/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Display a warning if the fields system plugin is disabled - if (!PluginHelper::isEnabled('system', 'fields')) - { - $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . FieldsHelper::getFieldsPluginId()); - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_FIELDS_SYSTEM_PLUGIN_NOT_ENABLED', $link), 'warning'); - } - - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - parent::display($tpl); - } - - /** - * Adds the toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $groupId = $this->state->get('filter.group_id'); - $component = ''; - $parts = FieldsHelper::extract($this->state->get('filter.context')); - - if ($parts) - { - $component = $parts[0]; - } - - $canDo = ContentHelper::getActions($component, 'fieldgroup', $groupId); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - // Avoid nonsense situation. - if ($component == 'com_fields') - { - return; - } - - // Load component language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - $title = Text::sprintf('COM_FIELDS_VIEW_GROUPS_TITLE', Text::_(strtoupper($component))); - - // Prepare the toolbar. - ToolbarHelper::title($title, 'puzzle-piece fields ' . substr($component, 4) . '-groups'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('group.add'); - } - - if ($canDo->get('core.edit.state') || Factory::getUser()->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('groups.publish')->listCheck(true); - - $childBar->unpublish('groups.unpublish')->listCheck(true); - - $childBar->archive('groups.archive')->listCheck(true); - } - - if (Factory::getUser()->authorise('core.admin')) - { - $childBar->checkin('groups.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && !$this->state->get('filter.state') == -2) - { - $childBar->trash('groups.trash')->listCheck(true); - } - - // Add a batch button - if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete', $component)) - { - $toolbar->delete('groups.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences($component); - } - - $toolbar->help('Component:_Field_Groups'); - } + /** + * @var \Joomla\CMS\Form\Form + * + * @since 3.7.0 + */ + public $filterForm; + + /** + * @var array + * + * @since 3.7.0 + */ + public $activeFilters; + + /** + * @var array + * + * @since 3.7.0 + */ + protected $items; + + /** + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.7.0 + */ + protected $pagination; + + /** + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see HtmlView::loadTemplate() + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Display a warning if the fields system plugin is disabled + if (!PluginHelper::isEnabled('system', 'fields')) { + $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . FieldsHelper::getFieldsPluginId()); + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_FIELDS_SYSTEM_PLUGIN_NOT_ENABLED', $link), 'warning'); + } + + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + parent::display($tpl); + } + + /** + * Adds the toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $groupId = $this->state->get('filter.group_id'); + $component = ''; + $parts = FieldsHelper::extract($this->state->get('filter.context')); + + if ($parts) { + $component = $parts[0]; + } + + $canDo = ContentHelper::getActions($component, 'fieldgroup', $groupId); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + // Avoid nonsense situation. + if ($component == 'com_fields') { + return; + } + + // Load component language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + $title = Text::sprintf('COM_FIELDS_VIEW_GROUPS_TITLE', Text::_(strtoupper($component))); + + // Prepare the toolbar. + ToolbarHelper::title($title, 'puzzle-piece fields ' . substr($component, 4) . '-groups'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('group.add'); + } + + if ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('groups.publish')->listCheck(true); + + $childBar->unpublish('groups.unpublish')->listCheck(true); + + $childBar->archive('groups.archive')->listCheck(true); + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + $childBar->checkin('groups.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && !$this->state->get('filter.state') == -2) { + $childBar->trash('groups.trash')->listCheck(true); + } + + // Add a batch button + if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete', $component)) { + $toolbar->delete('groups.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences($component); + } + + $toolbar->help('Component:_Field_Groups'); + } } diff --git a/code/administrator/components/com_fields/tmpl/field/edit.php b/code/administrator/components/com_fields/tmpl/field/edit.php index a357d89c..1f3a11fa 100644 --- a/code/administrator/components/com_fields/tmpl/field/edit.php +++ b/code/administrator/components/com_fields/tmpl/field/edit.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -22,77 +24,78 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_fields.admin-field-edit'); + ->useScript('form.validate') + ->useScript('com_fields.admin-field-edit'); ?>
- + -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
- form->renderField('type'); ?> - form->renderField('name'); ?> - form->renderField('label'); ?> - form->renderField('description'); ?> - form->renderField('required'); ?> - form->renderField('only_use_in_subform'); ?> - form->renderField('default_value'); ?> +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> + +
+
+ form->renderField('type'); ?> + form->renderField('name'); ?> + form->renderField('label'); ?> + form->renderField('description'); ?> + form->renderField('required'); ?> + form->renderField('only_use_in_subform'); ?> + form->renderField('default_value'); ?> - form->getFieldsets('fieldparams') as $name => $fieldSet) : ?> - form->getFieldset($name) as $field) : ?> - renderField(); ?> - - -
-
- set('fields', - array( - array( - 'published', - 'state', - 'enabled', - ), - 'group_id', - 'assigned_cat_ids', - 'access', - 'language', - 'note', - ) - ); ?> - - set('fields', null); ?> -
-
- - set('ignore_fieldsets', array('fieldparams')); ?> - - -
- -
- -
-
- - canDo->get('core.admin')) : ?> - -
- -
- form->getInput('rules'); ?> -
-
- - - - form->getInput('context'); ?> - - -
+ form->getFieldsets('fieldparams') as $name => $fieldSet) : ?> + form->getFieldset($name) as $field) : ?> + renderField(); ?> + + +
+
+ set( + 'fields', + array( + array( + 'published', + 'state', + 'enabled', + ), + 'group_id', + 'assigned_cat_ids', + 'access', + 'language', + 'note', + ) + ); ?> + + set('fields', null); ?> +
+
+ + set('ignore_fieldsets', array('fieldparams')); ?> + + +
+ +
+ +
+
+ + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + form->getInput('context'); ?> + + +
diff --git a/code/administrator/components/com_fields/tmpl/fields/default.php b/code/administrator/components/com_fields/tmpl/fields/default.php index 4b9666c9..7bc0654b 100644 --- a/code/administrator/components/com_fields/tmpl/fields/default.php +++ b/code/administrator/components/com_fields/tmpl/fields/default.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Categories\Categories; @@ -18,9 +20,10 @@ use Joomla\CMS\Session\Session; use Joomla\Component\Fields\Administrator\Helper\FieldsHelper; -/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ +/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -37,184 +40,186 @@ $category = Categories::getInstance(str_replace('com_', '', $component) . '.' . $section); // If there is no category for the component and section, then check the component only -if (!$category) -{ - $category = Categories::getInstance(str_replace('com_', '', $component)); +if (!$category) { + $category = Categories::getInstance(str_replace('com_', '', $component)); } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_fields&task=fields.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_fields&task=fields.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $searchToolsOptions = []; // Only show field contexts filter if there are more than one option -if (count($this->filterForm->getField('context')->options) > 1) -{ - $searchToolsOptions['selectorFieldName'] = 'context'; +if (count($this->filterForm->getField('context')->options) > 1) { + $searchToolsOptions['selectorFieldName'] = 'context'; } ?>
-
-
-
- $this, 'options' => $searchToolsOptions)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : ?> - - authorise('core.edit', $component . '.field.' . $item->id); ?> - authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); ?> - authorise('core.edit.own', $component . '.field.' . $item->id) && $item->created_user_id == $userId; ?> - authorise('core.edit.state', $component . '.field.' . $item->id) && $canCheckin; ?> - - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - - - - state, $i, 'fields.', $canChange, 'cb'); ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'fields.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- note)) : ?> - escape($item->name)); ?> - - escape($item->name), $this->escape($item->note)); ?> - -
- only_use_in_subform) : ?> -
- -
- -
- - id); ?> - - - - id); ?> - - -
- -
-
- escape($item->type); ?> - - escape($item->group_title); ?> - - escape($item->access_level); ?> - - - - id; ?> -
+
+
+
+ $this, 'options' => $searchToolsOptions)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : ?> + + authorise('core.edit', $component . '.field.' . $item->id); ?> + authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); ?> + authorise('core.edit.own', $component . '.field.' . $item->id) && $item->created_user_id == $userId; ?> + authorise('core.edit.state', $component . '.field.' . $item->id) && $canCheckin; ?> + + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + + + + state, $i, 'fields.', $canChange, 'cb'); ?> + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'fields.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ note)) : ?> + escape($item->name)); ?> + + escape($item->name), $this->escape($item->note)); ?> + +
+ only_use_in_subform) : ?> +
+ +
+ +
+ + id); ?> + + + + id); ?> + + +
+ +
+
+ escape($item->type); ?> + + escape($item->group_title); ?> + + escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', $component) - && $user->authorise('core.edit', $component) - && $user->authorise('core.edit.state', $component)) : ?> - Text::_('COM_FIELDS_VIEW_FIELDS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer') - ), - $this->loadTemplate('batch_body') - ); ?> - - - - - -
-
-
+ + authorise('core.create', $component) + && $user->authorise('core.edit', $component) + && $user->authorise('core.edit.state', $component) + ) : ?> + Text::_('COM_FIELDS_VIEW_FIELDS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer') + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + +
+
+
diff --git a/code/administrator/components/com_fields/tmpl/fields/default_batch_body.php b/code/administrator/components/com_fields/tmpl/fields/default_batch_body.php index 700f2669..c5b1e0c0 100644 --- a/code/administrator/components/com_fields/tmpl/fields/default_batch_body.php +++ b/code/administrator/components/com_fields/tmpl/fields/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\HTML\HTMLHelper; @@ -22,43 +24,43 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
- - -
- -
-
- - -
-
-
-
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + +
+ +
+
+ + +
+
+
+
diff --git a/code/administrator/components/com_fields/tmpl/fields/default_batch_footer.php b/code/administrator/components/com_fields/tmpl/fields/default_batch_footer.php index 512cb848..b48333c6 100644 --- a/code/administrator/components/com_fields/tmpl/fields/default_batch_footer.php +++ b/code/administrator/components/com_fields/tmpl/fields/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/code/administrator/components/com_fields/tmpl/fields/modal.php b/code/administrator/components/com_fields/tmpl/fields/modal.php index 5f7d1c11..15a39f1c 100644 --- a/code/administrator/components/com_fields/tmpl/fields/modal.php +++ b/code/administrator/components/com_fields/tmpl/fields/modal.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -15,9 +17,8 @@ use Joomla\CMS\Router\Route; use Joomla\CMS\Session\Session; -if (Factory::getApplication()->isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if (Factory::getApplication()->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -30,93 +31,93 @@ ?>
-
+ - $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - foreach ($this->items as $i => $item) : - ?> - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - -
- - - - - escape($item->title); ?> - - group_id ? $this->escape($item->group_title) : Text::_('JNONE'); ?> - - type; ?> - - escape($item->access_level); ?> - - - - id; ?> -
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + foreach ($this->items as $i => $item) : + ?> + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + +
+ + + + + escape($item->title); ?> + + group_id ? $this->escape($item->group_title) : Text::_('JNONE'); ?> + + type; ?> + + escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - + + + -
+
diff --git a/code/administrator/components/com_fields/tmpl/group/edit.php b/code/administrator/components/com_fields/tmpl/group/edit.php index 45761b81..97bb6db4 100644 --- a/code/administrator/components/com_fields/tmpl/group/edit.php +++ b/code/administrator/components/com_fields/tmpl/group/edit.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -17,7 +19,7 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $input = $app->input; @@ -27,56 +29,57 @@ ?>
- -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
- form->renderField('label'); ?> - form->renderField('description'); ?> -
-
- set('fields', - array( - array( - 'published', - 'state', - 'enabled', - ), - 'access', - 'language', - 'note', - ) - ); ?> - - set('fields', null); ?> -
-
- - -
- -
- -
-
- - set('ignore_fieldsets', array('fieldparams')); ?> - - canDo->get('core.admin')) : ?> - -
- -
- form->getInput('rules'); ?> -
-
- - - - form->getInput('context'); ?> - - -
+ +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> + +
+
+ form->renderField('label'); ?> + form->renderField('description'); ?> +
+
+ set( + 'fields', + array( + array( + 'published', + 'state', + 'enabled', + ), + 'access', + 'language', + 'note', + ) + ); ?> + + set('fields', null); ?> +
+
+ + set('ignore_fieldsets', array('fieldparams')); ?> + + +
+ +
+ +
+
+ + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + form->getInput('context'); ?> + + +
diff --git a/code/administrator/components/com_fields/tmpl/groups/default.php b/code/administrator/components/com_fields/tmpl/groups/default.php index 5bce2520..1ce17a17 100644 --- a/code/administrator/components/com_fields/tmpl/groups/default.php +++ b/code/administrator/components/com_fields/tmpl/groups/default.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -17,9 +19,10 @@ use Joomla\CMS\Session\Session; use Joomla\Component\Fields\Administrator\Helper\FieldsHelper; -/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ +/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -28,9 +31,8 @@ $component = ''; $parts = FieldsHelper::extract($this->state->get('filter.context')); -if ($parts) -{ - $component = $this->escape($parts[0]); +if ($parts) { + $component = $this->escape($parts[0]); } $listOrder = $this->escape($this->state->get('list.ordering')); @@ -38,10 +40,9 @@ $ordering = ($listOrder == 'a.ordering'); $saveOrder = ($listOrder == 'a.ordering' && strtolower($listDirn) == 'asc'); -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_fields&task=groups.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_fields&task=groups.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $context = $this->escape($this->state->get('filter.context')); @@ -49,140 +50,144 @@ $searchToolsOptions = []; // Only show field contexts filter if there are more than one option -if (count($this->filterForm->getField('context')->options) > 1) -{ - $searchToolsOptions['selectorFieldName'] = 'context'; +if (count($this->filterForm->getField('context')->options) > 1) { + $searchToolsOptions['selectorFieldName'] = 'context'; } ?>
-
-
-
- $this, 'options' => $searchToolsOptions)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : ?> - - authorise('core.edit', $component . '.fieldgroup.' . $item->id); ?> - authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); ?> - authorise('core.edit.own', $component . '.fieldgroup.' . $item->id) && $item->created_by == $userId; ?> - authorise('core.edit.state', $component . '.fieldgroup.' . $item->id) && $canCheckin; ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - - - - state, $i, 'groups.', $canChange, 'cb'); ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'groups.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- note) : ?> - escape($item->note)); ?> - -
-
-
- escape($item->access_level); ?> - - - - id; ?> -
+
+
+
+ $this, 'options' => $searchToolsOptions)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : ?> + + authorise('core.edit', $component . '.fieldgroup.' . $item->id); ?> + authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); ?> + authorise('core.edit.own', $component . '.fieldgroup.' . $item->id) && $item->created_by == $userId; ?> + authorise('core.edit.state', $component . '.fieldgroup.' . $item->id) && $canCheckin; ?> + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + + + + state, $i, 'groups.', $canChange, 'cb'); ?> + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'groups.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ note) : ?> + escape($item->note)); ?> + +
+
+
+ escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', $component) - && $user->authorise('core.edit', $component) - && $user->authorise('core.edit.state', $component)) : ?> - Text::_('COM_FIELDS_VIEW_GROUPS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer') - ), - $this->loadTemplate('batch_body') - ); ?> - - - - - -
-
-
+ + authorise('core.create', $component) + && $user->authorise('core.edit', $component) + && $user->authorise('core.edit.state', $component) + ) : ?> + Text::_('COM_FIELDS_VIEW_GROUPS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer') + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + +
+
+
diff --git a/code/administrator/components/com_fields/tmpl/groups/default_batch_body.php b/code/administrator/components/com_fields/tmpl/groups/default_batch_body.php index 9b44ea54..97e7bad0 100644 --- a/code/administrator/components/com_fields/tmpl/groups/default_batch_body.php +++ b/code/administrator/components/com_fields/tmpl/groups/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Multilanguage; @@ -13,18 +15,18 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
+
+ +
+
+ +
+
+ +
+
+ +
+
+
diff --git a/code/administrator/components/com_fields/tmpl/groups/default_batch_footer.php b/code/administrator/components/com_fields/tmpl/groups/default_batch_footer.php index a0b72157..bc7cd353 100644 --- a/code/administrator/components/com_fields/tmpl/groups/default_batch_footer.php +++ b/code/administrator/components/com_fields/tmpl/groups/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/code/administrator/components/com_finder/config.xml b/code/administrator/components/com_finder/config.xml index 33512e8a..261b7d7d 100644 --- a/code/administrator/components/com_finder/config.xml +++ b/code/administrator/components/com_finder/config.xml @@ -5,8 +5,18 @@
+ + + + + J300 - - com_finder Joomla! Project (C) 2011 Open Source Matters, Inc. - August 2011 + 2011-08 GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org www.joomla.org diff --git a/code/administrator/components/com_finder/helpers/indexer/adapter.php b/code/administrator/components/com_finder/helpers/indexer/adapter.php index 02316348..e00a38c6 100644 --- a/code/administrator/components/com_finder/helpers/indexer/adapter.php +++ b/code/administrator/components/com_finder/helpers/indexer/adapter.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\Component\Finder\Administrator\Helper\LanguageHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Finder language helper class. * diff --git a/code/administrator/components/com_finder/services/provider.php b/code/administrator/components/com_finder/services/provider.php index 7688f99c..2fc098a5 100644 --- a/code/administrator/components/com_finder/services/provider.php +++ b/code/administrator/components/com_finder/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Finder')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Finder')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Finder')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Finder')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Finder')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Finder')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new FinderComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new FinderComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_finder/src/Controller/DisplayController.php b/code/administrator/components/com_finder/src/Controller/DisplayController.php index aaa94590..820f6b65 100644 --- a/code/administrator/components/com_finder/src/Controller/DisplayController.php +++ b/code/administrator/components/com_finder/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'index', 'word'); - $layout = $this->input->get('layout', 'index', 'word'); - $filterId = $this->input->get('filter_id', null, 'int'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean A Controller object to support chaining or false on failure. + * + * @since 2.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'index', 'word'); + $layout = $this->input->get('layout', 'index', 'word'); + $filterId = $this->input->get('filter_id', null, 'int'); - if ($view === 'index') - { - $pluginEnabled = PluginHelper::isEnabled('content', 'finder'); + if ($view === 'index') { + $pluginEnabled = PluginHelper::isEnabled('content', 'finder'); - if (!$pluginEnabled) - { - $finderPluginId = FinderHelper::getFinderPluginId(); - $link = HTMLHelper::_( - 'link', - '#plugin' . $finderPluginId . 'Modal', - Text::_('COM_FINDER_CONTENT_PLUGIN'), - 'class="alert-link" data-bs-toggle="modal" id="title-' . $finderPluginId . '"' - ); - $this->app->enqueueMessage(Text::sprintf('COM_FINDER_INDEX_PLUGIN_CONTENT_NOT_ENABLED_LINK', $link), 'warning'); - } - } + if (!$pluginEnabled) { + $finderPluginId = FinderHelper::getFinderPluginId(); + $link = HTMLHelper::_( + 'link', + '#plugin' . $finderPluginId . 'Modal', + Text::_('COM_FINDER_CONTENT_PLUGIN'), + 'class="alert-link" data-bs-toggle="modal" id="title-' . $finderPluginId . '"' + ); + $this->app->enqueueMessage(Text::sprintf('COM_FINDER_INDEX_PLUGIN_CONTENT_NOT_ENABLED_LINK', $link), 'warning'); + } + } - // Check for edit form. - if ($view === 'filter' && $layout === 'edit' && !$this->checkEditId('com_finder.edit.filter', $filterId)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $f_id), 'error'); - } + // Check for edit form. + if ($view === 'filter' && $layout === 'edit' && !$this->checkEditId('com_finder.edit.filter', $filterId)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $f_id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_finder&view=filters', false)); + $this->setRedirect(Route::_('index.php?option=com_finder&view=filters', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/code/administrator/components/com_finder/src/Controller/FilterController.php b/code/administrator/components/com_finder/src/Controller/FilterController.php index bd85d980..5c65fcd6 100644 --- a/code/administrator/components/com_finder/src/Controller/FilterController.php +++ b/code/administrator/components/com_finder/src/Controller/FilterController.php @@ -1,4 +1,5 @@ checkToken(); - - /** @var \Joomla\Component\Finder\Administrator\Model\FilterModel $model */ - $model = $this->getModel(); - $table = $model->getTable(); - $data = $this->input->post->get('jform', array(), 'array'); - $checkin = $table->hasField('checked_out'); - $context = "$this->option.edit.$this->context"; - $task = $this->getTask(); - - // Determine the name of the primary key for the data. - if (empty($key)) - { - $key = $table->getKeyName(); - } - - // To avoid data collisions the urlVar may be different from the primary key. - if (empty($urlVar)) - { - $urlVar = $key; - } - - $recordId = $this->input->get($urlVar, '', 'int'); - - if (!$this->checkEditId($context, $recordId)) - { - // Somehow the person just went to the form and tried to save it. We don't allow that. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $recordId), 'error'); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - - return false; - } - - // Populate the row id from the session. - $data[$key] = $recordId; - - // The save2copy task needs to be handled slightly differently. - if ($task === 'save2copy') - { - // Check-in the original row. - if ($checkin && $model->checkin($data[$key]) === false) - { - // Check-in failed. Go back to the item and display a notice. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); - } - - $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $urlVar)); - - return false; - } - - // Reset the ID and then treat the request as for Apply. - $data[$key] = 0; - $task = 'apply'; - } - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - - return false; - } - - // Validate the posted data. - // Sometimes the form needs some posted data, such as for plugins and modules. - $form = $model->getForm($data, false); - - if (!$form) - { - $this->app->enqueueMessage($model->getError(), 'error'); - - return false; - } - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) - ); - - return false; - } - - // Get and sanitize the filter data. - $validData['data'] = $this->input->post->get('t', array(), 'array'); - $validData['data'] = array_unique($validData['data']); - $validData['data'] = ArrayHelper::toInteger($validData['data']); - - // Remove any values of zero. - if (array_search(0, $validData['data'], true)) - { - unset($validData['data'][array_search(0, $validData['data'], true)]); - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) - ); - - return false; - } - - // Save succeeded, so check-in the record. - if ($checkin && $model->checkin($validData[$key]) === false) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - // Check-in failed, so go back to the record and display a notice. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); - $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key)); - - return false; - } - - $this->setMessage( - Text::_( - ($this->app->getLanguage()->hasKey($this->text_prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS') - ? $this->text_prefix : 'JLIB_APPLICATION') . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS' - ) - ); - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the record data in the session. - $recordId = $model->getState($this->context . '.id'); - $this->holdEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - $model->checkout($recordId); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) - ); - - break; - - case 'save2new': - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(null, $key), false) - ); - - break; - - default: - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - - // Redirect to the list screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false) - ); - - break; - } - - // Invoke the postSave method to allow for the child class to access the model. - $this->postSaveHook($model, $validData); - - return true; - } + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 2.5 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var \Joomla\Component\Finder\Administrator\Model\FilterModel $model */ + $model = $this->getModel(); + $table = $model->getTable(); + $data = $this->input->post->get('jform', array(), 'array'); + $checkin = $table->hasField('checked_out'); + $context = "$this->option.edit.$this->context"; + $task = $this->getTask(); + + // Determine the name of the primary key for the data. + if (empty($key)) { + $key = $table->getKeyName(); + } + + // To avoid data collisions the urlVar may be different from the primary key. + if (empty($urlVar)) { + $urlVar = $key; + } + + $recordId = $this->input->get($urlVar, '', 'int'); + + if (!$this->checkEditId($context, $recordId)) { + // Somehow the person just went to the form and tried to save it. We don't allow that. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $recordId), 'error'); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + + return false; + } + + // Populate the row id from the session. + $data[$key] = $recordId; + + // The save2copy task needs to be handled slightly differently. + if ($task === 'save2copy') { + // Check-in the original row. + if ($checkin && $model->checkin($data[$key]) === false) { + // Check-in failed. Go back to the item and display a notice. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); + } + + $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $urlVar)); + + return false; + } + + // Reset the ID and then treat the request as for Apply. + $data[$key] = 0; + $task = 'apply'; + } + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + + return false; + } + + // Validate the posted data. + // Sometimes the form needs some posted data, such as for plugins and modules. + $form = $model->getForm($data, false); + + if (!$form) { + $this->app->enqueueMessage($model->getError(), 'error'); + + return false; + } + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) + ); + + return false; + } + + // Get and sanitize the filter data. + $validData['data'] = $this->input->post->get('t', array(), 'array'); + $validData['data'] = array_unique($validData['data']); + $validData['data'] = ArrayHelper::toInteger($validData['data']); + + // Remove any values of zero. + if (array_search(0, $validData['data'], true)) { + unset($validData['data'][array_search(0, $validData['data'], true)]); + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) + ); + + return false; + } + + // Save succeeded, so check-in the record. + if ($checkin && $model->checkin($validData[$key]) === false) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + // Check-in failed, so go back to the record and display a notice. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); + $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key)); + + return false; + } + + $this->setMessage( + Text::_( + ($this->app->getLanguage()->hasKey($this->text_prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS') + ? $this->text_prefix : 'JLIB_APPLICATION') . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS' + ) + ); + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the record data in the session. + $recordId = $model->getState($this->context . '.id'); + $this->holdEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + $model->checkout($recordId); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) + ); + + break; + + case 'save2new': + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(null, $key), false) + ); + + break; + + default: + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + + // Redirect to the list screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false) + ); + + break; + } + + // Invoke the postSave method to allow for the child class to access the model. + $this->postSaveHook($model, $validData); + + return true; + } } diff --git a/code/administrator/components/com_finder/src/Controller/FiltersController.php b/code/administrator/components/com_finder/src/Controller/FiltersController.php index ade78adb..39faa67c 100644 --- a/code/administrator/components/com_finder/src/Controller/FiltersController.php +++ b/code/administrator/components/com_finder/src/Controller/FiltersController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 2.5 + */ + public function getModel($name = 'Filter', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_finder/src/Controller/IndexController.php b/code/administrator/components/com_finder/src/Controller/IndexController.php index 234e8432..4a06e478 100644 --- a/code/administrator/components/com_finder/src/Controller/IndexController.php +++ b/code/administrator/components/com_finder/src/Controller/IndexController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to purge all indexed links from the database. - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function purge() - { - $this->checkToken(); - - // Remove the script time limit. - @set_time_limit(0); - - /** @var \Joomla\Component\Finder\Administrator\Model\IndexModel $model */ - $model = $this->getModel('Index', 'Administrator'); - - // Attempt to purge the index. - $return = $model->purge(); - - if (!$return) - { - $message = Text::_('COM_FINDER_INDEX_PURGE_FAILED', $model->getError()); - $this->setRedirect('index.php?option=com_finder&view=index', $message); - - return false; - } - else - { - $message = Text::_('COM_FINDER_INDEX_PURGE_SUCCESS'); - $this->setRedirect('index.php?option=com_finder&view=index', $message); - - return true; - } - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 2.5 + */ + public function getModel($name = 'Index', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to optimise the index by removing orphaned entries. + * + * @return boolean True on success. + * + * @since 4.2.0 + */ + public function optimise() + { + $this->checkToken(); + + // Optimise the index by first running the garbage collection + PluginHelper::importPlugin('finder'); + $this->app->triggerEvent('onFinderGarbageCollection'); + + // Now run the optimisation method from the indexer + $indexer = new Indexer(); + $indexer->optimize(); + + $message = Text::_('COM_FINDER_INDEX_OPTIMISE_FINISHED'); + $this->setRedirect('index.php?option=com_finder&view=index', $message); + + return true; + } + + /** + * Method to purge all indexed links from the database. + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function purge() + { + $this->checkToken(); + + // Remove the script time limit. + @set_time_limit(0); + + /** @var \Joomla\Component\Finder\Administrator\Model\IndexModel $model */ + $model = $this->getModel('Index', 'Administrator'); + + // Attempt to purge the index. + $return = $model->purge(); + + if (!$return) { + $message = Text::_('COM_FINDER_INDEX_PURGE_FAILED', $model->getError()); + $this->setRedirect('index.php?option=com_finder&view=index', $message); + + return false; + } else { + $message = Text::_('COM_FINDER_INDEX_PURGE_SUCCESS'); + $this->setRedirect('index.php?option=com_finder&view=index', $message); + + return true; + } + } } diff --git a/code/administrator/components/com_finder/src/Controller/IndexerController.php b/code/administrator/components/com_finder/src/Controller/IndexerController.php index 2852d739..ac732a3d 100644 --- a/code/administrator/components/com_finder/src/Controller/IndexerController.php +++ b/code/administrator/components/com_finder/src/Controller/IndexerController.php @@ -1,4 +1,5 @@ get('enable_logging', '0')) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'indexer.php'; - Log::addLogger($options); - } - - // Log the start - try - { - Log::add('Starting the indexer', Log::INFO); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // We don't want this form to be cached. - $this->app->allowCache(false); - - // Put in a buffer to silence noise. - ob_start(); - - // Reset the indexer state. - Indexer::resetState(); - - // Import the finder plugins. - PluginHelper::importPlugin('finder'); - - // Add the indexer language to \JS - Text::script('COM_FINDER_AN_ERROR_HAS_OCCURRED'); - Text::script('COM_FINDER_NO_ERROR_RETURNED'); - - // Start the indexer. - try - { - // Trigger the onStartIndex event. - $this->app->triggerEvent('onStartIndex'); - - // Get the indexer state. - $state = Indexer::getState(); - $state->start = 1; - - // Send the response. - static::sendResponse($state); - } - - // Catch an exception and return the response. - catch (\Exception $e) - { - static::sendResponse($e); - } - } - - /** - * Method to run the next batch of content through the indexer. - * - * @return void - * - * @since 2.5 - */ - public function batch() - { - // Check for a valid token. If invalid, send a 403 with the error message. - if (!Session::checkToken('request')) - { - static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); - - return; - } - - $params = ComponentHelper::getParams('com_finder'); - - if ($params->get('enable_logging', '0')) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'indexer.php'; - Log::addLogger($options); - } - - // Log the start - try - { - Log::add('Starting the indexer batch process', Log::INFO); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // We don't want this form to be cached. - $this->app->allowCache(false); - - // Put in a buffer to silence noise. - ob_start(); - - // Remove the script time limit. - @set_time_limit(0); - - // Get the indexer state. - $state = Indexer::getState(); - - // Reset the batch offset. - $state->batchOffset = 0; - - // Update the indexer state. - Indexer::setState($state); - - // Import the finder plugins. - PluginHelper::importPlugin('finder'); - - /* - * We are going to swap out the raw document object with an HTML document - * in order to work around some plugins that don't do proper environment - * checks before trying to use HTML document functions. - */ - $lang = Factory::getLanguage(); - - // Get the document properties. - $attributes = array ( - 'charset' => 'utf-8', - 'lineend' => 'unix', - 'tab' => ' ', - 'language' => $lang->getTag(), - 'direction' => $lang->isRtl() ? 'rtl' : 'ltr' - ); - - // Start the indexer. - try - { - // Trigger the onBeforeIndex event. - Factory::getApplication()->triggerEvent('onBeforeIndex'); - - // Trigger the onBuildIndex event. - Factory::getApplication()->triggerEvent('onBuildIndex'); - - // Get the indexer state. - $state = Indexer::getState(); - $state->start = 0; - $state->complete = 0; - - // Log batch completion and memory high-water mark. - try - { - Log::add('Batch completed, peak memory usage: ' . number_format(memory_get_peak_usage(true)) . ' bytes', Log::INFO); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // Send the response. - static::sendResponse($state); - } - - // Catch an exception and return the response. - catch (\Exception $e) - { - // Send the response. - static::sendResponse($e); - } - } - - /** - * Method to optimize the index and perform any necessary cleanup. - * - * @return void - * - * @since 2.5 - */ - public function optimize() - { - // Check for a valid token. If invalid, send a 403 with the error message. - if (!Session::checkToken('request')) - { - static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); - - return; - } - - // We don't want this form to be cached. - $this->app->allowCache(false); - - // Put in a buffer to silence noise. - ob_start(); - - // Import the finder plugins. - PluginHelper::importPlugin('finder'); - - try - { - // Optimize the index - $indexer = new Indexer; - $indexer->optimize(); - - // Get the indexer state. - $state = Indexer::getState(); - $state->start = 0; - $state->complete = 1; - - // Send the response. - static::sendResponse($state); - } - - // Catch an exception and return the response. - catch (\Exception $e) - { - static::sendResponse($e); - } - } - - /** - * Method to handle a send a \JSON response. The body parameter - * can be an \Exception object for when an error has occurred or - * a CMSObject for a good response. - * - * @param \Joomla\CMS\Object\CMSObject|\Exception $data CMSObject on success, \Exception on error. [optional] - * - * @return void - * - * @since 2.5 - */ - public static function sendResponse($data = null) - { - $app = Factory::getApplication(); - - $params = ComponentHelper::getParams('com_finder'); - - if ($params->get('enable_logging', '0')) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'indexer.php'; - Log::addLogger($options); - } - - // Send the assigned error code if we are catching an exception. - if ($data instanceof \Exception) - { - try - { - Log::add($data->getMessage(), Log::ERROR); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - $app->setHeader('status', $data->getCode()); - } - - // Create the response object. - $response = new Response($data); - - if (\JDEBUG) - { - // Add the buffer and memory usage - $response->buffer = ob_get_contents(); - $response->memory = memory_get_usage(true); - } - - // Send the JSON response. - echo json_encode($response); - } + /** + * Method to start the indexer. + * + * @return void + * + * @since 2.5 + */ + public function start() + { + // Check for a valid token. If invalid, send a 403 with the error message. + if (!Session::checkToken('request')) { + static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); + + return; + } + + $params = ComponentHelper::getParams('com_finder'); + + if ($params->get('enable_logging', '0')) { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'indexer.php'; + Log::addLogger($options); + } + + // Log the start + try { + Log::add('Starting the indexer', Log::INFO); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // We don't want this form to be cached. + $this->app->allowCache(false); + + // Put in a buffer to silence noise. + ob_start(); + + // Reset the indexer state. + Indexer::resetState(); + + // Import the finder plugins. + PluginHelper::importPlugin('finder'); + + // Add the indexer language to \JS + Text::script('COM_FINDER_AN_ERROR_HAS_OCCURRED'); + Text::script('COM_FINDER_NO_ERROR_RETURNED'); + + // Start the indexer. + try { + // Trigger the onStartIndex event. + $this->app->triggerEvent('onStartIndex'); + + // Get the indexer state. + $state = Indexer::getState(); + $state->start = 1; + + // Send the response. + static::sendResponse($state); + } catch (\Exception $e) { + // Catch an exception and return the response. + static::sendResponse($e); + } + } + + /** + * Method to run the next batch of content through the indexer. + * + * @return void + * + * @since 2.5 + */ + public function batch() + { + // Check for a valid token. If invalid, send a 403 with the error message. + if (!Session::checkToken('request')) { + static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); + + return; + } + + $params = ComponentHelper::getParams('com_finder'); + + if ($params->get('enable_logging', '0')) { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'indexer.php'; + Log::addLogger($options); + } + + // Log the start + try { + Log::add('Starting the indexer batch process', Log::INFO); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // We don't want this form to be cached. + $this->app->allowCache(false); + + // Put in a buffer to silence noise. + ob_start(); + + // Remove the script time limit. + @set_time_limit(0); + + // Get the indexer state. + $state = Indexer::getState(); + + // Reset the batch offset. + $state->batchOffset = 0; + + // Update the indexer state. + Indexer::setState($state); + + // Import the finder plugins. + PluginHelper::importPlugin('finder'); + + /* + * We are going to swap out the raw document object with an HTML document + * in order to work around some plugins that don't do proper environment + * checks before trying to use HTML document functions. + */ + $lang = Factory::getLanguage(); + + // Get the document properties. + $attributes = array ( + 'charset' => 'utf-8', + 'lineend' => 'unix', + 'tab' => ' ', + 'language' => $lang->getTag(), + 'direction' => $lang->isRtl() ? 'rtl' : 'ltr' + ); + + // Start the indexer. + try { + // Trigger the onBeforeIndex event. + $this->app->triggerEvent('onBeforeIndex'); + + // Trigger the onBuildIndex event. + $this->app->triggerEvent('onBuildIndex'); + + // Get the indexer state. + $state = Indexer::getState(); + $state->start = 0; + $state->complete = 0; + + // Log batch completion and memory high-water mark. + try { + Log::add('Batch completed, peak memory usage: ' . number_format(memory_get_peak_usage(true)) . ' bytes', Log::INFO); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // Send the response. + static::sendResponse($state); + } catch (\Exception $e) { + // Catch an exception and return the response. + // Send the response. + static::sendResponse($e); + } + } + + /** + * Method to optimize the index and perform any necessary cleanup. + * + * @return void + * + * @since 2.5 + */ + public function optimize() + { + // Check for a valid token. If invalid, send a 403 with the error message. + if (!Session::checkToken('request')) { + static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); + + return; + } + + // We don't want this form to be cached. + $this->app->allowCache(false); + + // Put in a buffer to silence noise. + ob_start(); + + // Import the finder plugins. + PluginHelper::importPlugin('finder'); + + try { + // Optimize the index + $indexer = new Indexer(); + $indexer->optimize(); + + // Get the indexer state. + $state = Indexer::getState(); + $state->start = 0; + $state->complete = 1; + + // Send the response. + static::sendResponse($state); + } catch (\Exception $e) { + // Catch an exception and return the response. + static::sendResponse($e); + } + } + + /** + * Method to handle a send a \JSON response. The body parameter + * can be an \Exception object for when an error has occurred or + * a CMSObject for a good response. + * + * @param \Joomla\CMS\Object\CMSObject|\Exception $data CMSObject on success, \Exception on error. [optional] + * + * @return void + * + * @since 2.5 + */ + public static function sendResponse($data = null) + { + $app = Factory::getApplication(); + + $params = ComponentHelper::getParams('com_finder'); + + if ($params->get('enable_logging', '0')) { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'indexer.php'; + Log::addLogger($options); + } + + // Send the assigned error code if we are catching an exception. + if ($data instanceof \Exception) { + try { + Log::add($data->getMessage(), Log::ERROR); + } catch (\RuntimeException $exception) { + // Informational log only + } + + $app->setHeader('status', $data->getCode()); + } + + // Create the response object. + $response = new Response($data); + + if (\JDEBUG) { + // Add the buffer and memory usage + $response->buffer = ob_get_contents(); + $response->memory = memory_get_usage(true); + } + + // Send the JSON response. + echo json_encode($response); + } } diff --git a/code/administrator/components/com_finder/src/Controller/MapsController.php b/code/administrator/components/com_finder/src/Controller/MapsController.php index 149520ed..b94c9ee1 100644 --- a/code/administrator/components/com_finder/src/Controller/MapsController.php +++ b/code/administrator/components/com_finder/src/Controller/MapsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Maps', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_finder/src/Controller/SearchesController.php b/code/administrator/components/com_finder/src/Controller/SearchesController.php index 325893b9..a78a7629 100644 --- a/code/administrator/components/com_finder/src/Controller/SearchesController.php +++ b/code/administrator/components/com_finder/src/Controller/SearchesController.php @@ -1,4 +1,5 @@ getModel('Searches'); - - if (!$model->reset()) - { - $this->app->enqueueMessage($model->getError(), 'error'); - } - - $this->setRedirect('index.php?option=com_finder&view=searches'); - } + /** + * Method to reset the search log table. + * + * @return void + */ + public function reset() + { + // Check for request forgeries. + Session::checkToken() or jexit(Text::_('JINVALID_TOKEN')); + + $model = $this->getModel('Searches'); + + if (!$model->reset()) { + $this->app->enqueueMessage($model->getError(), 'error'); + } + + $this->setRedirect('index.php?option=com_finder&view=searches'); + } } diff --git a/code/administrator/components/com_finder/src/Extension/FinderComponent.php b/code/administrator/components/com_finder/src/Extension/FinderComponent.php index 0f861b3a..3550ebc9 100644 --- a/code/administrator/components/com_finder/src/Extension/FinderComponent.php +++ b/code/administrator/components/com_finder/src/Extension/FinderComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('finder', new Finder); - $this->getRegistry()->register('filter', new Filter); - $this->getRegistry()->register('query', new Query); - } + use RouterServiceTrait; + use HTMLRegistryAwareTrait; + + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $finder = new Finder(); + $finder->setDatabase($container->get(DatabaseInterface::class)); + + $this->getRegistry()->register('finder', $finder); + + $filter = new Filter(); + $filter->setDatabase($container->get(DatabaseInterface::class)); + + $this->getRegistry()->register('filter', $filter); + + $this->getRegistry()->register('query', new Query()); + } } diff --git a/code/administrator/components/com_finder/src/Field/BranchesField.php b/code/administrator/components/com_finder/src/Field/BranchesField.php index 6d3994ff..5ce95235 100644 --- a/code/administrator/components/com_finder/src/Field/BranchesField.php +++ b/code/administrator/components/com_finder/src/Field/BranchesField.php @@ -1,4 +1,5 @@ bootComponent('com_finder'); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.5 + */ + public function getOptions() + { + Factory::getApplication()->bootComponent('com_finder'); - return HTMLHelper::_('finder.mapslist'); - } + return HTMLHelper::_('finder.mapslist'); + } } diff --git a/code/administrator/components/com_finder/src/Field/ContentmapField.php b/code/administrator/components/com_finder/src/Field/ContentmapField.php index e48d7fa4..7bca2774 100644 --- a/code/administrator/components/com_finder/src/Field/ContentmapField.php +++ b/code/administrator/components/com_finder/src/Field/ContentmapField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select($db->quoteName('a.title', 'text')) - ->select($db->quoteName('a.id', 'value')) - ->select($db->quoteName('a.parent_id')) - ->select($db->quoteName('a.level')) - ->from($db->quoteName('#__finder_taxonomy', 'a')) - ->where($db->quoteName('a.parent_id') . ' <> 0') - ->order('a.title ASC'); - - $db->setQuery($query); - - try - { - $contentMap = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - return []; - } - - // Build the grouped list array. - if ($contentMap) - { - $parents = []; - - foreach ($contentMap as $item) - { - if (!isset($parents[$item->parent_id])) - { - $parents[$item->parent_id] = []; - } - - $parents[$item->parent_id][] = $item; - } - - foreach ($parents[1] as $branch) - { - $groups[$branch->text] = $this->prepareLevel($branch->value, $parents); - } - } - - // Merge any additional groups in the XML definition. - $groups = array_merge(parent::getGroups(), $groups); - - return $groups; - } - - /** - * Indenting and translating options for the list - * - * @param int $parent Parent ID to process - * @param array $parents Array of arrays of items with parent IDs as keys - * - * @return array The indented list of entries for this branch - * - * @since 4.1.5 - */ - private function prepareLevel($parent, $parents) - { - $lang = Factory::getLanguage(); - $entries = []; - - foreach ($parents[$parent] as $item) - { - $levelPrefix = str_repeat('- ', $item->level - 1); - - if (trim($item->text, '*') === 'Language') - { - $text = LanguageHelper::branchLanguageTitle($item->text); - } - else - { - $key = LanguageHelper::branchSingular($item->text); - $text = $lang->hasKey($key) ? Text::_($key) : $item->text; - } - - $entries[] = HTMLHelper::_('select.option', $item->value, $levelPrefix . $text); - - if (isset($parents[$item->value])) - { - $entries = array_merge($entries, $this->prepareLevel($item->value, $parents)); - } - } - - return $entries; - } + /** + * The form field type. + * + * @var string + * @since 3.6.0 + */ + public $type = 'ContentMap'; + + /** + * Method to get the list of content map options grouped by first level. + * + * @return array The field option objects as a nested array in groups. + * + * @since 3.6.0 + */ + protected function getGroups() + { + $groups = array(); + + // Get the database object and a new query object. + $db = $this->getDatabase(); + + // Main query. + $query = $db->getQuery(true) + ->select($db->quoteName('a.title', 'text')) + ->select($db->quoteName('a.id', 'value')) + ->select($db->quoteName('a.parent_id')) + ->select($db->quoteName('a.level')) + ->from($db->quoteName('#__finder_taxonomy', 'a')) + ->where($db->quoteName('a.parent_id') . ' <> 0') + ->order('a.title ASC'); + + $db->setQuery($query); + + try { + $contentMap = $db->loadObjectList(); + } catch (\RuntimeException $e) { + return []; + } + + // Build the grouped list array. + if ($contentMap) { + $parents = []; + + foreach ($contentMap as $item) { + if (!isset($parents[$item->parent_id])) { + $parents[$item->parent_id] = []; + } + + $parents[$item->parent_id][] = $item; + } + + foreach ($parents[1] as $branch) { + $groups[$branch->text] = $this->prepareLevel($branch->value, $parents); + } + } + + // Merge any additional groups in the XML definition. + $groups = array_merge(parent::getGroups(), $groups); + + return $groups; + } + + /** + * Indenting and translating options for the list + * + * @param int $parent Parent ID to process + * @param array $parents Array of arrays of items with parent IDs as keys + * + * @return array The indented list of entries for this branch + * + * @since 4.1.5 + */ + private function prepareLevel($parent, $parents) + { + $lang = Factory::getLanguage(); + $entries = []; + + foreach ($parents[$parent] as $item) { + $levelPrefix = str_repeat('- ', $item->level - 1); + + if (trim($item->text, '*') === 'Language') { + $text = LanguageHelper::branchLanguageTitle($item->text); + } else { + $key = LanguageHelper::branchSingular($item->text); + $text = $lang->hasKey($key) ? Text::_($key) : $item->text; + } + + $entries[] = HTMLHelper::_('select.option', $item->value, $levelPrefix . $text); + + if (isset($parents[$item->value])) { + $entries = array_merge($entries, $this->prepareLevel($item->value, $parents)); + } + } + + return $entries; + } } diff --git a/code/administrator/components/com_finder/src/Field/ContenttypesField.php b/code/administrator/components/com_finder/src/Field/ContenttypesField.php index 6c076553..912d849c 100644 --- a/code/administrator/components/com_finder/src/Field/ContenttypesField.php +++ b/code/administrator/components/com_finder/src/Field/ContenttypesField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select($db->quoteName('id', 'value')) - ->select($db->quoteName('title', 'text')) - ->from($db->quoteName('#__finder_types')); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id', 'value')) + ->select($db->quoteName('title', 'text')) + ->from($db->quoteName('#__finder_types')); - // Get the options. - $db->setQuery($query); + // Get the options. + $db->setQuery($query); - try - { - $contentTypes = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } + try { + $contentTypes = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } - // Translate. - foreach ($contentTypes as $contentType) - { - $key = LanguageHelper::branchSingular($contentType->text); - $contentType->translatedText = $lang->hasKey($key) ? Text::_($key) : $contentType->text; - } + // Translate. + foreach ($contentTypes as $contentType) { + $key = LanguageHelper::branchSingular($contentType->text); + $contentType->translatedText = $lang->hasKey($key) ? Text::_($key) : $contentType->text; + } - // Order by title. - $contentTypes = ArrayHelper::sortObjects($contentTypes, 'translatedText', 1, true, true); + // Order by title. + $contentTypes = ArrayHelper::sortObjects($contentTypes, 'translatedText', 1, true, true); - // Convert the values to options. - foreach ($contentTypes as $contentType) - { - $options[] = HTMLHelper::_('select.option', $contentType->value, $contentType->translatedText); - } + // Convert the values to options. + foreach ($contentTypes as $contentType) { + $options[] = HTMLHelper::_('select.option', $contentType->value, $contentType->translatedText); + } - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); - return $options; - } + return $options; + } } diff --git a/code/administrator/components/com_finder/src/Field/SearchfilterField.php b/code/administrator/components/com_finder/src/Field/SearchfilterField.php index d823d298..ecb2b16d 100644 --- a/code/administrator/components/com_finder/src/Field/SearchfilterField.php +++ b/code/administrator/components/com_finder/src/Field/SearchfilterField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('f.title AS text, f.filter_id AS value') - ->from($db->quoteName('#__finder_filters') . ' AS f') - ->where('f.state = 1') - ->order('f.title ASC'); - $db->setQuery($query); - $options = $db->loadObjectList(); - - array_unshift($options, HTMLHelper::_('select.option', '', Text::_('COM_FINDER_SELECT_SEARCH_FILTER'), 'value', 'text')); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 2.5 + */ + protected $type = 'SearchFilter'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 2.5 + */ + public function getOptions() + { + // Build the query. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('f.title AS text, f.filter_id AS value') + ->from($db->quoteName('#__finder_filters') . ' AS f') + ->where('f.state = 1') + ->order('f.title ASC'); + $db->setQuery($query); + $options = $db->loadObjectList(); + + array_unshift($options, HTMLHelper::_('select.option', '', Text::_('COM_FINDER_SELECT_SEARCH_FILTER'), 'value', 'text')); + + return $options; + } } diff --git a/code/administrator/components/com_finder/src/Helper/FinderHelper.php b/code/administrator/components/com_finder/src/Helper/FinderHelper.php index ca51836d..3bdff6d9 100644 --- a/code/administrator/components/com_finder/src/Helper/FinderHelper.php +++ b/code/administrator/components/com_finder/src/Helper/FinderHelper.php @@ -1,4 +1,5 @@ extension_id : 0; - } + return $pluginRecord !== null ? $pluginRecord->extension_id : 0; + } } diff --git a/code/administrator/components/com_finder/src/Helper/LanguageHelper.php b/code/administrator/components/com_finder/src/Helper/LanguageHelper.php index a6e5edf8..588088a9 100644 --- a/code/administrator/components/com_finder/src/Helper/LanguageHelper.php +++ b/code/administrator/components/com_finder/src/Helper/LanguageHelper.php @@ -1,4 +1,5 @@ getLanguage(); - - if ($language->hasKey('PLG_FINDER_QUERY_FILTER_BRANCH_S_' . $return) || JDEBUG) - { - return 'PLG_FINDER_QUERY_FILTER_BRANCH_S_' . $return; - } - - return $branchName; - } - - /** - * Method to return the language name for a language taxonomy branch. - * - * @param string $branchName Language branch name. - * - * @return string The language title. - * - * @since 3.6.0 - */ - public static function branchLanguageTitle($branchName) - { - $title = $branchName; - - if ($branchName === '*') - { - $title = Text::_('JALL_LANGUAGE'); - } - else - { - $languages = CMSLanguageHelper::getLanguages('lang_code'); - - if (isset($languages[$branchName])) - { - $title = $languages[$branchName]->title; - } - } - - return $title; - } - - /** - * Method to load Smart Search component language file. - * - * @return void - * - * @since 2.5 - */ - public static function loadComponentLanguage() - { - Factory::getLanguage()->load('com_finder', JPATH_SITE); - } - - /** - * Method to load Smart Search plugin language files. - * - * @return void - * - * @since 2.5 - */ - public static function loadPluginLanguage() - { - static $loaded = false; - - // If already loaded, don't load again. - if ($loaded) - { - return; - } - - $loaded = true; - - // Get array of all the enabled Smart Search plugin names. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select(array($db->quoteName('name'), $db->quoteName('element'))) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('finder')) - ->where($db->quoteName('enabled') . ' = 1'); - $db->setQuery($query); - $plugins = $db->loadObjectList(); - - if (empty($plugins)) - { - return; - } - - // Load generic language strings. - $lang = Factory::getLanguage(); - $lang->load('plg_content_finder', JPATH_ADMINISTRATOR); - - // Load language file for each plugin. - foreach ($plugins as $plugin) - { - $lang->load($plugin->name, JPATH_ADMINISTRATOR) - || $lang->load($plugin->name, JPATH_PLUGINS . '/finder/' . $plugin->element); - } - } + /** + * Method to return a plural language code for a taxonomy branch. + * + * @param string $branchName Branch title. + * + * @return string Language key code. + * + * @since 2.5 + */ + public static function branchPlural($branchName) + { + $return = preg_replace('/[^a-zA-Z0-9]+/', '_', strtoupper($branchName)); + + if ($return !== '_') { + return 'PLG_FINDER_QUERY_FILTER_BRANCH_P_' . $return; + } + + return $branchName; + } + + /** + * Method to return a singular language code for a taxonomy branch. + * + * @param string $branchName Branch name. + * + * @return string Language key code. + * + * @since 2.5 + */ + public static function branchSingular($branchName) + { + $return = preg_replace('/[^a-zA-Z0-9]+/', '_', strtoupper($branchName)); + $language = Factory::getApplication()->getLanguage(); + + if ($language->hasKey('PLG_FINDER_QUERY_FILTER_BRANCH_S_' . $return) || JDEBUG) { + return 'PLG_FINDER_QUERY_FILTER_BRANCH_S_' . $return; + } + + return $branchName; + } + + /** + * Method to return the language name for a language taxonomy branch. + * + * @param string $branchName Language branch name. + * + * @return string The language title. + * + * @since 3.6.0 + */ + public static function branchLanguageTitle($branchName) + { + $title = $branchName; + + if ($branchName === '*') { + $title = Text::_('JALL_LANGUAGE'); + } else { + $languages = CMSLanguageHelper::getLanguages('lang_code'); + + if (isset($languages[$branchName])) { + $title = $languages[$branchName]->title; + } + } + + return $title; + } + + /** + * Method to load Smart Search component language file. + * + * @return void + * + * @since 2.5 + */ + public static function loadComponentLanguage() + { + Factory::getLanguage()->load('com_finder', JPATH_SITE); + } + + /** + * Method to load Smart Search plugin language files. + * + * @return void + * + * @since 2.5 + */ + public static function loadPluginLanguage() + { + static $loaded = false; + + // If already loaded, don't load again. + if ($loaded) { + return; + } + + $loaded = true; + + // Get array of all the enabled Smart Search plugin names. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select(array($db->quoteName('name'), $db->quoteName('element'))) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('finder')) + ->where($db->quoteName('enabled') . ' = 1'); + $db->setQuery($query); + $plugins = $db->loadObjectList(); + + if (empty($plugins)) { + return; + } + + // Load generic language strings. + $lang = Factory::getLanguage(); + $lang->load('plg_content_finder', JPATH_ADMINISTRATOR); + + // Load language file for each plugin. + foreach ($plugins as $plugin) { + $lang->load($plugin->name, JPATH_ADMINISTRATOR) + || $lang->load($plugin->name, JPATH_PLUGINS . '/finder/' . $plugin->element); + } + } } diff --git a/code/administrator/components/com_finder/src/Indexer/Adapter.php b/code/administrator/components/com_finder/src/Indexer/Adapter.php index 6d7f7a10..0db12092 100644 --- a/code/administrator/components/com_finder/src/Indexer/Adapter.php +++ b/code/administrator/components/com_finder/src/Indexer/Adapter.php @@ -1,4 +1,5 @@ db = Factory::getDbo(); - - // Call the parent constructor. - parent::__construct($subject, $config); - - // Get the type id. - $this->type_id = $this->getTypeId(); - - // Add the content type if it doesn't exist and is set. - if (empty($this->type_id) && !empty($this->type_title)) - { - $this->type_id = Helper::addContentType($this->type_title, $this->mime); - } - - // Check for a layout override. - if ($this->params->get('layout')) - { - $this->layout = $this->params->get('layout'); - } - - // Get the indexer object - $this->indexer = new Indexer; - } - - /** - * Method to get the adapter state and push it into the indexer. - * - * @return void - * - * @since 2.5 - * @throws Exception on error. - */ - public function onStartIndex() - { - // Get the indexer state. - $iState = Indexer::getState(); - - // Get the number of content items. - $total = (int) $this->getContentCount(); - - // Add the content count to the total number of items. - $iState->totalItems += $total; - - // Populate the indexer state information for the adapter. - $iState->pluginState[$this->context]['total'] = $total; - $iState->pluginState[$this->context]['offset'] = 0; - - // Set the indexer state. - Indexer::setState($iState); - } - - /** - * Method to prepare for the indexer to be run. This method will often - * be used to include dependencies and things of that nature. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on error. - */ - public function onBeforeIndex() - { - // Get the indexer and adapter state. - $iState = Indexer::getState(); - $aState = $iState->pluginState[$this->context]; - - // Check the progress of the indexer and the adapter. - if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) - { - return true; - } - - // Run the setup method. - return $this->setup(); - } - - /** - * Method to index a batch of content items. This method can be called by - * the indexer many times throughout the indexing process depending on how - * much content is available for indexing. It is important to track the - * progress correctly so we can display it to the user. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on error. - */ - public function onBuildIndex() - { - // Get the indexer and adapter state. - $iState = Indexer::getState(); - $aState = $iState->pluginState[$this->context]; - - // Check the progress of the indexer and the adapter. - if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) - { - return true; - } - - // Get the batch offset and size. - $offset = (int) $aState['offset']; - $limit = (int) ($iState->batchSize - $iState->batchOffset); - - // Get the content items to index. - $items = $this->getItems($offset, $limit); - - // Iterate through the items and index them. - for ($i = 0, $n = count($items); $i < $n; $i++) - { - // Index the item. - $this->index($items[$i]); - - // Adjust the offsets. - $offset++; - $iState->batchOffset++; - $iState->totalItems--; - } - - // Update the indexer state. - $aState['offset'] = $offset; - $iState->pluginState[$this->context] = $aState; - Indexer::setState($iState); - - return true; - } - - /** - * Method to change the value of a content item's property in the links - * table. This is used to synchronize published and access states that - * are changed when not editing an item directly. - * - * @param string $id The ID of the item to change. - * @param string $property The property that is being changed. - * @param integer $value The new value of that property. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function change($id, $property, $value) - { - // Check for a property we know how to handle. - if ($property !== 'state' && $property !== 'access') - { - return true; - } - - // Get the URL for the content id. - $item = $this->db->quote($this->getUrl($id, $this->extension, $this->layout)); - - // Update the content items. - $query = $this->db->getQuery(true) - ->update($this->db->quoteName('#__finder_links')) - ->set($this->db->quoteName($property) . ' = ' . (int) $value) - ->where($this->db->quoteName('url') . ' = ' . $item); - $this->db->setQuery($query); - $this->db->execute(); - - return true; - } - - /** - * Method to index an item. - * - * @param Result $item The item to index as a Result object. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - abstract protected function index(Result $item); - - /** - * Method to reindex an item. - * - * @param integer $id The ID of the item to reindex. - * - * @return void - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function reindex($id) - { - // Run the setup method. - $this->setup(); - - // Remove the old item. - $this->remove($id, false); - - // Get the item. - $item = $this->getItem($id); - - // Index the item. - $this->index($item); - - Taxonomy::removeOrphanNodes(); - } - - /** - * Method to remove an item from the index. - * - * @param string $id The ID of the item to remove. - * @param bool $removeTaxonomies Remove empty taxonomies - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function remove($id, $removeTaxonomies = true) - { - // Get the item's URL - $url = $this->db->quote($this->getUrl($id, $this->extension, $this->layout)); - - // Get the link ids for the content items. - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('link_id')) - ->from($this->db->quoteName('#__finder_links')) - ->where($this->db->quoteName('url') . ' = ' . $url); - $this->db->setQuery($query); - $items = $this->db->loadColumn(); - - // Check the items. - if (empty($items)) - { - Factory::getApplication()->triggerEvent('onFinderIndexAfterDelete', array($id)); - - return true; - } - - // Remove the items. - foreach ($items as $item) - { - $this->indexer->remove($item, $removeTaxonomies); - } - - return true; - } - - /** - * Method to setup the adapter before indexing. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - * @throws Exception on database error. - */ - abstract protected function setup(); - - /** - * Method to update index data on category access level changes - * - * @param Table $row A Table object - * - * @return void - * - * @since 2.5 - */ - protected function categoryAccessChange($row) - { - $query = clone $this->getStateQuery(); - $query->where('c.id = ' . (int) $row->id); - - // Get the access level. - $this->db->setQuery($query); - $items = $this->db->loadObjectList(); - - // Adjust the access level for each item within the category. - foreach ($items as $item) - { - // Set the access level. - $temp = max($item->access, $row->access); - - // Update the item. - $this->change((int) $item->id, 'access', $temp); - } - } - - /** - * Method to update index data on category access level changes - * - * @param array $pks A list of primary key ids of the content that has changed state. - * @param integer $value The value of the state that the content has been changed to. - * - * @return void - * - * @since 2.5 - */ - protected function categoryStateChange($pks, $value) - { - /* - * The item's published state is tied to the category - * published state so we need to look up all published states - * before we change anything. - */ - foreach ($pks as $pk) - { - $query = clone $this->getStateQuery(); - $query->where('c.id = ' . (int) $pk); - - // Get the published states. - $this->db->setQuery($query); - $items = $this->db->loadObjectList(); - - // Adjust the state for each item within the category. - foreach ($items as $item) - { - // Translate the state. - $temp = $this->translateState($item->state, $value); - - // Update the item. - $this->change($item->id, 'state', $temp); - } - } - } - - /** - * Method to check the existing access level for categories - * - * @param Table $row A Table object - * - * @return void - * - * @since 2.5 - */ - protected function checkCategoryAccess($row) - { - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('access')) - ->from($this->db->quoteName('#__categories')) - ->where($this->db->quoteName('id') . ' = ' . (int) $row->id); - $this->db->setQuery($query); - - // Store the access level to determine if it changes - $this->old_cataccess = $this->db->loadResult(); - } - - /** - * Method to check the existing access level for items - * - * @param Table $row A Table object - * - * @return void - * - * @since 2.5 - */ - protected function checkItemAccess($row) - { - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('access')) - ->from($this->db->quoteName($this->table)) - ->where($this->db->quoteName('id') . ' = ' . (int) $row->id); - $this->db->setQuery($query); - - // Store the access level to determine if it changes - $this->old_access = $this->db->loadResult(); - } - - /** - * Method to get the number of content items available to index. - * - * @return integer The number of content items available to index. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getContentCount() - { - $return = 0; - - // Get the list query. - $query = $this->getListQuery(); - - // Check if the query is valid. - if (empty($query)) - { - return $return; - } - - // Tweak the SQL query to make the total lookup faster. - if ($query instanceof QueryInterface) - { - $query = clone $query; - $query->clear('select') - ->select('COUNT(*)') - ->clear('order'); - } - - // Get the total number of content items to index. - $this->db->setQuery($query); - - return (int) $this->db->loadResult(); - } - - /** - * Method to get a content item to index. - * - * @param integer $id The id of the content item. - * - * @return Result A Result object. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getItem($id) - { - // Get the list query and add the extra WHERE clause. - $query = $this->getListQuery(); - $query->where('a.id = ' . (int) $id); - - // Get the item to index. - $this->db->setQuery($query); - $item = $this->db->loadAssoc(); - - // Convert the item to a result object. - $item = ArrayHelper::toObject((array) $item, Result::class); - - // Set the item type. - $item->type_id = $this->type_id; - - // Set the item layout. - $item->layout = $this->layout; - - return $item; - } - - /** - * Method to get a list of content items to index. - * - * @param integer $offset The list offset. - * @param integer $limit The list limit. - * @param QueryInterface $query A QueryInterface object. [optional] - * - * @return Result[] An array of Result objects. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getItems($offset, $limit, $query = null) - { - // Get the content items to index. - $this->db->setQuery($this->getListQuery($query)->setLimit($limit, $offset)); - $items = $this->db->loadAssocList(); - - foreach ($items as &$item) - { - $item = ArrayHelper::toObject($item, Result::class); - - // Set the item type. - $item->type_id = $this->type_id; - - // Set the mime type. - $item->mime = $this->mime; - - // Set the item layout. - $item->layout = $this->layout; - } - - return $items; - } - - /** - * Method to get the SQL query used to retrieve the list of content items. - * - * @param mixed $query A QueryInterface object. [optional] - * - * @return QueryInterface A database object. - * - * @since 2.5 - */ - protected function getListQuery($query = null) - { - // Check if we can use the supplied SQL query. - return $query instanceof QueryInterface ? $query : $this->db->getQuery(true); - } - - /** - * Method to get the plugin type - * - * @param integer $id The plugin ID - * - * @return string The plugin type - * - * @since 2.5 - */ - protected function getPluginType($id) - { - // Prepare the query - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('element')) - ->from($this->db->quoteName('#__extensions')) - ->where($this->db->quoteName('extension_id') . ' = ' . (int) $id); - $this->db->setQuery($query); - - return $this->db->loadResult(); - } - - /** - * Method to get a SQL query to load the published and access states for - * an article and category. - * - * @return QueryInterface A database object. - * - * @since 2.5 - */ - protected function getStateQuery() - { - $query = $this->db->getQuery(true); - - // Item ID - $query->select('a.id'); - - // Item and category published state - $query->select('a.' . $this->state_field . ' AS state, c.published AS cat_state'); - - // Item and category access levels - $query->select('a.access, c.access AS cat_access') - ->from($this->table . ' AS a') - ->join('LEFT', '#__categories AS c ON c.id = a.catid'); - - return $query; - } - - /** - * Method to get the query clause for getting items to update by time. - * - * @param string $time The modified timestamp. - * - * @return QueryInterface A database object. - * - * @since 2.5 - */ - protected function getUpdateQueryByTime($time) - { - // Build an SQL query based on the modified time. - $query = $this->db->getQuery(true) - ->where('a.modified >= ' . $this->db->quote($time)); - - return $query; - } - - /** - * Method to get the query clause for getting items to update by id. - * - * @param array $ids The ids to load. - * - * @return QueryInterface A database object. - * - * @since 2.5 - */ - protected function getUpdateQueryByIds($ids) - { - // Build an SQL query based on the item ids. - $query = $this->db->getQuery(true) - ->where('a.id IN(' . implode(',', $ids) . ')'); - - return $query; - } - - /** - * Method to get the type id for the adapter content. - * - * @return integer The numeric type id for the content. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getTypeId() - { - // Get the type id from the database. - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__finder_types')) - ->where($this->db->quoteName('title') . ' = ' . $this->db->quote($this->type_title)); - $this->db->setQuery($query); - - return (int) $this->db->loadResult(); - } - - /** - * Method to get the URL for the item. The URL is how we look up the link - * in the Finder index. - * - * @param integer $id The id of the item. - * @param string $extension The extension the category is in. - * @param string $view The view for the URL. - * - * @return string The URL of the item. - * - * @since 2.5 - */ - protected function getUrl($id, $extension, $view) - { - return 'index.php?option=' . $extension . '&view=' . $view . '&id=' . $id; - } - - /** - * Method to get the page title of any menu item that is linked to the - * content item, if it exists and is set. - * - * @param string $url The URL of the item. - * - * @return mixed The title on success, null if not found. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getItemMenuTitle($url) - { - $return = null; - - // Set variables - $user = Factory::getUser(); - $groups = implode(',', $user->getAuthorisedViewLevels()); - - // Build a query to get the menu params. - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('params')) - ->from($this->db->quoteName('#__menu')) - ->where($this->db->quoteName('link') . ' = ' . $this->db->quote($url)) - ->where($this->db->quoteName('published') . ' = 1') - ->where($this->db->quoteName('access') . ' IN (' . $groups . ')'); - - // Get the menu params from the database. - $this->db->setQuery($query); - $params = $this->db->loadResult(); - - // Check the results. - if (empty($params)) - { - return $return; - } - - // Instantiate the params. - $params = json_decode($params); - - // Get the page title if it is set. - if (isset($params->page_title) && $params->page_title) - { - $return = $params->page_title; - } - - return $return; - } - - /** - * Method to update index data on access level changes - * - * @param Table $row A Table object - * - * @return void - * - * @since 2.5 - */ - protected function itemAccessChange($row) - { - $query = clone $this->getStateQuery(); - $query->where('a.id = ' . (int) $row->id); - - // Get the access level. - $this->db->setQuery($query); - $item = $this->db->loadObject(); - - // Set the access level. - $temp = max($row->access, $item->cat_access); - - // Update the item. - $this->change((int) $row->id, 'access', $temp); - } - - /** - * Method to update index data on published state changes - * - * @param array $pks A list of primary key ids of the content that has changed state. - * @param integer $value The value of the state that the content has been changed to. - * - * @return void - * - * @since 2.5 - */ - protected function itemStateChange($pks, $value) - { - /* - * The item's published state is tied to the category - * published state so we need to look up all published states - * before we change anything. - */ - foreach ($pks as $pk) - { - $query = clone $this->getStateQuery(); - $query->where('a.id = ' . (int) $pk); - - // Get the published states. - $this->db->setQuery($query); - $item = $this->db->loadObject(); - - // Translate the state. - $temp = $this->translateState($value, $item->cat_state); - - // Update the item. - $this->change($pk, 'state', $temp); - } - } - - /** - * Method to update index data when a plugin is disabled - * - * @param array $pks A list of primary key ids of the content that has changed state. - * - * @return void - * - * @since 2.5 - */ - protected function pluginDisable($pks) - { - // Since multiple plugins may be disabled at a time, we need to check first - // that we're handling the appropriate one for the context - foreach ($pks as $pk) - { - if ($this->getPluginType($pk) == strtolower($this->context)) - { - // Get all of the items to unindex them - $query = clone $this->getStateQuery(); - $this->db->setQuery($query); - $items = $this->db->loadColumn(); - - // Remove each item - foreach ($items as $item) - { - $this->remove($item); - } - } - } - } - - /** - * Method to translate the native content states into states that the - * indexer can use. - * - * @param integer $item The item state. - * @param integer $category The category state. [optional] - * - * @return integer The translated indexer state. - * - * @since 2.5 - */ - protected function translateState($item, $category = null) - { - // If category is present, factor in its states as well - if ($category !== null && $category == 0) - { - $item = 0; - } - - // Translate the state - switch ($item) - { - // Published and archived items only should return a published state - case 1: - case 2: - return 1; - - // All other states should return an unpublished state - default: - return 0; - } - } + /** + * The context is somewhat arbitrary but it must be unique or there will be + * conflicts when managing plugin/indexer state. A good best practice is to + * use the plugin name suffix as the context. For example, if the plugin is + * named 'plgFinderContent', the context could be 'Content'. + * + * @var string + * @since 2.5 + */ + protected $context; + + /** + * The extension name. + * + * @var string + * @since 2.5 + */ + protected $extension; + + /** + * The sublayout to use when rendering the results. + * + * @var string + * @since 2.5 + */ + protected $layout; + + /** + * The mime type of the content the adapter indexes. + * + * @var string + * @since 2.5 + */ + protected $mime; + + /** + * The access level of an item before save. + * + * @var integer + * @since 2.5 + */ + protected $old_access; + + /** + * The access level of a category before save. + * + * @var integer + * @since 2.5 + */ + protected $old_cataccess; + + /** + * The type of content the adapter indexes. + * + * @var string + * @since 2.5 + */ + protected $type_title; + + /** + * The type id of the content. + * + * @var integer + * @since 2.5 + */ + protected $type_id; + + /** + * The database object. + * + * @var DatabaseInterface + * @since 2.5 + */ + protected $db; + + /** + * The table name. + * + * @var string + * @since 2.5 + */ + protected $table; + + /** + * The indexer object. + * + * @var Indexer + * @since 3.0 + */ + protected $indexer; + + /** + * The field the published state is stored in. + * + * @var string + * @since 2.5 + */ + protected $state_field = 'state'; + + /** + * Method to instantiate the indexer adapter. + * + * @param object $subject The object to observe. + * @param array $config An array that holds the plugin configuration. + * + * @since 2.5 + */ + public function __construct(&$subject, $config) + { + // Call the parent constructor. + parent::__construct($subject, $config); + + // Get the type id. + $this->type_id = $this->getTypeId(); + + // Add the content type if it doesn't exist and is set. + if (empty($this->type_id) && !empty($this->type_title)) { + $this->type_id = Helper::addContentType($this->type_title, $this->mime); + } + + // Check for a layout override. + if ($this->params->get('layout')) { + $this->layout = $this->params->get('layout'); + } + + // Get the indexer object + $this->indexer = new Indexer($this->db); + } + + /** + * Method to get the adapter state and push it into the indexer. + * + * @return void + * + * @since 2.5 + * @throws Exception on error. + */ + public function onStartIndex() + { + // Get the indexer state. + $iState = Indexer::getState(); + + // Get the number of content items. + $total = (int) $this->getContentCount(); + + // Add the content count to the total number of items. + $iState->totalItems += $total; + + // Populate the indexer state information for the adapter. + $iState->pluginState[$this->context]['total'] = $total; + $iState->pluginState[$this->context]['offset'] = 0; + + // Set the indexer state. + Indexer::setState($iState); + } + + /** + * Method to prepare for the indexer to be run. This method will often + * be used to include dependencies and things of that nature. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on error. + */ + public function onBeforeIndex() + { + // Get the indexer and adapter state. + $iState = Indexer::getState(); + $aState = $iState->pluginState[$this->context]; + + // Check the progress of the indexer and the adapter. + if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) { + return true; + } + + // Run the setup method. + return $this->setup(); + } + + /** + * Method to index a batch of content items. This method can be called by + * the indexer many times throughout the indexing process depending on how + * much content is available for indexing. It is important to track the + * progress correctly so we can display it to the user. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on error. + */ + public function onBuildIndex() + { + // Get the indexer and adapter state. + $iState = Indexer::getState(); + $aState = $iState->pluginState[$this->context]; + + // Check the progress of the indexer and the adapter. + if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) { + return true; + } + + // Get the batch offset and size. + $offset = (int) $aState['offset']; + $limit = (int) ($iState->batchSize - $iState->batchOffset); + + // Get the content items to index. + $items = $this->getItems($offset, $limit); + + // Iterate through the items and index them. + for ($i = 0, $n = count($items); $i < $n; $i++) { + // Index the item. + $this->index($items[$i]); + + // Adjust the offsets. + $offset++; + $iState->batchOffset++; + $iState->totalItems--; + } + + // Update the indexer state. + $aState['offset'] = $offset; + $iState->pluginState[$this->context] = $aState; + Indexer::setState($iState); + + return true; + } + + /** + * Method to remove outdated index entries + * + * @return integer + * + * @since 4.2.0 + */ + public function onFinderGarbageCollection() + { + $db = $this->db; + $type_id = $this->getTypeId(); + + $query = $db->getQuery(true); + $subquery = $db->getQuery(true); + $subquery->select('CONCAT(' . $db->quote($this->getUrl('', $this->extension, $this->layout)) . ', id)') + ->from($db->quoteName($this->table)); + $query->select($db->quoteName('l.link_id')) + ->from($db->quoteName('#__finder_links', 'l')) + ->where($db->quoteName('l.type_id') . ' = ' . $type_id) + ->where($db->quoteName('l.url') . ' LIKE ' . $db->quote($this->getUrl('%', $this->extension, $this->layout))) + ->where($db->quoteName('l.url') . ' NOT IN (' . $subquery . ')'); + $db->setQuery($query); + $items = $db->loadColumn(); + + foreach ($items as $item) { + $this->indexer->remove($item); + } + + return count($items); + } + + /** + * Method to change the value of a content item's property in the links + * table. This is used to synchronize published and access states that + * are changed when not editing an item directly. + * + * @param string $id The ID of the item to change. + * @param string $property The property that is being changed. + * @param integer $value The new value of that property. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function change($id, $property, $value) + { + // Check for a property we know how to handle. + if ($property !== 'state' && $property !== 'access') { + return true; + } + + // Get the URL for the content id. + $item = $this->db->quote($this->getUrl($id, $this->extension, $this->layout)); + + // Update the content items. + $query = $this->db->getQuery(true) + ->update($this->db->quoteName('#__finder_links')) + ->set($this->db->quoteName($property) . ' = ' . (int) $value) + ->where($this->db->quoteName('url') . ' = ' . $item); + $this->db->setQuery($query); + $this->db->execute(); + + return true; + } + + /** + * Method to index an item. + * + * @param Result $item The item to index as a Result object. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + abstract protected function index(Result $item); + + /** + * Method to reindex an item. + * + * @param integer $id The ID of the item to reindex. + * + * @return void + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function reindex($id) + { + // Run the setup method. + $this->setup(); + + // Remove the old item. + $this->remove($id, false); + + // Get the item. + $item = $this->getItem($id); + + // Index the item. + $this->index($item); + + Taxonomy::removeOrphanNodes(); + } + + /** + * Method to remove an item from the index. + * + * @param string $id The ID of the item to remove. + * @param bool $removeTaxonomies Remove empty taxonomies + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function remove($id, $removeTaxonomies = true) + { + // Get the item's URL + $url = $this->db->quote($this->getUrl($id, $this->extension, $this->layout)); + + // Get the link ids for the content items. + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('link_id')) + ->from($this->db->quoteName('#__finder_links')) + ->where($this->db->quoteName('url') . ' = ' . $url); + $this->db->setQuery($query); + $items = $this->db->loadColumn(); + + // Check the items. + if (empty($items)) { + Factory::getApplication()->triggerEvent('onFinderIndexAfterDelete', array($id)); + + return true; + } + + // Remove the items. + foreach ($items as $item) { + $this->indexer->remove($item, $removeTaxonomies); + } + + return true; + } + + /** + * Method to setup the adapter before indexing. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + * @throws Exception on database error. + */ + abstract protected function setup(); + + /** + * Method to update index data on category access level changes + * + * @param Table $row A Table object + * + * @return void + * + * @since 2.5 + */ + protected function categoryAccessChange($row) + { + $query = clone $this->getStateQuery(); + $query->where('c.id = ' . (int) $row->id); + + // Get the access level. + $this->db->setQuery($query); + $items = $this->db->loadObjectList(); + + // Adjust the access level for each item within the category. + foreach ($items as $item) { + // Set the access level. + $temp = max($item->access, $row->access); + + // Update the item. + $this->change((int) $item->id, 'access', $temp); + } + } + + /** + * Method to update index data on category access level changes + * + * @param array $pks A list of primary key ids of the content that has changed state. + * @param integer $value The value of the state that the content has been changed to. + * + * @return void + * + * @since 2.5 + */ + protected function categoryStateChange($pks, $value) + { + /* + * The item's published state is tied to the category + * published state so we need to look up all published states + * before we change anything. + */ + foreach ($pks as $pk) { + $query = clone $this->getStateQuery(); + $query->where('c.id = ' . (int) $pk); + + // Get the published states. + $this->db->setQuery($query); + $items = $this->db->loadObjectList(); + + // Adjust the state for each item within the category. + foreach ($items as $item) { + // Translate the state. + $temp = $this->translateState($item->state, $value); + + // Update the item. + $this->change($item->id, 'state', $temp); + } + } + } + + /** + * Method to check the existing access level for categories + * + * @param Table $row A Table object + * + * @return void + * + * @since 2.5 + */ + protected function checkCategoryAccess($row) + { + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('access')) + ->from($this->db->quoteName('#__categories')) + ->where($this->db->quoteName('id') . ' = ' . (int) $row->id); + $this->db->setQuery($query); + + // Store the access level to determine if it changes + $this->old_cataccess = $this->db->loadResult(); + } + + /** + * Method to check the existing access level for items + * + * @param Table $row A Table object + * + * @return void + * + * @since 2.5 + */ + protected function checkItemAccess($row) + { + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('access')) + ->from($this->db->quoteName($this->table)) + ->where($this->db->quoteName('id') . ' = ' . (int) $row->id); + $this->db->setQuery($query); + + // Store the access level to determine if it changes + $this->old_access = $this->db->loadResult(); + } + + /** + * Method to get the number of content items available to index. + * + * @return integer The number of content items available to index. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getContentCount() + { + $return = 0; + + // Get the list query. + $query = $this->getListQuery(); + + // Check if the query is valid. + if (empty($query)) { + return $return; + } + + // Tweak the SQL query to make the total lookup faster. + if ($query instanceof QueryInterface) { + $query = clone $query; + $query->clear('select') + ->select('COUNT(*)') + ->clear('order'); + } + + // Get the total number of content items to index. + $this->db->setQuery($query); + + return (int) $this->db->loadResult(); + } + + /** + * Method to get a content item to index. + * + * @param integer $id The id of the content item. + * + * @return Result A Result object. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getItem($id) + { + // Get the list query and add the extra WHERE clause. + $query = $this->getListQuery(); + $query->where('a.id = ' . (int) $id); + + // Get the item to index. + $this->db->setQuery($query); + $item = $this->db->loadAssoc(); + + // Convert the item to a result object. + $item = ArrayHelper::toObject((array) $item, Result::class); + + // Set the item type. + $item->type_id = $this->type_id; + + // Set the item layout. + $item->layout = $this->layout; + + return $item; + } + + /** + * Method to get a list of content items to index. + * + * @param integer $offset The list offset. + * @param integer $limit The list limit. + * @param QueryInterface $query A QueryInterface object. [optional] + * + * @return Result[] An array of Result objects. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getItems($offset, $limit, $query = null) + { + // Get the content items to index. + $this->db->setQuery($this->getListQuery($query)->setLimit($limit, $offset)); + $items = $this->db->loadAssocList(); + + foreach ($items as &$item) { + $item = ArrayHelper::toObject($item, Result::class); + + // Set the item type. + $item->type_id = $this->type_id; + + // Set the mime type. + $item->mime = $this->mime; + + // Set the item layout. + $item->layout = $this->layout; + } + + return $items; + } + + /** + * Method to get the SQL query used to retrieve the list of content items. + * + * @param mixed $query A QueryInterface object. [optional] + * + * @return QueryInterface A database object. + * + * @since 2.5 + */ + protected function getListQuery($query = null) + { + // Check if we can use the supplied SQL query. + return $query instanceof QueryInterface ? $query : $this->db->getQuery(true); + } + + /** + * Method to get the plugin type + * + * @param integer $id The plugin ID + * + * @return string The plugin type + * + * @since 2.5 + */ + protected function getPluginType($id) + { + // Prepare the query + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('element')) + ->from($this->db->quoteName('#__extensions')) + ->where($this->db->quoteName('extension_id') . ' = ' . (int) $id); + $this->db->setQuery($query); + + return $this->db->loadResult(); + } + + /** + * Method to get a SQL query to load the published and access states for + * an article and category. + * + * @return QueryInterface A database object. + * + * @since 2.5 + */ + protected function getStateQuery() + { + $query = $this->db->getQuery(true); + + // Item ID + $query->select('a.id'); + + // Item and category published state + $query->select('a.' . $this->state_field . ' AS state, c.published AS cat_state'); + + // Item and category access levels + $query->select('a.access, c.access AS cat_access') + ->from($this->table . ' AS a') + ->join('LEFT', '#__categories AS c ON c.id = a.catid'); + + return $query; + } + + /** + * Method to get the query clause for getting items to update by time. + * + * @param string $time The modified timestamp. + * + * @return QueryInterface A database object. + * + * @since 2.5 + */ + protected function getUpdateQueryByTime($time) + { + // Build an SQL query based on the modified time. + $query = $this->db->getQuery(true) + ->where('a.modified >= ' . $this->db->quote($time)); + + return $query; + } + + /** + * Method to get the query clause for getting items to update by id. + * + * @param array $ids The ids to load. + * + * @return QueryInterface A database object. + * + * @since 2.5 + */ + protected function getUpdateQueryByIds($ids) + { + // Build an SQL query based on the item ids. + $query = $this->db->getQuery(true) + ->where('a.id IN(' . implode(',', $ids) . ')'); + + return $query; + } + + /** + * Method to get the type id for the adapter content. + * + * @return integer The numeric type id for the content. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getTypeId() + { + // Get the type id from the database. + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__finder_types')) + ->where($this->db->quoteName('title') . ' = ' . $this->db->quote($this->type_title)); + $this->db->setQuery($query); + + return (int) $this->db->loadResult(); + } + + /** + * Method to get the URL for the item. The URL is how we look up the link + * in the Finder index. + * + * @param integer $id The id of the item. + * @param string $extension The extension the category is in. + * @param string $view The view for the URL. + * + * @return string The URL of the item. + * + * @since 2.5 + */ + protected function getUrl($id, $extension, $view) + { + return 'index.php?option=' . $extension . '&view=' . $view . '&id=' . $id; + } + + /** + * Method to get the page title of any menu item that is linked to the + * content item, if it exists and is set. + * + * @param string $url The URL of the item. + * + * @return mixed The title on success, null if not found. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getItemMenuTitle($url) + { + $return = null; + + // Set variables + $user = Factory::getUser(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + + // Build a query to get the menu params. + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('params')) + ->from($this->db->quoteName('#__menu')) + ->where($this->db->quoteName('link') . ' = ' . $this->db->quote($url)) + ->where($this->db->quoteName('published') . ' = 1') + ->where($this->db->quoteName('access') . ' IN (' . $groups . ')'); + + // Get the menu params from the database. + $this->db->setQuery($query); + $params = $this->db->loadResult(); + + // Check the results. + if (empty($params)) { + return $return; + } + + // Instantiate the params. + $params = json_decode($params); + + // Get the page title if it is set. + if (isset($params->page_title) && $params->page_title) { + $return = $params->page_title; + } + + return $return; + } + + /** + * Method to update index data on access level changes + * + * @param Table $row A Table object + * + * @return void + * + * @since 2.5 + */ + protected function itemAccessChange($row) + { + $query = clone $this->getStateQuery(); + $query->where('a.id = ' . (int) $row->id); + + // Get the access level. + $this->db->setQuery($query); + $item = $this->db->loadObject(); + + // Set the access level. + $temp = max($row->access, $item->cat_access); + + // Update the item. + $this->change((int) $row->id, 'access', $temp); + } + + /** + * Method to update index data on published state changes + * + * @param array $pks A list of primary key ids of the content that has changed state. + * @param integer $value The value of the state that the content has been changed to. + * + * @return void + * + * @since 2.5 + */ + protected function itemStateChange($pks, $value) + { + /* + * The item's published state is tied to the category + * published state so we need to look up all published states + * before we change anything. + */ + foreach ($pks as $pk) { + $query = clone $this->getStateQuery(); + $query->where('a.id = ' . (int) $pk); + + // Get the published states. + $this->db->setQuery($query); + $item = $this->db->loadObject(); + + // Translate the state. + $temp = $this->translateState($value, $item->cat_state); + + // Update the item. + $this->change($pk, 'state', $temp); + } + } + + /** + * Method to update index data when a plugin is disabled + * + * @param array $pks A list of primary key ids of the content that has changed state. + * + * @return void + * + * @since 2.5 + */ + protected function pluginDisable($pks) + { + // Since multiple plugins may be disabled at a time, we need to check first + // that we're handling the appropriate one for the context + foreach ($pks as $pk) { + if ($this->getPluginType($pk) == strtolower($this->context)) { + // Get all of the items to unindex them + $query = clone $this->getStateQuery(); + $this->db->setQuery($query); + $items = $this->db->loadColumn(); + + // Remove each item + foreach ($items as $item) { + $this->remove($item); + } + } + } + } + + /** + * Method to translate the native content states into states that the + * indexer can use. + * + * @param integer $item The item state. + * @param integer $category The category state. [optional] + * + * @return integer The translated indexer state. + * + * @since 2.5 + */ + protected function translateState($item, $category = null) + { + // If category is present, factor in its states as well + if ($category !== null && $category == 0) { + $item = 0; + } + + // Translate the state + switch ($item) { + // Published and archived items only should return a published state + case 1: + case 2: + return 1; + + // All other states should return an unpublished state + default: + return 0; + } + } } diff --git a/code/administrator/components/com_finder/src/Indexer/Helper.php b/code/administrator/components/com_finder/src/Indexer/Helper.php index fc3f13ab..a6e7b261 100644 --- a/code/administrator/components/com_finder/src/Indexer/Helper.php +++ b/code/administrator/components/com_finder/src/Indexer/Helper.php @@ -1,4 +1,5 @@ parse($input); - } - - /** - * Method to tokenize a text string. - * - * @param string $input The input to tokenize. - * @param string $lang The language of the input. - * @param boolean $phrase Flag to indicate whether input could be a phrase. [optional] - * - * @return Token[] An array of Token objects. - * - * @since 2.5 - */ - public static function tokenize($input, $lang, $phrase = false) - { - static $cache = [], $tuplecount; - static $multilingual; - static $defaultLanguage; - - if (!$tuplecount) - { - $params = ComponentHelper::getParams('com_finder'); - $tuplecount = $params->get('tuplecount', 1); - } - - if (is_null($multilingual)) - { - $multilingual = Multilanguage::isEnabled(); - $config = ComponentHelper::getParams('com_finder'); - - if ($config->get('language_default', '') == '') - { - $defaultLang = '*'; - } - elseif ($config->get('language_default', '') == '-1') - { - $defaultLang = self::getDefaultLanguage(); - } - else - { - $defaultLang = $config->get('language_default'); - } - - /* - * The default language always has the language code '*'. - * In order to not overwrite the language code of the language - * object that we are using, we are cloning it here. - */ - $obj = Language::getInstance($defaultLang); - $defaultLanguage = clone $obj; - $defaultLanguage->language = '*'; - } - - if (!$multilingual || $lang == '*') - { - $language = $defaultLanguage; - } - else - { - $language = Language::getInstance($lang); - } - - if (!isset($cache[$lang])) - { - $cache[$lang] = []; - } - - $tokens = array(); - $terms = $language->tokenise($input); - - // @todo: array_filter removes any number 0's from the terms. Not sure this is entirely intended - $terms = array_filter($terms); - $terms = array_values($terms); - - /* - * If we have to handle the input as a phrase, that means we don't - * tokenize the individual terms and we do not create the two and three - * term combinations. The phrase must contain more than one word! - */ - if ($phrase === true && count($terms) > 1) - { - // Create tokens from the phrase. - $tokens[] = new Token($terms, $language->language, $language->spacer); - } - else - { - // Create tokens from the terms. - for ($i = 0, $n = count($terms); $i < $n; $i++) - { - if (isset($cache[$lang][$terms[$i]])) - { - $tokens[] = $cache[$lang][$terms[$i]]; - } - else - { - $token = new Token($terms[$i], $language->language); - $tokens[] = $token; - $cache[$lang][$terms[$i]] = $token; - } - } - - // Create multi-word phrase tokens from the individual words. - if ($tuplecount > 1) - { - for ($i = 0, $n = count($tokens); $i < $n; $i++) - { - $temp = array($tokens[$i]->term); - - // Create tokens for 2 to $tuplecount length phrases - for ($j = 1; $j < $tuplecount; $j++) - { - if ($i + $j >= $n || !isset($tokens[$i + $j])) - { - break; - } - - $temp[] = $tokens[$i + $j]->term; - $key = implode('::', $temp); - - if (isset($cache[$lang][$key])) - { - $tokens[] = $cache[$lang][$key]; - } - else - { - $token = new Token($temp, $language->language, $language->spacer); - $token->derived = true; - $tokens[] = $token; - $cache[$lang][$key] = $token; - } - } - } - } - } - - // Prevent the cache to fill up the memory - while (count($cache[$lang]) > 1024) - { - /** - * We want to cache the most common words/tokens. At the same time - * we don't want to cache too much. The most common words will also - * be early in the text, so we are dropping all terms/tokens which - * have been cached later. - */ - array_pop($cache[$lang]); - } - - return $tokens; - } - - /** - * Method to get the base word of a token. - * - * @param string $token The token to stem. - * @param string $lang The language of the token. - * - * @return string The root token. - * - * @since 2.5 - */ - public static function stem($token, $lang) - { - static $multilingual; - static $defaultStemmer; - - if (is_null($multilingual)) - { - $multilingual = Multilanguage::isEnabled(); - $config = ComponentHelper::getParams('com_finder'); - - if ($config->get('language_default', '') == '') - { - $defaultStemmer = Language::getInstance('*'); - } - elseif ($config->get('language_default', '') == '-1') - { - $defaultStemmer = Language::getInstance(self::getDefaultLanguage()); - } - else - { - $defaultStemmer = Language::getInstance($config->get('language_default')); - } - } - - if (!$multilingual || $lang == '*') - { - $language = $defaultStemmer; - } - else - { - $language = Language::getInstance($lang); - } - - return $language->stem($token); - } - - /** - * Method to add a content type to the database. - * - * @param string $title The type of content. For example: PDF - * @param string $mime The mime type of the content. For example: PDF [optional] - * - * @return integer The id of the content type. - * - * @since 2.5 - * @throws Exception on database error. - */ - public static function addContentType($title, $mime = null) - { - static $types; - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - // Check if the types are loaded. - if (empty($types)) - { - // Build the query to get the types. - $query->select('*') - ->from($db->quoteName('#__finder_types')); - - // Get the types. - $db->setQuery($query); - $types = $db->loadObjectList('title'); - } - - // Check if the type already exists. - if (isset($types[$title])) - { - return (int) $types[$title]->id; - } - - // Add the type. - $query->clear() - ->insert($db->quoteName('#__finder_types')) - ->columns(array($db->quoteName('title'), $db->quoteName('mime'))) - ->values($db->quote($title) . ', ' . $db->quote($mime)); - $db->setQuery($query); - $db->execute(); - - // Return the new id. - return (int) $db->insertid(); - } - - /** - * Method to check if a token is common in a language. - * - * @param string $token The token to test. - * @param string $lang The language to reference. - * - * @return boolean True if common, false otherwise. - * - * @since 2.5 - */ - public static function isCommon($token, $lang) - { - static $data, $default, $multilingual; - - if (is_null($multilingual)) - { - $multilingual = Multilanguage::isEnabled(); - $config = ComponentHelper::getParams('com_finder'); - - if ($config->get('language_default', '') == '') - { - $default = '*'; - } - elseif ($config->get('language_default', '') == '-1') - { - $default = self::getPrimaryLanguage(self::getDefaultLanguage()); - } - else - { - $default = self::getPrimaryLanguage($config->get('language_default')); - } - } - - if (!$multilingual || $lang == '*') - { - $lang = $default; - } - - // Load the common tokens for the language if necessary. - if (!isset($data[$lang])) - { - $data[$lang] = self::getCommonWords($lang); - } - - // Check if the token is in the common array. - return in_array($token, $data[$lang], true); - } - - /** - * Method to get an array of common terms for a language. - * - * @param string $lang The language to use. - * - * @return array Array of common terms. - * - * @since 2.5 - * @throws Exception on database error. - */ - public static function getCommonWords($lang) - { - $db = Factory::getDbo(); - - // Create the query to load all the common terms for the language. - $query = $db->getQuery(true) - ->select($db->quoteName('term')) - ->from($db->quoteName('#__finder_terms_common')) - ->where($db->quoteName('language') . ' = ' . $db->quote($lang)); - - // Load all of the common terms for the language. - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Method to get the default language for the site. - * - * @return string The default language string. - * - * @since 2.5 - */ - public static function getDefaultLanguage() - { - static $lang; - - // We need to go to com_languages to get the site default language, it's the best we can guess. - if (empty($lang)) - { - $lang = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); - } - - return $lang; - } - - /** - * Method to parse a language/locale key and return a simple language string. - * - * @param string $lang The language/locale key. For example: en-GB - * - * @return string The simple language string. For example: en - * - * @since 2.5 - */ - public static function getPrimaryLanguage($lang) - { - static $data; - - // Only parse the identifier if necessary. - if (!isset($data[$lang])) - { - if (is_callable(array('Locale', 'getPrimaryLanguage'))) - { - // Get the language key using the Locale package. - $data[$lang] = \Locale::getPrimaryLanguage($lang); - } - else - { - // Get the language key using string position. - $data[$lang] = StringHelper::substr($lang, 0, StringHelper::strpos($lang, '-')); - } - } - - return $data[$lang]; - } - - /** - * Method to get extra data for a content before being indexed. This is how - * we add Comments, Tags, Labels, etc. that should be available to Finder. - * - * @param Result $item The item to index as a Result object. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - * @throws Exception on database error. - */ - public static function getContentExtras(Result $item) - { - // Load the finder plugin group. - PluginHelper::importPlugin('finder'); - - Factory::getApplication()->triggerEvent('onPrepareFinderContent', array(&$item)); - - return true; - } - - /** - * Method to process content text using the onContentPrepare event trigger. - * - * @param string $text The content to process. - * @param Registry $params The parameters object. [optional] - * @param Result $item The item which get prepared. [optional] - * - * @return string The processed content. - * - * @since 2.5 - */ - public static function prepareContent($text, $params = null, Result $item = null) - { - static $loaded; - - // Load the content plugins if necessary. - if (empty($loaded)) - { - PluginHelper::importPlugin('content'); - $loaded = true; - } - - // Instantiate the parameter object if necessary. - if (!($params instanceof Registry)) - { - $registry = new Registry($params); - $params = $registry; - } - - // Create a mock content object. - $content = Table::getInstance('Content'); - $content->text = $text; - - if ($item) - { - $content->bind((array) $item); - $content->bind($item->getElements()); - } - - if ($item && !empty($item->context)) - { - $content->context = $item->context; - } - - // Fire the onContentPrepare event. - Factory::getApplication()->triggerEvent('onContentPrepare', array('com_finder.indexer', &$content, &$params, 0)); - - return $content->text; - } + /** + * Method to parse input into plain text. + * + * @param string $input The raw input. + * @param string $format The format of the input. [optional] + * + * @return string The parsed input. + * + * @since 2.5 + * @throws Exception on invalid parser. + */ + public static function parse($input, $format = 'html') + { + // Get a parser for the specified format and parse the input. + return Parser::getInstance($format)->parse($input); + } + + /** + * Method to tokenize a text string. + * + * @param string $input The input to tokenize. + * @param string $lang The language of the input. + * @param boolean $phrase Flag to indicate whether input could be a phrase. [optional] + * + * @return Token[] An array of Token objects. + * + * @since 2.5 + */ + public static function tokenize($input, $lang, $phrase = false) + { + static $cache = [], $tuplecount; + static $multilingual; + static $defaultLanguage; + + if (!$tuplecount) { + $params = ComponentHelper::getParams('com_finder'); + $tuplecount = $params->get('tuplecount', 1); + } + + if (is_null($multilingual)) { + $multilingual = Multilanguage::isEnabled(); + $config = ComponentHelper::getParams('com_finder'); + + if ($config->get('language_default', '') == '') { + $defaultLang = '*'; + } elseif ($config->get('language_default', '') == '-1') { + $defaultLang = self::getDefaultLanguage(); + } else { + $defaultLang = $config->get('language_default'); + } + + /* + * The default language always has the language code '*'. + * In order to not overwrite the language code of the language + * object that we are using, we are cloning it here. + */ + $obj = Language::getInstance($defaultLang); + $defaultLanguage = clone $obj; + $defaultLanguage->language = '*'; + } + + if (!$multilingual || $lang == '*') { + $language = $defaultLanguage; + } else { + $language = Language::getInstance($lang); + } + + if (!isset($cache[$lang])) { + $cache[$lang] = []; + } + + $tokens = array(); + $terms = $language->tokenise($input); + + // @todo: array_filter removes any number 0's from the terms. Not sure this is entirely intended + $terms = array_filter($terms); + $terms = array_values($terms); + + /* + * If we have to handle the input as a phrase, that means we don't + * tokenize the individual terms and we do not create the two and three + * term combinations. The phrase must contain more than one word! + */ + if ($phrase === true && count($terms) > 1) { + // Create tokens from the phrase. + $tokens[] = new Token($terms, $language->language, $language->spacer); + } else { + // Create tokens from the terms. + for ($i = 0, $n = count($terms); $i < $n; $i++) { + if (isset($cache[$lang][$terms[$i]])) { + $tokens[] = $cache[$lang][$terms[$i]]; + } else { + $token = new Token($terms[$i], $language->language); + $tokens[] = $token; + $cache[$lang][$terms[$i]] = $token; + } + } + + // Create multi-word phrase tokens from the individual words. + if ($tuplecount > 1) { + for ($i = 0, $n = count($tokens); $i < $n; $i++) { + $temp = array($tokens[$i]->term); + + // Create tokens for 2 to $tuplecount length phrases + for ($j = 1; $j < $tuplecount; $j++) { + if ($i + $j >= $n || !isset($tokens[$i + $j])) { + break; + } + + $temp[] = $tokens[$i + $j]->term; + $key = implode('::', $temp); + + if (isset($cache[$lang][$key])) { + $tokens[] = $cache[$lang][$key]; + } else { + $token = new Token($temp, $language->language, $language->spacer); + $token->derived = true; + $tokens[] = $token; + $cache[$lang][$key] = $token; + } + } + } + } + } + + // Prevent the cache to fill up the memory + while (count($cache[$lang]) > 1024) { + /** + * We want to cache the most common words/tokens. At the same time + * we don't want to cache too much. The most common words will also + * be early in the text, so we are dropping all terms/tokens which + * have been cached later. + */ + array_pop($cache[$lang]); + } + + return $tokens; + } + + /** + * Method to get the base word of a token. + * + * @param string $token The token to stem. + * @param string $lang The language of the token. + * + * @return string The root token. + * + * @since 2.5 + */ + public static function stem($token, $lang) + { + static $multilingual; + static $defaultStemmer; + + if (is_null($multilingual)) { + $multilingual = Multilanguage::isEnabled(); + $config = ComponentHelper::getParams('com_finder'); + + if ($config->get('language_default', '') == '') { + $defaultStemmer = Language::getInstance('*'); + } elseif ($config->get('language_default', '') == '-1') { + $defaultStemmer = Language::getInstance(self::getDefaultLanguage()); + } else { + $defaultStemmer = Language::getInstance($config->get('language_default')); + } + } + + if (!$multilingual || $lang == '*') { + $language = $defaultStemmer; + } else { + $language = Language::getInstance($lang); + } + + return $language->stem($token); + } + + /** + * Method to add a content type to the database. + * + * @param string $title The type of content. For example: PDF + * @param string $mime The mime type of the content. For example: PDF [optional] + * + * @return integer The id of the content type. + * + * @since 2.5 + * @throws Exception on database error. + */ + public static function addContentType($title, $mime = null) + { + static $types; + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + // Check if the types are loaded. + if (empty($types)) { + // Build the query to get the types. + $query->select('*') + ->from($db->quoteName('#__finder_types')); + + // Get the types. + $db->setQuery($query); + $types = $db->loadObjectList('title'); + } + + // Check if the type already exists. + if (isset($types[$title])) { + return (int) $types[$title]->id; + } + + // Add the type. + $query->clear() + ->insert($db->quoteName('#__finder_types')) + ->columns(array($db->quoteName('title'), $db->quoteName('mime'))) + ->values($db->quote($title) . ', ' . $db->quote($mime)); + $db->setQuery($query); + $db->execute(); + + // Return the new id. + return (int) $db->insertid(); + } + + /** + * Method to check if a token is common in a language. + * + * @param string $token The token to test. + * @param string $lang The language to reference. + * + * @return boolean True if common, false otherwise. + * + * @since 2.5 + */ + public static function isCommon($token, $lang) + { + static $data, $default, $multilingual; + + if (is_null($multilingual)) { + $multilingual = Multilanguage::isEnabled(); + $config = ComponentHelper::getParams('com_finder'); + + if ($config->get('language_default', '') == '') { + $default = '*'; + } elseif ($config->get('language_default', '') == '-1') { + $default = self::getPrimaryLanguage(self::getDefaultLanguage()); + } else { + $default = self::getPrimaryLanguage($config->get('language_default')); + } + } + + if (!$multilingual || $lang == '*') { + $lang = $default; + } + + // Load the common tokens for the language if necessary. + if (!isset($data[$lang])) { + $data[$lang] = self::getCommonWords($lang); + } + + // Check if the token is in the common array. + return in_array($token, $data[$lang], true); + } + + /** + * Method to get an array of common terms for a language. + * + * @param string $lang The language to use. + * + * @return array Array of common terms. + * + * @since 2.5 + * @throws Exception on database error. + */ + public static function getCommonWords($lang) + { + $db = Factory::getDbo(); + + // Create the query to load all the common terms for the language. + $query = $db->getQuery(true) + ->select($db->quoteName('term')) + ->from($db->quoteName('#__finder_terms_common')) + ->where($db->quoteName('language') . ' = ' . $db->quote($lang)); + + // Load all of the common terms for the language. + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Method to get the default language for the site. + * + * @return string The default language string. + * + * @since 2.5 + */ + public static function getDefaultLanguage() + { + static $lang; + + // We need to go to com_languages to get the site default language, it's the best we can guess. + if (empty($lang)) { + $lang = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); + } + + return $lang; + } + + /** + * Method to parse a language/locale key and return a simple language string. + * + * @param string $lang The language/locale key. For example: en-GB + * + * @return string The simple language string. For example: en + * + * @since 2.5 + */ + public static function getPrimaryLanguage($lang) + { + static $data; + + // Only parse the identifier if necessary. + if (!isset($data[$lang])) { + if (is_callable(array('Locale', 'getPrimaryLanguage'))) { + // Get the language key using the Locale package. + $data[$lang] = \Locale::getPrimaryLanguage($lang); + } else { + // Get the language key using string position. + $data[$lang] = StringHelper::substr($lang, 0, StringHelper::strpos($lang, '-')); + } + } + + return $data[$lang]; + } + + /** + * Method to get extra data for a content before being indexed. This is how + * we add Comments, Tags, Labels, etc. that should be available to Finder. + * + * @param Result $item The item to index as a Result object. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + * @throws Exception on database error. + */ + public static function getContentExtras(Result $item) + { + // Load the finder plugin group. + PluginHelper::importPlugin('finder'); + + Factory::getApplication()->triggerEvent('onPrepareFinderContent', array(&$item)); + + return true; + } + + /** + * Method to process content text using the onContentPrepare event trigger. + * + * @param string $text The content to process. + * @param Registry $params The parameters object. [optional] + * @param Result $item The item which get prepared. [optional] + * + * @return string The processed content. + * + * @since 2.5 + */ + public static function prepareContent($text, $params = null, Result $item = null) + { + static $loaded; + + // Load the content plugins if necessary. + if (empty($loaded)) { + PluginHelper::importPlugin('content'); + $loaded = true; + } + + // Instantiate the parameter object if necessary. + if (!($params instanceof Registry)) { + $registry = new Registry($params); + $params = $registry; + } + + // Create a mock content object. + $content = Table::getInstance('Content'); + $content->text = $text; + + if ($item) { + $content->bind((array) $item); + $content->bind($item->getElements()); + } + + if ($item && !empty($item->context)) { + $content->context = $item->context; + } + + // Fire the onContentPrepare event. + Factory::getApplication()->triggerEvent('onContentPrepare', array('com_finder.indexer', &$content, &$params, 0)); + + return $content->text; + } } diff --git a/code/administrator/components/com_finder/src/Indexer/Indexer.php b/code/administrator/components/com_finder/src/Indexer/Indexer.php index a6daa68d..0692130b 100644 --- a/code/administrator/components/com_finder/src/Indexer/Indexer.php +++ b/code/administrator/components/com_finder/src/Indexer/Indexer.php @@ -1,4 +1,5 @@ db = Factory::getDbo(); - - $db = $this->db; - - // Set up query template for addTokensToDb - $this->addTokensToDbQueryTemplate = $db->getQuery(true)->insert($db->quoteName('#__finder_tokens')) - ->columns( - array( - $db->quoteName('term'), - $db->quoteName('stem'), - $db->quoteName('common'), - $db->quoteName('phrase'), - $db->quoteName('weight'), - $db->quoteName('context'), - $db->quoteName('language') - ) - ); - } - - /** - * Method to get the indexer state. - * - * @return object The indexer state object. - * - * @since 2.5 - */ - public static function getState() - { - // First, try to load from the internal state. - if ((bool) static::$state) - { - return static::$state; - } - - // If we couldn't load from the internal state, try the session. - $session = Factory::getSession(); - $data = $session->get('_finder.state', null); - - // If the state is empty, load the values for the first time. - if (empty($data)) - { - $data = new CMSObject; - - // Load the default configuration options. - $data->options = ComponentHelper::getParams('com_finder'); - - // Setup the weight lookup information. - $data->weights = array( - self::TITLE_CONTEXT => round($data->options->get('title_multiplier', 1.7), 2), - self::TEXT_CONTEXT => round($data->options->get('text_multiplier', 0.7), 2), - self::META_CONTEXT => round($data->options->get('meta_multiplier', 1.2), 2), - self::PATH_CONTEXT => round($data->options->get('path_multiplier', 2.0), 2), - self::MISC_CONTEXT => round($data->options->get('misc_multiplier', 0.3), 2) - ); - - // Set the current time as the start time. - $data->startTime = Factory::getDate()->toSql(); - - // Set the remaining default values. - $data->batchSize = (int) $data->options->get('batch_size', 50); - $data->batchOffset = 0; - $data->totalItems = 0; - $data->pluginState = array(); - } - - // Setup the profiler if debugging is enabled. - if (Factory::getApplication()->get('debug')) - { - static::$profiler = Profiler::getInstance('FinderIndexer'); - } - - // Set the state. - static::$state = $data; - - return static::$state; - } - - /** - * Method to set the indexer state. - * - * @param CMSObject $data A new indexer state object. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - */ - public static function setState($data) - { - // Check the state object. - if (empty($data) || !$data instanceof CMSObject) - { - return false; - } - - // Set the new internal state. - static::$state = $data; - - // Set the new session state. - Factory::getSession()->set('_finder.state', $data); - - return true; - } - - /** - * Method to reset the indexer state. - * - * @return void - * - * @since 2.5 - */ - public static function resetState() - { - // Reset the internal state to null. - self::$state = null; - - // Reset the session state to null. - Factory::getSession()->set('_finder.state', null); - } - - /** - * Method to index a content item. - * - * @param Result $item The content item to index. - * @param string $format The format of the content. [optional] - * - * @return integer The ID of the record in the links table. - * - * @since 2.5 - * @throws \Exception on database error. - */ - public function index($item, $format = 'html') - { - // Mark beforeIndexing in the profiler. - static::$profiler ? static::$profiler->mark('beforeIndexing') : null; - $db = $this->db; - $serverType = strtolower($db->getServerType()); - - // Check if the item is in the database. - $query = $db->getQuery(true) - ->select($db->quoteName('link_id') . ', ' . $db->quoteName('md5sum')) - ->from($db->quoteName('#__finder_links')) - ->where($db->quoteName('url') . ' = ' . $db->quote($item->url)); - - // Load the item from the database. - $db->setQuery($query); - $link = $db->loadObject(); - - // Get the indexer state. - $state = static::getState(); - - // Get the signatures of the item. - $curSig = static::getSignature($item); - $oldSig = $link->md5sum ?? null; - - // Get the other item information. - $linkId = empty($link->link_id) ? null : $link->link_id; - $isNew = empty($link->link_id); - - // Check the signatures. If they match, the item is up to date. - if (!$isNew && $curSig == $oldSig) - { - return $linkId; - } - - /* - * If the link already exists, flush all the term maps for the item. - * Maps are stored in 16 tables so we need to iterate through and flush - * each table one at a time. - */ - if (!$isNew) - { - // Flush the maps for the link. - $query->clear() - ->delete($db->quoteName('#__finder_links_terms')) - ->where($db->quoteName('link_id') . ' = ' . (int) $linkId); - $db->setQuery($query); - $db->execute(); - - // Remove the taxonomy maps. - Taxonomy::removeMaps($linkId); - } - - // Mark afterUnmapping in the profiler. - static::$profiler ? static::$profiler->mark('afterUnmapping') : null; - - // Perform cleanup on the item data. - $item->publish_start_date = (int) $item->publish_start_date != 0 ? $item->publish_start_date : null; - $item->publish_end_date = (int) $item->publish_end_date != 0 ? $item->publish_end_date : null; - $item->start_date = (int) $item->start_date != 0 ? $item->start_date : null; - $item->end_date = (int) $item->end_date != 0 ? $item->end_date : null; - - // Prepare the item description. - $item->description = Helper::parse($item->summary); - - /* - * Now, we need to enter the item into the links table. If the item - * already exists in the database, we need to use an UPDATE query. - * Otherwise, we need to use an INSERT to get the link id back. - */ - $entry = new \stdClass; - $entry->url = $item->url; - $entry->route = $item->route; - $entry->title = $item->title; - - // We are shortening the description in order to not run into length issues with this field - $entry->description = StringHelper::substr($item->description, 0, 32000); - $entry->indexdate = Factory::getDate()->toSql(); - $entry->state = (int) $item->state; - $entry->access = (int) $item->access; - $entry->language = $item->language; - $entry->type_id = (int) $item->type_id; - $entry->object = ''; - $entry->publish_start_date = $item->publish_start_date; - $entry->publish_end_date = $item->publish_end_date; - $entry->start_date = $item->start_date; - $entry->end_date = $item->end_date; - $entry->list_price = (double) ($item->list_price ?: 0); - $entry->sale_price = (double) ($item->sale_price ?: 0); - - if ($isNew) - { - // Insert the link and get its id. - $db->insertObject('#__finder_links', $entry); - $linkId = (int) $db->insertid(); - } - else - { - // Update the link. - $entry->link_id = $linkId; - $db->updateObject('#__finder_links', $entry, 'link_id'); - } - - // Set up the variables we will need during processing. - $count = 0; - - // Mark afterLinking in the profiler. - static::$profiler ? static::$profiler->mark('afterLinking') : null; - - // Truncate the tokens tables. - $db->truncateTable('#__finder_tokens'); - - // Truncate the tokens aggregate table. - $db->truncateTable('#__finder_tokens_aggregate'); - - /* - * Process the item's content. The items can customize their - * processing instructions to define extra properties to process - * or rearrange how properties are weighted. - */ - foreach ($item->getInstructions() as $group => $properties) - { - // Iterate through the properties of the group. - foreach ($properties as $property) - { - // Check if the property exists in the item. - if (empty($item->$property)) - { - continue; - } - - // Tokenize the property. - if (is_array($item->$property)) - { - // Tokenize an array of content and add it to the database. - foreach ($item->$property as $ip) - { - /* - * If the group is path, we need to a few extra processing - * steps to strip the extension and convert slashes and dashes - * to spaces. - */ - if ($group === static::PATH_CONTEXT) - { - $ip = File::stripExt($ip); - $ip = str_replace(array('/', '-'), ' ', $ip); - } - - // Tokenize a string of content and add it to the database. - $count += $this->tokenizeToDb($ip, $group, $item->language, $format); - - // Check if we're approaching the memory limit of the token table. - if ($count > static::$state->options->get('memory_table_limit', 30000)) - { - $this->toggleTables(false); - } - } - } - else - { - /* - * If the group is path, we need to a few extra processing - * steps to strip the extension and convert slashes and dashes - * to spaces. - */ - if ($group === static::PATH_CONTEXT) - { - $item->$property = File::stripExt($item->$property); - $item->$property = str_replace('/', ' ', $item->$property); - $item->$property = str_replace('-', ' ', $item->$property); - } - - // Tokenize a string of content and add it to the database. - $count += $this->tokenizeToDb($item->$property, $group, $item->language, $format); - - // Check if we're approaching the memory limit of the token table. - if ($count > static::$state->options->get('memory_table_limit', 30000)) - { - $this->toggleTables(false); - } - } - } - } - - /* - * Process the item's taxonomy. The items can customize their - * taxonomy mappings to define extra properties to map. - */ - foreach ($item->getTaxonomy() as $branch => $nodes) - { - // Iterate through the nodes and map them to the branch. - foreach ($nodes as $node) - { - // Add the node to the tree. - if ($node->nested) - { - $nodeId = Taxonomy::addNestedNode($branch, $node->node, $node->state, $node->access, $node->language); - } - else - { - $nodeId = Taxonomy::addNode($branch, $node->title, $node->state, $node->access, $node->language); - } - - // Add the link => node map. - Taxonomy::addMap($linkId, $nodeId); - $node->id = $nodeId; - } - } - - // Mark afterProcessing in the profiler. - static::$profiler ? static::$profiler->mark('afterProcessing') : null; - - /* - * At this point, all of the item's content has been parsed, tokenized - * and inserted into the #__finder_tokens table. Now, we need to - * aggregate all the data into that table into a more usable form. The - * aggregated data will be inserted into #__finder_tokens_aggregate - * table. - */ - $query = 'INSERT INTO ' . $db->quoteName('#__finder_tokens_aggregate') . - ' (' . $db->quoteName('term_id') . - ', ' . $db->quoteName('term') . - ', ' . $db->quoteName('stem') . - ', ' . $db->quoteName('common') . - ', ' . $db->quoteName('phrase') . - ', ' . $db->quoteName('term_weight') . - ', ' . $db->quoteName('context') . - ', ' . $db->quoteName('context_weight') . - ', ' . $db->quoteName('total_weight') . - ', ' . $db->quoteName('language') . ')' . - ' SELECT' . - ' COALESCE(t.term_id, 0), t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context,' . - ' ROUND( t1.weight * COUNT( t2.term ) * %F, 8 ) AS context_weight, 0, t1.language' . - ' FROM (' . - ' SELECT DISTINCT t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context, t1.language' . - ' FROM ' . $db->quoteName('#__finder_tokens') . ' AS t1' . - ' WHERE t1.context = %d' . - ' ) AS t1' . - ' JOIN ' . $db->quoteName('#__finder_tokens') . ' AS t2 ON t2.term = t1.term AND t2.language = t1.language' . - ' LEFT JOIN ' . $db->quoteName('#__finder_terms') . ' AS t ON t.term = t1.term AND t.language = t1.language' . - ' WHERE t2.context = %d' . - ' GROUP BY t1.term, t.term_id, t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context, t1.language' . - ' ORDER BY t1.term DESC'; - - // Iterate through the contexts and aggregate the tokens per context. - foreach ($state->weights as $context => $multiplier) - { - // Run the query to aggregate the tokens for this context.. - $db->setQuery(sprintf($query, $multiplier, $context, $context)); - $db->execute(); - } - - // Mark afterAggregating in the profiler. - static::$profiler ? static::$profiler->mark('afterAggregating') : null; - - /* - * When we pulled down all of the aggregate data, we did a LEFT JOIN - * over the terms table to try to find all the term ids that - * already exist for our tokens. If any of the rows in the aggregate - * table have a term of 0, then no term record exists for that - * term so we need to add it to the terms table. - */ - $db->setQuery( - 'INSERT INTO ' . $db->quoteName('#__finder_terms') . - ' (' . $db->quoteName('term') . - ', ' . $db->quoteName('stem') . - ', ' . $db->quoteName('common') . - ', ' . $db->quoteName('phrase') . - ', ' . $db->quoteName('weight') . - ', ' . $db->quoteName('soundex') . - ', ' . $db->quoteName('language') . ')' . - ' SELECT ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language' . - ' FROM ' . $db->quoteName('#__finder_tokens_aggregate') . ' AS ta' . - ' WHERE ta.term_id = 0' . - ' GROUP BY ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language' - ); - $db->execute(); - - /* - * Now, we just inserted a bunch of new records into the terms table - * so we need to go back and update the aggregate table with all the - * new term ids. - */ - $query = $db->getQuery(true) - ->update($db->quoteName('#__finder_tokens_aggregate', 'ta')) - ->innerJoin($db->quoteName('#__finder_terms', 't'), 't.term = ta.term AND t.language = ta.language') - ->where('ta.term_id = 0'); - - if ($serverType == 'mysql') - { - $query->set($db->quoteName('ta.term_id') . ' = ' . $db->quoteName('t.term_id')); - } - else - { - $query->set($db->quoteName('term_id') . ' = ' . $db->quoteName('t.term_id')); - } - - $db->setQuery($query); - $db->execute(); - - // Mark afterTerms in the profiler. - static::$profiler ? static::$profiler->mark('afterTerms') : null; - - /* - * After we've made sure that all of the terms are in the terms table - * and the aggregate table has the correct term ids, we need to update - * the links counter for each term by one. - */ - $query->clear() - ->update($db->quoteName('#__finder_terms', 't')) - ->innerJoin($db->quoteName('#__finder_tokens_aggregate', 'ta'), 'ta.term_id = t.term_id'); - - if ($serverType == 'mysql') - { - $query->set($db->quoteName('t.links') . ' = t.links + 1'); - } - else - { - $query->set($db->quoteName('links') . ' = t.links + 1'); - } - - $db->setQuery($query); - $db->execute(); - - // Mark afterTerms in the profiler. - static::$profiler ? static::$profiler->mark('afterTerms') : null; - - /* - * At this point, the aggregate table contains a record for each - * term in each context. So, we're going to pull down all of that - * data while grouping the records by term and add all of the - * sub-totals together to arrive at the final total for each token for - * this link. Then, we insert all of that data into the mapping table. - */ - $db->setQuery( - 'INSERT INTO ' . $db->quoteName('#__finder_links_terms') . - ' (' . $db->quoteName('link_id') . - ', ' . $db->quoteName('term_id') . - ', ' . $db->quoteName('weight') . ')' . - ' SELECT ' . (int) $linkId . ', ' . $db->quoteName('term_id') . ',' . - ' ROUND(SUM(' . $db->quoteName('context_weight') . '), 8)' . - ' FROM ' . $db->quoteName('#__finder_tokens_aggregate') . - ' GROUP BY ' . $db->quoteName('term') . ', ' . $db->quoteName('term_id') . - ' ORDER BY ' . $db->quoteName('term') . ' DESC' - ); - $db->execute(); - - // Mark afterMapping in the profiler. - static::$profiler ? static::$profiler->mark('afterMapping') : null; - - // Update the signature. - $object = serialize($item); - $query->clear() - ->update($db->quoteName('#__finder_links')) - ->set($db->quoteName('md5sum') . ' = :md5sum') - ->set($db->quoteName('object') . ' = :object') - ->where($db->quoteName('link_id') . ' = :linkid') - ->bind(':md5sum', $curSig) - ->bind(':object', $object, ParameterType::LARGE_OBJECT) - ->bind(':linkid', $linkId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Mark afterSigning in the profiler. - static::$profiler ? static::$profiler->mark('afterSigning') : null; - - // Truncate the tokens tables. - $db->truncateTable('#__finder_tokens'); - - // Truncate the tokens aggregate table. - $db->truncateTable('#__finder_tokens_aggregate'); - - // Toggle the token tables back to memory tables. - $this->toggleTables(true); - - // Mark afterTruncating in the profiler. - static::$profiler ? static::$profiler->mark('afterTruncating') : null; - - // Trigger a plugin event after indexing - PluginHelper::importPlugin('finder'); - Factory::getApplication()->triggerEvent('onFinderIndexAfterIndex', array($item, $linkId)); - - return $linkId; - } - - /** - * Method to remove a link from the index. - * - * @param integer $linkId The id of the link. - * @param bool $removeTaxonomies Remove empty taxonomies - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - public function remove($linkId, $removeTaxonomies = true) - { - $db = $this->db; - $query = $db->getQuery(true); - $linkId = (int) $linkId; - - // Update the link counts for the terms. - $query->clear() - ->update($db->quoteName('#__finder_terms', 't')) - ->join('INNER', $db->quoteName('#__finder_links_terms', 'm'), $db->quoteName('m.term_id') . ' = ' . $db->quoteName('t.term_id')) - ->set($db->quoteName('links') . ' = ' . $db->quoteName('links') . ' - 1') - ->where($db->quoteName('m.link_id') . ' = :linkid') - ->bind(':linkid', $linkId, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Remove all records from the mapping tables. - $query->clear() - ->delete($db->quoteName('#__finder_links_terms')) - ->where($db->quoteName('link_id') . ' = :linkid') - ->bind(':linkid', $linkId, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Delete all orphaned terms. - $query->clear() - ->delete($db->quoteName('#__finder_terms')) - ->where($db->quoteName('links') . ' <= 0'); - $db->setQuery($query)->execute(); - - // Delete the link from the index. - $query->clear() - ->delete($db->quoteName('#__finder_links')) - ->where($db->quoteName('link_id') . ' = :linkid') - ->bind(':linkid', $linkId, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Remove the taxonomy maps. - Taxonomy::removeMaps($linkId); - - // Remove the orphaned taxonomy nodes. - if ($removeTaxonomies) - { - Taxonomy::removeOrphanNodes(); - } - - PluginHelper::importPlugin('finder'); - Factory::getApplication()->triggerEvent('onFinderIndexAfterDelete', array($linkId)); - - return true; - } - - /** - * Method to optimize the index. We use this method to remove unused terms - * and any other optimizations that might be necessary. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - public function optimize() - { - // Get the database object. - $db = $this->db; - $serverType = strtolower($db->getServerType()); - $query = $db->getQuery(true); - - // Delete all orphaned terms. - $query->delete($db->quoteName('#__finder_terms')) - ->where($db->quoteName('links') . ' <= 0'); - $db->setQuery($query); - $db->execute(); - - // Remove the orphaned taxonomy nodes. - Taxonomy::removeOrphanNodes(); - - // Optimize the tables. - $tables = [ - '#__finder_links', - '#__finder_links_terms', - '#__finder_filters', - '#__finder_terms_common', - '#__finder_types', - '#__finder_taxonomy_map', - '#__finder_taxonomy' - ]; - - foreach ($tables as $table) - { - if ($serverType == 'mysql') - { - $db->setQuery('OPTIMIZE TABLE ' . $db->quoteName($table)); - $db->execute(); - } - else - { - $db->setQuery('VACUUM ' . $db->quoteName($table)); - $db->execute(); - $db->setQuery('REINDEX TABLE ' . $db->quoteName($table)); - $db->execute(); - } - } - - return true; - } - - /** - * Method to get a content item's signature. - * - * @param object $item The content item to index. - * - * @return string The content item's signature. - * - * @since 2.5 - */ - protected static function getSignature($item) - { - // Get the indexer state. - $state = static::getState(); - - // Get the relevant configuration variables. - $config = array( - $state->weights, - $state->options->get('stem', 1), - $state->options->get('stemmer', 'porter_en') - ); - - return md5(serialize(array($item, $config))); - } - - /** - * Method to parse input, tokenize it, and then add it to the database. - * - * @param mixed $input String or resource to use as input. A resource input will automatically be chunked to conserve - * memory. Strings will be chunked if longer than 2K in size. - * @param integer $context The context of the input. See context constants. - * @param string $lang The language of the input. - * @param string $format The format of the input. - * - * @return integer The number of tokens extracted from the input. - * - * @since 2.5 - */ - protected function tokenizeToDb($input, $context, $lang, $format) - { - $count = 0; - $buffer = null; - - if (empty($input)) - { - return $count; - } - - // If the input is a resource, batch the process out. - if (is_resource($input)) - { - // Batch the process out to avoid memory limits. - while (!feof($input)) - { - // Read into the buffer. - $buffer .= fread($input, 2048); - - /* - * If we haven't reached the end of the file, seek to the last - * space character and drop whatever is after that to make sure - * we didn't truncate a term while reading the input. - */ - if (!feof($input)) - { - // Find the last space character. - $ls = strrpos($buffer, ' '); - - // Adjust string based on the last space character. - if ($ls) - { - // Truncate the string to the last space character. - $string = substr($buffer, 0, $ls); - - // Adjust the buffer based on the last space for the next iteration and trim. - $buffer = StringHelper::trim(substr($buffer, $ls)); - } - // No space character was found. - else - { - $string = $buffer; - } - } - // We've reached the end of the file, so parse whatever remains. - else - { - $string = $buffer; - } - - // Parse, tokenise and add tokens to the database. - $count = $this->tokenizeToDbShort($string, $context, $lang, $format, $count); - - unset($string); - } - - return $count; - } - - // Parse, tokenise and add tokens to the database. - $count = $this->tokenizeToDbShort($input, $context, $lang, $format, $count); - - return $count; - } - - /** - * Method to parse input, tokenise it, then add the tokens to the database. - * - * @param string $input String to parse, tokenise and add to database. - * @param integer $context The context of the input. See context constants. - * @param string $lang The language of the input. - * @param string $format The format of the input. - * @param integer $count The number of tokens processed so far. - * - * @return integer Cumulative number of tokens extracted from the input so far. - * - * @since 3.7.0 - */ - private function tokenizeToDbShort($input, $context, $lang, $format, $count) - { - // Parse the input. - $input = Helper::parse($input, $format); - - // Check the input. - if (empty($input)) - { - return $count; - } - - // Tokenize the input. - $tokens = Helper::tokenize($input, $lang); - - if (count($tokens) == 0) - { - return $count; - } - - // Add the tokens to the database. - $count += $this->addTokensToDb($tokens, $context); - - // Check if we're approaching the memory limit of the token table. - if ($count > static::$state->options->get('memory_table_limit', 10000)) - { - $this->toggleTables(false); - } - - return $count; - } - - /** - * Method to add a set of tokens to the database. - * - * @param Token[]|Token $tokens An array or single Token object. - * @param mixed $context The context of the tokens. See context constants. [optional] - * - * @return integer The number of tokens inserted into the database. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function addTokensToDb($tokens, $context = '') - { - static $filterCommon, $filterNumeric; - - if (is_null($filterCommon)) - { - $params = ComponentHelper::getParams('com_finder'); - $filterCommon = $params->get('filter_commonwords', false); - $filterNumeric = $params->get('filter_numerics', false); - } - - // Get the database object. - $db = $this->db; - - $query = clone $this->addTokensToDbQueryTemplate; - - // Check if a single FinderIndexerToken object was given and make it to be an array of FinderIndexerToken objects - $tokens = is_array($tokens) ? $tokens : array($tokens); - - // Count the number of token values. - $values = 0; - - // Break into chunks of no more than 128 items - $chunks = array_chunk($tokens, 128); - - foreach ($chunks as $tokens) - { - $query->clear('values'); - - foreach ($tokens as $token) - { - // Database size for a term field - if ($token->length > 75) - { - continue; - } - - if ($filterCommon && $token->common) - { - continue; - } - - if ($filterNumeric && $token->numeric) - { - continue; - } - - $query->values( - $db->quote($token->term) . ', ' - . $db->quote($token->stem) . ', ' - . (int) $token->common . ', ' - . (int) $token->phrase . ', ' - . $db->quote($token->weight) . ', ' - . (int) $context . ', ' - . $db->quote($token->language) - ); - ++$values; - } - - // Only execute the query if there are tokens to insert - if ($query->values !== null) - { - $db->setQuery($query)->execute(); - } - - // Check if we're approaching the memory limit of the token table. - if ($values > static::$state->options->get('memory_table_limit', 10000)) - { - $this->toggleTables(false); - } - } - - return $values; - } - - /** - * Method to switch the token tables from Memory tables to Disk tables - * when they are close to running out of memory. - * Since this is not supported/implemented in all DB-drivers, the default is a stub method, which simply returns true. - * - * @param boolean $memory Flag to control how they should be toggled. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function toggleTables($memory) - { - if (strtolower(Factory::getDbo()->getServerType()) != 'mysql') - { - return true; - } - - static $state; - - // Get the database adapter. - $db = $this->db; - - // Check if we are setting the tables to the Memory engine. - if ($memory === true && $state !== true) - { - // Set the tokens table to Memory. - $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens') . ' ENGINE = MEMORY'); - $db->execute(); - - // Set the tokens aggregate table to Memory. - $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens_aggregate') . ' ENGINE = MEMORY'); - $db->execute(); - - // Set the internal state. - $state = $memory; - } - // We must be setting the tables to the InnoDB engine. - elseif ($memory === false && $state !== false) - { - // Set the tokens table to InnoDB. - $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens') . ' ENGINE = INNODB'); - $db->execute(); - - // Set the tokens aggregate table to InnoDB. - $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens_aggregate') . ' ENGINE = INNODB'); - $db->execute(); - - // Set the internal state. - $state = $memory; - } - - return true; - } + /** + * The title context identifier. + * + * @var integer + * @since 2.5 + */ + public const TITLE_CONTEXT = 1; + + /** + * The text context identifier. + * + * @var integer + * @since 2.5 + */ + public const TEXT_CONTEXT = 2; + + /** + * The meta context identifier. + * + * @var integer + * @since 2.5 + */ + public const META_CONTEXT = 3; + + /** + * The path context identifier. + * + * @var integer + * @since 2.5 + */ + public const PATH_CONTEXT = 4; + + /** + * The misc context identifier. + * + * @var integer + * @since 2.5 + */ + public const MISC_CONTEXT = 5; + + /** + * The indexer state object. + * + * @var CMSObject + * @since 2.5 + */ + public static $state; + + /** + * The indexer profiler object. + * + * @var Profiler + * @since 2.5 + */ + public static $profiler; + + /** + * Database driver cache. + * + * @var \Joomla\Database\DatabaseDriver + * @since 3.8.0 + */ + protected $db; + + /** + * Reusable Query Template. To be used with clone. + * + * @var QueryInterface + * @since 3.8.0 + */ + protected $addTokensToDbQueryTemplate; + + /** + * Indexer constructor. + * + * @param DatabaseInterface $db The database + * + * @since 3.8.0 + */ + public function __construct(DatabaseInterface $db = null) + { + if ($db === null) { + @trigger_error(sprintf('Database will be mandatory in 5.0.'), E_USER_DEPRECATED); + $db = Factory::getContainer()->get(DatabaseInterface::class); + } + + $this->db = $db; + + // Set up query template for addTokensToDb + $this->addTokensToDbQueryTemplate = $db->getQuery(true)->insert($db->quoteName('#__finder_tokens')) + ->columns( + array( + $db->quoteName('term'), + $db->quoteName('stem'), + $db->quoteName('common'), + $db->quoteName('phrase'), + $db->quoteName('weight'), + $db->quoteName('context'), + $db->quoteName('language') + ) + ); + } + + /** + * Method to get the indexer state. + * + * @return object The indexer state object. + * + * @since 2.5 + */ + public static function getState() + { + // First, try to load from the internal state. + if ((bool) static::$state) { + return static::$state; + } + + // If we couldn't load from the internal state, try the session. + $session = Factory::getSession(); + $data = $session->get('_finder.state', null); + + // If the state is empty, load the values for the first time. + if (empty($data)) { + $data = new CMSObject(); + $data->force = false; + + // Load the default configuration options. + $data->options = ComponentHelper::getParams('com_finder'); + $db = Factory::getDbo(); + + if ($db->getServerType() == 'mysql') { + /** + * Try to calculate the heapsize for the memory table for indexing. If this fails, + * we fall back on a reasonable small size. We want to prevent the system to fail + * and block saving content. + */ + try { + $db->setQuery('SHOW VARIABLES LIKE ' . $db->quote('max_heap_table_size')); + $heapsize = $db->loadObject(); + + /** + * In tests, the size of a row seems to have been around 720 bytes. + * We take 800 to be on the safe side. + */ + $memory_table_limit = (int) ($heapsize->Value / 800); + $data->options->set('memory_table_limit', $memory_table_limit); + } catch (Exception $e) { + // Something failed. We fall back to a reasonable guess. + $data->options->set('memory_table_limit', 7500); + } + } else { + // We are running on PostgreSQL and don't have this issue, so we set a rather high number. + $data->options->set('memory_table_limit', 50000); + } + + // Setup the weight lookup information. + $data->weights = array( + self::TITLE_CONTEXT => round($data->options->get('title_multiplier', 1.7), 2), + self::TEXT_CONTEXT => round($data->options->get('text_multiplier', 0.7), 2), + self::META_CONTEXT => round($data->options->get('meta_multiplier', 1.2), 2), + self::PATH_CONTEXT => round($data->options->get('path_multiplier', 2.0), 2), + self::MISC_CONTEXT => round($data->options->get('misc_multiplier', 0.3), 2) + ); + + // Set the current time as the start time. + $data->startTime = Factory::getDate()->toSql(); + + // Set the remaining default values. + $data->batchSize = (int) $data->options->get('batch_size', 50); + $data->batchOffset = 0; + $data->totalItems = 0; + $data->pluginState = array(); + } + + // Setup the profiler if debugging is enabled. + if (Factory::getApplication()->get('debug')) { + static::$profiler = Profiler::getInstance('FinderIndexer'); + } + + // Set the state. + static::$state = $data; + + return static::$state; + } + + /** + * Method to set the indexer state. + * + * @param CMSObject $data A new indexer state object. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + */ + public static function setState($data) + { + // Check the state object. + if (empty($data) || !$data instanceof CMSObject) { + return false; + } + + // Set the new internal state. + static::$state = $data; + + // Set the new session state. + Factory::getSession()->set('_finder.state', $data); + + return true; + } + + /** + * Method to reset the indexer state. + * + * @return void + * + * @since 2.5 + */ + public static function resetState() + { + // Reset the internal state to null. + self::$state = null; + + // Reset the session state to null. + Factory::getSession()->set('_finder.state', null); + } + + /** + * Method to index a content item. + * + * @param Result $item The content item to index. + * @param string $format The format of the content. [optional] + * + * @return integer The ID of the record in the links table. + * + * @since 2.5 + * @throws \Exception on database error. + */ + public function index($item, $format = 'html') + { + // Mark beforeIndexing in the profiler. + static::$profiler ? static::$profiler->mark('beforeIndexing') : null; + $db = $this->db; + $serverType = strtolower($db->getServerType()); + + // Check if the item is in the database. + $query = $db->getQuery(true) + ->select($db->quoteName('link_id') . ', ' . $db->quoteName('md5sum')) + ->from($db->quoteName('#__finder_links')) + ->where($db->quoteName('url') . ' = ' . $db->quote($item->url)); + + // Load the item from the database. + $db->setQuery($query); + $link = $db->loadObject(); + + // Get the indexer state. + $state = static::getState(); + + // Get the signatures of the item. + $curSig = static::getSignature($item); + $oldSig = $link->md5sum ?? null; + + // Get the other item information. + $linkId = empty($link->link_id) ? null : $link->link_id; + $isNew = empty($link->link_id); + + // Check the signatures. If they match, the item is up to date. + if (!$isNew && $curSig == $oldSig) { + return $linkId; + } + + /* + * If the link already exists, flush all the term maps for the item. + * Maps are stored in 16 tables so we need to iterate through and flush + * each table one at a time. + */ + if (!$isNew) { + // Flush the maps for the link. + $query->clear() + ->delete($db->quoteName('#__finder_links_terms')) + ->where($db->quoteName('link_id') . ' = ' . (int) $linkId); + $db->setQuery($query); + $db->execute(); + + // Remove the taxonomy maps. + Taxonomy::removeMaps($linkId); + } + + // Mark afterUnmapping in the profiler. + static::$profiler ? static::$profiler->mark('afterUnmapping') : null; + + // Perform cleanup on the item data. + $item->publish_start_date = (int) $item->publish_start_date != 0 ? $item->publish_start_date : null; + $item->publish_end_date = (int) $item->publish_end_date != 0 ? $item->publish_end_date : null; + $item->start_date = (int) $item->start_date != 0 ? $item->start_date : null; + $item->end_date = (int) $item->end_date != 0 ? $item->end_date : null; + + // Prepare the item description. + $item->description = Helper::parse($item->summary ?? ''); + + /* + * Now, we need to enter the item into the links table. If the item + * already exists in the database, we need to use an UPDATE query. + * Otherwise, we need to use an INSERT to get the link id back. + */ + $entry = new \stdClass(); + $entry->url = $item->url; + $entry->route = $item->route; + $entry->title = $item->title; + + // We are shortening the description in order to not run into length issues with this field + $entry->description = StringHelper::substr($item->description, 0, 32000); + $entry->indexdate = Factory::getDate()->toSql(); + $entry->state = (int) $item->state; + $entry->access = (int) $item->access; + $entry->language = $item->language; + $entry->type_id = (int) $item->type_id; + $entry->object = ''; + $entry->publish_start_date = $item->publish_start_date; + $entry->publish_end_date = $item->publish_end_date; + $entry->start_date = $item->start_date; + $entry->end_date = $item->end_date; + $entry->list_price = (double) ($item->list_price ?: 0); + $entry->sale_price = (double) ($item->sale_price ?: 0); + + if ($isNew) { + // Insert the link and get its id. + $db->insertObject('#__finder_links', $entry); + $linkId = (int) $db->insertid(); + } else { + // Update the link. + $entry->link_id = $linkId; + $db->updateObject('#__finder_links', $entry, 'link_id'); + } + + // Set up the variables we will need during processing. + $count = 0; + + // Mark afterLinking in the profiler. + static::$profiler ? static::$profiler->mark('afterLinking') : null; + + // Truncate the tokens tables. + $db->truncateTable('#__finder_tokens'); + + // Truncate the tokens aggregate table. + $db->truncateTable('#__finder_tokens_aggregate'); + + /* + * Process the item's content. The items can customize their + * processing instructions to define extra properties to process + * or rearrange how properties are weighted. + */ + foreach ($item->getInstructions() as $group => $properties) { + // Iterate through the properties of the group. + foreach ($properties as $property) { + // Check if the property exists in the item. + if (empty($item->$property)) { + continue; + } + + // Tokenize the property. + if (is_array($item->$property)) { + // Tokenize an array of content and add it to the database. + foreach ($item->$property as $ip) { + /* + * If the group is path, we need to a few extra processing + * steps to strip the extension and convert slashes and dashes + * to spaces. + */ + if ($group === static::PATH_CONTEXT) { + $ip = File::stripExt($ip); + $ip = str_replace(array('/', '-'), ' ', $ip); + } + + // Tokenize a string of content and add it to the database. + $count += $this->tokenizeToDb($ip, $group, $item->language, $format, $count); + + // Check if we're approaching the memory limit of the token table. + if ($count > static::$state->options->get('memory_table_limit', 7500)) { + $this->toggleTables(false); + } + } + } else { + /* + * If the group is path, we need to a few extra processing + * steps to strip the extension and convert slashes and dashes + * to spaces. + */ + if ($group === static::PATH_CONTEXT) { + $item->$property = File::stripExt($item->$property); + $item->$property = str_replace('/', ' ', $item->$property); + $item->$property = str_replace('-', ' ', $item->$property); + } + + // Tokenize a string of content and add it to the database. + $count += $this->tokenizeToDb($item->$property, $group, $item->language, $format, $count); + + // Check if we're approaching the memory limit of the token table. + if ($count > static::$state->options->get('memory_table_limit', 30000)) { + $this->toggleTables(false); + } + } + } + } + + /* + * Process the item's taxonomy. The items can customize their + * taxonomy mappings to define extra properties to map. + */ + foreach ($item->getTaxonomy() as $branch => $nodes) { + // Iterate through the nodes and map them to the branch. + foreach ($nodes as $node) { + // Add the node to the tree. + if ($node->nested) { + $nodeId = Taxonomy::addNestedNode($branch, $node->node, $node->state, $node->access, $node->language); + } else { + $nodeId = Taxonomy::addNode($branch, $node->title, $node->state, $node->access, $node->language); + } + + // Add the link => node map. + Taxonomy::addMap($linkId, $nodeId); + $node->id = $nodeId; + } + } + + // Mark afterProcessing in the profiler. + static::$profiler ? static::$profiler->mark('afterProcessing') : null; + + /* + * At this point, all of the item's content has been parsed, tokenized + * and inserted into the #__finder_tokens table. Now, we need to + * aggregate all the data into that table into a more usable form. The + * aggregated data will be inserted into #__finder_tokens_aggregate + * table. + */ + $query = 'INSERT INTO ' . $db->quoteName('#__finder_tokens_aggregate') . + ' (' . $db->quoteName('term_id') . + ', ' . $db->quoteName('term') . + ', ' . $db->quoteName('stem') . + ', ' . $db->quoteName('common') . + ', ' . $db->quoteName('phrase') . + ', ' . $db->quoteName('term_weight') . + ', ' . $db->quoteName('context') . + ', ' . $db->quoteName('context_weight') . + ', ' . $db->quoteName('total_weight') . + ', ' . $db->quoteName('language') . ')' . + ' SELECT' . + ' COALESCE(t.term_id, 0), t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context,' . + ' ROUND( t1.weight * COUNT( t2.term ) * %F, 8 ) AS context_weight, 0, t1.language' . + ' FROM (' . + ' SELECT DISTINCT t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context, t1.language' . + ' FROM ' . $db->quoteName('#__finder_tokens') . ' AS t1' . + ' WHERE t1.context = %d' . + ' ) AS t1' . + ' JOIN ' . $db->quoteName('#__finder_tokens') . ' AS t2 ON t2.term = t1.term AND t2.language = t1.language' . + ' LEFT JOIN ' . $db->quoteName('#__finder_terms') . ' AS t ON t.term = t1.term AND t.language = t1.language' . + ' WHERE t2.context = %d' . + ' GROUP BY t1.term, t.term_id, t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context, t1.language' . + ' ORDER BY t1.term DESC'; + + // Iterate through the contexts and aggregate the tokens per context. + foreach ($state->weights as $context => $multiplier) { + // Run the query to aggregate the tokens for this context.. + $db->setQuery(sprintf($query, $multiplier, $context, $context)); + $db->execute(); + } + + // Mark afterAggregating in the profiler. + static::$profiler ? static::$profiler->mark('afterAggregating') : null; + + /* + * When we pulled down all of the aggregate data, we did a LEFT JOIN + * over the terms table to try to find all the term ids that + * already exist for our tokens. If any of the rows in the aggregate + * table have a term of 0, then no term record exists for that + * term so we need to add it to the terms table. + */ + $db->setQuery( + 'INSERT INTO ' . $db->quoteName('#__finder_terms') . + ' (' . $db->quoteName('term') . + ', ' . $db->quoteName('stem') . + ', ' . $db->quoteName('common') . + ', ' . $db->quoteName('phrase') . + ', ' . $db->quoteName('weight') . + ', ' . $db->quoteName('soundex') . + ', ' . $db->quoteName('language') . ')' . + ' SELECT ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language' . + ' FROM ' . $db->quoteName('#__finder_tokens_aggregate') . ' AS ta' . + ' WHERE ta.term_id = 0' . + ' GROUP BY ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language' + ); + $db->execute(); + + /* + * Now, we just inserted a bunch of new records into the terms table + * so we need to go back and update the aggregate table with all the + * new term ids. + */ + $query = $db->getQuery(true) + ->update($db->quoteName('#__finder_tokens_aggregate', 'ta')) + ->innerJoin($db->quoteName('#__finder_terms', 't'), 't.term = ta.term AND t.language = ta.language') + ->where('ta.term_id = 0'); + + if ($serverType == 'mysql') { + $query->set($db->quoteName('ta.term_id') . ' = ' . $db->quoteName('t.term_id')); + } else { + $query->set($db->quoteName('term_id') . ' = ' . $db->quoteName('t.term_id')); + } + + $db->setQuery($query); + $db->execute(); + + // Mark afterTerms in the profiler. + static::$profiler ? static::$profiler->mark('afterTerms') : null; + + /* + * After we've made sure that all of the terms are in the terms table + * and the aggregate table has the correct term ids, we need to update + * the links counter for each term by one. + */ + $query->clear() + ->update($db->quoteName('#__finder_terms', 't')) + ->innerJoin($db->quoteName('#__finder_tokens_aggregate', 'ta'), 'ta.term_id = t.term_id'); + + if ($serverType == 'mysql') { + $query->set($db->quoteName('t.links') . ' = t.links + 1'); + } else { + $query->set($db->quoteName('links') . ' = t.links + 1'); + } + + $db->setQuery($query); + $db->execute(); + + // Mark afterTerms in the profiler. + static::$profiler ? static::$profiler->mark('afterTerms') : null; + + /* + * At this point, the aggregate table contains a record for each + * term in each context. So, we're going to pull down all of that + * data while grouping the records by term and add all of the + * sub-totals together to arrive at the final total for each token for + * this link. Then, we insert all of that data into the mapping table. + */ + $db->setQuery( + 'INSERT INTO ' . $db->quoteName('#__finder_links_terms') . + ' (' . $db->quoteName('link_id') . + ', ' . $db->quoteName('term_id') . + ', ' . $db->quoteName('weight') . ')' . + ' SELECT ' . (int) $linkId . ', ' . $db->quoteName('term_id') . ',' . + ' ROUND(SUM(' . $db->quoteName('context_weight') . '), 8)' . + ' FROM ' . $db->quoteName('#__finder_tokens_aggregate') . + ' GROUP BY ' . $db->quoteName('term') . ', ' . $db->quoteName('term_id') . + ' ORDER BY ' . $db->quoteName('term') . ' DESC' + ); + $db->execute(); + + // Mark afterMapping in the profiler. + static::$profiler ? static::$profiler->mark('afterMapping') : null; + + // Update the signature. + $object = serialize($item); + $query->clear() + ->update($db->quoteName('#__finder_links')) + ->set($db->quoteName('md5sum') . ' = :md5sum') + ->set($db->quoteName('object') . ' = :object') + ->where($db->quoteName('link_id') . ' = :linkid') + ->bind(':md5sum', $curSig) + ->bind(':object', $object, ParameterType::LARGE_OBJECT) + ->bind(':linkid', $linkId, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Mark afterSigning in the profiler. + static::$profiler ? static::$profiler->mark('afterSigning') : null; + + // Truncate the tokens tables. + $db->truncateTable('#__finder_tokens'); + + // Truncate the tokens aggregate table. + $db->truncateTable('#__finder_tokens_aggregate'); + + // Toggle the token tables back to memory tables. + $this->toggleTables(true); + + // Mark afterTruncating in the profiler. + static::$profiler ? static::$profiler->mark('afterTruncating') : null; + + // Trigger a plugin event after indexing + PluginHelper::importPlugin('finder'); + Factory::getApplication()->triggerEvent('onFinderIndexAfterIndex', array($item, $linkId)); + + return $linkId; + } + + /** + * Method to remove a link from the index. + * + * @param integer $linkId The id of the link. + * @param bool $removeTaxonomies Remove empty taxonomies + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + public function remove($linkId, $removeTaxonomies = true) + { + $db = $this->db; + $query = $db->getQuery(true); + $linkId = (int) $linkId; + + // Update the link counts for the terms. + $query->clear() + ->update($db->quoteName('#__finder_terms', 't')) + ->join('INNER', $db->quoteName('#__finder_links_terms', 'm'), $db->quoteName('m.term_id') . ' = ' . $db->quoteName('t.term_id')) + ->set($db->quoteName('links') . ' = ' . $db->quoteName('links') . ' - 1') + ->where($db->quoteName('m.link_id') . ' = :linkid') + ->bind(':linkid', $linkId, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Remove all records from the mapping tables. + $query->clear() + ->delete($db->quoteName('#__finder_links_terms')) + ->where($db->quoteName('link_id') . ' = :linkid') + ->bind(':linkid', $linkId, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Delete all orphaned terms. + $query->clear() + ->delete($db->quoteName('#__finder_terms')) + ->where($db->quoteName('links') . ' <= 0'); + $db->setQuery($query)->execute(); + + // Delete the link from the index. + $query->clear() + ->delete($db->quoteName('#__finder_links')) + ->where($db->quoteName('link_id') . ' = :linkid') + ->bind(':linkid', $linkId, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Remove the taxonomy maps. + Taxonomy::removeMaps($linkId); + + // Remove the orphaned taxonomy nodes. + if ($removeTaxonomies) { + Taxonomy::removeOrphanNodes(); + } + + PluginHelper::importPlugin('finder'); + Factory::getApplication()->triggerEvent('onFinderIndexAfterDelete', array($linkId)); + + return true; + } + + /** + * Method to optimize the index. We use this method to remove unused terms + * and any other optimizations that might be necessary. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + public function optimize() + { + // Get the database object. + $db = $this->db; + $serverType = strtolower($db->getServerType()); + $query = $db->getQuery(true); + + // Delete all orphaned terms. + $query->delete($db->quoteName('#__finder_terms')) + ->where($db->quoteName('links') . ' <= 0'); + $db->setQuery($query); + $db->execute(); + + // Delete all broken links. (Links missing the object) + $query = $db->getQuery(true) + ->delete('#__finder_links') + ->where($db->quoteName('object') . ' = ' . $db->quote('')); + $db->setQuery($query); + $db->execute(); + + // Delete all orphaned mappings of terms to links + $query2 = $db->getQuery(true) + ->select($db->quoteName('link_id')) + ->from($db->quoteName('#__finder_links')); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_links_terms')) + ->where($db->quoteName('link_id') . ' NOT IN (' . $query2 . ')'); + $db->setQuery($query); + $db->execute(); + + // Delete all orphaned terms + $query2 = $db->getQuery(true) + ->select($db->quoteName('term_id')) + ->from($db->quoteName('#__finder_links_terms')); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_terms')) + ->where($db->quoteName('term_id') . ' NOT IN (' . $query2 . ')'); + $db->setQuery($query); + $db->execute(); + + // Delete all orphaned taxonomies + Taxonomy::removeOrphanMaps(); + Taxonomy::removeOrphanNodes(); + + // Optimize the tables. + $tables = [ + '#__finder_links', + '#__finder_links_terms', + '#__finder_filters', + '#__finder_terms_common', + '#__finder_types', + '#__finder_taxonomy_map', + '#__finder_taxonomy' + ]; + + foreach ($tables as $table) { + if ($serverType == 'mysql') { + $db->setQuery('OPTIMIZE TABLE ' . $db->quoteName($table)); + $db->execute(); + } else { + $db->setQuery('VACUUM ' . $db->quoteName($table)); + $db->execute(); + $db->setQuery('REINDEX TABLE ' . $db->quoteName($table)); + $db->execute(); + } + } + + return true; + } + + /** + * Method to get a content item's signature. + * + * @param object $item The content item to index. + * + * @return string The content item's signature. + * + * @since 2.5 + */ + protected static function getSignature($item) + { + // Get the indexer state. + $state = static::getState(); + + // Get the relevant configuration variables. + $config = array( + $state->weights, + $state->options->get('tuplecount', 1), + $state->options->get('language_default', '') + ); + + return md5(serialize(array($item, $config))); + } + + /** + * Method to parse input, tokenize it, and then add it to the database. + * + * @param mixed $input String or resource to use as input. A resource input will automatically be chunked to conserve + * memory. Strings will be chunked if longer than 2K in size. + * @param integer $context The context of the input. See context constants. + * @param string $lang The language of the input. + * @param string $format The format of the input. + * @param integer $count Number of words indexed so far. + * + * @return integer The number of tokens extracted from the input. + * + * @since 2.5 + */ + protected function tokenizeToDb($input, $context, $lang, $format, $count = 0) + { + $buffer = null; + + if (empty($input)) { + return $count; + } + + // If the input is a resource, batch the process out. + if (is_resource($input)) { + // Batch the process out to avoid memory limits. + while (!feof($input)) { + // Read into the buffer. + $buffer .= fread($input, 2048); + + /* + * If we haven't reached the end of the file, seek to the last + * space character and drop whatever is after that to make sure + * we didn't truncate a term while reading the input. + */ + if (!feof($input)) { + // Find the last space character. + $ls = strrpos($buffer, ' '); + + // Adjust string based on the last space character. + if ($ls) { + // Truncate the string to the last space character. + $string = substr($buffer, 0, $ls); + + // Adjust the buffer based on the last space for the next iteration and trim. + $buffer = StringHelper::trim(substr($buffer, $ls)); + } else { + // No space character was found. + $string = $buffer; + } + } else { + // We've reached the end of the file, so parse whatever remains. + $string = $buffer; + } + + // Parse, tokenise and add tokens to the database. + $count = $this->tokenizeToDbShort($string, $context, $lang, $format, $count); + + unset($string); + } + + return $count; + } + + // Parse, tokenise and add tokens to the database. + $count = $this->tokenizeToDbShort($input, $context, $lang, $format, $count); + + return $count; + } + + /** + * Method to parse input, tokenise it, then add the tokens to the database. + * + * @param string $input String to parse, tokenise and add to database. + * @param integer $context The context of the input. See context constants. + * @param string $lang The language of the input. + * @param string $format The format of the input. + * @param integer $count The number of tokens processed so far. + * + * @return integer Cumulative number of tokens extracted from the input so far. + * + * @since 3.7.0 + */ + private function tokenizeToDbShort($input, $context, $lang, $format, $count) + { + static $filterCommon, $filterNumeric; + + if (is_null($filterCommon)) { + $params = ComponentHelper::getParams('com_finder'); + $filterCommon = $params->get('filter_commonwords', false); + $filterNumeric = $params->get('filter_numerics', false); + } + + // Parse the input. + $input = Helper::parse($input, $format); + + // Check the input. + if (empty($input)) { + return $count; + } + + // Tokenize the input. + $tokens = Helper::tokenize($input, $lang); + + if (count($tokens) == 0) { + return $count; + } + + $query = clone $this->addTokensToDbQueryTemplate; + + // Break into chunks of no more than 128 items + $chunks = array_chunk($tokens, 128); + + foreach ($chunks as $tokens) { + $query->clear('values'); + + foreach ($tokens as $token) { + // Database size for a term field + if ($token->length > 75) { + continue; + } + + if ($filterCommon && $token->common) { + continue; + } + + if ($filterNumeric && $token->numeric) { + continue; + } + + $query->values( + $this->db->quote($token->term) . ', ' + . $this->db->quote($token->stem) . ', ' + . (int) $token->common . ', ' + . (int) $token->phrase . ', ' + . $this->db->quote($token->weight) . ', ' + . (int) $context . ', ' + . $this->db->quote($token->language) + ); + $count++; + } + + // Check if we're approaching the memory limit of the token table. + if ($count > static::$state->options->get('memory_table_limit', 7500)) { + $this->toggleTables(false); + } + + // Only execute the query if there are tokens to insert + if ($query->values !== null) { + $this->db->setQuery($query)->execute(); + } + } + + return $count; + } + + /** + * Method to switch the token tables from Memory tables to Disk tables + * when they are close to running out of memory. + * Since this is not supported/implemented in all DB-drivers, the default is a stub method, which simply returns true. + * + * @param boolean $memory Flag to control how they should be toggled. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function toggleTables($memory) + { + if (strtolower($this->db->getServerType()) != 'mysql') { + return true; + } + + static $state; + + // Get the database adapter. + $db = $this->db; + + // Check if we are setting the tables to the Memory engine. + if ($memory === true && $state !== true) { + // Set the tokens table to Memory. + $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens') . ' ENGINE = MEMORY'); + $db->execute(); + + // Set the tokens aggregate table to Memory. + $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens_aggregate') . ' ENGINE = MEMORY'); + $db->execute(); + + // Set the internal state. + $state = $memory; + } elseif ($memory === false && $state !== false) { + // We must be setting the tables to the InnoDB engine. + // Set the tokens table to InnoDB. + $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens') . ' ENGINE = INNODB'); + $db->execute(); + + // Set the tokens aggregate table to InnoDB. + $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens_aggregate') . ' ENGINE = INNODB'); + $db->execute(); + + // Set the internal state. + $state = $memory; + } + + return true; + } } diff --git a/code/administrator/components/com_finder/src/Indexer/Language.php b/code/administrator/components/com_finder/src/Indexer/Language.php index 6f78889d..db138cf0 100644 --- a/code/administrator/components/com_finder/src/Indexer/Language.php +++ b/code/administrator/components/com_finder/src/Indexer/Language.php @@ -1,4 +1,5 @@ language = $locale; - } - - // Use our generic language handler if no language is set - if ($this->language === null) - { - $this->language = '*'; - } - - try - { - $this->stemmer = StemmerFactory::create($this->language); - } - catch (NotFoundException $e) - { - // We don't have a stemmer for the language - } - } - - /** - * Method to get a language support object. - * - * @param string $language The language of the support object. - * - * @return Language A Language instance. - * - * @since 4.0.0 - */ - public static function getInstance($language) - { - if (isset(self::$instances[$language])) - { - return self::$instances[$language]; - } - - $locale = '*'; - - if ($language !== '*') - { - $locale = Helper::getPrimaryLanguage($language); - $class = '\\Joomla\\Component\\Finder\\Administrator\\Indexer\\Language\\' . ucfirst($locale); - - if (class_exists($class)) - { - self::$instances[$language] = new $class; - - return self::$instances[$language]; - } - } - - self::$instances[$language] = new self($locale); - - return self::$instances[$language]; - } - - /** - * Method to tokenise a text string. - * - * @param string $input The input to tokenise. - * - * @return array An array of term strings. - * - * @since 4.0.0 - */ - public function tokenise($input) - { - $quotes = html_entity_decode('‘’'', ENT_QUOTES, 'UTF-8'); - - /* - * Parsing the string input into terms is a multi-step process. - * - * Regexes: - * 1. Remove everything except letters, numbers, quotes, apostrophe, plus, dash, period, and comma. - * 2. Remove plus, dash, period, and comma characters located before letter characters. - * 3. Remove plus, dash, period, and comma characters located after other characters. - * 4. Remove plus, period, and comma characters enclosed in alphabetical characters. Ungreedy. - * 5. Remove orphaned apostrophe, plus, dash, period, and comma characters. - * 6. Remove orphaned quote characters. - * 7. Replace the assorted single quotation marks with the ASCII standard single quotation. - * 8. Remove multiple space characters and replaces with a single space. - */ - $input = StringHelper::strtolower($input); - $input = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,]+#mui', ' ', $input); - $input = preg_replace('#(^|\s)[+-.,]+([\pL\pM]+)#mui', ' $1', $input); - $input = preg_replace('#([\pL\pM\pN]+)[+-.,]+(\s|$)#mui', '$1 ', $input); - $input = preg_replace('#([\pL\pM]+)[+.,]+([\pL\pM]+)#muiU', '$1 $2', $input); - $input = preg_replace('#(^|\s)[\'+-.,]+(\s|$)#mui', ' ', $input); - $input = preg_replace('#(^|\s)[\p{Pi}\p{Pf}]+(\s|$)#mui', ' ', $input); - $input = preg_replace('#[' . $quotes . ']+#mui', '\'', $input); - $input = preg_replace('#\s+#mui', ' ', $input); - $input = trim($input); - - // Explode the normalized string to get the terms. - $terms = explode(' ', $input); - - return $terms; - } - - /** - * Method to stem a token. - * - * @param string $token The token to stem. - * - * @return string The stemmed token. - * - * @since 4.0.0 - */ - public function stem($token) - { - if ($this->stemmer !== null) - { - return $this->stemmer->stem($token); - } - - return $token; - } + /** + * Language support instances container. + * + * @var Language[] + * @since 4.0.0 + */ + protected static $instances = array(); + + /** + * Language locale of the class + * + * @var string + * @since 4.0.0 + */ + public $language; + + /** + * Spacer to use between terms + * + * @var string + * @since 4.0.0 + */ + public $spacer = ' '; + + /** + * The stemmer object. + * + * @var Stemmer + * @since 4.0.0 + */ + protected $stemmer = null; + + /** + * Method to construct the language object. + * + * @since 4.0.0 + */ + public function __construct($locale = null) + { + if ($locale !== null) { + $this->language = $locale; + } + + // Use our generic language handler if no language is set + if ($this->language === null) { + $this->language = '*'; + } + + try { + $this->stemmer = StemmerFactory::create($this->language); + } catch (NotFoundException $e) { + // We don't have a stemmer for the language + } + } + + /** + * Method to get a language support object. + * + * @param string $language The language of the support object. + * + * @return Language A Language instance. + * + * @since 4.0.0 + */ + public static function getInstance($language) + { + if (isset(self::$instances[$language])) { + return self::$instances[$language]; + } + + $locale = '*'; + + if ($language !== '*') { + $locale = Helper::getPrimaryLanguage($language); + $class = '\\Joomla\\Component\\Finder\\Administrator\\Indexer\\Language\\' . ucfirst($locale); + + if (class_exists($class)) { + self::$instances[$language] = new $class(); + + return self::$instances[$language]; + } + } + + self::$instances[$language] = new self($locale); + + return self::$instances[$language]; + } + + /** + * Method to tokenise a text string. + * + * @param string $input The input to tokenise. + * + * @return array An array of term strings. + * + * @since 4.0.0 + */ + public function tokenise($input) + { + $quotes = html_entity_decode('‘’'', ENT_QUOTES, 'UTF-8'); + + /* + * Parsing the string input into terms is a multi-step process. + * + * Regexes: + * 1. Remove everything except letters, numbers, quotes, apostrophe, plus, dash, period, and comma. + * 2. Remove plus, dash, period, and comma characters located before letter characters. + * 3. Remove plus, dash, period, and comma characters located after other characters. + * 4. Remove plus, period, and comma characters enclosed in alphabetical characters. Ungreedy. + * 5. Remove orphaned apostrophe, plus, dash, period, and comma characters. + * 6. Remove orphaned quote characters. + * 7. Replace the assorted single quotation marks with the ASCII standard single quotation. + * 8. Remove multiple space characters and replaces with a single space. + */ + $input = StringHelper::strtolower($input); + $input = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,]+#mui', ' ', $input); + $input = preg_replace('#(^|\s)[+-.,]+([\pL\pM]+)#mui', ' $1', $input); + $input = preg_replace('#([\pL\pM\pN]+)[+-.,]+(\s|$)#mui', '$1 ', $input); + $input = preg_replace('#([\pL\pM]+)[+.,]+([\pL\pM]+)#muiU', '$1 $2', $input); + $input = preg_replace('#(^|\s)[\'+-.,]+(\s|$)#mui', ' ', $input); + $input = preg_replace('#(^|\s)[\p{Pi}\p{Pf}]+(\s|$)#mui', ' ', $input); + $input = preg_replace('#[' . $quotes . ']+#mui', '\'', $input); + $input = preg_replace('#\s+#mui', ' ', $input); + $input = trim($input); + + // Explode the normalized string to get the terms. + $terms = explode(' ', $input); + + return $terms; + } + + /** + * Method to stem a token. + * + * @param string $token The token to stem. + * + * @return string The stemmed token. + * + * @since 4.0.0 + */ + public function stem($token) + { + if ($this->stemmer !== null) { + return $this->stemmer->stem($token); + } + + return $token; + } } diff --git a/code/administrator/components/com_finder/src/Indexer/Language/El.php b/code/administrator/components/com_finder/src/Indexer/Language/El.php index 895feecb..9a9c8c55 100644 --- a/code/administrator/components/com_finder/src/Indexer/Language/El.php +++ b/code/administrator/components/com_finder/src/Indexer/Language/El.php @@ -1,4 +1,5 @@ toUpperCase($token, $wCase); - - // Stop-word removal - $stop_words = '/^(ΕΚΟ|ΑΒΑ|ΑΓΑ|ΑΓΗ|ΑΓΩ|ΑΔΗ|ΑΔΩ|ΑΕ|ΑΕΙ|ΑΘΩ|ΑΙ|ΑΙΚ|ΑΚΗ|ΑΚΟΜΑ|ΑΚΟΜΗ|ΑΚΡΙΒΩΣ|ΑΛΑ|ΑΛΗΘΕΙΑ|ΑΛΗΘΙΝΑ|ΑΛΛΑΧΟΥ|ΑΛΛΙΩΣ|ΑΛΛΙΩΤΙΚΑ|' - . 'ΑΛΛΟΙΩΣ|ΑΛΛΟΙΩΤΙΚΑ|ΑΛΛΟΤΕ|ΑΛΤ|ΑΛΩ|ΑΜΑ|ΑΜΕ|ΑΜΕΣΑ|ΑΜΕΣΩΣ|ΑΜΩ|ΑΝ|ΑΝΑ|ΑΝΑΜΕΣΑ|ΑΝΑΜΕΤΑΞΥ|ΑΝΕΥ|ΑΝΤΙ|ΑΝΤΙΠΕΡΑ|ΑΝΤΙΣ|ΑΝΩ|ΑΝΩΤΕΡΩ|ΑΞΑΦΝΑ|' - . 'ΑΠ|ΑΠΕΝΑΝΤΙ|ΑΠΟ|ΑΠΟΨΕ|ΑΠΩ|ΑΡΑ|ΑΡΑΓΕ|ΑΡΕ|ΑΡΚ|ΑΡΚΕΤΑ|ΑΡΛ|ΑΡΜ|ΑΡΤ|ΑΡΥ|ΑΡΩ|ΑΣ|ΑΣΑ|ΑΣΟ|ΑΤΑ|ΑΤΕ|ΑΤΗ|ΑΤΙ|ΑΤΜ|ΑΤΟ|ΑΥΡΙΟ|ΑΦΗ|ΑΦΟΤΟΥ|ΑΦΟΥ|' - . 'ΑΧ|ΑΧΕ|ΑΧΟ|ΑΨΑ|ΑΨΕ|ΑΨΗ|ΑΨΥ|ΑΩΕ|ΑΩΟ|ΒΑΝ|ΒΑΤ|ΒΑΧ|ΒΕΑ|ΒΕΒΑΙΟΤΑΤΑ|ΒΗΞ|ΒΙΑ|ΒΙΕ|ΒΙΗ|ΒΙΟ|ΒΟΗ|ΒΟΩ|ΒΡΕ|ΓΑ|ΓΑΒ|ΓΑΡ|ΓΕΝ|ΓΕΣ||ΓΗ|ΓΗΝ|ΓΙ|ΓΙΑ|' - . 'ΓΙΕ|ΓΙΝ|ΓΙΟ|ΓΚΙ|ΓΙΑΤΙ|ΓΚΥ|ΓΟΗ|ΓΟΟ|ΓΡΗΓΟΡΑ|ΓΡΙ|ΓΡΥ|ΓΥΗ|ΓΥΡΩ|ΔΑ|ΔΕ|ΔΕΗ|ΔΕΙ|ΔΕΝ|ΔΕΣ|ΔΗ|ΔΗΘΕΝ|ΔΗΛΑΔΗ|ΔΗΩ|ΔΙ|ΔΙΑ|ΔΙΑΡΚΩΣ|ΔΙΟΛΟΥ|ΔΙΣ|' - . 'ΔΙΧΩΣ|ΔΟΛ|ΔΟΝ|ΔΡΑ|ΔΡΥ|ΔΡΧ|ΔΥΕ|ΔΥΟ|ΔΩ|ΕΑΜ|ΕΑΝ|ΕΑΡ|ΕΘΗ|ΕΙ|ΕΙΔΕΜΗ|ΕΙΘΕ|ΕΙΜΑΙ|ΕΙΜΑΣΤΕ|ΕΙΝΑΙ|ΕΙΣ|ΕΙΣΑΙ|ΕΙΣΑΣΤΕ|ΕΙΣΤΕ|ΕΙΤΕ|ΕΙΧΑ|ΕΙΧΑΜΕ|' - . 'ΕΙΧΑΝ|ΕΙΧΑΤΕ|ΕΙΧΕ|ΕΙΧΕΣ|ΕΚ|ΕΚΕΙ|ΕΛΑ|ΕΛΙ|ΕΜΠ|ΕΝ|ΕΝΤΕΛΩΣ|ΕΝΤΟΣ|ΕΝΤΩΜΕΤΑΞΥ|ΕΝΩ|ΕΞ|ΕΞΑΦΝΑ|ΕΞΙ|ΕΞΙΣΟΥ|ΕΞΩ|ΕΟΚ|ΕΠΑΝΩ|ΕΠΕΙΔΗ|ΕΠΕΙΤΑ|ΕΠΗ|' - . 'ΕΠΙ|ΕΠΙΣΗΣ|ΕΠΟΜΕΝΩΣ|ΕΡΑ|ΕΣ|ΕΣΑΣ|ΕΣΕ|ΕΣΕΙΣ|ΕΣΕΝΑ|ΕΣΗ|ΕΣΤΩ|ΕΣΥ|ΕΣΩ|ΕΤΙ|ΕΤΣΙ|ΕΥ|ΕΥΑ|ΕΥΓΕ|ΕΥΘΥΣ|ΕΥΤΥΧΩΣ|ΕΦΕ|ΕΦΕΞΗΣ|ΕΦΤ|ΕΧΕ|ΕΧΕΙ|' - . 'ΕΧΕΙΣ|ΕΧΕΤΕ|ΕΧΘΕΣ|ΕΧΟΜΕ|ΕΧΟΥΜΕ|ΕΧΟΥΝ|ΕΧΤΕΣ|ΕΧΩ|ΕΩΣ|ΖΕΑ|ΖΕΗ|ΖΕΙ|ΖΕΝ|ΖΗΝ|ΖΩ|Η|ΗΔΗ|ΗΔΥ|ΗΘΗ|ΗΛΟ|ΗΜΙ|ΗΠΑ|ΗΣΑΣΤΕ|ΗΣΟΥΝ|ΗΤΑ|ΗΤΑΝ|ΗΤΑΝΕ|' - . 'ΗΤΟΙ|ΗΤΤΟΝ|ΗΩ|ΘΑ|ΘΥΕ|ΘΩΡ|Ι|ΙΑ|ΙΒΟ|ΙΔΗ|ΙΔΙΩΣ|ΙΕ|ΙΙ|ΙΙΙ|ΙΚΑ|ΙΛΟ|ΙΜΑ|ΙΝΑ|ΙΝΩ|ΙΞΕ|ΙΞΟ|ΙΟ|ΙΟΙ|ΙΣΑ|ΙΣΑΜΕ|ΙΣΕ|ΙΣΗ|ΙΣΙΑ|ΙΣΟ|ΙΣΩΣ|ΙΩΒ|ΙΩΝ|' - . 'ΙΩΣ|ΙΑΝ|ΚΑΘ|ΚΑΘΕ|ΚΑΘΕΤΙ|ΚΑΘΟΛΟΥ|ΚΑΘΩΣ|ΚΑΙ|ΚΑΝ|ΚΑΠΟΤΕ|ΚΑΠΟΥ|ΚΑΠΩΣ|ΚΑΤ|ΚΑΤΑ|ΚΑΤΙ|ΚΑΤΙΤΙ|ΚΑΤΟΠΙΝ|ΚΑΤΩ|ΚΑΩ|ΚΒΟ|ΚΕΑ|ΚΕΙ|ΚΕΝ|ΚΙ|ΚΙΜ|' - . 'ΚΙΟΛΑΣ|ΚΙΤ|ΚΙΧ|ΚΚΕ|ΚΛΙΣΕ|ΚΛΠ|ΚΟΚ|ΚΟΝΤΑ|ΚΟΧ|ΚΤΛ|ΚΥΡ|ΚΥΡΙΩΣ|ΚΩ|ΚΩΝ|ΛΑ|ΛΕΑ|ΛΕΝ|ΛΕΟ|ΛΙΑ|ΛΙΓΑΚΙ|ΛΙΓΟΥΛΑΚΙ|ΛΙΓΟ|ΛΙΓΩΤΕΡΟ|ΛΙΟ|ΛΙΡ|ΛΟΓΩ|' - . 'ΛΟΙΠΑ|ΛΟΙΠΟΝ|ΛΟΣ|ΛΣ|ΛΥΩ|ΜΑ|ΜΑΖΙ|ΜΑΚΑΡΙ|ΜΑΛΙΣΤΑ|ΜΑΛΛΟΝ|ΜΑΝ|ΜΑΞ|ΜΑΣ|ΜΑΤ|ΜΕ|ΜΕΘΑΥΡΙΟ|ΜΕΙ|ΜΕΙΟΝ|ΜΕΛ|ΜΕΛΕΙ|ΜΕΛΛΕΤΑΙ|ΜΕΜΙΑΣ|ΜΕΝ|ΜΕΣ|' - . 'ΜΕΣΑ|ΜΕΤ|ΜΕΤΑ|ΜΕΤΑΞΥ|ΜΕΧΡΙ|ΜΗ|ΜΗΔΕ|ΜΗΝ|ΜΗΠΩΣ|ΜΗΤΕ|ΜΙ|ΜΙΞ|ΜΙΣ|ΜΜΕ|ΜΝΑ|ΜΟΒ|ΜΟΛΙΣ|ΜΟΛΟΝΟΤΙ|ΜΟΝΑΧΑ|ΜΟΝΟΜΙΑΣ|ΜΙΑ|ΜΟΥ|ΜΠΑ|ΜΠΟΡΕΙ|' - . 'ΜΠΟΡΟΥΝ|ΜΠΡΑΒΟ|ΜΠΡΟΣ|ΜΠΩ|ΜΥ|ΜΥΑ|ΜΥΝ|ΝΑ|ΝΑΕ|ΝΑΙ|ΝΑΟ|ΝΔ|ΝΕΐ|ΝΕΑ|ΝΕΕ|ΝΕΟ|ΝΙ|ΝΙΑ|ΝΙΚ|ΝΙΛ|ΝΙΝ|ΝΙΟ|ΝΤΑ|ΝΤΕ|ΝΤΙ|ΝΤΟ|ΝΥΝ|ΝΩΕ|ΝΩΡΙΣ|ΞΑΝΑ|' - . 'ΞΑΦΝΙΚΑ|ΞΕΩ|ΞΙ|Ο|ΟΑ|ΟΑΠ|ΟΔΟ|ΟΕ|ΟΖΟ|ΟΗΕ|ΟΙ|ΟΙΑ|ΟΙΗ|ΟΚΑ|ΟΛΟΓΥΡΑ|ΟΛΟΝΕΝ|ΟΛΟΤΕΛΑ|ΟΛΩΣΔΙΟΛΟΥ|ΟΜΩΣ|ΟΝ|ΟΝΕ|ΟΝΟ|ΟΠΑ|ΟΠΕ|ΟΠΗ|ΟΠΟ|' - . 'ΟΠΟΙΑΔΗΠΟΤΕ|ΟΠΟΙΑΝΔΗΠΟΤΕ|ΟΠΟΙΑΣΔΗΠΟΤΕ|ΟΠΟΙΔΗΠΟΤΕ|ΟΠΟΙΕΣΔΗΠΟΤΕ|ΟΠΟΙΟΔΗΠΟΤΕ|ΟΠΟΙΟΝΔΗΠΟΤΕ|ΟΠΟΙΟΣΔΗΠΟΤΕ|ΟΠΟΙΟΥΔΗΠΟΤΕ|ΟΠΟΙΟΥΣΔΗΠΟΤΕ|' - . 'ΟΠΟΙΩΝΔΗΠΟΤΕ|ΟΠΟΤΕΔΗΠΟΤΕ|ΟΠΟΥ|ΟΠΟΥΔΗΠΟΤΕ|ΟΠΩΣ|ΟΡΑ|ΟΡΕ|ΟΡΗ|ΟΡΟ|ΟΡΦ|ΟΡΩ|ΟΣΑ|ΟΣΑΔΗΠΟΤΕ|ΟΣΕ|ΟΣΕΣΔΗΠΟΤΕ|ΟΣΗΔΗΠΟΤΕ|ΟΣΗΝΔΗΠΟΤΕ|' - . 'ΟΣΗΣΔΗΠΟΤΕ|ΟΣΟΔΗΠΟΤΕ|ΟΣΟΙΔΗΠΟΤΕ|ΟΣΟΝΔΗΠΟΤΕ|ΟΣΟΣΔΗΠΟΤΕ|ΟΣΟΥΔΗΠΟΤΕ|ΟΣΟΥΣΔΗΠΟΤΕ|ΟΣΩΝΔΗΠΟΤΕ|ΟΤΑΝ|ΟΤΕ|ΟΤΙ|ΟΤΙΔΗΠΟΤΕ|ΟΥ|ΟΥΔΕ|ΟΥΚ|ΟΥΣ|' - . 'ΟΥΤΕ|ΟΥΦ|ΟΧΙ|ΟΨΑ|ΟΨΕ|ΟΨΗ|ΟΨΙ|ΟΨΟ|ΠΑ|ΠΑΛΙ|ΠΑΝ|ΠΑΝΤΟΤΕ|ΠΑΝΤΟΥ|ΠΑΝΤΩΣ|ΠΑΠ|ΠΑΡ|ΠΑΡΑ|ΠΕΙ|ΠΕΡ|ΠΕΡΑ|ΠΕΡΙ|ΠΕΡΙΠΟΥ|ΠΕΡΣΙ|ΠΕΡΥΣΙ|ΠΕΣ|ΠΙ|' - . 'ΠΙΑ|ΠΙΘΑΝΟΝ|ΠΙΚ|ΠΙΟ|ΠΙΣΩ|ΠΙΤ|ΠΙΩ|ΠΛΑΙ|ΠΛΕΟΝ|ΠΛΗΝ|ΠΛΩ|ΠΜ|ΠΟΑ|ΠΟΕ|ΠΟΛ|ΠΟΛΥ|ΠΟΠ|ΠΟΤΕ|ΠΟΥ|ΠΟΥΘΕ|ΠΟΥΘΕΝΑ|ΠΡΕΠΕΙ|ΠΡΙ|ΠΡΙΝ|ΠΡΟ|' - . 'ΠΡΟΚΕΙΜΕΝΟΥ|ΠΡΟΚΕΙΤΑΙ|ΠΡΟΠΕΡΣΙ|ΠΡΟΣ|ΠΡΟΤΟΥ|ΠΡΟΧΘΕΣ|ΠΡΟΧΤΕΣ|ΠΡΩΤΥΤΕΡΑ|ΠΥΑ|ΠΥΞ|ΠΥΟ|ΠΥΡ|ΠΧ|ΠΩ|ΠΩΛ|ΠΩΣ|ΡΑ|ΡΑΙ|ΡΑΠ|ΡΑΣ|ΡΕ|ΡΕΑ|ΡΕΕ|ΡΕΙ|' - . 'ΡΗΣ|ΡΘΩ|ΡΙΟ|ΡΟ|ΡΟΐ|ΡΟΕ|ΡΟΖ|ΡΟΗ|ΡΟΘ|ΡΟΙ|ΡΟΚ|ΡΟΛ|ΡΟΝ|ΡΟΣ|ΡΟΥ|ΣΑΙ|ΣΑΝ|ΣΑΟ|ΣΑΣ|ΣΕ|ΣΕΙΣ|ΣΕΚ|ΣΕΞ|ΣΕΡ|ΣΕΤ|ΣΕΦ|ΣΗΜΕΡΑ|ΣΙ|ΣΙΑ|ΣΙΓΑ|ΣΙΚ|' - . 'ΣΙΧ|ΣΚΙ|ΣΟΙ|ΣΟΚ|ΣΟΛ|ΣΟΝ|ΣΟΣ|ΣΟΥ|ΣΡΙ|ΣΤΑ|ΣΤΗ|ΣΤΗΝ|ΣΤΗΣ|ΣΤΙΣ|ΣΤΟ|ΣΤΟΝ|ΣΤΟΥ|ΣΤΟΥΣ|ΣΤΩΝ|ΣΥ|ΣΥΓΧΡΟΝΩΣ|ΣΥΝ|ΣΥΝΑΜΑ|ΣΥΝΕΠΩΣ|ΣΥΝΗΘΩΣ|' - . 'ΣΧΕΔΟΝ|ΣΩΣΤΑ|ΤΑ|ΤΑΔΕ|ΤΑΚ|ΤΑΝ|ΤΑΟ|ΤΑΥ|ΤΑΧΑ|ΤΑΧΑΤΕ|ΤΕ|ΤΕΙ|ΤΕΛ|ΤΕΛΙΚΑ|ΤΕΛΙΚΩΣ|ΤΕΣ|ΤΕΤ|ΤΖΟ|ΤΗ|ΤΗΛ|ΤΗΝ|ΤΗΣ|ΤΙ|ΤΙΚ|ΤΙΜ|ΤΙΠΟΤΑ|ΤΙΠΟΤΕ|' - . 'ΤΙΣ|ΤΝΤ|ΤΟ|ΤΟΙ|ΤΟΚ|ΤΟΜ|ΤΟΝ|ΤΟΠ|ΤΟΣ|ΤΟΣ?Ν|ΤΟΣΑ|ΤΟΣΕΣ|ΤΟΣΗ|ΤΟΣΗΝ|ΤΟΣΗΣ|ΤΟΣΟ|ΤΟΣΟΙ|ΤΟΣΟΝ|ΤΟΣΟΣ|ΤΟΣΟΥ|ΤΟΣΟΥΣ|ΤΟΤΕ|ΤΟΥ|ΤΟΥΛΑΧΙΣΤΟ|' - . 'ΤΟΥΛΑΧΙΣΤΟΝ|ΤΟΥΣ|ΤΣ|ΤΣΑ|ΤΣΕ|ΤΥΧΟΝ|ΤΩ|ΤΩΝ|ΤΩΡΑ|ΥΑΣ|ΥΒΑ|ΥΒΟ|ΥΙΕ|ΥΙΟ|ΥΛΑ|ΥΛΗ|ΥΝΙ|ΥΠ|ΥΠΕΡ|ΥΠΟ|ΥΠΟΨΗ|ΥΠΟΨΙΝ|ΥΣΤΕΡΑ|ΥΦΗ|ΥΨΗ|ΦΑ|ΦΑΐ|ΦΑΕ|' - . 'ΦΑΝ|ΦΑΞ|ΦΑΣ|ΦΑΩ|ΦΕΖ|ΦΕΙ|ΦΕΤΟΣ|ΦΕΥ|ΦΙ|ΦΙΛ|ΦΙΣ|ΦΟΞ|ΦΠΑ|ΦΡΙ|ΧΑ|ΧΑΗ|ΧΑΛ|ΧΑΝ|ΧΑΦ|ΧΕ|ΧΕΙ|ΧΘΕΣ|ΧΙ|ΧΙΑ|ΧΙΛ|ΧΙΟ|ΧΛΜ|ΧΜ|ΧΟΗ|ΧΟΛ|ΧΡΩ|ΧΤΕΣ|' - . 'ΧΩΡΙΣ|ΧΩΡΙΣΤΑ|ΨΕΣ|ΨΗΛΑ|ΨΙ|ΨΙΤ|Ω|ΩΑ|ΩΑΣ|ΩΔΕ|ΩΕΣ|ΩΘΩ|ΩΜΑ|ΩΜΕ|ΩΝ|ΩΟ|ΩΟΝ|ΩΟΥ|ΩΣ|ΩΣΑΝ|ΩΣΗ|ΩΣΟΤΟΥ|ΩΣΠΟΥ|ΩΣΤΕ|ΩΣΤΟΣΟ|ΩΤΑ|ΩΧ|ΩΩΝ)$/'; - - if (preg_match($stop_words, $token)) - { - return $this->toLowerCase($token, $wCase); - } - - // Vowels - $v = '(Α|Ε|Η|Ι|Ο|Υ|Ω)'; - - // Vowels without Y - $v2 = '(Α|Ε|Η|Ι|Ο|Ω)'; - - $test1 = true; - - // Step S1. 14 stems - $re = '/^(.+?)(ΙΖΑ|ΙΖΕΣ|ΙΖΕ|ΙΖΑΜΕ|ΙΖΑΤΕ|ΙΖΑΝ|ΙΖΑΝΕ|ΙΖΩ|ΙΖΕΙΣ|ΙΖΕΙ|ΙΖΟΥΜΕ|ΙΖΕΤΕ|ΙΖΟΥΝ|ΙΖΟΥΝΕ)$/'; - $exceptS1 = '/^(ΑΝΑΜΠΑ|ΕΜΠΑ|ΕΠΑ|ΞΑΝΑΠΑ|ΠΑ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ)$/'; - $exceptS2 = '/^(ΜΑΡΚ|ΚΟΡΝ|ΑΜΠΑΡ|ΑΡΡ|ΒΑΘΥΡΙ|ΒΑΡΚ|Β|ΒΟΛΒΟΡ|ΓΚΡ|ΓΛΥΚΟΡ|ΓΛΥΚΥΡ|ΙΜΠ|Λ|ΛΟΥ|ΜΑΡ|Μ|ΠΡ|ΜΠΡ|ΠΟΛΥΡ|Π|Ρ|ΠΙΠΕΡΟΡ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'I'; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . 'IΖ'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S2. 7 stems - $re = '/^(.+?)(ΩΘΗΚΑ|ΩΘΗΚΕΣ|ΩΘΗΚΕ|ΩΘΗΚΑΜΕ|ΩΘΗΚΑΤΕ|ΩΘΗΚΑΝ|ΩΘΗΚΑΝΕ)$/'; - $exceptS1 = '/^(ΑΛ|ΒΙ|ΕΝ|ΥΨ|ΛΙ|ΖΩ|Σ|Χ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'ΩΝ'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S3. 7 stems - $re = '/^(.+?)(ΙΣΑ|ΙΣΕΣ|ΙΣΕ|ΙΣΑΜΕ|ΙΣΑΤΕ|ΙΣΑΝ|ΙΣΑΝΕ)$/'; - $exceptS1 = '/^(ΑΝΑΜΠΑ|ΑΘΡΟ|ΕΜΠΑ|ΕΣΕ|ΕΣΩΚΛΕ|ΕΠΑ|ΞΑΝΑΠΑ|ΕΠΕ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ|ΚΛΕ|ΧΑΡΤΟΠΑ|ΕΞΑΡΧΑ|ΜΕΤΕΠΕ|ΑΠΟΚΛΕ|ΑΠΕΚΛΕ|ΕΚΛΕ|ΠΕ|ΠΕΡΙΠΑ)$/'; - $exceptS2 = '/^(ΑΝ|ΑΦ|ΓΕ|ΓΙΓΑΝΤΟΑΦ|ΓΚΕ|ΔΗΜΟΚΡΑΤ|ΚΟΜ|ΓΚ|Μ|Π|ΠΟΥΚΑΜ|ΟΛΟ|ΛΑΡ)$/'; - - if ($token == "ΙΣΑ") - { - $token = "ΙΣ"; - - return $token; - } - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'Ι'; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . 'ΙΣ'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S4. 7 stems - $re = '/^(.+?)(ΙΣΩ|ΙΣΕΙΣ|ΙΣΕΙ|ΙΣΟΥΜΕ|ΙΣΕΤΕ|ΙΣΟΥΝ|ΙΣΟΥΝΕ)$/'; - $exceptS1 = '/^(ΑΝΑΜΠΑ|ΕΜΠΑ|ΕΣΕ|ΕΣΩΚΛΕ|ΕΠΑ|ΞΑΝΑΠΑ|ΕΠΕ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ|ΚΛΕ|ΧΑΡΤΟΠΑ|ΕΞΑΡΧΑ|ΜΕΤΕΠΕ|ΑΠΟΚΛΕ|ΑΠΕΚΛΕ|ΕΚΛΕ|ΠΕ|ΠΕΡΙΠΑ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'Ι'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S5. 11 stems - $re = '/^(.+?)(ΙΣΤΟΣ|ΙΣΤΟΥ|ΙΣΤΟ|ΙΣΤΕ|ΙΣΤΟΙ|ΙΣΤΩΝ|ΙΣΤΟΥΣ|ΙΣΤΗ|ΙΣΤΗΣ|ΙΣΤΑ|ΙΣΤΕΣ)$/'; - $exceptS1 = '/^(Μ|Π|ΑΠ|ΑΡ|ΗΔ|ΚΤ|ΣΚ|ΣΧ|ΥΨ|ΦΑ|ΧΡ|ΧΤ|ΑΚΤ|ΑΟΡ|ΑΣΧ|ΑΤΑ|ΑΧΝ|ΑΧΤ|ΓΕΜ|ΓΥΡ|ΕΜΠ|ΕΥΠ|ΕΧΘ|ΗΦΑ|ΚΑΘ|ΚΑΚ|ΚΥΛ|ΛΥΓ|ΜΑΚ|ΜΕΓ|ΤΑΧ|ΦΙΛ|ΧΩΡ)$/'; - $exceptS2 = '/^(ΔΑΝΕ|ΣΥΝΑΘΡΟ|ΚΛΕ|ΣΕ|ΕΣΩΚΛΕ|ΑΣΕ|ΠΛΕ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'ΙΣΤ'; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . 'Ι'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S6. 6 stems - $re = '/^(.+?)(ΙΣΜΟ|ΙΣΜΟΙ|ΙΣΜΟΣ|ΙΣΜΟΥ|ΙΣΜΟΥΣ|ΙΣΜΩΝ)$/'; - $exceptS1 = '/^(ΑΓΝΩΣΤΙΚ|ΑΤΟΜΙΚ|ΓΝΩΣΤΙΚ|ΕΘΝΙΚ|ΕΚΛΕΚΤΙΚ|ΣΚΕΠΤΙΚ|ΤΟΠΙΚ)$/'; - $exceptS2 = '/^(ΣΕ|ΜΕΤΑΣΕ|ΜΙΚΡΟΣΕ|ΕΓΚΛΕ|ΑΠΟΚΛΕ)$/'; - $exceptS3 = '/^(ΔΑΝΕ|ΑΝΤΙΔΑΝΕ)$/'; - $exceptS4 = '/^(ΑΛΕΞΑΝΔΡΙΝ|ΒΥΖΑΝΤΙΝ|ΘΕΑΤΡΙΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = str_replace('ΙΚ', "", $token); - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . "ΙΣΜ"; - } - - if (preg_match($exceptS3, $token)) - { - $token = $token . "Ι"; - } - - if (preg_match($exceptS4, $token)) - { - $token = str_replace('ΙΝ', "", $token); - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S7. 4 stems - $re = '/^(.+?)(ΑΡΑΚΙ|ΑΡΑΚΙΑ|ΟΥΔΑΚΙ|ΟΥΔΑΚΙΑ)$/'; - $exceptS1 = '/^(Σ|Χ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . "AΡΑΚ"; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S8. 8 stems - $re = '/^(.+?)(ΑΚΙ|ΑΚΙΑ|ΙΤΣΑ|ΙΤΣΑΣ|ΙΤΣΕΣ|ΙΤΣΩΝ|ΑΡΑΚΙ|ΑΡΑΚΙΑ)$/'; - $exceptS1 = '/^(ΑΝΘΡ|ΒΑΜΒ|ΒΡ|ΚΑΙΜ|ΚΟΝ|ΚΟΡ|ΛΑΒΡ|ΛΟΥΛ|ΜΕΡ|ΜΟΥΣΤ|ΝΑΓΚΑΣ|ΠΛ|Ρ|ΡΥ|Σ|ΣΚ|ΣΟΚ|ΣΠΑΝ|ΤΖ|ΦΑΡΜ|Χ|' - . 'ΚΑΠΑΚ|ΑΛΙΣΦ|ΑΜΒΡ|ΑΝΘΡ|Κ|ΦΥΛ|ΚΑΤΡΑΠ|ΚΛΙΜ|ΜΑΛ|ΣΛΟΒ|Φ|ΣΦ|ΤΣΕΧΟΣΛΟΒ)$/'; - $exceptS2 = '/^(Β|ΒΑΛ|ΓΙΑΝ|ΓΛ|Ζ|ΗΓΟΥΜΕΝ|ΚΑΡΔ|ΚΟΝ|ΜΑΚΡΥΝ|ΝΥΦ|ΠΑΤΕΡ|Π|ΣΚ|ΤΟΣ|ΤΡΙΠΟΛ)$/'; - - // For words like ΠΛΟΥΣΙΟΚΟΡΙΤΣΑ, ΠΑΛΙΟΚΟΡΙΤΣΑ etc - $exceptS3 = '/(ΚΟΡ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . "ΑΚ"; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . "ΙΤΣ"; - } - - if (preg_match($exceptS3, $token)) - { - $token = $token . "ΙΤΣ"; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S9. 3 stems - $re = '/^(.+?)(ΙΔΙΟ|ΙΔΙΑ|ΙΔΙΩΝ)$/'; - $exceptS1 = '/^(ΑΙΦΝ|ΙΡ|ΟΛΟ|ΨΑΛ)$/'; - $exceptS2 = '/(Ε|ΠΑΙΧΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . "ΙΔ"; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . "ΙΔ"; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S10. 4 stems - $re = '/^(.+?)(ΙΣΚΟΣ|ΙΣΚΟΥ|ΙΣΚΟ|ΙΣΚΕ)$/'; - $exceptS1 = '/^(Δ|ΙΒ|ΜΗΝ|Ρ|ΦΡΑΓΚ|ΛΥΚ|ΟΒΕΛ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . "ΙΣΚ"; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step 1 - // step1list is used in Step 1. 41 stems - $step1list = Array(); - $step1list["ΦΑΓΙΑ"] = "ΦΑ"; - $step1list["ΦΑΓΙΟΥ"] = "ΦΑ"; - $step1list["ΦΑΓΙΩΝ"] = "ΦΑ"; - $step1list["ΣΚΑΓΙΑ"] = "ΣΚΑ"; - $step1list["ΣΚΑΓΙΟΥ"] = "ΣΚΑ"; - $step1list["ΣΚΑΓΙΩΝ"] = "ΣΚΑ"; - $step1list["ΟΛΟΓΙΟΥ"] = "ΟΛΟ"; - $step1list["ΟΛΟΓΙΑ"] = "ΟΛΟ"; - $step1list["ΟΛΟΓΙΩΝ"] = "ΟΛΟ"; - $step1list["ΣΟΓΙΟΥ"] = "ΣΟ"; - $step1list["ΣΟΓΙΑ"] = "ΣΟ"; - $step1list["ΣΟΓΙΩΝ"] = "ΣΟ"; - $step1list["ΤΑΤΟΓΙΑ"] = "ΤΑΤΟ"; - $step1list["ΤΑΤΟΓΙΟΥ"] = "ΤΑΤΟ"; - $step1list["ΤΑΤΟΓΙΩΝ"] = "ΤΑΤΟ"; - $step1list["ΚΡΕΑΣ"] = "ΚΡΕ"; - $step1list["ΚΡΕΑΤΟΣ"] = "ΚΡΕ"; - $step1list["ΚΡΕΑΤΑ"] = "ΚΡΕ"; - $step1list["ΚΡΕΑΤΩΝ"] = "ΚΡΕ"; - $step1list["ΠΕΡΑΣ"] = "ΠΕΡ"; - $step1list["ΠΕΡΑΤΟΣ"] = "ΠΕΡ"; - - // Added by Spyros. Also at $re in step1 - $step1list["ΠΕΡΑΤΗ"] = "ΠΕΡ"; - $step1list["ΠΕΡΑΤΑ"] = "ΠΕΡ"; - $step1list["ΠΕΡΑΤΩΝ"] = "ΠΕΡ"; - $step1list["ΤΕΡΑΣ"] = "ΤΕΡ"; - $step1list["ΤΕΡΑΤΟΣ"] = "ΤΕΡ"; - $step1list["ΤΕΡΑΤΑ"] = "ΤΕΡ"; - $step1list["ΤΕΡΑΤΩΝ"] = "ΤΕΡ"; - $step1list["ΦΩΣ"] = "ΦΩ"; - $step1list["ΦΩΤΟΣ"] = "ΦΩ"; - $step1list["ΦΩΤΑ"] = "ΦΩ"; - $step1list["ΦΩΤΩΝ"] = "ΦΩ"; - $step1list["ΚΑΘΕΣΤΩΣ"] = "ΚΑΘΕΣΤ"; - $step1list["ΚΑΘΕΣΤΩΤΟΣ"] = "ΚΑΘΕΣΤ"; - $step1list["ΚΑΘΕΣΤΩΤΑ"] = "ΚΑΘΕΣΤ"; - $step1list["ΚΑΘΕΣΤΩΤΩΝ"] = "ΚΑΘΕΣΤ"; - $step1list["ΓΕΓΟΝΟΣ"] = "ΓΕΓΟΝ"; - $step1list["ΓΕΓΟΝΟΤΟΣ"] = "ΓΕΓΟΝ"; - $step1list["ΓΕΓΟΝΟΤΑ"] = "ΓΕΓΟΝ"; - $step1list["ΓΕΓΟΝΟΤΩΝ"] = "ΓΕΓΟΝ"; - - $re = '/(.*)(ΦΑΓΙΑ|ΦΑΓΙΟΥ|ΦΑΓΙΩΝ|ΣΚΑΓΙΑ|ΣΚΑΓΙΟΥ|ΣΚΑΓΙΩΝ|ΟΛΟΓΙΟΥ|ΟΛΟΓΙΑ|ΟΛΟΓΙΩΝ|ΣΟΓΙΟΥ|ΣΟΓΙΑ|ΣΟΓΙΩΝ|ΤΑΤΟΓΙΑ|ΤΑΤΟΓΙΟΥ|ΤΑΤΟΓΙΩΝ|ΚΡΕΑΣ|ΚΡΕΑΤΟΣ|' - . 'ΚΡΕΑΤΑ|ΚΡΕΑΤΩΝ|ΠΕΡΑΣ|ΠΕΡΑΤΟΣ|ΠΕΡΑΤΗ|ΠΕΡΑΤΑ|ΠΕΡΑΤΩΝ|ΤΕΡΑΣ|ΤΕΡΑΤΟΣ|ΤΕΡΑΤΑ|ΤΕΡΑΤΩΝ|ΦΩΣ|ΦΩΤΟΣ|ΦΩΤΑ|ΦΩΤΩΝ|ΚΑΘΕΣΤΩΣ|ΚΑΘΕΣΤΩΤΟΣ|' - . 'ΚΑΘΕΣΤΩΤΑ|ΚΑΘΕΣΤΩΤΩΝ|ΓΕΓΟΝΟΣ|ΓΕΓΟΝΟΤΟΣ|ΓΕΓΟΝΟΤΑ|ΓΕΓΟΝΟΤΩΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $stem = $match[1]; - $suffix = $match[2]; - $token = $stem . (array_key_exists($suffix, $step1list) ? $step1list[$suffix] : ''); - $test1 = false; - } - - // Step 2a. 2 stems - $re = '/^(.+?)(ΑΔΕΣ|ΑΔΩΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - $re = '/(ΟΚ|ΜΑΜ|ΜΑΝ|ΜΠΑΜΠ|ΠΑΤΕΡ|ΓΙΑΓΙ|ΝΤΑΝΤ|ΚΥΡ|ΘΕΙ|ΠΕΘΕΡ)$/'; - - if (!preg_match($re, $token)) - { - $token = $token . "ΑΔ"; - } - } - - // Step 2b. 2 stems - $re = '/^(.+?)(ΕΔΕΣ|ΕΔΩΝ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $exept2 = '/(ΟΠ|ΙΠ|ΕΜΠ|ΥΠ|ΓΗΠ|ΔΑΠ|ΚΡΑΣΠ|ΜΙΛ)$/'; - - if (preg_match($exept2, $token)) - { - $token = $token . 'ΕΔ'; - } - } - - // Step 2c - $re = '/^(.+?)(ΟΥΔΕΣ|ΟΥΔΩΝ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - - $exept3 = '/(ΑΡΚ|ΚΑΛΙΑΚ|ΠΕΤΑΛ|ΛΙΧ|ΠΛΕΞ|ΣΚ|Σ|ΦΛ|ΦΡ|ΒΕΛ|ΛΟΥΛ|ΧΝ|ΣΠ|ΤΡΑΓ|ΦΕ)$/'; - - if (preg_match($exept3, $token)) - { - $token = $token . 'ΟΥΔ'; - } - } - - // Step 2d - $re = '/^(.+?)(ΕΩΣ|ΕΩΝ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept4 = '/^(Θ|Δ|ΕΛ|ΓΑΛ|Ν|Π|ΙΔ|ΠΑΡ)$/'; - - if (preg_match($exept4, $token)) - { - $token = $token . 'Ε'; - } - } - - // Step 3 - $re = '/^(.+?)(ΙΑ|ΙΟΥ|ΙΩΝ)$/'; - - if (preg_match($re, $token, $fp)) - { - $stem = $fp[1]; - $token = $stem; - $re = '/' . $v . '$/'; - $test1 = false; - - if (preg_match($re, $token)) - { - $token = $stem . 'Ι'; - } - } - - // Step 4 - $re = '/^(.+?)(ΙΚΑ|ΙΚΟ|ΙΚΟΥ|ΙΚΩΝ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $re = '/' . $v . '$/'; - $exept5 = '/^(ΑΛ|ΑΔ|ΕΝΔ|ΑΜΑΝ|ΑΜΜΟΧΑΛ|ΗΘ|ΑΝΗΘ|ΑΝΤΙΔ|ΦΥΣ|ΒΡΩΜ|ΓΕΡ|ΕΞΩΔ|ΚΑΛΠ|ΚΑΛΛΙΝ|ΚΑΤΑΔ|ΜΟΥΛ|ΜΠΑΝ|ΜΠΑΓΙΑΤ|ΜΠΟΛ|ΜΠΟΣ|ΝΙΤ|ΞΙΚ|ΣΥΝΟΜΗΛ|ΠΕΤΣ|' - . 'ΠΙΤΣ|ΠΙΚΑΝΤ|ΠΛΙΑΤΣ|ΠΟΣΤΕΛΝ|ΠΡΩΤΟΔ|ΣΕΡΤ|ΣΥΝΑΔ|ΤΣΑΜ|ΥΠΟΔ|ΦΙΛΟΝ|ΦΥΛΟΔ|ΧΑΣ)$/'; - - if (preg_match($re, $token) || preg_match($exept5, $token)) - { - $token = $token . 'ΙΚ'; - } - } - - // Step 5a - $re = '/^(.+?)(ΑΜΕ)$/'; - $re2 = '/^(.+?)(ΑΓΑΜΕ|ΗΣΑΜΕ|ΟΥΣΑΜΕ|ΗΚΑΜΕ|ΗΘΗΚΑΜΕ)$/'; - - if ($token == "ΑΓΑΜΕ") - { - $token = "ΑΓΑΜ"; - } - - if (preg_match($re2, $token)) - { - preg_match($re2, $token, $match); - $token = $match[1]; - $test1 = false; - } - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept6 = '/^(ΑΝΑΠ|ΑΠΟΘ|ΑΠΟΚ|ΑΠΟΣΤ|ΒΟΥΒ|ΞΕΘ|ΟΥΛ|ΠΕΘ|ΠΙΚΡ|ΠΟΤ|ΣΙΧ|Χ)$/'; - - if (preg_match($exept6, $token)) - { - $token = $token . "ΑΜ"; - } - } - - // Step 5b - $re2 = '/^(.+?)(ΑΝΕ)$/'; - $re3 = '/^(.+?)(ΑΓΑΝΕ|ΗΣΑΝΕ|ΟΥΣΑΝΕ|ΙΟΝΤΑΝΕ|ΙΟΤΑΝΕ|ΙΟΥΝΤΑΝΕ|ΟΝΤΑΝΕ|ΟΤΑΝΕ|ΟΥΝΤΑΝΕ|ΗΚΑΝΕ|ΗΘΗΚΑΝΕ)$/'; - - if (preg_match($re3, $token)) - { - preg_match($re3, $token, $match); - $token = $match[1]; - $test1 = false; - $re3 = '/^(ΤΡ|ΤΣ)$/'; - - if (preg_match($re3, $token)) - { - $token = $token . "ΑΓΑΝ"; - } - } - - if (preg_match($re2, $token)) - { - preg_match($re2, $token, $match); - $token = $match[1]; - $test1 = false; - $re2 = '/' . $v2 . '$/'; - $exept7 = '/^(ΒΕΤΕΡ|ΒΟΥΛΚ|ΒΡΑΧΜ|Γ|ΔΡΑΔΟΥΜ|Θ|ΚΑΛΠΟΥΖ|ΚΑΣΤΕΛ|ΚΟΡΜΟΡ|ΛΑΟΠΛ|ΜΩΑΜΕΘ|Μ|ΜΟΥΣΟΥΛΜ|Ν|ΟΥΛ|Π|ΠΕΛΕΚ|ΠΛ|ΠΟΛΙΣ|ΠΟΡΤΟΛ|ΣΑΡΑΚΑΤΣ|ΣΟΥΛΤ|' - . 'ΤΣΑΡΛΑΤ|ΟΡΦ|ΤΣΙΓΓ|ΤΣΟΠ|ΦΩΤΟΣΤΕΦ|Χ|ΨΥΧΟΠΛ|ΑΓ|ΟΡΦ|ΓΑΛ|ΓΕΡ|ΔΕΚ|ΔΙΠΛ|ΑΜΕΡΙΚΑΝ|ΟΥΡ|ΠΙΘ|ΠΟΥΡΙΤ|Σ|ΖΩΝΤ|ΙΚ|ΚΑΣΤ|ΚΟΠ|ΛΙΧ|ΛΟΥΘΗΡ|ΜΑΙΝΤ|' - . 'ΜΕΛ|ΣΙΓ|ΣΠ|ΣΤΕΓ|ΤΡΑΓ|ΤΣΑΓ|Φ|ΕΡ|ΑΔΑΠ|ΑΘΙΓΓ|ΑΜΗΧ|ΑΝΙΚ|ΑΝΟΡΓ|ΑΠΗΓ|ΑΠΙΘ|ΑΤΣΙΓΓ|ΒΑΣ|ΒΑΣΚ|ΒΑΘΥΓΑΛ|ΒΙΟΜΗΧ|ΒΡΑΧΥΚ|ΔΙΑΤ|ΔΙΑΦ|ΕΝΟΡΓ|' - . 'ΘΥΣ|ΚΑΠΝΟΒΙΟΜΗΧ|ΚΑΤΑΓΑΛ|ΚΛΙΒ|ΚΟΙΛΑΡΦ|ΛΙΒ|ΜΕΓΛΟΒΙΟΜΗΧ|ΜΙΚΡΟΒΙΟΜΗΧ|ΝΤΑΒ|ΞΗΡΟΚΛΙΒ|ΟΛΙΓΟΔΑΜ|ΟΛΟΓΑΛ|ΠΕΝΤΑΡΦ|ΠΕΡΗΦ|ΠΕΡΙΤΡ|ΠΛΑΤ|' - . 'ΠΟΛΥΔΑΠ|ΠΟΛΥΜΗΧ|ΣΤΕΦ|ΤΑΒ|ΤΕΤ|ΥΠΕΡΗΦ|ΥΠΟΚΟΠ|ΧΑΜΗΛΟΔΑΠ|ΨΗΛΟΤΑΒ)$/'; - - if (preg_match($re2, $token) || preg_match($exept7, $token)) - { - $token = $token . "ΑΝ"; - } - } - - // Step 5c - $re3 = '/^(.+?)(ΕΤΕ)$/'; - $re4 = '/^(.+?)(ΗΣΕΤΕ)$/'; - - if (preg_match($re4, $token)) - { - preg_match($re4, $token, $match); - $token = $match[1]; - $test1 = false; - } - - if (preg_match($re3, $token)) - { - preg_match($re3, $token, $match); - $token = $match[1]; - $test1 = false; - $re3 = '/' . $v2 . '$/'; - $exept8 = '/(ΟΔ|ΑΙΡ|ΦΟΡ|ΤΑΘ|ΔΙΑΘ|ΣΧ|ΕΝΔ|ΕΥΡ|ΤΙΘ|ΥΠΕΡΘ|ΡΑΘ|ΕΝΘ|ΡΟΘ|ΣΘ|ΠΥΡ|ΑΙΝ|ΣΥΝΔ|ΣΥΝ|ΣΥΝΘ|ΧΩΡ|ΠΟΝ|ΒΡ|ΚΑΘ|ΕΥΘ|ΕΚΘ|ΝΕΤ|ΡΟΝ|ΑΡΚ|ΒΑΡ|ΒΟΛ|ΩΦΕΛ)$/'; - $exept9 = '/^(ΑΒΑΡ|ΒΕΝ|ΕΝΑΡ|ΑΒΡ|ΑΔ|ΑΘ|ΑΝ|ΑΠΛ|ΒΑΡΟΝ|ΝΤΡ|ΣΚ|ΚΟΠ|ΜΠΟΡ|ΝΙΦ|ΠΑΓ|ΠΑΡΑΚΑΛ|ΣΕΡΠ|ΣΚΕΛ|ΣΥΡΦ|ΤΟΚ|Υ|Δ|ΕΜ|ΘΑΡΡ|Θ)$/'; - - if (preg_match($re3, $token) || preg_match($exept8, $token) || preg_match($exept9, $token)) - { - $token = $token . "ΕΤ"; - } - } - - // Step 5d - $re = '/^(.+?)(ΟΝΤΑΣ|ΩΝΤΑΣ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept10 = '/^(ΑΡΧ)$/'; - $exept11 = '/(ΚΡΕ)$/'; - - if (preg_match($exept10, $token)) - { - $token = $token . "ΟΝΤ"; - } - - if (preg_match($exept11, $token)) - { - $token = $token . "ΩΝΤ"; - } - } - - // Step 5e - $re = '/^(.+?)(ΟΜΑΣΤΕ|ΙΟΜΑΣΤΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept11 = '/^(ΟΝ)$/'; - - if (preg_match($exept11, $token)) - { - $token = $token . "ΟΜΑΣΤ"; - } - } - - // Step 5f - $re = '/^(.+?)(ΕΣΤΕ)$/'; - $re2 = '/^(.+?)(ΙΕΣΤΕ)$/'; - - if (preg_match($re2, $token)) - { - preg_match($re2, $token, $match); - $token = $match[1]; - $test1 = false; - $re2 = '/^(Π|ΑΠ|ΣΥΜΠ|ΑΣΥΜΠ|ΑΚΑΤΑΠ|ΑΜΕΤΑΜΦ)$/'; - - if (preg_match($re2, $token)) - { - $token = $token . "ΙΕΣΤ"; - } - } - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept12 = '/^(ΑΛ|ΑΡ|ΕΚΤΕΛ|Ζ|Μ|Ξ|ΠΑΡΑΚΑΛ|ΑΡ|ΠΡΟ|ΝΙΣ)$/'; - - if (preg_match($exept12, $token)) - { - $token = $token . "ΕΣΤ"; - } - } - - // Step 5g - $re = '/^(.+?)(ΗΚΑ|ΗΚΕΣ|ΗΚΕ)$/'; - $re2 = '/^(.+?)(ΗΘΗΚΑ|ΗΘΗΚΕΣ|ΗΘΗΚΕ)$/'; - - if (preg_match($re2, $token)) - { - preg_match($re2, $token, $match); - $token = $match[1]; - $test1 = false; - } - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept13 = '/(ΣΚΩΛ|ΣΚΟΥΛ|ΝΑΡΘ|ΣΦ|ΟΘ|ΠΙΘ)$/'; - $exept14 = '/^(ΔΙΑΘ|Θ|ΠΑΡΑΚΑΤΑΘ|ΠΡΟΣΘ|ΣΥΝΘ|)$/'; - - if (preg_match($exept13, $token) || preg_match($exept14, $token)) - { - $token = $token . "ΗΚ"; - } - } - - // Step 5h - $re = '/^(.+?)(ΟΥΣΑ|ΟΥΣΕΣ|ΟΥΣΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept15 = '/^(ΦΑΡΜΑΚ|ΧΑΔ|ΑΓΚ|ΑΝΑΡΡ|ΒΡΟΜ|ΕΚΛΙΠ|ΛΑΜΠΙΔ|ΛΕΧ|Μ|ΠΑΤ|Ρ|Λ|ΜΕΔ|ΜΕΣΑΖ|ΥΠΟΤΕΙΝ|ΑΜ|ΑΙΘ|ΑΝΗΚ|ΔΕΣΠΟΖ|ΕΝΔΙΑΦΕΡ|ΔΕ|ΔΕΥΤΕΡΕΥ|ΚΑΘΑΡΕΥ|ΠΛΕ|ΤΣΑ)$/'; - $exept16 = '/(ΠΟΔΑΡ|ΒΛΕΠ|ΠΑΝΤΑΧ|ΦΡΥΔ|ΜΑΝΤΙΛ|ΜΑΛΛ|ΚΥΜΑΤ|ΛΑΧ|ΛΗΓ|ΦΑΓ|ΟΜ|ΠΡΩΤ)$/'; - - if (preg_match($exept15, $token) || preg_match($exept16, $token)) - { - $token = $token . "ΟΥΣ"; - } - } - - // Step 5i - $re = '/^(.+?)(ΑΓΑ|ΑΓΕΣ|ΑΓΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept17 = '/^(ΨΟΦ|ΝΑΥΛΟΧ)$/'; - $exept20 = '/(ΚΟΛΛ)$/'; - $exept18 = '/^(ΑΒΑΣΤ|ΠΟΛΥΦ|ΑΔΗΦ|ΠΑΜΦ|Ρ|ΑΣΠ|ΑΦ|ΑΜΑΛ|ΑΜΑΛΛΙ|ΑΝΥΣΤ|ΑΠΕΡ|ΑΣΠΑΡ|ΑΧΑΡ|ΔΕΡΒΕΝ|ΔΡΟΣΟΠ|ΞΕΦ|ΝΕΟΠ|ΝΟΜΟΤ|ΟΛΟΠ|ΟΜΟΤ|ΠΡΟΣΤ|ΠΡΟΣΩΠΟΠ|' - . 'ΣΥΜΠ|ΣΥΝΤ|Τ|ΥΠΟΤ|ΧΑΡ|ΑΕΙΠ|ΑΙΜΟΣΤ|ΑΝΥΠ|ΑΠΟΤ|ΑΡΤΙΠ|ΔΙΑΤ|ΕΝ|ΕΠΙΤ|ΚΡΟΚΑΛΟΠ|ΣΙΔΗΡΟΠ|Λ|ΝΑΥ|ΟΥΛΑΜ|ΟΥΡ|Π|ΤΡ|Μ)$/'; - $exept19 = '/(ΟΦ|ΠΕΛ|ΧΟΡΤ|ΛΛ|ΣΦ|ΡΠ|ΦΡ|ΠΡ|ΛΟΧ|ΣΜΗΝ)$/'; - - if ((preg_match($exept18, $token) || preg_match($exept19, $token)) - && !(preg_match($exept17, $token) || preg_match($exept20, $token))) - { - $token = $token . "ΑΓ"; - } - } - - // Step 5j - $re = '/^(.+?)(ΗΣΕ|ΗΣΟΥ|ΗΣΑ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept21 = '/^(Ν|ΧΕΡΣΟΝ|ΔΩΔΕΚΑΝ|ΕΡΗΜΟΝ|ΜΕΓΑΛΟΝ|ΕΠΤΑΝ)$/'; - - if (preg_match($exept21, $token)) - { - $token = $token . "ΗΣ"; - } - } - - // Step 5k - $re = '/^(.+?)(ΗΣΤΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept22 = '/^(ΑΣΒ|ΣΒ|ΑΧΡ|ΧΡ|ΑΠΛ|ΑΕΙΜΝ|ΔΥΣΧΡ|ΕΥΧΡ|ΚΟΙΝΟΧΡ|ΠΑΛΙΜΨ)$/'; - - if (preg_match($exept22, $token)) - { - $token = $token . "ΗΣΤ"; - } - } - - // Step 5l - $re = '/^(.+?)(ΟΥΝΕ|ΗΣΟΥΝΕ|ΗΘΟΥΝΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept23 = '/^(Ν|Ρ|ΣΠΙ|ΣΤΡΑΒΟΜΟΥΤΣ|ΚΑΚΟΜΟΥΤΣ|ΕΞΩΝ)$/'; - - if (preg_match($exept23, $token)) - { - $token = $token . "ΟΥΝ"; - } - } - - // Step 5m - $re = '/^(.+?)(ΟΥΜΕ|ΗΣΟΥΜΕ|ΗΘΟΥΜΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept24 = '/^(ΠΑΡΑΣΟΥΣ|Φ|Χ|ΩΡΙΟΠΛ|ΑΖ|ΑΛΛΟΣΟΥΣ|ΑΣΟΥΣ)$/'; - - if (preg_match($exept24, $token)) - { - $token = $token . "ΟΥΜ"; - } - } - - // Step 6 - $re = '/^(.+?)(ΜΑΤΑ|ΜΑΤΩΝ|ΜΑΤΟΣ)$/'; - $re2 = '/^(.+?)(Α|ΑΓΑΤΕ|ΑΓΑΝ|ΑΕΙ|ΑΜΑΙ|ΑΝ|ΑΣ|ΑΣΑΙ|ΑΤΑΙ|ΑΩ|Ε|ΕΙ|ΕΙΣ|ΕΙΤΕ|ΕΣΑΙ|ΕΣ|ΕΤΑΙ|Ι|ΙΕΜΑΙ|ΙΕΜΑΣΤΕ|ΙΕΤΑΙ|ΙΕΣΑΙ|ΙΕΣΑΣΤΕ|ΙΟΜΑΣΤΑΝ|ΙΟΜΟΥΝ|' - . 'ΙΟΜΟΥΝΑ|ΙΟΝΤΑΝ|ΙΟΝΤΟΥΣΑΝ|ΙΟΣΑΣΤΑΝ|ΙΟΣΑΣΤΕ|ΙΟΣΟΥΝ|ΙΟΣΟΥΝΑ|ΙΟΤΑΝ|ΙΟΥΜΑ|ΙΟΥΜΑΣΤΕ|ΙΟΥΝΤΑΙ|ΙΟΥΝΤΑΝ|Η|ΗΔΕΣ|ΗΔΩΝ|ΗΘΕΙ|ΗΘΕΙΣ|ΗΘΕΙΤΕ|' - . 'ΗΘΗΚΑΤΕ|ΗΘΗΚΑΝ|ΗΘΟΥΝ|ΗΘΩ|ΗΚΑΤΕ|ΗΚΑΝ|ΗΣ|ΗΣΑΝ|ΗΣΑΤΕ|ΗΣΕΙ|ΗΣΕΣ|ΗΣΟΥΝ|ΗΣΩ|Ο|ΟΙ|ΟΜΑΙ|ΟΜΑΣΤΑΝ|ΟΜΟΥΝ|ΟΜΟΥΝΑ|ΟΝΤΑΙ|ΟΝΤΑΝ|ΟΝΤΟΥΣΑΝ|ΟΣ|' - . 'ΟΣΑΣΤΑΝ|ΟΣΑΣΤΕ|ΟΣΟΥΝ|ΟΣΟΥΝΑ|ΟΤΑΝ|ΟΥ|ΟΥΜΑΙ|ΟΥΜΑΣΤΕ|ΟΥΝ|ΟΥΝΤΑΙ|ΟΥΝΤΑΝ|ΟΥΣ|ΟΥΣΑΝ|ΟΥΣΑΤΕ|Υ|ΥΣ|Ω|ΩΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1] . "ΜΑ"; - } - - if (preg_match($re2, $token) && $test1) - { - preg_match($re2, $token, $match); - $token = $match[1]; - } - - // Step 7 (ΠΑΡΑΘΕΤΙΚΑ) - $re = '/^(.+?)(ΕΣΤΕΡ|ΕΣΤΑΤ|ΟΤΕΡ|ΟΤΑΤ|ΥΤΕΡ|ΥΤΑΤ|ΩΤΕΡ|ΩΤΑΤ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - } - - return $this->toLowerCase($token, $wCase); - } - - /** - * Converts the token to uppercase, suppressing accents and diaeresis. The array $wCase contains a special map of - * the uppercase rule used to convert each character at each position. - * - * @param string $token Token to process - * @param array &$wCase Map of uppercase rules - * - * @return string - * - * @since 4.0.0 - */ - protected function toUpperCase($token, &$wCase) - { - $wCase = array_fill(0, mb_strlen($token, 'UTF-8'), 0); - $caseConvert = array( - "α" => 'Α', - "β" => 'Β', - "γ" => 'Γ', - "δ" => 'Δ', - "ε" => 'Ε', - "ζ" => 'Ζ', - "η" => 'Η', - "θ" => 'Θ', - "ι" => 'Ι', - "κ" => 'Κ', - "λ" => 'Λ', - "μ" => 'Μ', - "ν" => 'Ν', - "ξ" => 'Ξ', - "ο" => 'Ο', - "π" => 'Π', - "ρ" => 'Ρ', - "σ" => 'Σ', - "τ" => 'Τ', - "υ" => 'Υ', - "φ" => 'Φ', - "χ" => 'Χ', - "ψ" => 'Ψ', - "ω" => 'Ω', - "ά" => 'Α', - "έ" => 'Ε', - "ή" => 'Η', - "ί" => 'Ι', - "ό" => 'Ο', - "ύ" => 'Υ', - "ώ" => 'Ω', - "ς" => 'Σ', - "ϊ" => 'Ι', - "ϋ" => 'Ι', - "ΐ" => 'Ι', - "ΰ" => 'Υ', - ); - $newToken = ''; - - for ($i = 0; $i < mb_strlen($token); $i++) - { - $char = mb_substr($token, $i, 1); - $isLower = array_key_exists($char, $caseConvert); - - if (!$isLower) - { - $newToken .= $char; - - continue; - } - - $upperCase = $caseConvert[$char]; - $newToken .= $upperCase; - - $wCase[$i] = 1; - - if (in_array($char, ['ά', 'έ', 'ή', 'ί', 'ό', 'ύ', 'ώ', 'ς'])) - { - $wCase[$i] = 2; - } - - if (in_array($char, ['ϊ', 'ϋ'])) - { - $wCase[$i] = 3; - } - - if (in_array($char, ['ΐ', 'ΰ'])) - { - $wCase[$i] = 4; - } - } - - return $newToken; - } - - /** - * Converts the suppressed uppercase token back to lowercase, using the $wCase map to add back the accents, - * diaeresis and handle the special case of final sigma (different lowercase glyph than the regular sigma, only - * used at the end of words). - * - * @param string $token Token to process - * @param array $wCase Map of lowercase rules - * - * @return string - * - * @since 4.0.0 - */ - protected function toLowerCase($token, $wCase) - { - $newToken = ''; - - for ($i = 0; $i < mb_strlen($token); $i++) - { - $char = mb_substr($token, $i, 1); - - // Is $wCase not set at this position? We assume no case conversion ever took place. - if (!isset($wCase[$i])) - { - $newToken .= $char; - - continue; - } - - // The character was not case-converted - if ($wCase[$i] == 0) - { - $newToken .= $char; - - continue; - } - - // Case 1: Unaccented letter - if ($wCase[$i] == 1) - { - $newToken .= mb_strtolower($char); - - continue; - } - - // Case 2: Vowel with accent (tonos); or the special case of final sigma - if ($wCase[$i] == 2) - { - $charMap = [ - 'Α' => 'ά', - 'Ε' => 'έ', - 'Η' => 'ή', - 'Ι' => 'ί', - 'Ο' => 'ό', - 'Υ' => 'ύ', - 'Ω' => 'ώ', - 'Σ' => 'ς' - ]; - - $newToken .= $charMap[$char]; - - continue; - } - - // Case 3: vowels with diaeresis (dialytika) - if ($wCase[$i] == 3) - { - $charMap = [ - 'Ι' => 'ϊ', - 'Υ' => 'ϋ' - ]; - - $newToken .= $charMap[$char]; - - continue; - } - - // Case 4: vowels with both diaeresis (dialytika) and accent (tonos) - if ($wCase[$i] == 4) - { - $charMap = [ - 'Ι' => 'ΐ', - 'Υ' => 'ΰ' - ]; - - $newToken .= $charMap[$char]; - - continue; - } - - // This should never happen! - $newToken .= $char; - } - - return $newToken; - } + /** + * Language locale of the class + * + * @var string + * @since 4.0.0 + */ + public $language = 'el'; + + /** + * Method to construct the language object. + * + * @since 4.0.0 + */ + public function __construct($locale = null) + { + // Override parent constructor since we don't need to load an external stemmer + } + + /** + * Method to tokenise a text string. It takes into account the odd punctuation commonly used in Greek text, mapping + * it to ASCII punctuation. + * + * Reference: http://www.teicrete.gr/users/kutrulis/Glosika/Stixi.htm + * + * @param string $input The input to tokenise. + * + * @return array An array of term strings. + * + * @since 4.0.0 + */ + public function tokenise($input) + { + // Replace Greek calligraphic double quotes (various styles) to dumb double quotes + $input = str_replace(['“', '”', '„', '«' ,'»'], '"', $input); + + // Replace Greek calligraphic single quotes (various styles) to dumb single quotes + $input = str_replace(['‘','’','‚'], "'", $input); + + // Replace the middle dot (ano teleia) with a comma, adequate for the purpose of stemming + $input = str_replace('·', ',', $input); + + // Dot and dash (τελεία και παύλα), used to denote the end of a context at the end of a paragraph. + $input = str_replace('.–', '.', $input); + + // Ellipsis, two styles (separate dots or single glyph) + $input = str_replace(['...', '…'], '.', $input); + + // Cross. Marks the death date of a person. Removed. + $input = str_replace('†', '', $input); + + // Star. Reference, supposition word (in philology), birth date of a person. + $input = str_replace('*', '', $input); + + // Paragraph. Indicates change of subject. + $input = str_replace('§', '.', $input); + + // Plus/minus. Shows approximation. Not relevant for the stemmer, hence its conversion to a space. + $input = str_replace('±', ' ', $input); + + return parent::tokenise($input); + } + + /** + * Method to stem a token. + * + * @param string $token The token to stem. + * + * @return string The stemmed token. + * + * @since 4.0.0 + */ + public function stem($token) + { + $token = $this->toUpperCase($token, $wCase); + + // Stop-word removal + $stop_words = '/^(ΕΚΟ|ΑΒΑ|ΑΓΑ|ΑΓΗ|ΑΓΩ|ΑΔΗ|ΑΔΩ|ΑΕ|ΑΕΙ|ΑΘΩ|ΑΙ|ΑΙΚ|ΑΚΗ|ΑΚΟΜΑ|ΑΚΟΜΗ|ΑΚΡΙΒΩΣ|ΑΛΑ|ΑΛΗΘΕΙΑ|ΑΛΗΘΙΝΑ|ΑΛΛΑΧΟΥ|ΑΛΛΙΩΣ|ΑΛΛΙΩΤΙΚΑ|' + . 'ΑΛΛΟΙΩΣ|ΑΛΛΟΙΩΤΙΚΑ|ΑΛΛΟΤΕ|ΑΛΤ|ΑΛΩ|ΑΜΑ|ΑΜΕ|ΑΜΕΣΑ|ΑΜΕΣΩΣ|ΑΜΩ|ΑΝ|ΑΝΑ|ΑΝΑΜΕΣΑ|ΑΝΑΜΕΤΑΞΥ|ΑΝΕΥ|ΑΝΤΙ|ΑΝΤΙΠΕΡΑ|ΑΝΤΙΣ|ΑΝΩ|ΑΝΩΤΕΡΩ|ΑΞΑΦΝΑ|' + . 'ΑΠ|ΑΠΕΝΑΝΤΙ|ΑΠΟ|ΑΠΟΨΕ|ΑΠΩ|ΑΡΑ|ΑΡΑΓΕ|ΑΡΕ|ΑΡΚ|ΑΡΚΕΤΑ|ΑΡΛ|ΑΡΜ|ΑΡΤ|ΑΡΥ|ΑΡΩ|ΑΣ|ΑΣΑ|ΑΣΟ|ΑΤΑ|ΑΤΕ|ΑΤΗ|ΑΤΙ|ΑΤΜ|ΑΤΟ|ΑΥΡΙΟ|ΑΦΗ|ΑΦΟΤΟΥ|ΑΦΟΥ|' + . 'ΑΧ|ΑΧΕ|ΑΧΟ|ΑΨΑ|ΑΨΕ|ΑΨΗ|ΑΨΥ|ΑΩΕ|ΑΩΟ|ΒΑΝ|ΒΑΤ|ΒΑΧ|ΒΕΑ|ΒΕΒΑΙΟΤΑΤΑ|ΒΗΞ|ΒΙΑ|ΒΙΕ|ΒΙΗ|ΒΙΟ|ΒΟΗ|ΒΟΩ|ΒΡΕ|ΓΑ|ΓΑΒ|ΓΑΡ|ΓΕΝ|ΓΕΣ||ΓΗ|ΓΗΝ|ΓΙ|ΓΙΑ|' + . 'ΓΙΕ|ΓΙΝ|ΓΙΟ|ΓΚΙ|ΓΙΑΤΙ|ΓΚΥ|ΓΟΗ|ΓΟΟ|ΓΡΗΓΟΡΑ|ΓΡΙ|ΓΡΥ|ΓΥΗ|ΓΥΡΩ|ΔΑ|ΔΕ|ΔΕΗ|ΔΕΙ|ΔΕΝ|ΔΕΣ|ΔΗ|ΔΗΘΕΝ|ΔΗΛΑΔΗ|ΔΗΩ|ΔΙ|ΔΙΑ|ΔΙΑΡΚΩΣ|ΔΙΟΛΟΥ|ΔΙΣ|' + . 'ΔΙΧΩΣ|ΔΟΛ|ΔΟΝ|ΔΡΑ|ΔΡΥ|ΔΡΧ|ΔΥΕ|ΔΥΟ|ΔΩ|ΕΑΜ|ΕΑΝ|ΕΑΡ|ΕΘΗ|ΕΙ|ΕΙΔΕΜΗ|ΕΙΘΕ|ΕΙΜΑΙ|ΕΙΜΑΣΤΕ|ΕΙΝΑΙ|ΕΙΣ|ΕΙΣΑΙ|ΕΙΣΑΣΤΕ|ΕΙΣΤΕ|ΕΙΤΕ|ΕΙΧΑ|ΕΙΧΑΜΕ|' + . 'ΕΙΧΑΝ|ΕΙΧΑΤΕ|ΕΙΧΕ|ΕΙΧΕΣ|ΕΚ|ΕΚΕΙ|ΕΛΑ|ΕΛΙ|ΕΜΠ|ΕΝ|ΕΝΤΕΛΩΣ|ΕΝΤΟΣ|ΕΝΤΩΜΕΤΑΞΥ|ΕΝΩ|ΕΞ|ΕΞΑΦΝΑ|ΕΞΙ|ΕΞΙΣΟΥ|ΕΞΩ|ΕΟΚ|ΕΠΑΝΩ|ΕΠΕΙΔΗ|ΕΠΕΙΤΑ|ΕΠΗ|' + . 'ΕΠΙ|ΕΠΙΣΗΣ|ΕΠΟΜΕΝΩΣ|ΕΡΑ|ΕΣ|ΕΣΑΣ|ΕΣΕ|ΕΣΕΙΣ|ΕΣΕΝΑ|ΕΣΗ|ΕΣΤΩ|ΕΣΥ|ΕΣΩ|ΕΤΙ|ΕΤΣΙ|ΕΥ|ΕΥΑ|ΕΥΓΕ|ΕΥΘΥΣ|ΕΥΤΥΧΩΣ|ΕΦΕ|ΕΦΕΞΗΣ|ΕΦΤ|ΕΧΕ|ΕΧΕΙ|' + . 'ΕΧΕΙΣ|ΕΧΕΤΕ|ΕΧΘΕΣ|ΕΧΟΜΕ|ΕΧΟΥΜΕ|ΕΧΟΥΝ|ΕΧΤΕΣ|ΕΧΩ|ΕΩΣ|ΖΕΑ|ΖΕΗ|ΖΕΙ|ΖΕΝ|ΖΗΝ|ΖΩ|Η|ΗΔΗ|ΗΔΥ|ΗΘΗ|ΗΛΟ|ΗΜΙ|ΗΠΑ|ΗΣΑΣΤΕ|ΗΣΟΥΝ|ΗΤΑ|ΗΤΑΝ|ΗΤΑΝΕ|' + . 'ΗΤΟΙ|ΗΤΤΟΝ|ΗΩ|ΘΑ|ΘΥΕ|ΘΩΡ|Ι|ΙΑ|ΙΒΟ|ΙΔΗ|ΙΔΙΩΣ|ΙΕ|ΙΙ|ΙΙΙ|ΙΚΑ|ΙΛΟ|ΙΜΑ|ΙΝΑ|ΙΝΩ|ΙΞΕ|ΙΞΟ|ΙΟ|ΙΟΙ|ΙΣΑ|ΙΣΑΜΕ|ΙΣΕ|ΙΣΗ|ΙΣΙΑ|ΙΣΟ|ΙΣΩΣ|ΙΩΒ|ΙΩΝ|' + . 'ΙΩΣ|ΙΑΝ|ΚΑΘ|ΚΑΘΕ|ΚΑΘΕΤΙ|ΚΑΘΟΛΟΥ|ΚΑΘΩΣ|ΚΑΙ|ΚΑΝ|ΚΑΠΟΤΕ|ΚΑΠΟΥ|ΚΑΠΩΣ|ΚΑΤ|ΚΑΤΑ|ΚΑΤΙ|ΚΑΤΙΤΙ|ΚΑΤΟΠΙΝ|ΚΑΤΩ|ΚΑΩ|ΚΒΟ|ΚΕΑ|ΚΕΙ|ΚΕΝ|ΚΙ|ΚΙΜ|' + . 'ΚΙΟΛΑΣ|ΚΙΤ|ΚΙΧ|ΚΚΕ|ΚΛΙΣΕ|ΚΛΠ|ΚΟΚ|ΚΟΝΤΑ|ΚΟΧ|ΚΤΛ|ΚΥΡ|ΚΥΡΙΩΣ|ΚΩ|ΚΩΝ|ΛΑ|ΛΕΑ|ΛΕΝ|ΛΕΟ|ΛΙΑ|ΛΙΓΑΚΙ|ΛΙΓΟΥΛΑΚΙ|ΛΙΓΟ|ΛΙΓΩΤΕΡΟ|ΛΙΟ|ΛΙΡ|ΛΟΓΩ|' + . 'ΛΟΙΠΑ|ΛΟΙΠΟΝ|ΛΟΣ|ΛΣ|ΛΥΩ|ΜΑ|ΜΑΖΙ|ΜΑΚΑΡΙ|ΜΑΛΙΣΤΑ|ΜΑΛΛΟΝ|ΜΑΝ|ΜΑΞ|ΜΑΣ|ΜΑΤ|ΜΕ|ΜΕΘΑΥΡΙΟ|ΜΕΙ|ΜΕΙΟΝ|ΜΕΛ|ΜΕΛΕΙ|ΜΕΛΛΕΤΑΙ|ΜΕΜΙΑΣ|ΜΕΝ|ΜΕΣ|' + . 'ΜΕΣΑ|ΜΕΤ|ΜΕΤΑ|ΜΕΤΑΞΥ|ΜΕΧΡΙ|ΜΗ|ΜΗΔΕ|ΜΗΝ|ΜΗΠΩΣ|ΜΗΤΕ|ΜΙ|ΜΙΞ|ΜΙΣ|ΜΜΕ|ΜΝΑ|ΜΟΒ|ΜΟΛΙΣ|ΜΟΛΟΝΟΤΙ|ΜΟΝΑΧΑ|ΜΟΝΟΜΙΑΣ|ΜΙΑ|ΜΟΥ|ΜΠΑ|ΜΠΟΡΕΙ|' + . 'ΜΠΟΡΟΥΝ|ΜΠΡΑΒΟ|ΜΠΡΟΣ|ΜΠΩ|ΜΥ|ΜΥΑ|ΜΥΝ|ΝΑ|ΝΑΕ|ΝΑΙ|ΝΑΟ|ΝΔ|ΝΕΐ|ΝΕΑ|ΝΕΕ|ΝΕΟ|ΝΙ|ΝΙΑ|ΝΙΚ|ΝΙΛ|ΝΙΝ|ΝΙΟ|ΝΤΑ|ΝΤΕ|ΝΤΙ|ΝΤΟ|ΝΥΝ|ΝΩΕ|ΝΩΡΙΣ|ΞΑΝΑ|' + . 'ΞΑΦΝΙΚΑ|ΞΕΩ|ΞΙ|Ο|ΟΑ|ΟΑΠ|ΟΔΟ|ΟΕ|ΟΖΟ|ΟΗΕ|ΟΙ|ΟΙΑ|ΟΙΗ|ΟΚΑ|ΟΛΟΓΥΡΑ|ΟΛΟΝΕΝ|ΟΛΟΤΕΛΑ|ΟΛΩΣΔΙΟΛΟΥ|ΟΜΩΣ|ΟΝ|ΟΝΕ|ΟΝΟ|ΟΠΑ|ΟΠΕ|ΟΠΗ|ΟΠΟ|' + . 'ΟΠΟΙΑΔΗΠΟΤΕ|ΟΠΟΙΑΝΔΗΠΟΤΕ|ΟΠΟΙΑΣΔΗΠΟΤΕ|ΟΠΟΙΔΗΠΟΤΕ|ΟΠΟΙΕΣΔΗΠΟΤΕ|ΟΠΟΙΟΔΗΠΟΤΕ|ΟΠΟΙΟΝΔΗΠΟΤΕ|ΟΠΟΙΟΣΔΗΠΟΤΕ|ΟΠΟΙΟΥΔΗΠΟΤΕ|ΟΠΟΙΟΥΣΔΗΠΟΤΕ|' + . 'ΟΠΟΙΩΝΔΗΠΟΤΕ|ΟΠΟΤΕΔΗΠΟΤΕ|ΟΠΟΥ|ΟΠΟΥΔΗΠΟΤΕ|ΟΠΩΣ|ΟΡΑ|ΟΡΕ|ΟΡΗ|ΟΡΟ|ΟΡΦ|ΟΡΩ|ΟΣΑ|ΟΣΑΔΗΠΟΤΕ|ΟΣΕ|ΟΣΕΣΔΗΠΟΤΕ|ΟΣΗΔΗΠΟΤΕ|ΟΣΗΝΔΗΠΟΤΕ|' + . 'ΟΣΗΣΔΗΠΟΤΕ|ΟΣΟΔΗΠΟΤΕ|ΟΣΟΙΔΗΠΟΤΕ|ΟΣΟΝΔΗΠΟΤΕ|ΟΣΟΣΔΗΠΟΤΕ|ΟΣΟΥΔΗΠΟΤΕ|ΟΣΟΥΣΔΗΠΟΤΕ|ΟΣΩΝΔΗΠΟΤΕ|ΟΤΑΝ|ΟΤΕ|ΟΤΙ|ΟΤΙΔΗΠΟΤΕ|ΟΥ|ΟΥΔΕ|ΟΥΚ|ΟΥΣ|' + . 'ΟΥΤΕ|ΟΥΦ|ΟΧΙ|ΟΨΑ|ΟΨΕ|ΟΨΗ|ΟΨΙ|ΟΨΟ|ΠΑ|ΠΑΛΙ|ΠΑΝ|ΠΑΝΤΟΤΕ|ΠΑΝΤΟΥ|ΠΑΝΤΩΣ|ΠΑΠ|ΠΑΡ|ΠΑΡΑ|ΠΕΙ|ΠΕΡ|ΠΕΡΑ|ΠΕΡΙ|ΠΕΡΙΠΟΥ|ΠΕΡΣΙ|ΠΕΡΥΣΙ|ΠΕΣ|ΠΙ|' + . 'ΠΙΑ|ΠΙΘΑΝΟΝ|ΠΙΚ|ΠΙΟ|ΠΙΣΩ|ΠΙΤ|ΠΙΩ|ΠΛΑΙ|ΠΛΕΟΝ|ΠΛΗΝ|ΠΛΩ|ΠΜ|ΠΟΑ|ΠΟΕ|ΠΟΛ|ΠΟΛΥ|ΠΟΠ|ΠΟΤΕ|ΠΟΥ|ΠΟΥΘΕ|ΠΟΥΘΕΝΑ|ΠΡΕΠΕΙ|ΠΡΙ|ΠΡΙΝ|ΠΡΟ|' + . 'ΠΡΟΚΕΙΜΕΝΟΥ|ΠΡΟΚΕΙΤΑΙ|ΠΡΟΠΕΡΣΙ|ΠΡΟΣ|ΠΡΟΤΟΥ|ΠΡΟΧΘΕΣ|ΠΡΟΧΤΕΣ|ΠΡΩΤΥΤΕΡΑ|ΠΥΑ|ΠΥΞ|ΠΥΟ|ΠΥΡ|ΠΧ|ΠΩ|ΠΩΛ|ΠΩΣ|ΡΑ|ΡΑΙ|ΡΑΠ|ΡΑΣ|ΡΕ|ΡΕΑ|ΡΕΕ|ΡΕΙ|' + . 'ΡΗΣ|ΡΘΩ|ΡΙΟ|ΡΟ|ΡΟΐ|ΡΟΕ|ΡΟΖ|ΡΟΗ|ΡΟΘ|ΡΟΙ|ΡΟΚ|ΡΟΛ|ΡΟΝ|ΡΟΣ|ΡΟΥ|ΣΑΙ|ΣΑΝ|ΣΑΟ|ΣΑΣ|ΣΕ|ΣΕΙΣ|ΣΕΚ|ΣΕΞ|ΣΕΡ|ΣΕΤ|ΣΕΦ|ΣΗΜΕΡΑ|ΣΙ|ΣΙΑ|ΣΙΓΑ|ΣΙΚ|' + . 'ΣΙΧ|ΣΚΙ|ΣΟΙ|ΣΟΚ|ΣΟΛ|ΣΟΝ|ΣΟΣ|ΣΟΥ|ΣΡΙ|ΣΤΑ|ΣΤΗ|ΣΤΗΝ|ΣΤΗΣ|ΣΤΙΣ|ΣΤΟ|ΣΤΟΝ|ΣΤΟΥ|ΣΤΟΥΣ|ΣΤΩΝ|ΣΥ|ΣΥΓΧΡΟΝΩΣ|ΣΥΝ|ΣΥΝΑΜΑ|ΣΥΝΕΠΩΣ|ΣΥΝΗΘΩΣ|' + . 'ΣΧΕΔΟΝ|ΣΩΣΤΑ|ΤΑ|ΤΑΔΕ|ΤΑΚ|ΤΑΝ|ΤΑΟ|ΤΑΥ|ΤΑΧΑ|ΤΑΧΑΤΕ|ΤΕ|ΤΕΙ|ΤΕΛ|ΤΕΛΙΚΑ|ΤΕΛΙΚΩΣ|ΤΕΣ|ΤΕΤ|ΤΖΟ|ΤΗ|ΤΗΛ|ΤΗΝ|ΤΗΣ|ΤΙ|ΤΙΚ|ΤΙΜ|ΤΙΠΟΤΑ|ΤΙΠΟΤΕ|' + . 'ΤΙΣ|ΤΝΤ|ΤΟ|ΤΟΙ|ΤΟΚ|ΤΟΜ|ΤΟΝ|ΤΟΠ|ΤΟΣ|ΤΟΣ?Ν|ΤΟΣΑ|ΤΟΣΕΣ|ΤΟΣΗ|ΤΟΣΗΝ|ΤΟΣΗΣ|ΤΟΣΟ|ΤΟΣΟΙ|ΤΟΣΟΝ|ΤΟΣΟΣ|ΤΟΣΟΥ|ΤΟΣΟΥΣ|ΤΟΤΕ|ΤΟΥ|ΤΟΥΛΑΧΙΣΤΟ|' + . 'ΤΟΥΛΑΧΙΣΤΟΝ|ΤΟΥΣ|ΤΣ|ΤΣΑ|ΤΣΕ|ΤΥΧΟΝ|ΤΩ|ΤΩΝ|ΤΩΡΑ|ΥΑΣ|ΥΒΑ|ΥΒΟ|ΥΙΕ|ΥΙΟ|ΥΛΑ|ΥΛΗ|ΥΝΙ|ΥΠ|ΥΠΕΡ|ΥΠΟ|ΥΠΟΨΗ|ΥΠΟΨΙΝ|ΥΣΤΕΡΑ|ΥΦΗ|ΥΨΗ|ΦΑ|ΦΑΐ|ΦΑΕ|' + . 'ΦΑΝ|ΦΑΞ|ΦΑΣ|ΦΑΩ|ΦΕΖ|ΦΕΙ|ΦΕΤΟΣ|ΦΕΥ|ΦΙ|ΦΙΛ|ΦΙΣ|ΦΟΞ|ΦΠΑ|ΦΡΙ|ΧΑ|ΧΑΗ|ΧΑΛ|ΧΑΝ|ΧΑΦ|ΧΕ|ΧΕΙ|ΧΘΕΣ|ΧΙ|ΧΙΑ|ΧΙΛ|ΧΙΟ|ΧΛΜ|ΧΜ|ΧΟΗ|ΧΟΛ|ΧΡΩ|ΧΤΕΣ|' + . 'ΧΩΡΙΣ|ΧΩΡΙΣΤΑ|ΨΕΣ|ΨΗΛΑ|ΨΙ|ΨΙΤ|Ω|ΩΑ|ΩΑΣ|ΩΔΕ|ΩΕΣ|ΩΘΩ|ΩΜΑ|ΩΜΕ|ΩΝ|ΩΟ|ΩΟΝ|ΩΟΥ|ΩΣ|ΩΣΑΝ|ΩΣΗ|ΩΣΟΤΟΥ|ΩΣΠΟΥ|ΩΣΤΕ|ΩΣΤΟΣΟ|ΩΤΑ|ΩΧ|ΩΩΝ)$/'; + + if (preg_match($stop_words, $token)) { + return $this->toLowerCase($token, $wCase); + } + + // Vowels + $v = '(Α|Ε|Η|Ι|Ο|Υ|Ω)'; + + // Vowels without Y + $v2 = '(Α|Ε|Η|Ι|Ο|Ω)'; + + $test1 = true; + + // Step S1. 14 stems + $re = '/^(.+?)(ΙΖΑ|ΙΖΕΣ|ΙΖΕ|ΙΖΑΜΕ|ΙΖΑΤΕ|ΙΖΑΝ|ΙΖΑΝΕ|ΙΖΩ|ΙΖΕΙΣ|ΙΖΕΙ|ΙΖΟΥΜΕ|ΙΖΕΤΕ|ΙΖΟΥΝ|ΙΖΟΥΝΕ)$/'; + $exceptS1 = '/^(ΑΝΑΜΠΑ|ΕΜΠΑ|ΕΠΑ|ΞΑΝΑΠΑ|ΠΑ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ)$/'; + $exceptS2 = '/^(ΜΑΡΚ|ΚΟΡΝ|ΑΜΠΑΡ|ΑΡΡ|ΒΑΘΥΡΙ|ΒΑΡΚ|Β|ΒΟΛΒΟΡ|ΓΚΡ|ΓΛΥΚΟΡ|ΓΛΥΚΥΡ|ΙΜΠ|Λ|ΛΟΥ|ΜΑΡ|Μ|ΠΡ|ΜΠΡ|ΠΟΛΥΡ|Π|Ρ|ΠΙΠΕΡΟΡ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'I'; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . 'IΖ'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S2. 7 stems + $re = '/^(.+?)(ΩΘΗΚΑ|ΩΘΗΚΕΣ|ΩΘΗΚΕ|ΩΘΗΚΑΜΕ|ΩΘΗΚΑΤΕ|ΩΘΗΚΑΝ|ΩΘΗΚΑΝΕ)$/'; + $exceptS1 = '/^(ΑΛ|ΒΙ|ΕΝ|ΥΨ|ΛΙ|ΖΩ|Σ|Χ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'ΩΝ'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S3. 7 stems + $re = '/^(.+?)(ΙΣΑ|ΙΣΕΣ|ΙΣΕ|ΙΣΑΜΕ|ΙΣΑΤΕ|ΙΣΑΝ|ΙΣΑΝΕ)$/'; + $exceptS1 = '/^(ΑΝΑΜΠΑ|ΑΘΡΟ|ΕΜΠΑ|ΕΣΕ|ΕΣΩΚΛΕ|ΕΠΑ|ΞΑΝΑΠΑ|ΕΠΕ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ|ΚΛΕ|ΧΑΡΤΟΠΑ|ΕΞΑΡΧΑ|ΜΕΤΕΠΕ|ΑΠΟΚΛΕ|ΑΠΕΚΛΕ|ΕΚΛΕ|ΠΕ|ΠΕΡΙΠΑ)$/'; + $exceptS2 = '/^(ΑΝ|ΑΦ|ΓΕ|ΓΙΓΑΝΤΟΑΦ|ΓΚΕ|ΔΗΜΟΚΡΑΤ|ΚΟΜ|ΓΚ|Μ|Π|ΠΟΥΚΑΜ|ΟΛΟ|ΛΑΡ)$/'; + + if ($token == "ΙΣΑ") { + $token = "ΙΣ"; + + return $token; + } + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'Ι'; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . 'ΙΣ'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S4. 7 stems + $re = '/^(.+?)(ΙΣΩ|ΙΣΕΙΣ|ΙΣΕΙ|ΙΣΟΥΜΕ|ΙΣΕΤΕ|ΙΣΟΥΝ|ΙΣΟΥΝΕ)$/'; + $exceptS1 = '/^(ΑΝΑΜΠΑ|ΕΜΠΑ|ΕΣΕ|ΕΣΩΚΛΕ|ΕΠΑ|ΞΑΝΑΠΑ|ΕΠΕ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ|ΚΛΕ|ΧΑΡΤΟΠΑ|ΕΞΑΡΧΑ|ΜΕΤΕΠΕ|ΑΠΟΚΛΕ|ΑΠΕΚΛΕ|ΕΚΛΕ|ΠΕ|ΠΕΡΙΠΑ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'Ι'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S5. 11 stems + $re = '/^(.+?)(ΙΣΤΟΣ|ΙΣΤΟΥ|ΙΣΤΟ|ΙΣΤΕ|ΙΣΤΟΙ|ΙΣΤΩΝ|ΙΣΤΟΥΣ|ΙΣΤΗ|ΙΣΤΗΣ|ΙΣΤΑ|ΙΣΤΕΣ)$/'; + $exceptS1 = '/^(Μ|Π|ΑΠ|ΑΡ|ΗΔ|ΚΤ|ΣΚ|ΣΧ|ΥΨ|ΦΑ|ΧΡ|ΧΤ|ΑΚΤ|ΑΟΡ|ΑΣΧ|ΑΤΑ|ΑΧΝ|ΑΧΤ|ΓΕΜ|ΓΥΡ|ΕΜΠ|ΕΥΠ|ΕΧΘ|ΗΦΑ|ΚΑΘ|ΚΑΚ|ΚΥΛ|ΛΥΓ|ΜΑΚ|ΜΕΓ|ΤΑΧ|ΦΙΛ|ΧΩΡ)$/'; + $exceptS2 = '/^(ΔΑΝΕ|ΣΥΝΑΘΡΟ|ΚΛΕ|ΣΕ|ΕΣΩΚΛΕ|ΑΣΕ|ΠΛΕ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'ΙΣΤ'; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . 'Ι'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S6. 6 stems + $re = '/^(.+?)(ΙΣΜΟ|ΙΣΜΟΙ|ΙΣΜΟΣ|ΙΣΜΟΥ|ΙΣΜΟΥΣ|ΙΣΜΩΝ)$/'; + $exceptS1 = '/^(ΑΓΝΩΣΤΙΚ|ΑΤΟΜΙΚ|ΓΝΩΣΤΙΚ|ΕΘΝΙΚ|ΕΚΛΕΚΤΙΚ|ΣΚΕΠΤΙΚ|ΤΟΠΙΚ)$/'; + $exceptS2 = '/^(ΣΕ|ΜΕΤΑΣΕ|ΜΙΚΡΟΣΕ|ΕΓΚΛΕ|ΑΠΟΚΛΕ)$/'; + $exceptS3 = '/^(ΔΑΝΕ|ΑΝΤΙΔΑΝΕ)$/'; + $exceptS4 = '/^(ΑΛΕΞΑΝΔΡΙΝ|ΒΥΖΑΝΤΙΝ|ΘΕΑΤΡΙΝ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = str_replace('ΙΚ', "", $token); + } + + if (preg_match($exceptS2, $token)) { + $token = $token . "ΙΣΜ"; + } + + if (preg_match($exceptS3, $token)) { + $token = $token . "Ι"; + } + + if (preg_match($exceptS4, $token)) { + $token = str_replace('ΙΝ', "", $token); + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S7. 4 stems + $re = '/^(.+?)(ΑΡΑΚΙ|ΑΡΑΚΙΑ|ΟΥΔΑΚΙ|ΟΥΔΑΚΙΑ)$/'; + $exceptS1 = '/^(Σ|Χ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . "AΡΑΚ"; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S8. 8 stems + $re = '/^(.+?)(ΑΚΙ|ΑΚΙΑ|ΙΤΣΑ|ΙΤΣΑΣ|ΙΤΣΕΣ|ΙΤΣΩΝ|ΑΡΑΚΙ|ΑΡΑΚΙΑ)$/'; + $exceptS1 = '/^(ΑΝΘΡ|ΒΑΜΒ|ΒΡ|ΚΑΙΜ|ΚΟΝ|ΚΟΡ|ΛΑΒΡ|ΛΟΥΛ|ΜΕΡ|ΜΟΥΣΤ|ΝΑΓΚΑΣ|ΠΛ|Ρ|ΡΥ|Σ|ΣΚ|ΣΟΚ|ΣΠΑΝ|ΤΖ|ΦΑΡΜ|Χ|' + . 'ΚΑΠΑΚ|ΑΛΙΣΦ|ΑΜΒΡ|ΑΝΘΡ|Κ|ΦΥΛ|ΚΑΤΡΑΠ|ΚΛΙΜ|ΜΑΛ|ΣΛΟΒ|Φ|ΣΦ|ΤΣΕΧΟΣΛΟΒ)$/'; + $exceptS2 = '/^(Β|ΒΑΛ|ΓΙΑΝ|ΓΛ|Ζ|ΗΓΟΥΜΕΝ|ΚΑΡΔ|ΚΟΝ|ΜΑΚΡΥΝ|ΝΥΦ|ΠΑΤΕΡ|Π|ΣΚ|ΤΟΣ|ΤΡΙΠΟΛ)$/'; + + // For words like ΠΛΟΥΣΙΟΚΟΡΙΤΣΑ, ΠΑΛΙΟΚΟΡΙΤΣΑ etc + $exceptS3 = '/(ΚΟΡ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . "ΑΚ"; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . "ΙΤΣ"; + } + + if (preg_match($exceptS3, $token)) { + $token = $token . "ΙΤΣ"; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S9. 3 stems + $re = '/^(.+?)(ΙΔΙΟ|ΙΔΙΑ|ΙΔΙΩΝ)$/'; + $exceptS1 = '/^(ΑΙΦΝ|ΙΡ|ΟΛΟ|ΨΑΛ)$/'; + $exceptS2 = '/(Ε|ΠΑΙΧΝ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . "ΙΔ"; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . "ΙΔ"; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S10. 4 stems + $re = '/^(.+?)(ΙΣΚΟΣ|ΙΣΚΟΥ|ΙΣΚΟ|ΙΣΚΕ)$/'; + $exceptS1 = '/^(Δ|ΙΒ|ΜΗΝ|Ρ|ΦΡΑΓΚ|ΛΥΚ|ΟΒΕΛ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . "ΙΣΚ"; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step 1 + // step1list is used in Step 1. 41 stems + $step1list = array(); + $step1list["ΦΑΓΙΑ"] = "ΦΑ"; + $step1list["ΦΑΓΙΟΥ"] = "ΦΑ"; + $step1list["ΦΑΓΙΩΝ"] = "ΦΑ"; + $step1list["ΣΚΑΓΙΑ"] = "ΣΚΑ"; + $step1list["ΣΚΑΓΙΟΥ"] = "ΣΚΑ"; + $step1list["ΣΚΑΓΙΩΝ"] = "ΣΚΑ"; + $step1list["ΟΛΟΓΙΟΥ"] = "ΟΛΟ"; + $step1list["ΟΛΟΓΙΑ"] = "ΟΛΟ"; + $step1list["ΟΛΟΓΙΩΝ"] = "ΟΛΟ"; + $step1list["ΣΟΓΙΟΥ"] = "ΣΟ"; + $step1list["ΣΟΓΙΑ"] = "ΣΟ"; + $step1list["ΣΟΓΙΩΝ"] = "ΣΟ"; + $step1list["ΤΑΤΟΓΙΑ"] = "ΤΑΤΟ"; + $step1list["ΤΑΤΟΓΙΟΥ"] = "ΤΑΤΟ"; + $step1list["ΤΑΤΟΓΙΩΝ"] = "ΤΑΤΟ"; + $step1list["ΚΡΕΑΣ"] = "ΚΡΕ"; + $step1list["ΚΡΕΑΤΟΣ"] = "ΚΡΕ"; + $step1list["ΚΡΕΑΤΑ"] = "ΚΡΕ"; + $step1list["ΚΡΕΑΤΩΝ"] = "ΚΡΕ"; + $step1list["ΠΕΡΑΣ"] = "ΠΕΡ"; + $step1list["ΠΕΡΑΤΟΣ"] = "ΠΕΡ"; + + // Added by Spyros. Also at $re in step1 + $step1list["ΠΕΡΑΤΗ"] = "ΠΕΡ"; + $step1list["ΠΕΡΑΤΑ"] = "ΠΕΡ"; + $step1list["ΠΕΡΑΤΩΝ"] = "ΠΕΡ"; + $step1list["ΤΕΡΑΣ"] = "ΤΕΡ"; + $step1list["ΤΕΡΑΤΟΣ"] = "ΤΕΡ"; + $step1list["ΤΕΡΑΤΑ"] = "ΤΕΡ"; + $step1list["ΤΕΡΑΤΩΝ"] = "ΤΕΡ"; + $step1list["ΦΩΣ"] = "ΦΩ"; + $step1list["ΦΩΤΟΣ"] = "ΦΩ"; + $step1list["ΦΩΤΑ"] = "ΦΩ"; + $step1list["ΦΩΤΩΝ"] = "ΦΩ"; + $step1list["ΚΑΘΕΣΤΩΣ"] = "ΚΑΘΕΣΤ"; + $step1list["ΚΑΘΕΣΤΩΤΟΣ"] = "ΚΑΘΕΣΤ"; + $step1list["ΚΑΘΕΣΤΩΤΑ"] = "ΚΑΘΕΣΤ"; + $step1list["ΚΑΘΕΣΤΩΤΩΝ"] = "ΚΑΘΕΣΤ"; + $step1list["ΓΕΓΟΝΟΣ"] = "ΓΕΓΟΝ"; + $step1list["ΓΕΓΟΝΟΤΟΣ"] = "ΓΕΓΟΝ"; + $step1list["ΓΕΓΟΝΟΤΑ"] = "ΓΕΓΟΝ"; + $step1list["ΓΕΓΟΝΟΤΩΝ"] = "ΓΕΓΟΝ"; + + $re = '/(.*)(ΦΑΓΙΑ|ΦΑΓΙΟΥ|ΦΑΓΙΩΝ|ΣΚΑΓΙΑ|ΣΚΑΓΙΟΥ|ΣΚΑΓΙΩΝ|ΟΛΟΓΙΟΥ|ΟΛΟΓΙΑ|ΟΛΟΓΙΩΝ|ΣΟΓΙΟΥ|ΣΟΓΙΑ|ΣΟΓΙΩΝ|ΤΑΤΟΓΙΑ|ΤΑΤΟΓΙΟΥ|ΤΑΤΟΓΙΩΝ|ΚΡΕΑΣ|ΚΡΕΑΤΟΣ|' + . 'ΚΡΕΑΤΑ|ΚΡΕΑΤΩΝ|ΠΕΡΑΣ|ΠΕΡΑΤΟΣ|ΠΕΡΑΤΗ|ΠΕΡΑΤΑ|ΠΕΡΑΤΩΝ|ΤΕΡΑΣ|ΤΕΡΑΤΟΣ|ΤΕΡΑΤΑ|ΤΕΡΑΤΩΝ|ΦΩΣ|ΦΩΤΟΣ|ΦΩΤΑ|ΦΩΤΩΝ|ΚΑΘΕΣΤΩΣ|ΚΑΘΕΣΤΩΤΟΣ|' + . 'ΚΑΘΕΣΤΩΤΑ|ΚΑΘΕΣΤΩΤΩΝ|ΓΕΓΟΝΟΣ|ΓΕΓΟΝΟΤΟΣ|ΓΕΓΟΝΟΤΑ|ΓΕΓΟΝΟΤΩΝ)$/'; + + if (preg_match($re, $token, $match)) { + $stem = $match[1]; + $suffix = $match[2]; + $token = $stem . (array_key_exists($suffix, $step1list) ? $step1list[$suffix] : ''); + $test1 = false; + } + + // Step 2a. 2 stems + $re = '/^(.+?)(ΑΔΕΣ|ΑΔΩΝ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + $re = '/(ΟΚ|ΜΑΜ|ΜΑΝ|ΜΠΑΜΠ|ΠΑΤΕΡ|ΓΙΑΓΙ|ΝΤΑΝΤ|ΚΥΡ|ΘΕΙ|ΠΕΘΕΡ)$/'; + + if (!preg_match($re, $token)) { + $token = $token . "ΑΔ"; + } + } + + // Step 2b. 2 stems + $re = '/^(.+?)(ΕΔΕΣ|ΕΔΩΝ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $exept2 = '/(ΟΠ|ΙΠ|ΕΜΠ|ΥΠ|ΓΗΠ|ΔΑΠ|ΚΡΑΣΠ|ΜΙΛ)$/'; + + if (preg_match($exept2, $token)) { + $token = $token . 'ΕΔ'; + } + } + + // Step 2c + $re = '/^(.+?)(ΟΥΔΕΣ|ΟΥΔΩΝ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + + $exept3 = '/(ΑΡΚ|ΚΑΛΙΑΚ|ΠΕΤΑΛ|ΛΙΧ|ΠΛΕΞ|ΣΚ|Σ|ΦΛ|ΦΡ|ΒΕΛ|ΛΟΥΛ|ΧΝ|ΣΠ|ΤΡΑΓ|ΦΕ)$/'; + + if (preg_match($exept3, $token)) { + $token = $token . 'ΟΥΔ'; + } + } + + // Step 2d + $re = '/^(.+?)(ΕΩΣ|ΕΩΝ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept4 = '/^(Θ|Δ|ΕΛ|ΓΑΛ|Ν|Π|ΙΔ|ΠΑΡ)$/'; + + if (preg_match($exept4, $token)) { + $token = $token . 'Ε'; + } + } + + // Step 3 + $re = '/^(.+?)(ΙΑ|ΙΟΥ|ΙΩΝ)$/'; + + if (preg_match($re, $token, $fp)) { + $stem = $fp[1]; + $token = $stem; + $re = '/' . $v . '$/'; + $test1 = false; + + if (preg_match($re, $token)) { + $token = $stem . 'Ι'; + } + } + + // Step 4 + $re = '/^(.+?)(ΙΚΑ|ΙΚΟ|ΙΚΟΥ|ΙΚΩΝ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $re = '/' . $v . '$/'; + $exept5 = '/^(ΑΛ|ΑΔ|ΕΝΔ|ΑΜΑΝ|ΑΜΜΟΧΑΛ|ΗΘ|ΑΝΗΘ|ΑΝΤΙΔ|ΦΥΣ|ΒΡΩΜ|ΓΕΡ|ΕΞΩΔ|ΚΑΛΠ|ΚΑΛΛΙΝ|ΚΑΤΑΔ|ΜΟΥΛ|ΜΠΑΝ|ΜΠΑΓΙΑΤ|ΜΠΟΛ|ΜΠΟΣ|ΝΙΤ|ΞΙΚ|ΣΥΝΟΜΗΛ|ΠΕΤΣ|' + . 'ΠΙΤΣ|ΠΙΚΑΝΤ|ΠΛΙΑΤΣ|ΠΟΣΤΕΛΝ|ΠΡΩΤΟΔ|ΣΕΡΤ|ΣΥΝΑΔ|ΤΣΑΜ|ΥΠΟΔ|ΦΙΛΟΝ|ΦΥΛΟΔ|ΧΑΣ)$/'; + + if (preg_match($re, $token) || preg_match($exept5, $token)) { + $token = $token . 'ΙΚ'; + } + } + + // Step 5a + $re = '/^(.+?)(ΑΜΕ)$/'; + $re2 = '/^(.+?)(ΑΓΑΜΕ|ΗΣΑΜΕ|ΟΥΣΑΜΕ|ΗΚΑΜΕ|ΗΘΗΚΑΜΕ)$/'; + + if ($token == "ΑΓΑΜΕ") { + $token = "ΑΓΑΜ"; + } + + if (preg_match($re2, $token)) { + preg_match($re2, $token, $match); + $token = $match[1]; + $test1 = false; + } + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept6 = '/^(ΑΝΑΠ|ΑΠΟΘ|ΑΠΟΚ|ΑΠΟΣΤ|ΒΟΥΒ|ΞΕΘ|ΟΥΛ|ΠΕΘ|ΠΙΚΡ|ΠΟΤ|ΣΙΧ|Χ)$/'; + + if (preg_match($exept6, $token)) { + $token = $token . "ΑΜ"; + } + } + + // Step 5b + $re2 = '/^(.+?)(ΑΝΕ)$/'; + $re3 = '/^(.+?)(ΑΓΑΝΕ|ΗΣΑΝΕ|ΟΥΣΑΝΕ|ΙΟΝΤΑΝΕ|ΙΟΤΑΝΕ|ΙΟΥΝΤΑΝΕ|ΟΝΤΑΝΕ|ΟΤΑΝΕ|ΟΥΝΤΑΝΕ|ΗΚΑΝΕ|ΗΘΗΚΑΝΕ)$/'; + + if (preg_match($re3, $token)) { + preg_match($re3, $token, $match); + $token = $match[1]; + $test1 = false; + $re3 = '/^(ΤΡ|ΤΣ)$/'; + + if (preg_match($re3, $token)) { + $token = $token . "ΑΓΑΝ"; + } + } + + if (preg_match($re2, $token)) { + preg_match($re2, $token, $match); + $token = $match[1]; + $test1 = false; + $re2 = '/' . $v2 . '$/'; + $exept7 = '/^(ΒΕΤΕΡ|ΒΟΥΛΚ|ΒΡΑΧΜ|Γ|ΔΡΑΔΟΥΜ|Θ|ΚΑΛΠΟΥΖ|ΚΑΣΤΕΛ|ΚΟΡΜΟΡ|ΛΑΟΠΛ|ΜΩΑΜΕΘ|Μ|ΜΟΥΣΟΥΛΜ|Ν|ΟΥΛ|Π|ΠΕΛΕΚ|ΠΛ|ΠΟΛΙΣ|ΠΟΡΤΟΛ|ΣΑΡΑΚΑΤΣ|ΣΟΥΛΤ|' + . 'ΤΣΑΡΛΑΤ|ΟΡΦ|ΤΣΙΓΓ|ΤΣΟΠ|ΦΩΤΟΣΤΕΦ|Χ|ΨΥΧΟΠΛ|ΑΓ|ΟΡΦ|ΓΑΛ|ΓΕΡ|ΔΕΚ|ΔΙΠΛ|ΑΜΕΡΙΚΑΝ|ΟΥΡ|ΠΙΘ|ΠΟΥΡΙΤ|Σ|ΖΩΝΤ|ΙΚ|ΚΑΣΤ|ΚΟΠ|ΛΙΧ|ΛΟΥΘΗΡ|ΜΑΙΝΤ|' + . 'ΜΕΛ|ΣΙΓ|ΣΠ|ΣΤΕΓ|ΤΡΑΓ|ΤΣΑΓ|Φ|ΕΡ|ΑΔΑΠ|ΑΘΙΓΓ|ΑΜΗΧ|ΑΝΙΚ|ΑΝΟΡΓ|ΑΠΗΓ|ΑΠΙΘ|ΑΤΣΙΓΓ|ΒΑΣ|ΒΑΣΚ|ΒΑΘΥΓΑΛ|ΒΙΟΜΗΧ|ΒΡΑΧΥΚ|ΔΙΑΤ|ΔΙΑΦ|ΕΝΟΡΓ|' + . 'ΘΥΣ|ΚΑΠΝΟΒΙΟΜΗΧ|ΚΑΤΑΓΑΛ|ΚΛΙΒ|ΚΟΙΛΑΡΦ|ΛΙΒ|ΜΕΓΛΟΒΙΟΜΗΧ|ΜΙΚΡΟΒΙΟΜΗΧ|ΝΤΑΒ|ΞΗΡΟΚΛΙΒ|ΟΛΙΓΟΔΑΜ|ΟΛΟΓΑΛ|ΠΕΝΤΑΡΦ|ΠΕΡΗΦ|ΠΕΡΙΤΡ|ΠΛΑΤ|' + . 'ΠΟΛΥΔΑΠ|ΠΟΛΥΜΗΧ|ΣΤΕΦ|ΤΑΒ|ΤΕΤ|ΥΠΕΡΗΦ|ΥΠΟΚΟΠ|ΧΑΜΗΛΟΔΑΠ|ΨΗΛΟΤΑΒ)$/'; + + if (preg_match($re2, $token) || preg_match($exept7, $token)) { + $token = $token . "ΑΝ"; + } + } + + // Step 5c + $re3 = '/^(.+?)(ΕΤΕ)$/'; + $re4 = '/^(.+?)(ΗΣΕΤΕ)$/'; + + if (preg_match($re4, $token)) { + preg_match($re4, $token, $match); + $token = $match[1]; + $test1 = false; + } + + if (preg_match($re3, $token)) { + preg_match($re3, $token, $match); + $token = $match[1]; + $test1 = false; + $re3 = '/' . $v2 . '$/'; + $exept8 = '/(ΟΔ|ΑΙΡ|ΦΟΡ|ΤΑΘ|ΔΙΑΘ|ΣΧ|ΕΝΔ|ΕΥΡ|ΤΙΘ|ΥΠΕΡΘ|ΡΑΘ|ΕΝΘ|ΡΟΘ|ΣΘ|ΠΥΡ|ΑΙΝ|ΣΥΝΔ|ΣΥΝ|ΣΥΝΘ|ΧΩΡ|ΠΟΝ|ΒΡ|ΚΑΘ|ΕΥΘ|ΕΚΘ|ΝΕΤ|ΡΟΝ|ΑΡΚ|ΒΑΡ|ΒΟΛ|ΩΦΕΛ)$/'; + $exept9 = '/^(ΑΒΑΡ|ΒΕΝ|ΕΝΑΡ|ΑΒΡ|ΑΔ|ΑΘ|ΑΝ|ΑΠΛ|ΒΑΡΟΝ|ΝΤΡ|ΣΚ|ΚΟΠ|ΜΠΟΡ|ΝΙΦ|ΠΑΓ|ΠΑΡΑΚΑΛ|ΣΕΡΠ|ΣΚΕΛ|ΣΥΡΦ|ΤΟΚ|Υ|Δ|ΕΜ|ΘΑΡΡ|Θ)$/'; + + if (preg_match($re3, $token) || preg_match($exept8, $token) || preg_match($exept9, $token)) { + $token = $token . "ΕΤ"; + } + } + + // Step 5d + $re = '/^(.+?)(ΟΝΤΑΣ|ΩΝΤΑΣ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept10 = '/^(ΑΡΧ)$/'; + $exept11 = '/(ΚΡΕ)$/'; + + if (preg_match($exept10, $token)) { + $token = $token . "ΟΝΤ"; + } + + if (preg_match($exept11, $token)) { + $token = $token . "ΩΝΤ"; + } + } + + // Step 5e + $re = '/^(.+?)(ΟΜΑΣΤΕ|ΙΟΜΑΣΤΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept11 = '/^(ΟΝ)$/'; + + if (preg_match($exept11, $token)) { + $token = $token . "ΟΜΑΣΤ"; + } + } + + // Step 5f + $re = '/^(.+?)(ΕΣΤΕ)$/'; + $re2 = '/^(.+?)(ΙΕΣΤΕ)$/'; + + if (preg_match($re2, $token)) { + preg_match($re2, $token, $match); + $token = $match[1]; + $test1 = false; + $re2 = '/^(Π|ΑΠ|ΣΥΜΠ|ΑΣΥΜΠ|ΑΚΑΤΑΠ|ΑΜΕΤΑΜΦ)$/'; + + if (preg_match($re2, $token)) { + $token = $token . "ΙΕΣΤ"; + } + } + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept12 = '/^(ΑΛ|ΑΡ|ΕΚΤΕΛ|Ζ|Μ|Ξ|ΠΑΡΑΚΑΛ|ΑΡ|ΠΡΟ|ΝΙΣ)$/'; + + if (preg_match($exept12, $token)) { + $token = $token . "ΕΣΤ"; + } + } + + // Step 5g + $re = '/^(.+?)(ΗΚΑ|ΗΚΕΣ|ΗΚΕ)$/'; + $re2 = '/^(.+?)(ΗΘΗΚΑ|ΗΘΗΚΕΣ|ΗΘΗΚΕ)$/'; + + if (preg_match($re2, $token)) { + preg_match($re2, $token, $match); + $token = $match[1]; + $test1 = false; + } + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept13 = '/(ΣΚΩΛ|ΣΚΟΥΛ|ΝΑΡΘ|ΣΦ|ΟΘ|ΠΙΘ)$/'; + $exept14 = '/^(ΔΙΑΘ|Θ|ΠΑΡΑΚΑΤΑΘ|ΠΡΟΣΘ|ΣΥΝΘ|)$/'; + + if (preg_match($exept13, $token) || preg_match($exept14, $token)) { + $token = $token . "ΗΚ"; + } + } + + // Step 5h + $re = '/^(.+?)(ΟΥΣΑ|ΟΥΣΕΣ|ΟΥΣΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept15 = '/^(ΦΑΡΜΑΚ|ΧΑΔ|ΑΓΚ|ΑΝΑΡΡ|ΒΡΟΜ|ΕΚΛΙΠ|ΛΑΜΠΙΔ|ΛΕΧ|Μ|ΠΑΤ|Ρ|Λ|ΜΕΔ|ΜΕΣΑΖ|ΥΠΟΤΕΙΝ|ΑΜ|ΑΙΘ|ΑΝΗΚ|ΔΕΣΠΟΖ|ΕΝΔΙΑΦΕΡ|ΔΕ|ΔΕΥΤΕΡΕΥ|ΚΑΘΑΡΕΥ|ΠΛΕ|ΤΣΑ)$/'; + $exept16 = '/(ΠΟΔΑΡ|ΒΛΕΠ|ΠΑΝΤΑΧ|ΦΡΥΔ|ΜΑΝΤΙΛ|ΜΑΛΛ|ΚΥΜΑΤ|ΛΑΧ|ΛΗΓ|ΦΑΓ|ΟΜ|ΠΡΩΤ)$/'; + + if (preg_match($exept15, $token) || preg_match($exept16, $token)) { + $token = $token . "ΟΥΣ"; + } + } + + // Step 5i + $re = '/^(.+?)(ΑΓΑ|ΑΓΕΣ|ΑΓΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept17 = '/^(ΨΟΦ|ΝΑΥΛΟΧ)$/'; + $exept20 = '/(ΚΟΛΛ)$/'; + $exept18 = '/^(ΑΒΑΣΤ|ΠΟΛΥΦ|ΑΔΗΦ|ΠΑΜΦ|Ρ|ΑΣΠ|ΑΦ|ΑΜΑΛ|ΑΜΑΛΛΙ|ΑΝΥΣΤ|ΑΠΕΡ|ΑΣΠΑΡ|ΑΧΑΡ|ΔΕΡΒΕΝ|ΔΡΟΣΟΠ|ΞΕΦ|ΝΕΟΠ|ΝΟΜΟΤ|ΟΛΟΠ|ΟΜΟΤ|ΠΡΟΣΤ|ΠΡΟΣΩΠΟΠ|' + . 'ΣΥΜΠ|ΣΥΝΤ|Τ|ΥΠΟΤ|ΧΑΡ|ΑΕΙΠ|ΑΙΜΟΣΤ|ΑΝΥΠ|ΑΠΟΤ|ΑΡΤΙΠ|ΔΙΑΤ|ΕΝ|ΕΠΙΤ|ΚΡΟΚΑΛΟΠ|ΣΙΔΗΡΟΠ|Λ|ΝΑΥ|ΟΥΛΑΜ|ΟΥΡ|Π|ΤΡ|Μ)$/'; + $exept19 = '/(ΟΦ|ΠΕΛ|ΧΟΡΤ|ΛΛ|ΣΦ|ΡΠ|ΦΡ|ΠΡ|ΛΟΧ|ΣΜΗΝ)$/'; + + if ( + (preg_match($exept18, $token) || preg_match($exept19, $token)) + && !(preg_match($exept17, $token) || preg_match($exept20, $token)) + ) { + $token = $token . "ΑΓ"; + } + } + + // Step 5j + $re = '/^(.+?)(ΗΣΕ|ΗΣΟΥ|ΗΣΑ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept21 = '/^(Ν|ΧΕΡΣΟΝ|ΔΩΔΕΚΑΝ|ΕΡΗΜΟΝ|ΜΕΓΑΛΟΝ|ΕΠΤΑΝ)$/'; + + if (preg_match($exept21, $token)) { + $token = $token . "ΗΣ"; + } + } + + // Step 5k + $re = '/^(.+?)(ΗΣΤΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept22 = '/^(ΑΣΒ|ΣΒ|ΑΧΡ|ΧΡ|ΑΠΛ|ΑΕΙΜΝ|ΔΥΣΧΡ|ΕΥΧΡ|ΚΟΙΝΟΧΡ|ΠΑΛΙΜΨ)$/'; + + if (preg_match($exept22, $token)) { + $token = $token . "ΗΣΤ"; + } + } + + // Step 5l + $re = '/^(.+?)(ΟΥΝΕ|ΗΣΟΥΝΕ|ΗΘΟΥΝΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept23 = '/^(Ν|Ρ|ΣΠΙ|ΣΤΡΑΒΟΜΟΥΤΣ|ΚΑΚΟΜΟΥΤΣ|ΕΞΩΝ)$/'; + + if (preg_match($exept23, $token)) { + $token = $token . "ΟΥΝ"; + } + } + + // Step 5m + $re = '/^(.+?)(ΟΥΜΕ|ΗΣΟΥΜΕ|ΗΘΟΥΜΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept24 = '/^(ΠΑΡΑΣΟΥΣ|Φ|Χ|ΩΡΙΟΠΛ|ΑΖ|ΑΛΛΟΣΟΥΣ|ΑΣΟΥΣ)$/'; + + if (preg_match($exept24, $token)) { + $token = $token . "ΟΥΜ"; + } + } + + // Step 6 + $re = '/^(.+?)(ΜΑΤΑ|ΜΑΤΩΝ|ΜΑΤΟΣ)$/'; + $re2 = '/^(.+?)(Α|ΑΓΑΤΕ|ΑΓΑΝ|ΑΕΙ|ΑΜΑΙ|ΑΝ|ΑΣ|ΑΣΑΙ|ΑΤΑΙ|ΑΩ|Ε|ΕΙ|ΕΙΣ|ΕΙΤΕ|ΕΣΑΙ|ΕΣ|ΕΤΑΙ|Ι|ΙΕΜΑΙ|ΙΕΜΑΣΤΕ|ΙΕΤΑΙ|ΙΕΣΑΙ|ΙΕΣΑΣΤΕ|ΙΟΜΑΣΤΑΝ|ΙΟΜΟΥΝ|' + . 'ΙΟΜΟΥΝΑ|ΙΟΝΤΑΝ|ΙΟΝΤΟΥΣΑΝ|ΙΟΣΑΣΤΑΝ|ΙΟΣΑΣΤΕ|ΙΟΣΟΥΝ|ΙΟΣΟΥΝΑ|ΙΟΤΑΝ|ΙΟΥΜΑ|ΙΟΥΜΑΣΤΕ|ΙΟΥΝΤΑΙ|ΙΟΥΝΤΑΝ|Η|ΗΔΕΣ|ΗΔΩΝ|ΗΘΕΙ|ΗΘΕΙΣ|ΗΘΕΙΤΕ|' + . 'ΗΘΗΚΑΤΕ|ΗΘΗΚΑΝ|ΗΘΟΥΝ|ΗΘΩ|ΗΚΑΤΕ|ΗΚΑΝ|ΗΣ|ΗΣΑΝ|ΗΣΑΤΕ|ΗΣΕΙ|ΗΣΕΣ|ΗΣΟΥΝ|ΗΣΩ|Ο|ΟΙ|ΟΜΑΙ|ΟΜΑΣΤΑΝ|ΟΜΟΥΝ|ΟΜΟΥΝΑ|ΟΝΤΑΙ|ΟΝΤΑΝ|ΟΝΤΟΥΣΑΝ|ΟΣ|' + . 'ΟΣΑΣΤΑΝ|ΟΣΑΣΤΕ|ΟΣΟΥΝ|ΟΣΟΥΝΑ|ΟΤΑΝ|ΟΥ|ΟΥΜΑΙ|ΟΥΜΑΣΤΕ|ΟΥΝ|ΟΥΝΤΑΙ|ΟΥΝΤΑΝ|ΟΥΣ|ΟΥΣΑΝ|ΟΥΣΑΤΕ|Υ|ΥΣ|Ω|ΩΝ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1] . "ΜΑ"; + } + + if (preg_match($re2, $token) && $test1) { + preg_match($re2, $token, $match); + $token = $match[1]; + } + + // Step 7 (ΠΑΡΑΘΕΤΙΚΑ) + $re = '/^(.+?)(ΕΣΤΕΡ|ΕΣΤΑΤ|ΟΤΕΡ|ΟΤΑΤ|ΥΤΕΡ|ΥΤΑΤ|ΩΤΕΡ|ΩΤΑΤ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + } + + return $this->toLowerCase($token, $wCase); + } + + /** + * Converts the token to uppercase, suppressing accents and diaeresis. The array $wCase contains a special map of + * the uppercase rule used to convert each character at each position. + * + * @param string $token Token to process + * @param array &$wCase Map of uppercase rules + * + * @return string + * + * @since 4.0.0 + */ + protected function toUpperCase($token, &$wCase) + { + $wCase = array_fill(0, mb_strlen($token, 'UTF-8'), 0); + $caseConvert = array( + "α" => 'Α', + "β" => 'Β', + "γ" => 'Γ', + "δ" => 'Δ', + "ε" => 'Ε', + "ζ" => 'Ζ', + "η" => 'Η', + "θ" => 'Θ', + "ι" => 'Ι', + "κ" => 'Κ', + "λ" => 'Λ', + "μ" => 'Μ', + "ν" => 'Ν', + "ξ" => 'Ξ', + "ο" => 'Ο', + "π" => 'Π', + "ρ" => 'Ρ', + "σ" => 'Σ', + "τ" => 'Τ', + "υ" => 'Υ', + "φ" => 'Φ', + "χ" => 'Χ', + "ψ" => 'Ψ', + "ω" => 'Ω', + "ά" => 'Α', + "έ" => 'Ε', + "ή" => 'Η', + "ί" => 'Ι', + "ό" => 'Ο', + "ύ" => 'Υ', + "ώ" => 'Ω', + "ς" => 'Σ', + "ϊ" => 'Ι', + "ϋ" => 'Ι', + "ΐ" => 'Ι', + "ΰ" => 'Υ', + ); + $newToken = ''; + + for ($i = 0; $i < mb_strlen($token); $i++) { + $char = mb_substr($token, $i, 1); + $isLower = array_key_exists($char, $caseConvert); + + if (!$isLower) { + $newToken .= $char; + + continue; + } + + $upperCase = $caseConvert[$char]; + $newToken .= $upperCase; + + $wCase[$i] = 1; + + if (in_array($char, ['ά', 'έ', 'ή', 'ί', 'ό', 'ύ', 'ώ', 'ς'])) { + $wCase[$i] = 2; + } + + if (in_array($char, ['ϊ', 'ϋ'])) { + $wCase[$i] = 3; + } + + if (in_array($char, ['ΐ', 'ΰ'])) { + $wCase[$i] = 4; + } + } + + return $newToken; + } + + /** + * Converts the suppressed uppercase token back to lowercase, using the $wCase map to add back the accents, + * diaeresis and handle the special case of final sigma (different lowercase glyph than the regular sigma, only + * used at the end of words). + * + * @param string $token Token to process + * @param array $wCase Map of lowercase rules + * + * @return string + * + * @since 4.0.0 + */ + protected function toLowerCase($token, $wCase) + { + $newToken = ''; + + for ($i = 0; $i < mb_strlen($token); $i++) { + $char = mb_substr($token, $i, 1); + + // Is $wCase not set at this position? We assume no case conversion ever took place. + if (!isset($wCase[$i])) { + $newToken .= $char; + + continue; + } + + // The character was not case-converted + if ($wCase[$i] == 0) { + $newToken .= $char; + + continue; + } + + // Case 1: Unaccented letter + if ($wCase[$i] == 1) { + $newToken .= mb_strtolower($char); + + continue; + } + + // Case 2: Vowel with accent (tonos); or the special case of final sigma + if ($wCase[$i] == 2) { + $charMap = [ + 'Α' => 'ά', + 'Ε' => 'έ', + 'Η' => 'ή', + 'Ι' => 'ί', + 'Ο' => 'ό', + 'Υ' => 'ύ', + 'Ω' => 'ώ', + 'Σ' => 'ς' + ]; + + $newToken .= $charMap[$char]; + + continue; + } + + // Case 3: vowels with diaeresis (dialytika) + if ($wCase[$i] == 3) { + $charMap = [ + 'Ι' => 'ϊ', + 'Υ' => 'ϋ' + ]; + + $newToken .= $charMap[$char]; + + continue; + } + + // Case 4: vowels with both diaeresis (dialytika) and accent (tonos) + if ($wCase[$i] == 4) { + $charMap = [ + 'Ι' => 'ΐ', + 'Υ' => 'ΰ' + ]; + + $newToken .= $charMap[$char]; + + continue; + } + + // This should never happen! + $newToken .= $char; + } + + return $newToken; + } } diff --git a/code/administrator/components/com_finder/src/Indexer/Language/Zh.php b/code/administrator/components/com_finder/src/Indexer/Language/Zh.php index 7e5bcbfa..5a230d8f 100644 --- a/code/administrator/components/com_finder/src/Indexer/Language/Zh.php +++ b/code/administrator/components/com_finder/src/Indexer/Language/Zh.php @@ -1,4 +1,5 @@ clean($format, 'cmd'); - - // Only create one parser for each format. - if (isset(self::$instances[$format])) - { - return self::$instances[$format]; - } - - // Setup the adapter for the parser. - $class = '\\Joomla\\Component\\Finder\\Administrator\\Indexer\\Parser\\' . ucfirst($format); - - // Check if a parser exists for the format. - if (class_exists($class)) - { - self::$instances[$format] = new $class; - - return self::$instances[$format]; - } - - // Throw invalid format exception. - throw new \Exception(Text::sprintf('COM_FINDER_INDEXER_INVALID_PARSER', $format)); - } - - /** - * Method to parse input and extract the plain text. Because this method is - * called from both inside and outside the indexer, it needs to be able to - * batch out its parsing functionality to deal with the inefficiencies of - * regular expressions. We will parse recursively in 2KB chunks. - * - * @param string $input The input to parse. - * - * @return string The plain text input. - * - * @since 2.5 - */ - public function parse($input) - { - // If the input is less than 2KB we can parse it in one go. - if (strlen($input) <= 2048) - { - return $this->process($input); - } - - // Input is longer than 2Kb so parse it in chunks of 2Kb or less. - $start = 0; - $end = strlen($input); - $chunk = 2048; - $return = null; - - while ($start < $end) - { - // Setup the string. - $string = substr($input, $start, $chunk); - - // Find the last space character if we aren't at the end. - $ls = (($start + $chunk) < $end ? strrpos($string, ' ') : false); - - // Truncate to the last space character. - if ($ls !== false) - { - $string = substr($string, 0, $ls); - } - - // Adjust the start position for the next iteration. - $start += ($ls !== false ? ($ls + 1 - $chunk) + $chunk : $chunk); - - // Parse the chunk. - $return .= $this->process($string); - } - - return $return; - } - - /** - * Method to process input and extract the plain text. - * - * @param string $input The input to process. - * - * @return string The plain text input. - * - * @since 2.5 - */ - abstract protected function process($input); + /** + * Parser support instances container. + * + * @var Parser[] + * @since 4.0.0 + */ + protected static $instances = array(); + + /** + * Method to get a parser, creating it if necessary. + * + * @param string $format The type of parser to load. + * + * @return Parser A Parser instance. + * + * @since 2.5 + * @throws \Exception on invalid parser. + */ + public static function getInstance($format) + { + $format = InputFilter::getInstance()->clean($format, 'cmd'); + + // Only create one parser for each format. + if (isset(self::$instances[$format])) { + return self::$instances[$format]; + } + + // Setup the adapter for the parser. + $class = '\\Joomla\\Component\\Finder\\Administrator\\Indexer\\Parser\\' . ucfirst($format); + + // Check if a parser exists for the format. + if (class_exists($class)) { + self::$instances[$format] = new $class(); + + return self::$instances[$format]; + } + + // Throw invalid format exception. + throw new \Exception(Text::sprintf('COM_FINDER_INDEXER_INVALID_PARSER', $format)); + } + + /** + * Method to parse input and extract the plain text. Because this method is + * called from both inside and outside the indexer, it needs to be able to + * batch out its parsing functionality to deal with the inefficiencies of + * regular expressions. We will parse recursively in 2KB chunks. + * + * @param string $input The input to parse. + * + * @return string The plain text input. + * + * @since 2.5 + */ + public function parse($input) + { + // If the input is less than 2KB we can parse it in one go. + if (strlen($input) <= 2048) { + return $this->process($input); + } + + // Input is longer than 2Kb so parse it in chunks of 2Kb or less. + $start = 0; + $end = strlen($input); + $chunk = 2048; + $return = null; + + while ($start < $end) { + // Setup the string. + $string = substr($input, $start, $chunk); + + // Find the last space character if we aren't at the end. + $ls = (($start + $chunk) < $end ? strrpos($string, ' ') : false); + + // Truncate to the last space character. + if ($ls !== false) { + $string = substr($string, 0, $ls); + } + + // Adjust the start position for the next iteration. + $start += ($ls !== false ? ($ls + 1 - $chunk) + $chunk : $chunk); + + // Parse the chunk. + $return .= $this->process($string); + } + + return $return; + } + + /** + * Method to process input and extract the plain text. + * + * @param string $input The input to process. + * + * @return string The plain text input. + * + * @since 2.5 + */ + abstract protected function process($input); } diff --git a/code/administrator/components/com_finder/src/Indexer/Parser/Html.php b/code/administrator/components/com_finder/src/Indexer/Parser/Html.php index 2c592810..c5429de3 100644 --- a/code/administrator/components/com_finder/src/Indexer/Parser/Html.php +++ b/code/administrator/components/com_finder/src/Indexer/Parser/Html.php @@ -1,4 +1,5 @@ and tags. Do this first - // because there might be '); - - // Decode HTML entities. - $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8'); - - // Convert entities equivalent to spaces to actual spaces. - $input = str_replace(array(' ', ' '), ' ', $input); - - // Add a space before both the OPEN and CLOSE tags of BLOCK and LINE BREAKING elements, - // e.g. 'all

mobile List

' will become 'all mobile List' - $input = preg_replace('/(<|<\/)(' . - 'address|article|aside|blockquote|br|canvas|dd|div|dl|dt|' . - 'fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|li|' . - 'main|nav|noscript|ol|output|p|pre|section|table|tfoot|ul|video' . - ')\b/i', ' $1$2', $input - ); - - // Strip HTML tags. - $input = strip_tags($input); - - return parent::parse($input); - } - - /** - * Method to process HTML input and extract the plain text. - * - * @param string $input The input to process. - * - * @return string The plain text input. - * - * @since 2.5 - */ - protected function process($input) - { - // Replace any amount of white space with a single space. - return preg_replace('#\s+#u', ' ', $input); - } - - /** - * Method to remove blocks of text between a start and an end tag. - * Each block removed is effectively replaced by a single space. - * - * Note: The start tag and the end tag must be different. - * Note: Blocks must not be nested. - * Note: This method will function correctly with multi-byte strings. - * - * @param string $input String to be processed. - * @param string $startTag String representing the start tag. - * @param string $endTag String representing the end tag. - * - * @return string with blocks removed. - * - * @since 3.4 - */ - private function removeBlocks($input, $startTag, $endTag) - { - $return = ''; - $offset = 0; - $startTagLength = strlen($startTag); - $endTagLength = strlen($endTag); - - // Find the first start tag. - $start = stripos($input, $startTag); - - // If no start tags were found, return the string unchanged. - if ($start === false) - { - return $input; - } - - // Look for all blocks defined by the start and end tags. - while ($start !== false) - { - // Accumulate the substring up to the start tag. - $return .= substr($input, $offset, $start - $offset) . ' '; - - // Look for an end tag corresponding to the start tag. - $end = stripos($input, $endTag, $start + $startTagLength); - - // If no corresponding end tag, leave the string alone. - if ($end === false) - { - // Fix the offset so part of the string is not duplicated. - $offset = $start; - break; - } - - // Advance the start position. - $offset = $end + $endTagLength; - - // Look for the next start tag and loop. - $start = stripos($input, $startTag, $offset); - } - - // Add in the final substring after the last end tag. - $return .= substr($input, $offset); - - return $return; - } + /** + * Method to parse input and extract the plain text. Because this method is + * called from both inside and outside the indexer, it needs to be able to + * batch out its parsing functionality to deal with the inefficiencies of + * regular expressions. We will parse recursively in 2KB chunks. + * + * @param string $input The input to parse. + * + * @return string The plain text input. + * + * @since 2.5 + */ + public function parse($input) + { + // Strip invalid UTF-8 characters. + $oldSetting = ini_get('mbstring.substitute_character'); + ini_set('mbstring.substitute_character', 'none'); + $input = mb_convert_encoding($input, 'UTF-8', 'UTF-8'); + ini_set('mbstring.substitute_character', $oldSetting); + + // Remove anything between and tags. Do this first + // because there might be '); + + // Decode HTML entities. + $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8'); + + // Convert entities equivalent to spaces to actual spaces. + $input = str_replace(array(' ', ' '), ' ', $input); + + // Add a space before both the OPEN and CLOSE tags of BLOCK and LINE BREAKING elements, + // e.g. 'all

mobile List

' will become 'all mobile List' + $input = preg_replace('/(<|<\/)(' . + 'address|article|aside|blockquote|br|canvas|dd|div|dl|dt|' . + 'fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|li|' . + 'main|nav|noscript|ol|output|p|pre|section|table|tfoot|ul|video' . + ')\b/i', ' $1$2', $input); + + // Strip HTML tags. + $input = strip_tags($input); + + return parent::parse($input); + } + + /** + * Method to process HTML input and extract the plain text. + * + * @param string $input The input to process. + * + * @return string The plain text input. + * + * @since 2.5 + */ + protected function process($input) + { + // Replace any amount of white space with a single space. + return preg_replace('#\s+#u', ' ', $input); + } + + /** + * Method to remove blocks of text between a start and an end tag. + * Each block removed is effectively replaced by a single space. + * + * Note: The start tag and the end tag must be different. + * Note: Blocks must not be nested. + * Note: This method will function correctly with multi-byte strings. + * + * @param string $input String to be processed. + * @param string $startTag String representing the start tag. + * @param string $endTag String representing the end tag. + * + * @return string with blocks removed. + * + * @since 3.4 + */ + private function removeBlocks($input, $startTag, $endTag) + { + $return = ''; + $offset = 0; + $startTagLength = strlen($startTag); + $endTagLength = strlen($endTag); + + // Find the first start tag. + $start = stripos($input, $startTag); + + // If no start tags were found, return the string unchanged. + if ($start === false) { + return $input; + } + + // Look for all blocks defined by the start and end tags. + while ($start !== false) { + // Accumulate the substring up to the start tag. + $return .= substr($input, $offset, $start - $offset) . ' '; + + // Look for an end tag corresponding to the start tag. + $end = stripos($input, $endTag, $start + $startTagLength); + + // If no corresponding end tag, leave the string alone. + if ($end === false) { + // Fix the offset so part of the string is not duplicated. + $offset = $start; + break; + } + + // Advance the start position. + $offset = $end + $endTagLength; + + // Look for the next start tag and loop. + $start = stripos($input, $startTag, $offset); + } + + // Add in the final substring after the last end tag. + $return .= substr($input, $offset); + + return $return; + } } diff --git a/code/administrator/components/com_finder/src/Indexer/Parser/Rtf.php b/code/administrator/components/com_finder/src/Indexer/Parser/Rtf.php index 3006ad81..db6c768e 100644 --- a/code/administrator/components/com_finder/src/Indexer/Parser/Rtf.php +++ b/code/administrator/components/com_finder/src/Indexer/Parser/Rtf.php @@ -1,4 +1,5 @@ input = $options['input'] ?? ''; - - // Get the empty query setting. - $this->empty = isset($options['empty']) ? (bool) $options['empty'] : false; - - // Get the input language. - $this->language = !empty($options['language']) ? $options['language'] : Helper::getDefaultLanguage(); - - // Get the matching mode. - $this->mode = 'AND'; - - // Initialize the temporary date storage. - $this->dates = new Registry; - - // Populate the temporary date storage. - if (!empty($options['date1'])) - { - $this->dates->set('date1', $options['date1']); - } - - if (!empty($options['date2'])) - { - $this->dates->set('date2', $options['date2']); - } - - if (!empty($options['when1'])) - { - $this->dates->set('when1', $options['when1']); - } - - if (!empty($options['when2'])) - { - $this->dates->set('when2', $options['when2']); - } - - // Process the static taxonomy filters. - if (!empty($options['filter'])) - { - $this->processStaticTaxonomy($options['filter']); - } - - // Process the dynamic taxonomy filters. - if (!empty($options['filters'])) - { - $this->processDynamicTaxonomy($options['filters']); - } - - // Get the date filters. - $d1 = $this->dates->get('date1'); - $d2 = $this->dates->get('date2'); - $w1 = $this->dates->get('when1'); - $w2 = $this->dates->get('when2'); - - // Process the date filters. - if (!empty($d1) || !empty($d2)) - { - $this->processDates($d1, $d2, $w1, $w2); - } - - // Process the input string. - $this->processString($this->input, $this->language, $this->mode); - - // Get the number of matching terms. - foreach ($this->included as $token) - { - $this->terms += count($token->matches); - } - - // Remove the temporary date storage. - unset($this->dates); - - // Lastly, determine whether this query can return a result set. - - // Check if we have a query string. - if (!empty($this->input)) - { - $this->search = true; - } - // Check if we can search without a query string. - elseif ($this->empty && (!empty($this->filter) || !empty($this->filters) || !empty($this->date1) || !empty($this->date2))) - { - $this->search = true; - } - // We do not have a valid search query. - else - { - $this->search = false; - } - } - - /** - * Method to convert the query object into a URI string. - * - * @param string $base The base URI. [optional] - * - * @return string The complete query URI. - * - * @since 2.5 - */ - public function toUri($base = '') - { - // Set the base if not specified. - if ($base === '') - { - $base = 'index.php?option=com_finder&view=search'; - } - - // Get the base URI. - $uri = Uri::getInstance($base); - - // Add the static taxonomy filter if present. - if ((bool) $this->filter) - { - $uri->setVar('f', $this->filter); - } - - // Get the filters in the request. - $t = Factory::getApplication()->input->request->get('t', array(), 'array'); - - // Add the dynamic taxonomy filters if present. - if ((bool) $this->filters) - { - foreach ($this->filters as $nodes) - { - foreach ($nodes as $node) - { - if (!in_array($node, $t)) - { - continue; - } - - $uri->setVar('t[]', $node); - } - } - } - - // Add the input string if present. - if (!empty($this->input)) - { - $uri->setVar('q', $this->input); - } - - // Add the start date if present. - if (!empty($this->date1)) - { - $uri->setVar('d1', $this->date1); - } - - // Add the end date if present. - if (!empty($this->date2)) - { - $uri->setVar('d2', $this->date2); - } - - // Add the start date modifier if present. - if (!empty($this->when1)) - { - $uri->setVar('w1', $this->when1); - } - - // Add the end date modifier if present. - if (!empty($this->when2)) - { - $uri->setVar('w2', $this->when2); - } - - // Add a menu item id if one is not present. - if (!$uri->getVar('Itemid')) - { - // Get the menu item id. - $query = array( - 'view' => $uri->getVar('view'), - 'f' => $uri->getVar('f'), - 'q' => $uri->getVar('q'), - ); - - $item = \FinderHelperRoute::getItemid($query); - - // Add the menu item id if present. - if ($item !== null) - { - $uri->setVar('Itemid', $item); - } - } - - return $uri->toString(array('path', 'query')); - } - - /** - * Method to get a list of excluded search term ids. - * - * @return array An array of excluded term ids. - * - * @since 2.5 - */ - public function getExcludedTermIds() - { - $results = array(); - - // Iterate through the excluded tokens and compile the matching terms. - for ($i = 0, $c = count($this->excluded); $i < $c; $i++) - { - foreach ($this->excluded[$i]->matches as $match) - { - $results = array_merge($results, $match); - } - } - - // Sanitize the terms. - $results = array_unique($results); - - return ArrayHelper::toInteger($results); - } - - /** - * Method to get a list of included search term ids. - * - * @return array An array of included term ids. - * - * @since 2.5 - */ - public function getIncludedTermIds() - { - $results = array(); - - // Iterate through the included tokens and compile the matching terms. - for ($i = 0, $c = count($this->included); $i < $c; $i++) - { - // Check if we have any terms. - if (empty($this->included[$i]->matches)) - { - continue; - } - - // Get the term. - $term = $this->included[$i]->term; - - // Prepare the container for the term if necessary. - if (!array_key_exists($term, $results)) - { - $results[$term] = array(); - } - - // Add the matches to the stack. - foreach ($this->included[$i]->matches as $match) - { - $results[$term] = array_merge($results[$term], $match); - } - } - - // Sanitize the terms. - foreach ($results as $key => $value) - { - $results[$key] = array_unique($results[$key]); - $results[$key] = ArrayHelper::toInteger($results[$key]); - } - - return $results; - } - - /** - * Method to get a list of required search term ids. - * - * @return array An array of required term ids. - * - * @since 2.5 - */ - public function getRequiredTermIds() - { - $results = array(); - - // Iterate through the included tokens and compile the matching terms. - for ($i = 0, $c = count($this->included); $i < $c; $i++) - { - // Check if the token is required. - if ($this->included[$i]->required) - { - // Get the term. - $term = $this->included[$i]->term; - - // Prepare the container for the term if necessary. - if (!array_key_exists($term, $results)) - { - $results[$term] = array(); - } - - // Add the matches to the stack. - foreach ($this->included[$i]->matches as $match) - { - $results[$term] = array_merge($results[$term], $match); - } - } - } - - // Sanitize the terms. - foreach ($results as $key => $value) - { - $results[$key] = array_unique($results[$key]); - $results[$key] = ArrayHelper::toInteger($results[$key]); - } - - return $results; - } - - /** - * Method to process the static taxonomy input. The static taxonomy input - * comes in the form of a pre-defined search filter that is assigned to the - * search form. - * - * @param integer $filterId The id of static filter. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function processStaticTaxonomy($filterId) - { - // Get the database object. - $db = Factory::getDbo(); - - // Initialize user variables - $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); - - // Load the predefined filter. - $query = $db->getQuery(true) - ->select('f.data, f.params') - ->from($db->quoteName('#__finder_filters') . ' AS f') - ->where('f.filter_id = ' . (int) $filterId); - - $db->setQuery($query); - $return = $db->loadObject(); - - // Check the returned filter. - if (empty($return)) - { - return false; - } - - // Set the filter. - $this->filter = (int) $filterId; - - // Get a parameter object for the filter date options. - $registry = new Registry($return->params); - $params = $registry; - - // Set the dates if not already set. - $this->dates->def('d1', $params->get('d1')); - $this->dates->def('d2', $params->get('d2')); - $this->dates->def('w1', $params->get('w1')); - $this->dates->def('w2', $params->get('w2')); - - // Remove duplicates and sanitize. - $filters = explode(',', $return->data); - $filters = array_unique($filters); - $filters = ArrayHelper::toInteger($filters); - - // Remove any values of zero. - if (in_array(0, $filters, true) !== false) - { - unset($filters[array_search(0, $filters, true)]); - } - - // Check if we have any real input. - if (empty($filters)) - { - return true; - } - - /* - * Create the query to get filters from the database. We do this for - * two reasons: one, it allows us to ensure that the filters being used - * are real; two, we need to sort the filters by taxonomy branch. - */ - $query->clear() - ->select('t1.id, t1.title, t2.title AS branch') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') - ->where('t1.state = 1') - ->where('t1.access IN (' . $groups . ')') - ->where('t1.id IN (' . implode(',', $filters) . ')') - ->where('t2.state = 1') - ->where('t2.access IN (' . $groups . ')'); - - // Load the filters. - $db->setQuery($query); - $results = $db->loadObjectList(); - - // Sort the filter ids by branch. - foreach ($results as $result) - { - $this->filters[$result->branch][$result->title] = (int) $result->id; - } - - return true; - } - - /** - * Method to process the dynamic taxonomy input. The dynamic taxonomy input - * comes in the form of select fields that the user chooses from. The - * dynamic taxonomy input is processed AFTER the static taxonomy input - * because the dynamic options can be used to further narrow a static - * taxonomy filter. - * - * @param array $filters An array of taxonomy node ids. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function processDynamicTaxonomy($filters) - { - // Initialize user variables - $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); - - // Remove duplicates and sanitize. - $filters = array_unique($filters); - $filters = ArrayHelper::toInteger($filters); - - // Remove any values of zero. - if (in_array(0, $filters, true) !== false) - { - unset($filters[array_search(0, $filters, true)]); - } - - // Check if we have any real input. - if (empty($filters)) - { - return true; - } - - // Get the database object. - $db = Factory::getDbo(); - - $query = $db->getQuery(true); - - /* - * Create the query to get filters from the database. We do this for - * two reasons: one, it allows us to ensure that the filters being used - * are real; two, we need to sort the filters by taxonomy branch. - */ - $query->select('t1.id, t1.title, t2.title AS branch') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') - ->where('t1.state = 1') - ->where('t1.access IN (' . $groups . ')') - ->where('t1.id IN (' . implode(',', $filters) . ')') - ->where('t2.state = 1') - ->where('t2.access IN (' . $groups . ')'); - - // Load the filters. - $db->setQuery($query); - $results = $db->loadObjectList(); - - // Cleared filter branches. - $cleared = array(); - - /* - * Sort the filter ids by branch. Because these filters are designed to - * override and further narrow the items selected in the static filter, - * we will clear the values from the static filter on a branch by - * branch basis before adding the dynamic filters. So, if the static - * filter defines a type filter of "articles" and three "category" - * filters but the user only limits the category further, the category - * filters will be flushed but the type filters will not. - */ - foreach ($results as $result) - { - // Check if the branch has been cleared. - if (!in_array($result->branch, $cleared, true)) - { - // Clear the branch. - $this->filters[$result->branch] = array(); - - // Add the branch to the cleared list. - $cleared[] = $result->branch; - } - - // Add the filter to the list. - $this->filters[$result->branch][$result->title] = (int) $result->id; - } - - return true; - } - - /** - * Method to process the query date filters to determine start and end - * date limitations. - * - * @param string $date1 The first date filter. - * @param string $date2 The second date filter. - * @param string $when1 The first date modifier. - * @param string $when2 The second date modifier. - * - * @return boolean True on success. - * - * @since 2.5 - */ - protected function processDates($date1, $date2, $when1, $when2) - { - // Clean up the inputs. - $date1 = trim(StringHelper::strtolower($date1)); - $date2 = trim(StringHelper::strtolower($date2)); - $when1 = trim(StringHelper::strtolower($when1)); - $when2 = trim(StringHelper::strtolower($when2)); - - // Get the time offset. - $offset = Factory::getApplication()->get('offset'); - - // Array of allowed when values. - $whens = array('before', 'after', 'exact'); - - // The value of 'today' is a special case that we need to handle. - if ($date1 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) - { - $date1 = Factory::getDate('now', $offset)->format('%Y-%m-%d'); - } - - // Try to parse the date string. - $date = Factory::getDate($date1, $offset); - - // Check if the date was parsed successfully. - if ($date->toUnix() !== null) - { - // Set the date filter. - $this->date1 = $date->toSql(); - $this->when1 = in_array($when1, $whens, true) ? $when1 : 'before'; - } - - // The value of 'today' is a special case that we need to handle. - if ($date2 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) - { - $date2 = Factory::getDate('now', $offset)->format('%Y-%m-%d'); - } - - // Try to parse the date string. - $date = Factory::getDate($date2, $offset); - - // Check if the date was parsed successfully. - if ($date->toUnix() !== null) - { - // Set the date filter. - $this->date2 = $date->toSql(); - $this->when2 = in_array($when2, $whens, true) ? $when2 : 'before'; - } - - return true; - } - - /** - * Method to process the query input string and extract required, optional, - * and excluded tokens; taxonomy filters; and date filters. - * - * @param string $input The query input string. - * @param string $lang The query input language. - * @param string $mode The query matching mode. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function processString($input, $lang, $mode) - { - if ($input === null) - { - $input = ''; - } - - // Clean up the input string. - $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8'); - $input = StringHelper::strtolower($input); - $input = preg_replace('#\s+#mi', ' ', $input); - $input = trim($input); - $debug = Factory::getApplication()->get('debug_lang'); - $params = ComponentHelper::getParams('com_finder'); - - /* - * First, we need to handle string based modifiers. String based - * modifiers could potentially include things like "category:blah" or - * "before:2009-10-21" or "type:article", etc. - */ - $patterns = array( - 'before' => Text::_('COM_FINDER_FILTER_WHEN_BEFORE'), - 'after' => Text::_('COM_FINDER_FILTER_WHEN_AFTER'), - ); - - // Add the taxonomy branch titles to the possible patterns. - foreach (Taxonomy::getBranchTitles() as $branch) - { - // Add the pattern. - $patterns[$branch] = StringHelper::strtolower(Text::_(LanguageHelper::branchSingular($branch))); - } - - // Container for search terms and phrases. - $terms = array(); - $phrases = array(); - - // Cleared filter branches. - $cleared = array(); - - /* - * Compile the suffix pattern. This is used to match the values of the - * filter input string. Single words can be input directly, multi-word - * values have to be wrapped in double quotes. - */ - $quotes = html_entity_decode('‘’'', ENT_QUOTES, 'UTF-8'); - $suffix = '(([\w\d' . $quotes . '-]+)|\"([\w\d\s' . $quotes . '-]+)\")'; - - /* - * Iterate through the possible filter patterns and search for matches. - * We need to match the key, colon, and a value pattern for the match - * to be valid. - */ - foreach ($patterns as $modifier => $pattern) - { - $matches = array(); - - if ($debug) - { - $pattern = substr($pattern, 2, -2); - } - - // Check if the filter pattern is in the input string. - if (preg_match('#' . $pattern . '\s*:\s*' . $suffix . '#mi', $input, $matches)) - { - // Get the value given to the modifier. - $value = $matches[3] ?? $matches[1]; - - // Now we have to handle the filter string. - switch ($modifier) - { - // Handle a before and after date filters. - case 'before': - case 'after': - { - // Get the time offset. - $offset = Factory::getApplication()->get('offset'); - - // Array of allowed when values. - $whens = array('before', 'after', 'exact'); - - // The value of 'today' is a special case that we need to handle. - if ($value === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) - { - $value = Factory::getDate('now', $offset)->format('%Y-%m-%d'); - } - - // Try to parse the date string. - $date = Factory::getDate($value, $offset); - - // Check if the date was parsed successfully. - if ($date->toUnix() !== null) - { - // Set the date filter. - $this->date1 = $date->toSql(); - $this->when1 = in_array($modifier, $whens, true) ? $modifier : 'before'; - } - - break; - } - - // Handle a taxonomy branch filter. - default: - { - // Try to find the node id. - $return = Taxonomy::getNodeByTitle($modifier, $value); - - // Check if the node id was found. - if ($return) - { - // Check if the branch has been cleared. - if (!in_array($modifier, $cleared, true)) - { - // Clear the branch. - $this->filters[$modifier] = array(); - - // Add the branch to the cleared list. - $cleared[] = $modifier; - } - - // Add the filter to the list. - $this->filters[$modifier][$return->title] = (int) $return->id; - } - - break; - } - } - - // Clean up the input string again. - $input = str_replace($matches[0], '', $input); - $input = preg_replace('#\s+#mi', ' ', $input); - $input = trim($input); - } - } - - /* - * Extract the tokens enclosed in double quotes so that we can handle - * them as phrases. - */ - if (StringHelper::strpos($input, '"') !== false) - { - $matches = array(); - - // Extract the tokens enclosed in double quotes. - if (preg_match_all('#\"([^"]+)\"#m', $input, $matches)) - { - /* - * One or more phrases were found so we need to iterate through - * them, tokenize them as phrases, and remove them from the raw - * input string before we move on to the next processing step. - */ - foreach ($matches[1] as $key => $match) - { - // Find the complete phrase in the input string. - $pos = StringHelper::strpos($input, $matches[0][$key]); - $len = StringHelper::strlen($matches[0][$key]); - - // Add any terms that are before this phrase to the stack. - if (trim(StringHelper::substr($input, 0, $pos))) - { - $terms = array_merge($terms, explode(' ', trim(StringHelper::substr($input, 0, $pos)))); - } - - // Strip out everything up to and including the phrase. - $input = StringHelper::substr($input, $pos + $len); - - // Clean up the input string again. - $input = preg_replace('#\s+#mi', ' ', $input); - $input = trim($input); - - // Get the number of words in the phrase. - $parts = explode(' ', $match); - $tuplecount = $params->get('tuplecount', 1); - - // Check if the phrase is longer than our $tuplecount. - if (count($parts) > $tuplecount && $tuplecount > 1) - { - $chunk = array_slice($parts, 0, $tuplecount); - $parts = array_slice($parts, $tuplecount); - - // If the chunk is not empty, add it as a phrase. - if (count($chunk)) - { - $phrases[] = implode(' ', $chunk); - $terms[] = implode(' ', $chunk); - } - - /* - * If the phrase is longer than $tuplecount words, we need to - * break it down into smaller chunks of phrases that - * are less than or equal to $tuplecount words. We overlap - * the chunks so that we can ensure that a match is - * found for the complete phrase and not just portions - * of it. - */ - for ($i = 0, $c = count($parts); $i < $c; $i++) - { - array_shift($chunk); - $chunk[] = array_shift($parts); - - // If the chunk is not empty, add it as a phrase. - if (count($chunk)) - { - $phrases[] = implode(' ', $chunk); - $terms[] = implode(' ', $chunk); - } - } - } - else - { - // The phrase is <= $tuplecount words so we can use it as is. - $phrases[] = $match; - $terms[] = $match; - } - } - } - } - - // Add the remaining terms if present. - if ((bool) $input) - { - $terms = array_merge($terms, explode(' ', $input)); - } - - // An array of our boolean operators. $operator => $translation - $operators = array( - 'AND' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_AND')), - 'OR' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_OR')), - 'NOT' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_NOT')), - ); - - // If language debugging is enabled you need to ignore the debug strings in matching. - if (JDEBUG) - { - $debugStrings = array('**', '??'); - $operators = str_replace($debugStrings, '', $operators); - } - - /* - * Iterate through the terms and perform any sorting that needs to be - * done based on boolean search operators. Terms that are before an - * and/or/not modifier have to be handled in relation to their operator. - */ - for ($i = 0, $c = count($terms); $i < $c; $i++) - { - // Check if the term is followed by an operator that we understand. - if (isset($terms[$i + 1]) && in_array($terms[$i + 1], $operators, true)) - { - // Get the operator mode. - $op = array_search($terms[$i + 1], $operators, true); - - // Handle the AND operator. - if ($op === 'AND' && isset($terms[$i + 2])) - { - // Tokenize the current term. - $token = Helper::tokenize($terms[$i], $lang, true); - - // @todo: The previous function call may return an array, which seems not to be handled by the next one, which expects an object - $token = $this->getTokenData(array_shift($token)); - - if ($params->get('filter_commonwords', 0) && $token->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $token->numeric) - { - continue; - } - - // Set the required flag. - $token->required = true; - - // Add the current token to the stack. - $this->included[] = $token; - $this->highlight = array_merge($this->highlight, array_keys($token->matches)); - - // Skip the next token (the mode operator). - $this->operators[] = $terms[$i + 1]; - - // Tokenize the term after the next term (current plus two). - $other = Helper::tokenize($terms[$i + 2], $lang, true); - $other = $this->getTokenData(array_shift($other)); - - // Set the required flag. - $other->required = true; - - // Add the token after the next token to the stack. - $this->included[] = $other; - $this->highlight = array_merge($this->highlight, array_keys($other->matches)); - - // Remove the processed phrases if possible. - if (($pk = array_search($terms[$i], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - // Remove the processed terms. - unset($terms[$i], $terms[$i + 1], $terms[$i + 2]); - - // Adjust the loop. - $i += 2; - } - // Handle the OR operator. - elseif ($op === 'OR' && isset($terms[$i + 2])) - { - // Tokenize the current term. - $token = Helper::tokenize($terms[$i], $lang, true); - $token = $this->getTokenData(array_shift($token)); - - if ($params->get('filter_commonwords', 0) && $token->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $token->numeric) - { - continue; - } - - // Set the required flag. - $token->required = false; - - // Add the current token to the stack. - if ((bool) $token->matches) - { - $this->included[] = $token; - $this->highlight = array_merge($this->highlight, array_keys($token->matches)); - } - else - { - $this->ignored[] = $token; - } - - // Skip the next token (the mode operator). - $this->operators[] = $terms[$i + 1]; - - // Tokenize the term after the next term (current plus two). - $other = Helper::tokenize($terms[$i + 2], $lang, true); - $other = $this->getTokenData(array_shift($other)); - - // Set the required flag. - $other->required = false; - - // Add the token after the next token to the stack. - if ((bool) $other->matches) - { - $this->included[] = $other; - $this->highlight = array_merge($this->highlight, array_keys($other->matches)); - } - else - { - $this->ignored[] = $other; - } - - // Remove the processed phrases if possible. - if (($pk = array_search($terms[$i], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - // Remove the processed terms. - unset($terms[$i], $terms[$i + 1], $terms[$i + 2]); - - // Adjust the loop. - $i += 2; - } - } - // Handle an orphaned OR operator. - elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'OR') - { - // Skip the next token (the mode operator). - $this->operators[] = $terms[$i]; - - // Tokenize the next term (current plus one). - $other = Helper::tokenize($terms[$i + 1], $lang, true); - $other = $this->getTokenData(array_shift($other)); - - if ($params->get('filter_commonwords', 0) && $other->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $other->numeric) - { - continue; - } - - // Set the required flag. - $other->required = false; - - // Add the token after the next token to the stack. - if ((bool) $other->matches) - { - $this->included[] = $other; - $this->highlight = array_merge($this->highlight, array_keys($other->matches)); - } - else - { - $this->ignored[] = $other; - } - - // Remove the processed phrase if possible. - if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - // Remove the processed terms. - unset($terms[$i], $terms[$i + 1]); - - // Adjust the loop. - $i++; - } - // Handle the NOT operator. - elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'NOT') - { - // Skip the next token (the mode operator). - $this->operators[] = $terms[$i]; - - // Tokenize the next term (current plus one). - $other = Helper::tokenize($terms[$i + 1], $lang, true); - $other = $this->getTokenData(array_shift($other)); - - if ($params->get('filter_commonwords', 0) && $other->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $other->numeric) - { - continue; - } - - // Set the required flag. - $other->required = false; - - // Add the next token to the stack. - if ((bool) $other->matches) - { - $this->excluded[] = $other; - } - else - { - $this->ignored[] = $other; - } - - // Remove the processed phrase if possible. - if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - // Remove the processed terms. - unset($terms[$i], $terms[$i + 1]); - - // Adjust the loop. - $i++; - } - } - - /* - * Iterate through any search phrases and tokenize them. We handle - * phrases as autonomous units and do not break them down into two and - * three word combinations. - */ - for ($i = 0, $c = count($phrases); $i < $c; $i++) - { - // Tokenize the phrase. - $token = Helper::tokenize($phrases[$i], $lang, true); - - if (!count($token)) - { - continue; - } - - $token = $this->getTokenData(array_shift($token)); - - if ($params->get('filter_commonwords', 0) && $token->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $token->numeric) - { - continue; - } - - // Set the required flag. - $token->required = true; - - // Add the current token to the stack. - $this->included[] = $token; - $this->highlight = array_merge($this->highlight, array_keys($token->matches)); - - // Remove the processed term if possible. - if (($pk = array_search($phrases[$i], $terms, true)) !== false) - { - unset($terms[$pk]); - } - - // Remove the processed phrase. - unset($phrases[$i]); - } - - /* - * Handle any remaining tokens using the standard processing mechanism. - */ - if ((bool) $terms) - { - // Tokenize the terms. - $terms = implode(' ', $terms); - $tokens = Helper::tokenize($terms, $lang, false); - - // Make sure we are working with an array. - $tokens = is_array($tokens) ? $tokens : array($tokens); - - // Get the token data and required state for all the tokens. - foreach ($tokens as $token) - { - // Get the token data. - $token = $this->getTokenData($token); - - if ($params->get('filter_commonwords', 0) && $token->common) - { - continue; - } - - if ($params->get('filter_numerics', 0) && $token->numeric) - { - continue; - } - - // Set the required flag for the token. - $token->required = $mode === 'AND' ? (!$token->phrase) : false; - - // Add the token to the appropriate stack. - if ($token->required || (bool) $token->matches) - { - $this->included[] = $token; - $this->highlight = array_merge($this->highlight, array_keys($token->matches)); - } - else - { - $this->ignored[] = $token; - } - } - } - - return true; - } - - /** - * Method to get the base and similar term ids and, if necessary, suggested - * term data from the database. The terms ids are identified based on a - * 'like' match in MySQL and/or a common stem. If no term ids could be - * found, then we know that we will not be able to return any results for - * that term and we should try to find a similar term to use that we can - * match so that we can suggest the alternative search query to the user. - * - * @param Token $token A Token object. - * - * @return Token A Token object. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getTokenData($token) - { - // Get the database object. - $db = Factory::getDbo(); - - // Create a database query to build match the token. - $query = $db->getQuery(true) - ->select('t.term, t.term_id') - ->from('#__finder_terms AS t'); - - if ($token->phrase) - { - // Add the phrase to the query. - $query->where('t.term = ' . $db->quote($token->term)) - ->where('t.phrase = 1'); - } - else - { - // Add the term to the query. - $query->where('(t.term = ' . $db->quote($token->term) . ' OR t.stem = ' . $db->quote($token->stem) . ')') - ->where('t.phrase = 0') - ->where('t.language IN (\'*\',' . $db->quote($token->language) . ')'); - } - - // Get the terms. - $db->setQuery($query); - $matches = $db->loadObjectList(); - - // Check the matching terms. - if ((bool) $matches) - { - // Add the matches to the token. - for ($i = 0, $c = count($matches); $i < $c; $i++) - { - if (!isset($token->matches[$matches[$i]->term])) - { - $token->matches[$matches[$i]->term] = array(); - } - - $token->matches[$matches[$i]->term][] = (int) $matches[$i]->term_id; - } - } - - // If no matches were found, try to find a similar but better token. - if (empty($token->matches)) - { - // Create a database query to get the similar terms. - $query->clear() - ->select('DISTINCT t.term_id AS id, t.term AS term') - ->from('#__finder_terms AS t') - // ->where('t.soundex = ' . soundex($db->quote($token->term))) - ->where('t.soundex = SOUNDEX(' . $db->quote($token->term) . ')') - ->where('t.phrase = ' . (int) $token->phrase); - - // Get the terms. - $db->setQuery($query); - $results = $db->loadObjectList(); - - // Check if any similar terms were found. - if (empty($results)) - { - return $token; - } - - // Stack for sorting the similar terms. - $suggestions = array(); - - // Get the levnshtein distance for all suggested terms. - foreach ($results as $sk => $st) - { - // Get the levenshtein distance between terms. - $distance = levenshtein($st->term, $token->term); - - // Make sure the levenshtein distance isn't over 50. - if ($distance < 50) - { - $suggestions[$sk] = $distance; - } - } - - // Sort the suggestions. - asort($suggestions, SORT_NUMERIC); - - // Get the closest match. - $keys = array_keys($suggestions); - $key = $keys[0]; - - // Add the suggested term. - $token->suggestion = $results[$key]->term; - } - - return $token; - } + use DatabaseAwareTrait; + + /** + * Flag to show whether the query can return results. + * + * @var boolean + * @since 2.5 + */ + public $search; + + /** + * The query input string. + * + * @var string + * @since 2.5 + */ + public $input; + + /** + * The language of the query. + * + * @var string + * @since 2.5 + */ + public $language; + + /** + * The query string matching mode. + * + * @var string + * @since 2.5 + */ + public $mode; + + /** + * The included tokens. + * + * @var Token[] + * @since 2.5 + */ + public $included = array(); + + /** + * The excluded tokens. + * + * @var Token[] + * @since 2.5 + */ + public $excluded = array(); + + /** + * The tokens to ignore because no matches exist. + * + * @var Token[] + * @since 2.5 + */ + public $ignored = array(); + + /** + * The operators used in the query input string. + * + * @var array + * @since 2.5 + */ + public $operators = array(); + + /** + * The terms to highlight as matches. + * + * @var array + * @since 2.5 + */ + public $highlight = array(); + + /** + * The number of matching terms for the query input. + * + * @var integer + * @since 2.5 + */ + public $terms; + + /** + * Allow empty searches + * + * @var boolean + * @since 4.0.0 + */ + public $empty; + + /** + * The static filter id. + * + * @var string + * @since 2.5 + */ + public $filter; + + /** + * The taxonomy filters. This is a multi-dimensional array of taxonomy + * branches as the first level and then the taxonomy nodes as the values. + * + * For example: + * $filters = array( + * 'Type' = array(10, 32, 29, 11, ...); + * 'Label' = array(20, 314, 349, 91, 82, ...); + * ... + * ); + * + * @var array + * @since 2.5 + */ + public $filters = array(); + + /** + * The start date filter. + * + * @var string + * @since 2.5 + */ + public $date1; + + /** + * The end date filter. + * + * @var string + * @since 2.5 + */ + public $date2; + + /** + * The start date filter modifier. + * + * @var string + * @since 2.5 + */ + public $when1; + + /** + * The end date filter modifier. + * + * @var string + * @since 2.5 + */ + public $when2; + + /** + * Match search terms exactly or with a LIKE scheme + * + * @var string + * @since 4.2.0 + */ + public $wordmode; + + /** + * Method to instantiate the query object. + * + * @param array $options An array of query options. + * + * @since 2.5 + * @throws Exception on database error. + */ + public function __construct($options, DatabaseInterface $db = null) + { + if ($db === null) { + @trigger_error(sprintf('Database will be mandatory in 5.0.'), E_USER_DEPRECATED); + $db = Factory::getContainer()->get(DatabaseInterface::class); + } + + $this->setDatabase($db); + + // Get the input string. + $this->input = $options['input'] ?? ''; + + // Get the empty query setting. + $this->empty = isset($options['empty']) ? (bool) $options['empty'] : false; + + // Get the input language. + $this->language = !empty($options['language']) ? $options['language'] : Helper::getDefaultLanguage(); + + // Get the matching mode. + $this->mode = 'AND'; + + // Set the word matching mode + $this->wordmode = !empty($options['word_match']) ? $options['word_match'] : 'exact'; + + // Initialize the temporary date storage. + $this->dates = new Registry(); + + // Populate the temporary date storage. + if (!empty($options['date1'])) { + $this->dates->set('date1', $options['date1']); + } + + if (!empty($options['date2'])) { + $this->dates->set('date2', $options['date2']); + } + + if (!empty($options['when1'])) { + $this->dates->set('when1', $options['when1']); + } + + if (!empty($options['when2'])) { + $this->dates->set('when2', $options['when2']); + } + + // Process the static taxonomy filters. + if (!empty($options['filter'])) { + $this->processStaticTaxonomy($options['filter']); + } + + // Process the dynamic taxonomy filters. + if (!empty($options['filters'])) { + $this->processDynamicTaxonomy($options['filters']); + } + + // Get the date filters. + $d1 = $this->dates->get('date1'); + $d2 = $this->dates->get('date2'); + $w1 = $this->dates->get('when1'); + $w2 = $this->dates->get('when2'); + + // Process the date filters. + if (!empty($d1) || !empty($d2)) { + $this->processDates($d1, $d2, $w1, $w2); + } + + // Process the input string. + $this->processString($this->input, $this->language, $this->mode); + + // Get the number of matching terms. + foreach ($this->included as $token) { + $this->terms += count($token->matches); + } + + // Remove the temporary date storage. + unset($this->dates); + + // Lastly, determine whether this query can return a result set. + + // Check if we have a query string. + if (!empty($this->input)) { + $this->search = true; + } elseif ($this->empty && (!empty($this->filter) || !empty($this->filters) || !empty($this->date1) || !empty($this->date2))) { + // Check if we can search without a query string. + $this->search = true; + } else { + // We do not have a valid search query. + $this->search = false; + } + } + + /** + * Method to convert the query object into a URI string. + * + * @param string $base The base URI. [optional] + * + * @return string The complete query URI. + * + * @since 2.5 + */ + public function toUri($base = '') + { + // Set the base if not specified. + if ($base === '') { + $base = 'index.php?option=com_finder&view=search'; + } + + // Get the base URI. + $uri = Uri::getInstance($base); + + // Add the static taxonomy filter if present. + if ((bool) $this->filter) { + $uri->setVar('f', $this->filter); + } + + // Get the filters in the request. + $t = Factory::getApplication()->input->request->get('t', array(), 'array'); + + // Add the dynamic taxonomy filters if present. + if ((bool) $this->filters) { + foreach ($this->filters as $nodes) { + foreach ($nodes as $node) { + if (!in_array($node, $t)) { + continue; + } + + $uri->setVar('t[]', $node); + } + } + } + + // Add the input string if present. + if (!empty($this->input)) { + $uri->setVar('q', $this->input); + } + + // Add the start date if present. + if (!empty($this->date1)) { + $uri->setVar('d1', $this->date1); + } + + // Add the end date if present. + if (!empty($this->date2)) { + $uri->setVar('d2', $this->date2); + } + + // Add the start date modifier if present. + if (!empty($this->when1)) { + $uri->setVar('w1', $this->when1); + } + + // Add the end date modifier if present. + if (!empty($this->when2)) { + $uri->setVar('w2', $this->when2); + } + + // Add a menu item id if one is not present. + if (!$uri->getVar('Itemid')) { + // Get the menu item id. + $query = array( + 'view' => $uri->getVar('view'), + 'f' => $uri->getVar('f'), + 'q' => $uri->getVar('q'), + ); + + $item = RouteHelper::getItemid($query); + + // Add the menu item id if present. + if ($item !== null) { + $uri->setVar('Itemid', $item); + } + } + + return $uri->toString(array('path', 'query')); + } + + /** + * Method to get a list of excluded search term ids. + * + * @return array An array of excluded term ids. + * + * @since 2.5 + */ + public function getExcludedTermIds() + { + $results = array(); + + // Iterate through the excluded tokens and compile the matching terms. + for ($i = 0, $c = count($this->excluded); $i < $c; $i++) { + foreach ($this->excluded[$i]->matches as $match) { + $results = array_merge($results, $match); + } + } + + // Sanitize the terms. + $results = array_unique($results); + + return ArrayHelper::toInteger($results); + } + + /** + * Method to get a list of included search term ids. + * + * @return array An array of included term ids. + * + * @since 2.5 + */ + public function getIncludedTermIds() + { + $results = array(); + + // Iterate through the included tokens and compile the matching terms. + for ($i = 0, $c = count($this->included); $i < $c; $i++) { + // Check if we have any terms. + if (empty($this->included[$i]->matches)) { + continue; + } + + // Get the term. + $term = $this->included[$i]->term; + + // Prepare the container for the term if necessary. + if (!array_key_exists($term, $results)) { + $results[$term] = array(); + } + + // Add the matches to the stack. + foreach ($this->included[$i]->matches as $match) { + $results[$term] = array_merge($results[$term], $match); + } + } + + // Sanitize the terms. + foreach ($results as $key => $value) { + $results[$key] = array_unique($results[$key]); + $results[$key] = ArrayHelper::toInteger($results[$key]); + } + + return $results; + } + + /** + * Method to get a list of required search term ids. + * + * @return array An array of required term ids. + * + * @since 2.5 + */ + public function getRequiredTermIds() + { + $results = array(); + + // Iterate through the included tokens and compile the matching terms. + for ($i = 0, $c = count($this->included); $i < $c; $i++) { + // Check if the token is required. + if ($this->included[$i]->required) { + // Get the term. + $term = $this->included[$i]->term; + + // Prepare the container for the term if necessary. + if (!array_key_exists($term, $results)) { + $results[$term] = array(); + } + + // Add the matches to the stack. + foreach ($this->included[$i]->matches as $match) { + $results[$term] = array_merge($results[$term], $match); + } + } + } + + // Sanitize the terms. + foreach ($results as $key => $value) { + $results[$key] = array_unique($results[$key]); + $results[$key] = ArrayHelper::toInteger($results[$key]); + } + + return $results; + } + + /** + * Method to process the static taxonomy input. The static taxonomy input + * comes in the form of a pre-defined search filter that is assigned to the + * search form. + * + * @param integer $filterId The id of static filter. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function processStaticTaxonomy($filterId) + { + // Get the database object. + $db = $this->getDatabase(); + + // Initialize user variables + $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); + + // Load the predefined filter. + $query = $db->getQuery(true) + ->select('f.data, f.params') + ->from($db->quoteName('#__finder_filters') . ' AS f') + ->where('f.filter_id = ' . (int) $filterId); + + $db->setQuery($query); + $return = $db->loadObject(); + + // Check the returned filter. + if (empty($return)) { + return false; + } + + // Set the filter. + $this->filter = (int) $filterId; + + // Get a parameter object for the filter date options. + $registry = new Registry($return->params); + $params = $registry; + + // Set the dates if not already set. + $this->dates->def('d1', $params->get('d1')); + $this->dates->def('d2', $params->get('d2')); + $this->dates->def('w1', $params->get('w1')); + $this->dates->def('w2', $params->get('w2')); + + // Remove duplicates and sanitize. + $filters = explode(',', $return->data); + $filters = array_unique($filters); + $filters = ArrayHelper::toInteger($filters); + + // Remove any values of zero. + if (in_array(0, $filters, true) !== false) { + unset($filters[array_search(0, $filters, true)]); + } + + // Check if we have any real input. + if (empty($filters)) { + return true; + } + + /* + * Create the query to get filters from the database. We do this for + * two reasons: one, it allows us to ensure that the filters being used + * are real; two, we need to sort the filters by taxonomy branch. + */ + $query->clear() + ->select('t1.id, t1.title, t2.title AS branch') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') + ->where('t1.state = 1') + ->where('t1.access IN (' . $groups . ')') + ->where('t1.id IN (' . implode(',', $filters) . ')') + ->where('t2.state = 1') + ->where('t2.access IN (' . $groups . ')'); + + // Load the filters. + $db->setQuery($query); + $results = $db->loadObjectList(); + + // Sort the filter ids by branch. + foreach ($results as $result) { + $this->filters[$result->branch][$result->title] = (int) $result->id; + } + + return true; + } + + /** + * Method to process the dynamic taxonomy input. The dynamic taxonomy input + * comes in the form of select fields that the user chooses from. The + * dynamic taxonomy input is processed AFTER the static taxonomy input + * because the dynamic options can be used to further narrow a static + * taxonomy filter. + * + * @param array $filters An array of taxonomy node ids. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function processDynamicTaxonomy($filters) + { + // Initialize user variables + $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); + + // Remove duplicates and sanitize. + $filters = array_unique($filters); + $filters = ArrayHelper::toInteger($filters); + + // Remove any values of zero. + if (in_array(0, $filters, true) !== false) { + unset($filters[array_search(0, $filters, true)]); + } + + // Check if we have any real input. + if (empty($filters)) { + return true; + } + + // Get the database object. + $db = $this->getDatabase(); + + $query = $db->getQuery(true); + + /* + * Create the query to get filters from the database. We do this for + * two reasons: one, it allows us to ensure that the filters being used + * are real; two, we need to sort the filters by taxonomy branch. + */ + $query->select('t1.id, t1.title, t2.title AS branch') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') + ->where('t1.state = 1') + ->where('t1.access IN (' . $groups . ')') + ->where('t1.id IN (' . implode(',', $filters) . ')') + ->where('t2.state = 1') + ->where('t2.access IN (' . $groups . ')'); + + // Load the filters. + $db->setQuery($query); + $results = $db->loadObjectList(); + + // Cleared filter branches. + $cleared = array(); + + /* + * Sort the filter ids by branch. Because these filters are designed to + * override and further narrow the items selected in the static filter, + * we will clear the values from the static filter on a branch by + * branch basis before adding the dynamic filters. So, if the static + * filter defines a type filter of "articles" and three "category" + * filters but the user only limits the category further, the category + * filters will be flushed but the type filters will not. + */ + foreach ($results as $result) { + // Check if the branch has been cleared. + if (!in_array($result->branch, $cleared, true)) { + // Clear the branch. + $this->filters[$result->branch] = array(); + + // Add the branch to the cleared list. + $cleared[] = $result->branch; + } + + // Add the filter to the list. + $this->filters[$result->branch][$result->title] = (int) $result->id; + } + + return true; + } + + /** + * Method to process the query date filters to determine start and end + * date limitations. + * + * @param string $date1 The first date filter. + * @param string $date2 The second date filter. + * @param string $when1 The first date modifier. + * @param string $when2 The second date modifier. + * + * @return boolean True on success. + * + * @since 2.5 + */ + protected function processDates($date1, $date2, $when1, $when2) + { + // Clean up the inputs. + $date1 = trim(StringHelper::strtolower($date1)); + $date2 = trim(StringHelper::strtolower($date2)); + $when1 = trim(StringHelper::strtolower($when1)); + $when2 = trim(StringHelper::strtolower($when2)); + + // Get the time offset. + $offset = Factory::getApplication()->get('offset'); + + // Array of allowed when values. + $whens = array('before', 'after', 'exact'); + + // The value of 'today' is a special case that we need to handle. + if ($date1 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) { + $date1 = Factory::getDate('now', $offset)->format('%Y-%m-%d'); + } + + // Try to parse the date string. + $date = Factory::getDate($date1, $offset); + + // Check if the date was parsed successfully. + if ($date->toUnix() !== null) { + // Set the date filter. + $this->date1 = $date->toSql(); + $this->when1 = in_array($when1, $whens, true) ? $when1 : 'before'; + } + + // The value of 'today' is a special case that we need to handle. + if ($date2 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) { + $date2 = Factory::getDate('now', $offset)->format('%Y-%m-%d'); + } + + // Try to parse the date string. + $date = Factory::getDate($date2, $offset); + + // Check if the date was parsed successfully. + if ($date->toUnix() !== null) { + // Set the date filter. + $this->date2 = $date->toSql(); + $this->when2 = in_array($when2, $whens, true) ? $when2 : 'before'; + } + + return true; + } + + /** + * Method to process the query input string and extract required, optional, + * and excluded tokens; taxonomy filters; and date filters. + * + * @param string $input The query input string. + * @param string $lang The query input language. + * @param string $mode The query matching mode. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function processString($input, $lang, $mode) + { + if ($input === null) { + $input = ''; + } + + // Clean up the input string. + $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8'); + $input = StringHelper::strtolower($input); + $input = preg_replace('#\s+#mi', ' ', $input); + $input = trim($input); + $debug = Factory::getApplication()->get('debug_lang'); + $params = ComponentHelper::getParams('com_finder'); + + /* + * First, we need to handle string based modifiers. String based + * modifiers could potentially include things like "category:blah" or + * "before:2009-10-21" or "type:article", etc. + */ + $patterns = array( + 'before' => Text::_('COM_FINDER_FILTER_WHEN_BEFORE'), + 'after' => Text::_('COM_FINDER_FILTER_WHEN_AFTER'), + ); + + // Add the taxonomy branch titles to the possible patterns. + foreach (Taxonomy::getBranchTitles() as $branch) { + // Add the pattern. + $patterns[$branch] = StringHelper::strtolower(Text::_(LanguageHelper::branchSingular($branch))); + } + + // Container for search terms and phrases. + $terms = array(); + $phrases = array(); + + // Cleared filter branches. + $cleared = array(); + + /* + * Compile the suffix pattern. This is used to match the values of the + * filter input string. Single words can be input directly, multi-word + * values have to be wrapped in double quotes. + */ + $quotes = html_entity_decode('‘’'', ENT_QUOTES, 'UTF-8'); + $suffix = '(([\w\d' . $quotes . '-]+)|\"([\w\d\s' . $quotes . '-]+)\")'; + + /* + * Iterate through the possible filter patterns and search for matches. + * We need to match the key, colon, and a value pattern for the match + * to be valid. + */ + foreach ($patterns as $modifier => $pattern) { + $matches = array(); + + if ($debug) { + $pattern = substr($pattern, 2, -2); + } + + // Check if the filter pattern is in the input string. + if (preg_match('#' . $pattern . '\s*:\s*' . $suffix . '#mi', $input, $matches)) { + // Get the value given to the modifier. + $value = $matches[3] ?? $matches[1]; + + // Now we have to handle the filter string. + switch ($modifier) { + // Handle a before and after date filters. + case 'before': + case 'after': + // Get the time offset. + $offset = Factory::getApplication()->get('offset'); + + // Array of allowed when values. + $whens = array('before', 'after', 'exact'); + + // The value of 'today' is a special case that we need to handle. + if ($value === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) { + $value = Factory::getDate('now', $offset)->format('%Y-%m-%d'); + } + + // Try to parse the date string. + $date = Factory::getDate($value, $offset); + + // Check if the date was parsed successfully. + if ($date->toUnix() !== null) { + // Set the date filter. + $this->date1 = $date->toSql(); + $this->when1 = in_array($modifier, $whens, true) ? $modifier : 'before'; + } + + break; + + // Handle a taxonomy branch filter. + default: + // Try to find the node id. + $return = Taxonomy::getNodeByTitle($modifier, $value); + + // Check if the node id was found. + if ($return) { + // Check if the branch has been cleared. + if (!in_array($modifier, $cleared, true)) { + // Clear the branch. + $this->filters[$modifier] = array(); + + // Add the branch to the cleared list. + $cleared[] = $modifier; + } + + // Add the filter to the list. + $this->filters[$modifier][$return->title] = (int) $return->id; + } + + break; + } + + // Clean up the input string again. + $input = str_replace($matches[0], '', $input); + $input = preg_replace('#\s+#mi', ' ', $input); + $input = trim($input); + } + } + + /* + * Extract the tokens enclosed in double quotes so that we can handle + * them as phrases. + */ + if (StringHelper::strpos($input, '"') !== false) { + $matches = array(); + + // Extract the tokens enclosed in double quotes. + if (preg_match_all('#\"([^"]+)\"#m', $input, $matches)) { + /* + * One or more phrases were found so we need to iterate through + * them, tokenize them as phrases, and remove them from the raw + * input string before we move on to the next processing step. + */ + foreach ($matches[1] as $key => $match) { + // Find the complete phrase in the input string. + $pos = StringHelper::strpos($input, $matches[0][$key]); + $len = StringHelper::strlen($matches[0][$key]); + + // Add any terms that are before this phrase to the stack. + if (trim(StringHelper::substr($input, 0, $pos))) { + $terms = array_merge($terms, explode(' ', trim(StringHelper::substr($input, 0, $pos)))); + } + + // Strip out everything up to and including the phrase. + $input = StringHelper::substr($input, $pos + $len); + + // Clean up the input string again. + $input = preg_replace('#\s+#mi', ' ', $input); + $input = trim($input); + + // Get the number of words in the phrase. + $parts = explode(' ', $match); + $tuplecount = $params->get('tuplecount', 1); + + // Check if the phrase is longer than our $tuplecount. + if (count($parts) > $tuplecount && $tuplecount > 1) { + $chunk = array_slice($parts, 0, $tuplecount); + $parts = array_slice($parts, $tuplecount); + + // If the chunk is not empty, add it as a phrase. + if (count($chunk)) { + $phrases[] = implode(' ', $chunk); + $terms[] = implode(' ', $chunk); + } + + /* + * If the phrase is longer than $tuplecount words, we need to + * break it down into smaller chunks of phrases that + * are less than or equal to $tuplecount words. We overlap + * the chunks so that we can ensure that a match is + * found for the complete phrase and not just portions + * of it. + */ + for ($i = 0, $c = count($parts); $i < $c; $i++) { + array_shift($chunk); + $chunk[] = array_shift($parts); + + // If the chunk is not empty, add it as a phrase. + if (count($chunk)) { + $phrases[] = implode(' ', $chunk); + $terms[] = implode(' ', $chunk); + } + } + } else { + // The phrase is <= $tuplecount words so we can use it as is. + $phrases[] = $match; + $terms[] = $match; + } + } + } + } + + // Add the remaining terms if present. + if ((bool) $input) { + $terms = array_merge($terms, explode(' ', $input)); + } + + // An array of our boolean operators. $operator => $translation + $operators = array( + 'AND' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_AND')), + 'OR' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_OR')), + 'NOT' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_NOT')), + ); + + // If language debugging is enabled you need to ignore the debug strings in matching. + if (JDEBUG) { + $debugStrings = array('**', '??'); + $operators = str_replace($debugStrings, '', $operators); + } + + /* + * Iterate through the terms and perform any sorting that needs to be + * done based on boolean search operators. Terms that are before an + * and/or/not modifier have to be handled in relation to their operator. + */ + for ($i = 0, $c = count($terms); $i < $c; $i++) { + // Check if the term is followed by an operator that we understand. + if (isset($terms[$i + 1]) && in_array($terms[$i + 1], $operators, true)) { + // Get the operator mode. + $op = array_search($terms[$i + 1], $operators, true); + + // Handle the AND operator. + if ($op === 'AND' && isset($terms[$i + 2])) { + // Tokenize the current term. + $token = Helper::tokenize($terms[$i], $lang, true); + + // @todo: The previous function call may return an array, which seems not to be handled by the next one, which expects an object + $token = $this->getTokenData(array_shift($token)); + + if ($params->get('filter_commonwords', 0) && $token->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $token->numeric) { + continue; + } + + // Set the required flag. + $token->required = true; + + // Add the current token to the stack. + $this->included[] = $token; + $this->highlight = array_merge($this->highlight, array_keys($token->matches)); + + // Skip the next token (the mode operator). + $this->operators[] = $terms[$i + 1]; + + // Tokenize the term after the next term (current plus two). + $other = Helper::tokenize($terms[$i + 2], $lang, true); + $other = $this->getTokenData(array_shift($other)); + + // Set the required flag. + $other->required = true; + + // Add the token after the next token to the stack. + $this->included[] = $other; + $this->highlight = array_merge($this->highlight, array_keys($other->matches)); + + // Remove the processed phrases if possible. + if (($pk = array_search($terms[$i], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + // Remove the processed terms. + unset($terms[$i], $terms[$i + 1], $terms[$i + 2]); + + // Adjust the loop. + $i += 2; + } elseif ($op === 'OR' && isset($terms[$i + 2])) { + // Handle the OR operator. + // Tokenize the current term. + $token = Helper::tokenize($terms[$i], $lang, true); + $token = $this->getTokenData(array_shift($token)); + + if ($params->get('filter_commonwords', 0) && $token->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $token->numeric) { + continue; + } + + // Set the required flag. + $token->required = false; + + // Add the current token to the stack. + if ((bool) $token->matches) { + $this->included[] = $token; + $this->highlight = array_merge($this->highlight, array_keys($token->matches)); + } else { + $this->ignored[] = $token; + } + + // Skip the next token (the mode operator). + $this->operators[] = $terms[$i + 1]; + + // Tokenize the term after the next term (current plus two). + $other = Helper::tokenize($terms[$i + 2], $lang, true); + $other = $this->getTokenData(array_shift($other)); + + // Set the required flag. + $other->required = false; + + // Add the token after the next token to the stack. + if ((bool) $other->matches) { + $this->included[] = $other; + $this->highlight = array_merge($this->highlight, array_keys($other->matches)); + } else { + $this->ignored[] = $other; + } + + // Remove the processed phrases if possible. + if (($pk = array_search($terms[$i], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + // Remove the processed terms. + unset($terms[$i], $terms[$i + 1], $terms[$i + 2]); + + // Adjust the loop. + $i += 2; + } + } elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'OR') { + // Handle an orphaned OR operator. + // Skip the next token (the mode operator). + $this->operators[] = $terms[$i]; + + // Tokenize the next term (current plus one). + $other = Helper::tokenize($terms[$i + 1], $lang, true); + $other = $this->getTokenData(array_shift($other)); + + if ($params->get('filter_commonwords', 0) && $other->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $other->numeric) { + continue; + } + + // Set the required flag. + $other->required = false; + + // Add the token after the next token to the stack. + if ((bool) $other->matches) { + $this->included[] = $other; + $this->highlight = array_merge($this->highlight, array_keys($other->matches)); + } else { + $this->ignored[] = $other; + } + + // Remove the processed phrase if possible. + if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + // Remove the processed terms. + unset($terms[$i], $terms[$i + 1]); + + // Adjust the loop. + $i++; + } elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'NOT') { + // Handle the NOT operator. + // Skip the next token (the mode operator). + $this->operators[] = $terms[$i]; + + // Tokenize the next term (current plus one). + $other = Helper::tokenize($terms[$i + 1], $lang, true); + $other = $this->getTokenData(array_shift($other)); + + if ($params->get('filter_commonwords', 0) && $other->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $other->numeric) { + continue; + } + + // Set the required flag. + $other->required = false; + + // Add the next token to the stack. + if ((bool) $other->matches) { + $this->excluded[] = $other; + } else { + $this->ignored[] = $other; + } + + // Remove the processed phrase if possible. + if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + // Remove the processed terms. + unset($terms[$i], $terms[$i + 1]); + + // Adjust the loop. + $i++; + } + } + + /* + * Iterate through any search phrases and tokenize them. We handle + * phrases as autonomous units and do not break them down into two and + * three word combinations. + */ + for ($i = 0, $c = count($phrases); $i < $c; $i++) { + // Tokenize the phrase. + $token = Helper::tokenize($phrases[$i], $lang, true); + + if (!count($token)) { + continue; + } + + $token = $this->getTokenData(array_shift($token)); + + if ($params->get('filter_commonwords', 0) && $token->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $token->numeric) { + continue; + } + + // Set the required flag. + $token->required = true; + + // Add the current token to the stack. + $this->included[] = $token; + $this->highlight = array_merge($this->highlight, array_keys($token->matches)); + + // Remove the processed term if possible. + if (($pk = array_search($phrases[$i], $terms, true)) !== false) { + unset($terms[$pk]); + } + + // Remove the processed phrase. + unset($phrases[$i]); + } + + /* + * Handle any remaining tokens using the standard processing mechanism. + */ + if ((bool) $terms) { + // Tokenize the terms. + $terms = implode(' ', $terms); + $tokens = Helper::tokenize($terms, $lang, false); + + // Make sure we are working with an array. + $tokens = is_array($tokens) ? $tokens : array($tokens); + + // Get the token data and required state for all the tokens. + foreach ($tokens as $token) { + // Get the token data. + $token = $this->getTokenData($token); + + if ($params->get('filter_commonwords', 0) && $token->common) { + continue; + } + + if ($params->get('filter_numerics', 0) && $token->numeric) { + continue; + } + + // Set the required flag for the token. + $token->required = $mode === 'AND' ? (!$token->phrase) : false; + + // Add the token to the appropriate stack. + if ($token->required || (bool) $token->matches) { + $this->included[] = $token; + $this->highlight = array_merge($this->highlight, array_keys($token->matches)); + } else { + $this->ignored[] = $token; + } + } + } + + return true; + } + + /** + * Method to get the base and similar term ids and, if necessary, suggested + * term data from the database. The terms ids are identified based on a + * 'like' match in MySQL and/or a common stem. If no term ids could be + * found, then we know that we will not be able to return any results for + * that term and we should try to find a similar term to use that we can + * match so that we can suggest the alternative search query to the user. + * + * @param Token $token A Token object. + * + * @return Token A Token object. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getTokenData($token) + { + // Get the database object. + $db = $this->getDatabase(); + + // Create a database query to build match the token. + $query = $db->getQuery(true) + ->select('t.term, t.term_id') + ->from('#__finder_terms AS t'); + + if ($token->phrase) { + // Add the phrase to the query. + $query->where('t.term = ' . $db->quote($token->term)) + ->where('t.phrase = 1'); + } else { + // Add the term to the query. + + $searchTerm = $token->term; + $searchStem = $token->stem; + $term = $query->quoteName('t.term'); + $stem = $query->quoteName('t.stem'); + + if ($this->wordmode === 'begin') { + $searchTerm .= '%'; + $searchStem .= '%'; + $query->where('(' . $term . ' LIKE :searchTerm OR ' . $stem . ' LIKE :searchStem)'); + } elseif ($this->wordmode === 'fuzzy') { + $searchTerm = '%' . $searchTerm . '%'; + $searchStem = '%' . $searchStem . '%'; + $query->where('(' . $term . ' LIKE :searchTerm OR ' . $stem . ' LIKE :searchStem)'); + } else { + $query->where('(' . $term . ' = :searchTerm OR ' . $stem . ' = :searchStem)'); + } + + $query->bind(':searchTerm', $searchTerm, ParameterType::STRING) + ->bind(':searchStem', $searchStem, ParameterType::STRING); + + $query->where('t.phrase = 0') + ->where('t.language IN (\'*\',' . $db->quote($token->language) . ')'); + } + + // Get the terms. + $db->setQuery($query); + $matches = $db->loadObjectList(); + + // Check the matching terms. + if ((bool) $matches) { + // Add the matches to the token. + for ($i = 0, $c = count($matches); $i < $c; $i++) { + if (!isset($token->matches[$matches[$i]->term])) { + $token->matches[$matches[$i]->term] = array(); + } + + $token->matches[$matches[$i]->term][] = (int) $matches[$i]->term_id; + } + } + + // If no matches were found, try to find a similar but better token. + if (empty($token->matches)) { + // Create a database query to get the similar terms. + $query->clear() + ->select('DISTINCT t.term_id AS id, t.term AS term') + ->from('#__finder_terms AS t') + // ->where('t.soundex = ' . soundex($db->quote($token->term))) + ->where('t.soundex = SOUNDEX(' . $db->quote($token->term) . ')') + ->where('t.phrase = ' . (int) $token->phrase); + + // Get the terms. + $db->setQuery($query); + $results = $db->loadObjectList(); + + // Check if any similar terms were found. + if (empty($results)) { + return $token; + } + + // Stack for sorting the similar terms. + $suggestions = array(); + + // Get the levnshtein distance for all suggested terms. + foreach ($results as $sk => $st) { + // Get the levenshtein distance between terms. + $distance = levenshtein($st->term, $token->term); + + // Make sure the levenshtein distance isn't over 50. + if ($distance < 50) { + $suggestions[$sk] = $distance; + } + } + + // Sort the suggestions. + asort($suggestions, SORT_NUMERIC); + + // Get the closest match. + $keys = array_keys($suggestions); + $key = $keys[0]; + + // Add the suggested term. + $token->suggestion = $results[$key]->term; + } + + return $token; + } } diff --git a/code/administrator/components/com_finder/src/Indexer/Result.php b/code/administrator/components/com_finder/src/Indexer/Result.php index cef7d04f..4d7e9abc 100644 --- a/code/administrator/components/com_finder/src/Indexer/Result.php +++ b/code/administrator/components/com_finder/src/Indexer/Result.php @@ -1,4 +1,5 @@ array('title', 'subtitle', 'id'), - Indexer::TEXT_CONTEXT => array('summary', 'body'), - Indexer::META_CONTEXT => array('meta', 'list_price', 'sale_price'), - Indexer::PATH_CONTEXT => array('path', 'alias'), - Indexer::MISC_CONTEXT => array('comments'), - ); - - /** - * The indexer will use this data to create taxonomy mapping entries for - * the item so that it can be filtered by type, label, category, - * or whatever. - * - * @var array - * @since 2.5 - */ - protected $taxonomy = array(); - - /** - * The content URL. - * - * @var string - * @since 2.5 - */ - public $url; - - /** - * The content route. - * - * @var string - * @since 2.5 - */ - public $route; - - /** - * The content title. - * - * @var string - * @since 2.5 - */ - public $title; - - /** - * The content description. - * - * @var string - * @since 2.5 - */ - public $description; - - /** - * The published state of the result. - * - * @var integer - * @since 2.5 - */ - public $published; - - /** - * The content published state. - * - * @var integer - * @since 2.5 - */ - public $state; - - /** - * The content access level. - * - * @var integer - * @since 2.5 - */ - public $access; - - /** - * The content language. - * - * @var string - * @since 2.5 - */ - public $language = '*'; - - /** - * The publishing start date. - * - * @var string - * @since 2.5 - */ - public $publish_start_date; - - /** - * The publishing end date. - * - * @var string - * @since 2.5 - */ - public $publish_end_date; - - /** - * The generic start date. - * - * @var string - * @since 2.5 - */ - public $start_date; - - /** - * The generic end date. - * - * @var string - * @since 2.5 - */ - public $end_date; - - /** - * The item list price. - * - * @var mixed - * @since 2.5 - */ - public $list_price; - - /** - * The item sale price. - * - * @var mixed - * @since 2.5 - */ - public $sale_price; - - /** - * The content type id. This is set by the adapter. - * - * @var integer - * @since 2.5 - */ - public $type_id; - - /** - * The default language for content. - * - * @var string - * @since 3.0.2 - */ - public $defaultLanguage; - - /** - * Constructor - * - * @since 3.0.3 - */ - public function __construct() - { - $this->defaultLanguage = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); - } - - /** - * The magic set method is used to push additional values into the elements - * array in order to preserve the cleanliness of the object. - * - * @param string $name The name of the element. - * @param mixed $value The value of the element. - * - * @return void - * - * @since 2.5 - */ - public function __set($name, $value) - { - $this->setElement($name, $value); - } - - /** - * The magic get method is used to retrieve additional element values from the elements array. - * - * @param string $name The name of the element. - * - * @return mixed The value of the element if set, null otherwise. - * - * @since 2.5 - */ - public function __get($name) - { - return $this->getElement($name); - } - - /** - * The magic isset method is used to check the state of additional element values in the elements array. - * - * @param string $name The name of the element. - * - * @return boolean True if set, false otherwise. - * - * @since 2.5 - */ - public function __isset($name) - { - return isset($this->elements[$name]); - } - - /** - * The magic unset method is used to unset additional element values in the elements array. - * - * @param string $name The name of the element. - * - * @return void - * - * @since 2.5 - */ - public function __unset($name) - { - unset($this->elements[$name]); - } - - /** - * Method to retrieve additional element values from the elements array. - * - * @param string $name The name of the element. - * - * @return mixed The value of the element if set, null otherwise. - * - * @since 2.5 - */ - public function getElement($name) - { - // Get the element value if set. - if (array_key_exists($name, $this->elements)) - { - return $this->elements[$name]; - } - - return null; - } - - /** - * Method to retrieve all elements. - * - * @return array The elements - * - * @since 3.8.3 - */ - public function getElements() - { - return $this->elements; - } - - /** - * Method to set additional element values in the elements array. - * - * @param string $name The name of the element. - * @param mixed $value The value of the element. - * - * @return void - * - * @since 2.5 - */ - public function setElement($name, $value) - { - $this->elements[$name] = $value; - } - - /** - * Method to get all processing instructions. - * - * @return array An array of processing instructions. - * - * @since 2.5 - */ - public function getInstructions() - { - return $this->instructions; - } - - /** - * Method to add a processing instruction for an item property. - * - * @param string $group The group to associate the property with. - * @param string $property The property to process. - * - * @return void - * - * @since 2.5 - */ - public function addInstruction($group, $property) - { - // Check if the group exists. We can't add instructions for unknown groups. - // Check if the property exists in the group. - if (array_key_exists($group, $this->instructions) && !in_array($property, $this->instructions[$group], true)) - { - // Add the property to the group. - $this->instructions[$group][] = $property; - } - } - - /** - * Method to remove a processing instruction for an item property. - * - * @param string $group The group to associate the property with. - * @param string $property The property to process. - * - * @return void - * - * @since 2.5 - */ - public function removeInstruction($group, $property) - { - // Check if the group exists. We can't remove instructions for unknown groups. - if (array_key_exists($group, $this->instructions)) - { - // Search for the property in the group. - $key = array_search($property, $this->instructions[$group]); - - // If the property was found, remove it. - if ($key !== false) - { - unset($this->instructions[$group][$key]); - } - } - } - - /** - * Method to get the taxonomy maps for an item. - * - * @param string $branch The taxonomy branch to get. [optional] - * - * @return array An array of taxonomy maps. - * - * @since 2.5 - */ - public function getTaxonomy($branch = null) - { - // Get the taxonomy branch if available. - if ($branch !== null && isset($this->taxonomy[$branch])) - { - return $this->taxonomy[$branch]; - } - - return $this->taxonomy; - } - - /** - * Method to add a taxonomy map for an item. - * - * @param string $branch The title of the taxonomy branch to add the node to. - * @param string $title The title of the taxonomy node. - * @param integer $state The published state of the taxonomy node. [optional] - * @param integer $access The access level of the taxonomy node. [optional] - * @param string $language The language of the taxonomy. [optional] - * - * @return void - * - * @since 2.5 - */ - public function addTaxonomy($branch, $title, $state = 1, $access = 1, $language = '') - { - // Filter the input. - $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); - - // Create the taxonomy node. - $node = new \stdClass; - $node->title = $title; - $node->state = (int) $state; - $node->access = (int) $access; - $node->language = $language; - $node->nested = false; - - // Add the node to the taxonomy branch. - $this->taxonomy[$branch][] = $node; - } - - /** - * Method to add a nested taxonomy map for an item. - * - * @param string $branch The title of the taxonomy branch to add the node to. - * @param ImmutableNodeInterface $contentNode The node object. - * @param integer $state The published state of the taxonomy node. [optional] - * @param integer $access The access level of the taxonomy node. [optional] - * @param string $language The language of the taxonomy. [optional] - * - * @return void - * - * @since 4.0.0 - */ - public function addNestedTaxonomy($branch, ImmutableNodeInterface $contentNode, $state = 1, $access = 1, $language = '') - { - // Filter the input. - $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); - - // Create the taxonomy node. - $node = new \stdClass; - $node->title = $contentNode->title; - $node->state = (int) $state; - $node->access = (int) $access; - $node->language = $language; - $node->nested = true; - $node->node = $contentNode; - - // Add the node to the taxonomy branch. - $this->taxonomy[$branch][] = $node; - } - - /** - * Method to set the item language - * - * @return void - * - * @since 3.0 - */ - public function setLanguage() - { - if ($this->language == '') - { - $this->language = $this->defaultLanguage; - } - } - - /** - * Helper function to serialise the data of a Result object - * - * @return string The serialised data - * - * @since 4.0.0 - */ - public function serialize() - { - return serialize($this->__serialize()); - } - - /** - * Helper function to unserialise the data for this object - * - * @param string $serialized Serialised data to unserialise - * - * @return void - * - * @since 4.0.0 - */ - public function unserialize($serialized): void - { - $this->__unserialize(unserialize($serialized)); - } - - /** - * Magic method used for serializing. - * - * @since 4.1.3 - */ - public function __serialize(): array - { - $taxonomy = []; - - foreach ($this->taxonomy as $branch => $nodes) - { - $taxonomy[$branch] = []; - - foreach ($nodes as $node) - { - if ($node->nested) - { - $n = clone $node; - unset($n->node); - $taxonomy[$branch][] = $n; - } - else - { - $taxonomy[$branch][] = $node; - } - } - } - - // This order must match EXACTLY the order of the $properties in the self::__unserialize method - return [ - $this->access, - $this->defaultLanguage, - $this->description, - $this->elements, - $this->end_date, - $this->instructions, - $this->language, - $this->list_price, - $this->publish_end_date, - $this->publish_start_date, - $this->published, - $this->route, - $this->sale_price, - $this->start_date, - $this->state, - $taxonomy, - $this->title, - $this->type_id, - $this->url - ]; - } - - /** - * Magic method used for unserializing. - * - * @since 4.1.3 - */ - public function __unserialize(array $serialized): void - { - // This order must match EXACTLY the order of the array in the self::__serialize method - $properties = [ - 'access', - 'defaultLanguage', - 'description', - 'elements', - 'end_date', - 'instructions', - 'language', - 'list_price', - 'publish_end_date', - 'publish_start_date', - 'published', - 'route', - 'sale_price', - 'start_date', - 'state', - 'taxonomy', - 'title', - 'type_id', - 'url', - ]; - - foreach ($properties as $k => $v) - { - $this->$v = $serialized[$k]; - } - - foreach ($this->taxonomy as $nodes) - { - foreach ($nodes as $node) - { - $curTaxonomy = Taxonomy::getTaxonomy($node->id); - $node->state = $curTaxonomy->state; - $node->access = $curTaxonomy->access; - } - } - } + /** + * An array of extra result properties. + * + * @var array + * @since 2.5 + */ + protected $elements = array(); + + /** + * This array tells the indexer which properties should be indexed and what + * weights to use for those properties. + * + * @var array + * @since 2.5 + */ + protected $instructions = array( + Indexer::TITLE_CONTEXT => array('title', 'subtitle', 'id'), + Indexer::TEXT_CONTEXT => array('summary', 'body'), + Indexer::META_CONTEXT => array('meta', 'list_price', 'sale_price'), + Indexer::PATH_CONTEXT => array('path', 'alias'), + Indexer::MISC_CONTEXT => array('comments'), + ); + + /** + * The indexer will use this data to create taxonomy mapping entries for + * the item so that it can be filtered by type, label, category, + * or whatever. + * + * @var array + * @since 2.5 + */ + protected $taxonomy = array(); + + /** + * The content URL. + * + * @var string + * @since 2.5 + */ + public $url; + + /** + * The content route. + * + * @var string + * @since 2.5 + */ + public $route; + + /** + * The content title. + * + * @var string + * @since 2.5 + */ + public $title; + + /** + * The content description. + * + * @var string + * @since 2.5 + */ + public $description; + + /** + * The published state of the result. + * + * @var integer + * @since 2.5 + */ + public $published; + + /** + * The content published state. + * + * @var integer + * @since 2.5 + */ + public $state; + + /** + * The content access level. + * + * @var integer + * @since 2.5 + */ + public $access; + + /** + * The content language. + * + * @var string + * @since 2.5 + */ + public $language = '*'; + + /** + * The publishing start date. + * + * @var string + * @since 2.5 + */ + public $publish_start_date; + + /** + * The publishing end date. + * + * @var string + * @since 2.5 + */ + public $publish_end_date; + + /** + * The generic start date. + * + * @var string + * @since 2.5 + */ + public $start_date; + + /** + * The generic end date. + * + * @var string + * @since 2.5 + */ + public $end_date; + + /** + * The item list price. + * + * @var mixed + * @since 2.5 + */ + public $list_price; + + /** + * The item sale price. + * + * @var mixed + * @since 2.5 + */ + public $sale_price; + + /** + * The content type id. This is set by the adapter. + * + * @var integer + * @since 2.5 + */ + public $type_id; + + /** + * The default language for content. + * + * @var string + * @since 3.0.2 + */ + public $defaultLanguage; + + /** + * Constructor + * + * @since 3.0.3 + */ + public function __construct() + { + $this->defaultLanguage = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); + } + + /** + * The magic set method is used to push additional values into the elements + * array in order to preserve the cleanliness of the object. + * + * @param string $name The name of the element. + * @param mixed $value The value of the element. + * + * @return void + * + * @since 2.5 + */ + public function __set($name, $value) + { + $this->setElement($name, $value); + } + + /** + * The magic get method is used to retrieve additional element values from the elements array. + * + * @param string $name The name of the element. + * + * @return mixed The value of the element if set, null otherwise. + * + * @since 2.5 + */ + public function __get($name) + { + return $this->getElement($name); + } + + /** + * The magic isset method is used to check the state of additional element values in the elements array. + * + * @param string $name The name of the element. + * + * @return boolean True if set, false otherwise. + * + * @since 2.5 + */ + public function __isset($name) + { + return isset($this->elements[$name]); + } + + /** + * The magic unset method is used to unset additional element values in the elements array. + * + * @param string $name The name of the element. + * + * @return void + * + * @since 2.5 + */ + public function __unset($name) + { + unset($this->elements[$name]); + } + + /** + * Method to retrieve additional element values from the elements array. + * + * @param string $name The name of the element. + * + * @return mixed The value of the element if set, null otherwise. + * + * @since 2.5 + */ + public function getElement($name) + { + // Get the element value if set. + if (array_key_exists($name, $this->elements)) { + return $this->elements[$name]; + } + + return null; + } + + /** + * Method to retrieve all elements. + * + * @return array The elements + * + * @since 3.8.3 + */ + public function getElements() + { + return $this->elements; + } + + /** + * Method to set additional element values in the elements array. + * + * @param string $name The name of the element. + * @param mixed $value The value of the element. + * + * @return void + * + * @since 2.5 + */ + public function setElement($name, $value) + { + $this->elements[$name] = $value; + } + + /** + * Method to get all processing instructions. + * + * @return array An array of processing instructions. + * + * @since 2.5 + */ + public function getInstructions() + { + return $this->instructions; + } + + /** + * Method to add a processing instruction for an item property. + * + * @param string $group The group to associate the property with. + * @param string $property The property to process. + * + * @return void + * + * @since 2.5 + */ + public function addInstruction($group, $property) + { + // Check if the group exists. We can't add instructions for unknown groups. + // Check if the property exists in the group. + if (array_key_exists($group, $this->instructions) && !in_array($property, $this->instructions[$group], true)) { + // Add the property to the group. + $this->instructions[$group][] = $property; + } + } + + /** + * Method to remove a processing instruction for an item property. + * + * @param string $group The group to associate the property with. + * @param string $property The property to process. + * + * @return void + * + * @since 2.5 + */ + public function removeInstruction($group, $property) + { + // Check if the group exists. We can't remove instructions for unknown groups. + if (array_key_exists($group, $this->instructions)) { + // Search for the property in the group. + $key = array_search($property, $this->instructions[$group]); + + // If the property was found, remove it. + if ($key !== false) { + unset($this->instructions[$group][$key]); + } + } + } + + /** + * Method to get the taxonomy maps for an item. + * + * @param string $branch The taxonomy branch to get. [optional] + * + * @return array An array of taxonomy maps. + * + * @since 2.5 + */ + public function getTaxonomy($branch = null) + { + // Get the taxonomy branch if available. + if ($branch !== null && isset($this->taxonomy[$branch])) { + return $this->taxonomy[$branch]; + } + + return $this->taxonomy; + } + + /** + * Method to add a taxonomy map for an item. + * + * @param string $branch The title of the taxonomy branch to add the node to. + * @param string $title The title of the taxonomy node. + * @param integer $state The published state of the taxonomy node. [optional] + * @param integer $access The access level of the taxonomy node. [optional] + * @param string $language The language of the taxonomy. [optional] + * + * @return void + * + * @since 2.5 + */ + public function addTaxonomy($branch, $title, $state = 1, $access = 1, $language = '') + { + // Filter the input. + $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); + + // Create the taxonomy node. + $node = new \stdClass(); + $node->title = $title; + $node->state = (int) $state; + $node->access = (int) $access; + $node->language = $language; + $node->nested = false; + + // Add the node to the taxonomy branch. + $this->taxonomy[$branch][] = $node; + } + + /** + * Method to add a nested taxonomy map for an item. + * + * @param string $branch The title of the taxonomy branch to add the node to. + * @param ImmutableNodeInterface $contentNode The node object. + * @param integer $state The published state of the taxonomy node. [optional] + * @param integer $access The access level of the taxonomy node. [optional] + * @param string $language The language of the taxonomy. [optional] + * + * @return void + * + * @since 4.0.0 + */ + public function addNestedTaxonomy($branch, ImmutableNodeInterface $contentNode, $state = 1, $access = 1, $language = '') + { + // Filter the input. + $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); + + // Create the taxonomy node. + $node = new \stdClass(); + $node->title = $contentNode->title; + $node->state = (int) $state; + $node->access = (int) $access; + $node->language = $language; + $node->nested = true; + $node->node = $contentNode; + + // Add the node to the taxonomy branch. + $this->taxonomy[$branch][] = $node; + } + + /** + * Method to set the item language + * + * @return void + * + * @since 3.0 + */ + public function setLanguage() + { + if ($this->language == '') { + $this->language = $this->defaultLanguage; + } + } + + /** + * Helper function to serialise the data of a Result object + * + * @return string The serialised data + * + * @since 4.0.0 + */ + public function serialize() + { + return serialize($this->__serialize()); + } + + /** + * Helper function to unserialise the data for this object + * + * @param string $serialized Serialised data to unserialise + * + * @return void + * + * @since 4.0.0 + */ + public function unserialize($serialized): void + { + $this->__unserialize(unserialize($serialized)); + } + + /** + * Magic method used for serializing. + * + * @since 4.1.3 + */ + public function __serialize(): array + { + $taxonomy = []; + + foreach ($this->taxonomy as $branch => $nodes) { + $taxonomy[$branch] = []; + + foreach ($nodes as $node) { + if ($node->nested) { + $n = clone $node; + unset($n->node); + $taxonomy[$branch][] = $n; + } else { + $taxonomy[$branch][] = $node; + } + } + } + + // This order must match EXACTLY the order of the $properties in the self::__unserialize method + return [ + $this->access, + $this->defaultLanguage, + $this->description, + $this->elements, + $this->end_date, + $this->instructions, + $this->language, + $this->list_price, + $this->publish_end_date, + $this->publish_start_date, + $this->published, + $this->route, + $this->sale_price, + $this->start_date, + $this->state, + $taxonomy, + $this->title, + $this->type_id, + $this->url + ]; + } + + /** + * Magic method used for unserializing. + * + * @since 4.1.3 + */ + public function __unserialize(array $serialized): void + { + // This order must match EXACTLY the order of the array in the self::__serialize method + $properties = [ + 'access', + 'defaultLanguage', + 'description', + 'elements', + 'end_date', + 'instructions', + 'language', + 'list_price', + 'publish_end_date', + 'publish_start_date', + 'published', + 'route', + 'sale_price', + 'start_date', + 'state', + 'taxonomy', + 'title', + 'type_id', + 'url', + ]; + + foreach ($properties as $k => $v) { + $this->$v = $serialized[$k]; + } + + foreach ($this->taxonomy as $nodes) { + foreach ($nodes as $node) { + $curTaxonomy = Taxonomy::getTaxonomy($node->id); + $node->state = $curTaxonomy->state; + $node->access = $curTaxonomy->access; + } + } + } } diff --git a/code/administrator/components/com_finder/src/Indexer/Taxonomy.php b/code/administrator/components/com_finder/src/Indexer/Taxonomy.php index c19653cb..010983ac 100644 --- a/code/administrator/components/com_finder/src/Indexer/Taxonomy.php +++ b/code/administrator/components/com_finder/src/Indexer/Taxonomy.php @@ -1,4 +1,5 @@ title = $title; - $node->state = $state; - $node->access = $access; - $node->parent_id = 1; - $node->language = ''; - - return self::storeNode($node, 1); - } - - /** - * Method to add a node to the taxonomy tree. - * - * @param string $branch The title of the branch to store the node in. - * @param string $title The title of the node. - * @param integer $state The published state of the node. [optional] - * @param integer $access The access state of the node. [optional] - * @param string $language The language of the node. [optional] - * - * @return integer The id of the node. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function addNode($branch, $title, $state = 1, $access = 1, $language = '') - { - // Get the branch id, insert it if it does not exist. - $branchId = static::addBranch($branch); - - $node = new \stdClass; - $node->title = $title; - $node->state = $state; - $node->access = $access; - $node->parent_id = $branchId; - $node->language = $language; - - return self::storeNode($node, $branchId); - } - - /** - * Method to add a nested node to the taxonomy tree. - * - * @param string $branch The title of the branch to store the node in. - * @param NodeInterface $node The source-node of the taxonomy node. - * @param integer $state The published state of the node. [optional] - * @param integer $access The access state of the node. [optional] - * @param string $language The language of the node. [optional] - * @param integer $branchId ID of a branch if known. [optional] - * - * @return integer The id of the node. - * - * @since 4.0.0 - */ - public static function addNestedNode($branch, NodeInterface $node, $state = 1, $access = 1, $language = '', $branchId = null) - { - if (!$branchId) - { - // Get the branch id, insert it if it does not exist. - $branchId = static::addBranch($branch); - } - - $parent = $node->getParent(); - - if ($parent && $parent->title != 'ROOT') - { - $parentId = self::addNestedNode($branch, $parent, $state, $access, $language, $branchId); - } - else - { - $parentId = $branchId; - } - - $temp = new \stdClass; - $temp->title = $node->title; - $temp->state = $state; - $temp->access = $access; - $temp->parent_id = $parentId; - $temp->language = $language; - - return self::storeNode($temp, $parentId); - } - - /** - * A helper method to store a node in the taxonomy - * - * @param object $node The node data to include - * @param integer $parentId The parent id of the node to add. - * - * @return integer The id of the inserted node. - * - * @since 4.0.0 - * @throws \RuntimeException - */ - protected static function storeNode($node, $parentId) - { - // Check to see if the node is in the cache. - if (isset(static::$nodes[$parentId . ':' . $node->title])) - { - return static::$nodes[$parentId . ':' . $node->title]->id; - } - - // Check to see if the node is in the table. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' = ' . $db->quote($parentId)) - ->where($db->quoteName('title') . ' = ' . $db->quote($node->title)) - ->where($db->quoteName('language') . ' = ' . $db->quote($node->language)); - - $db->setQuery($query); - - // Get the result. - $result = $db->loadObject(); - - // Check if the database matches the input data. - if ((bool) $result && $result->state == $node->state && $result->access == $node->access) - { - // The data matches, add the item to the cache. - static::$nodes[$parentId . ':' . $node->title] = $result; - - return static::$nodes[$parentId . ':' . $node->title]->id; - } - - /* - * The database did not match the input. This could be because the - * state has changed or because the node does not exist. Let's figure - * out which case is true and deal with it. - * @todo: use factory? - */ - $nodeTable = new MapTable($db); - - if (empty($result)) - { - // Prepare the node object. - $nodeTable->title = $node->title; - $nodeTable->state = (int) $node->state; - $nodeTable->access = (int) $node->access; - $nodeTable->language = $node->language; - $nodeTable->setLocation((int) $parentId, 'last-child'); - } - else - { - // Prepare the node object. - $nodeTable->id = (int) $result->id; - $nodeTable->title = $result->title; - $nodeTable->state = (int) ($node->state > 0 ? $node->state : $result->state); - $nodeTable->access = (int) $result->access; - $nodeTable->language = $node->language; - $nodeTable->setLocation($result->parent_id, 'last-child'); - } - - // Check the data. - if (!$nodeTable->check()) - { - $error = $nodeTable->getError(); - - if ($error instanceof \Exception) - { - // \Joomla\CMS\Table\NestedTable sets errors of exceptions, so in this case we can pass on more - // information - throw new \RuntimeException( - $error->getMessage(), - $error->getCode(), - $error - ); - } - - // Standard string returned. Probably from the \Joomla\CMS\Table\Table class - throw new \RuntimeException($error, 500); - } - - // Store the data. - if (!$nodeTable->store()) - { - $error = $nodeTable->getError(); - - if ($error instanceof \Exception) - { - // \Joomla\CMS\Table\NestedTable sets errors of exceptions, so in this case we can pass on more - // information - throw new \RuntimeException( - $error->getMessage(), - $error->getCode(), - $error - ); - } - - // Standard string returned. Probably from the \Joomla\CMS\Table\Table class - throw new \RuntimeException($error, 500); - } - - $nodeTable->rebuildPath($nodeTable->id); - - // Add the node to the cache. - static::$nodes[$parentId . ':' . $nodeTable->title] = (object) $nodeTable->getProperties(); - - return static::$nodes[$parentId . ':' . $nodeTable->title]->id; - } - - /** - * Method to add a map entry between a link and a taxonomy node. - * - * @param integer $linkId The link to map to. - * @param integer $nodeId The node to map to. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function addMap($linkId, $nodeId) - { - // Insert the map. - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName('link_id')) - ->from($db->quoteName('#__finder_taxonomy_map')) - ->where($db->quoteName('link_id') . ' = ' . (int) $linkId) - ->where($db->quoteName('node_id') . ' = ' . (int) $nodeId); - $db->setQuery($query); - $db->execute(); - $id = (int) $db->loadResult(); - - if (!$id) - { - $map = new \stdClass; - $map->link_id = (int) $linkId; - $map->node_id = (int) $nodeId; - $db->insertObject('#__finder_taxonomy_map', $map); - } - - return true; - } - - /** - * Method to get the title of all taxonomy branches. - * - * @return array An array of branch titles. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function getBranchTitles() - { - $db = Factory::getDbo(); - - // Set user variables - $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); - - // Create a query to get the taxonomy branch titles. - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' = 1') - ->where($db->quoteName('state') . ' = 1') - ->where($db->quoteName('access') . ' IN (' . $groups . ')'); - - // Get the branch titles. - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Method to find a taxonomy node in a branch. - * - * @param string $branch The branch to search. - * @param string $title The title of the node. - * - * @return mixed Integer id on success, null on no match. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function getNodeByTitle($branch, $title) - { - $db = Factory::getDbo(); - - // Set user variables - $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); - - // Create a query to get the node. - $query = $db->getQuery(true) - ->select('t1.*') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') - ->where('t1.access IN (' . $groups . ')') - ->where('t1.state = 1') - ->where('t1.title LIKE ' . $db->quote($db->escape($title) . '%')) - ->where('t2.access IN (' . $groups . ')') - ->where('t2.state = 1') - ->where('t2.title = ' . $db->quote($branch)); - - // Get the node. - $query->setLimit(1); - $db->setQuery($query); - - return $db->loadObject(); - } - - /** - * Method to remove map entries for a link. - * - * @param integer $linkId The link to remove. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function removeMaps($linkId) - { - // Delete the maps. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__finder_taxonomy_map')) - ->where($db->quoteName('link_id') . ' = ' . (int) $linkId); - $db->setQuery($query); - $db->execute(); - - return true; - } - - /** - * Method to remove orphaned taxonomy nodes and branches. - * - * @return integer The number of deleted rows. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function removeOrphanNodes() - { - // Delete all orphaned nodes. - $affectedRows = 0; - $db = Factory::getDbo(); - $nodeTable = new MapTable($db); - $query = $db->getQuery(true); - - $query->select($db->quoteName('t.id')) - ->from($db->quoteName('#__finder_taxonomy', 't')) - ->join('LEFT', $db->quoteName('#__finder_taxonomy_map', 'm') . ' ON ' . $db->quoteName('m.node_id') . '=' . $db->quoteName('t.id')) - ->where($db->quoteName('t.parent_id') . ' > 1 ') - ->where('t.lft + 1 = t.rgt') - ->where($db->quoteName('m.link_id') . ' IS NULL'); - - do - { - $db->setQuery($query); - $nodes = $db->loadColumn(); - - foreach ($nodes as $node) - { - $nodeTable->delete($node); - $affectedRows++; - } - } - while ($nodes); - - return $affectedRows; - } - - /** - * Get a taxonomy based on its id or all taxonomies - * - * @param integer $id Id of the taxonomy - * - * @return object|array A taxonomy object or an array of all taxonomies - * - * @since 4.0.0 - */ - public static function getTaxonomy($id = 0) - { - if (!count(self::$taxonomies)) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - $query->select(array('id','parent_id','lft','rgt','level','path','title','alias','state','access','language')) - ->from($db->quoteName('#__finder_taxonomy')) - ->order($db->quoteName('lft')); - - $db->setQuery($query); - self::$taxonomies = $db->loadObjectList('id'); - } - - if ($id == 0) - { - return self::$taxonomies; - } - - if (isset(self::$taxonomies[$id])) - { - return self::$taxonomies[$id]; - } - - return false; - } - - /** - * Get a taxonomy branch object based on its title or all branches - * - * @param string $title Title of the branch - * - * @return object|array The object with the branch data or an array of all branches - * - * @since 4.0.0 - */ - public static function getBranch($title = '') - { - if (!count(self::$branches)) - { - $taxonomies = self::getTaxonomy(); - - foreach ($taxonomies as $t) - { - if ($t->level == 1) - { - self::$branches[$t->title] = $t; - } - } - } - - if ($title == '') - { - return self::$branches; - } - - if (isset(self::$branches[$title])) - { - return self::$branches[$title]; - } - - return false; - } + /** + * An internal cache of taxonomy data. + * + * @var object[] + * @since 4.0.0 + */ + public static $taxonomies = array(); + + /** + * An internal cache of branch data. + * + * @var object[] + * @since 4.0.0 + */ + public static $branches = array(); + + /** + * An internal cache of taxonomy node data for inserting it. + * + * @var object[] + * @since 2.5 + */ + public static $nodes = array(); + + /** + * Method to add a branch to the taxonomy tree. + * + * @param string $title The title of the branch. + * @param integer $state The published state of the branch. [optional] + * @param integer $access The access state of the branch. [optional] + * + * @return integer The id of the branch. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function addBranch($title, $state = 1, $access = 1) + { + $node = new \stdClass(); + $node->title = $title; + $node->state = $state; + $node->access = $access; + $node->parent_id = 1; + $node->language = ''; + + return self::storeNode($node, 1); + } + + /** + * Method to add a node to the taxonomy tree. + * + * @param string $branch The title of the branch to store the node in. + * @param string $title The title of the node. + * @param integer $state The published state of the node. [optional] + * @param integer $access The access state of the node. [optional] + * @param string $language The language of the node. [optional] + * + * @return integer The id of the node. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function addNode($branch, $title, $state = 1, $access = 1, $language = '') + { + // Get the branch id, insert it if it does not exist. + $branchId = static::addBranch($branch); + + $node = new \stdClass(); + $node->title = $title; + $node->state = $state; + $node->access = $access; + $node->parent_id = $branchId; + $node->language = $language; + + return self::storeNode($node, $branchId); + } + + /** + * Method to add a nested node to the taxonomy tree. + * + * @param string $branch The title of the branch to store the node in. + * @param NodeInterface $node The source-node of the taxonomy node. + * @param integer $state The published state of the node. [optional] + * @param integer $access The access state of the node. [optional] + * @param string $language The language of the node. [optional] + * @param integer $branchId ID of a branch if known. [optional] + * + * @return integer The id of the node. + * + * @since 4.0.0 + */ + public static function addNestedNode($branch, NodeInterface $node, $state = 1, $access = 1, $language = '', $branchId = null) + { + if (!$branchId) { + // Get the branch id, insert it if it does not exist. + $branchId = static::addBranch($branch); + } + + $parent = $node->getParent(); + + if ($parent && $parent->title != 'ROOT') { + $parentId = self::addNestedNode($branch, $parent, $state, $access, $language, $branchId); + } else { + $parentId = $branchId; + } + + $temp = new \stdClass(); + $temp->title = $node->title; + $temp->state = $state; + $temp->access = $access; + $temp->parent_id = $parentId; + $temp->language = $language; + + return self::storeNode($temp, $parentId); + } + + /** + * A helper method to store a node in the taxonomy + * + * @param object $node The node data to include + * @param integer $parentId The parent id of the node to add. + * + * @return integer The id of the inserted node. + * + * @since 4.0.0 + * @throws \RuntimeException + */ + protected static function storeNode($node, $parentId) + { + // Check to see if the node is in the cache. + if (isset(static::$nodes[$parentId . ':' . $node->title])) { + return static::$nodes[$parentId . ':' . $node->title]->id; + } + + // Check to see if the node is in the table. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' = ' . $db->quote($parentId)) + ->where($db->quoteName('title') . ' = ' . $db->quote($node->title)) + ->where($db->quoteName('language') . ' = ' . $db->quote($node->language)); + + $db->setQuery($query); + + // Get the result. + $result = $db->loadObject(); + + // Check if the database matches the input data. + if ((bool) $result && $result->state == $node->state && $result->access == $node->access) { + // The data matches, add the item to the cache. + static::$nodes[$parentId . ':' . $node->title] = $result; + + return static::$nodes[$parentId . ':' . $node->title]->id; + } + + /* + * The database did not match the input. This could be because the + * state has changed or because the node does not exist. Let's figure + * out which case is true and deal with it. + * @todo: use factory? + */ + $nodeTable = new MapTable($db); + + if (empty($result)) { + // Prepare the node object. + $nodeTable->title = $node->title; + $nodeTable->state = (int) $node->state; + $nodeTable->access = (int) $node->access; + $nodeTable->language = $node->language; + $nodeTable->setLocation((int) $parentId, 'last-child'); + } else { + // Prepare the node object. + $nodeTable->id = (int) $result->id; + $nodeTable->title = $result->title; + $nodeTable->state = (int) ($node->state > 0 ? $node->state : $result->state); + $nodeTable->access = (int) $result->access; + $nodeTable->language = $node->language; + $nodeTable->setLocation($result->parent_id, 'last-child'); + } + + // Check the data. + if (!$nodeTable->check()) { + $error = $nodeTable->getError(); + + if ($error instanceof \Exception) { + // \Joomla\CMS\Table\NestedTable sets errors of exceptions, so in this case we can pass on more + // information + throw new \RuntimeException( + $error->getMessage(), + $error->getCode(), + $error + ); + } + + // Standard string returned. Probably from the \Joomla\CMS\Table\Table class + throw new \RuntimeException($error, 500); + } + + // Store the data. + if (!$nodeTable->store()) { + $error = $nodeTable->getError(); + + if ($error instanceof \Exception) { + // \Joomla\CMS\Table\NestedTable sets errors of exceptions, so in this case we can pass on more + // information + throw new \RuntimeException( + $error->getMessage(), + $error->getCode(), + $error + ); + } + + // Standard string returned. Probably from the \Joomla\CMS\Table\Table class + throw new \RuntimeException($error, 500); + } + + $nodeTable->rebuildPath($nodeTable->id); + + // Add the node to the cache. + static::$nodes[$parentId . ':' . $nodeTable->title] = (object) $nodeTable->getProperties(); + + return static::$nodes[$parentId . ':' . $nodeTable->title]->id; + } + + /** + * Method to add a map entry between a link and a taxonomy node. + * + * @param integer $linkId The link to map to. + * @param integer $nodeId The node to map to. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function addMap($linkId, $nodeId) + { + // Insert the map. + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('link_id')) + ->from($db->quoteName('#__finder_taxonomy_map')) + ->where($db->quoteName('link_id') . ' = ' . (int) $linkId) + ->where($db->quoteName('node_id') . ' = ' . (int) $nodeId); + $db->setQuery($query); + $db->execute(); + $id = (int) $db->loadResult(); + + if (!$id) { + $map = new \stdClass(); + $map->link_id = (int) $linkId; + $map->node_id = (int) $nodeId; + $db->insertObject('#__finder_taxonomy_map', $map); + } + + return true; + } + + /** + * Method to get the title of all taxonomy branches. + * + * @return array An array of branch titles. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function getBranchTitles() + { + $db = Factory::getDbo(); + + // Set user variables + $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); + + // Create a query to get the taxonomy branch titles. + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' = 1') + ->where($db->quoteName('state') . ' = 1') + ->where($db->quoteName('access') . ' IN (' . $groups . ')'); + + // Get the branch titles. + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Method to find a taxonomy node in a branch. + * + * @param string $branch The branch to search. + * @param string $title The title of the node. + * + * @return mixed Integer id on success, null on no match. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function getNodeByTitle($branch, $title) + { + $db = Factory::getDbo(); + + // Set user variables + $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); + + // Create a query to get the node. + $query = $db->getQuery(true) + ->select('t1.*') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') + ->where('t1.access IN (' . $groups . ')') + ->where('t1.state = 1') + ->where('t1.title LIKE ' . $db->quote($db->escape($title) . '%')) + ->where('t2.access IN (' . $groups . ')') + ->where('t2.state = 1') + ->where('t2.title = ' . $db->quote($branch)); + + // Get the node. + $query->setLimit(1); + $db->setQuery($query); + + return $db->loadObject(); + } + + /** + * Method to remove map entries for a link. + * + * @param integer $linkId The link to remove. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function removeMaps($linkId) + { + // Delete the maps. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_taxonomy_map')) + ->where($db->quoteName('link_id') . ' = ' . (int) $linkId); + $db->setQuery($query); + $db->execute(); + + return true; + } + + /** + * Method to remove orphaned taxonomy maps + * + * @return integer The number of deleted rows. + * + * @since 4.2.0 + * @throws \RuntimeException on database error. + */ + public static function removeOrphanMaps() + { + // Delete all orphaned maps + $db = Factory::getDbo(); + $query2 = $db->getQuery(true) + ->select($db->quoteName('link_id')) + ->from($db->quoteName('#__finder_links')); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_taxonomy_map')) + ->where($db->quoteName('link_id') . ' NOT IN (' . $query2 . ')'); + $db->setQuery($query); + $db->execute(); + $count = $db->getAffectedRows(); + + return $count; + } + + /** + * Method to remove orphaned taxonomy nodes and branches. + * + * @return integer The number of deleted rows. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function removeOrphanNodes() + { + // Delete all orphaned nodes. + $affectedRows = 0; + $db = Factory::getDbo(); + $nodeTable = new MapTable($db); + $query = $db->getQuery(true); + + $query->select($db->quoteName('t.id')) + ->from($db->quoteName('#__finder_taxonomy', 't')) + ->join('LEFT', $db->quoteName('#__finder_taxonomy_map', 'm') . ' ON ' . $db->quoteName('m.node_id') . '=' . $db->quoteName('t.id')) + ->where($db->quoteName('t.parent_id') . ' > 1 ') + ->where('t.lft + 1 = t.rgt') + ->where($db->quoteName('m.link_id') . ' IS NULL'); + + do { + $db->setQuery($query); + $nodes = $db->loadColumn(); + + foreach ($nodes as $node) { + $nodeTable->delete($node); + $affectedRows++; + } + } while ($nodes); + + return $affectedRows; + } + + /** + * Get a taxonomy based on its id or all taxonomies + * + * @param integer $id Id of the taxonomy + * + * @return object|array A taxonomy object or an array of all taxonomies + * + * @since 4.0.0 + */ + public static function getTaxonomy($id = 0) + { + if (!count(self::$taxonomies)) { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select(array('id','parent_id','lft','rgt','level','path','title','alias','state','access','language')) + ->from($db->quoteName('#__finder_taxonomy')) + ->order($db->quoteName('lft')); + + $db->setQuery($query); + self::$taxonomies = $db->loadObjectList('id'); + } + + if ($id == 0) { + return self::$taxonomies; + } + + if (isset(self::$taxonomies[$id])) { + return self::$taxonomies[$id]; + } + + return false; + } + + /** + * Get a taxonomy branch object based on its title or all branches + * + * @param string $title Title of the branch + * + * @return object|array The object with the branch data or an array of all branches + * + * @since 4.0.0 + */ + public static function getBranch($title = '') + { + if (!count(self::$branches)) { + $taxonomies = self::getTaxonomy(); + + foreach ($taxonomies as $t) { + if ($t->level == 1) { + self::$branches[$t->title] = $t; + } + } + } + + if ($title == '') { + return self::$branches; + } + + if (isset(self::$branches[$title])) { + return self::$branches[$title]; + } + + return false; + } } diff --git a/code/administrator/components/com_finder/src/Indexer/Token.php b/code/administrator/components/com_finder/src/Indexer/Token.php index d52d9d18..4f2f13d1 100644 --- a/code/administrator/components/com_finder/src/Indexer/Token.php +++ b/code/administrator/components/com_finder/src/Indexer/Token.php @@ -1,4 +1,5 @@ language = '*'; - } - else - { - $this->language = $lang; - } - - // Tokens can be a single word or an array of words representing a phrase. - if (is_array($term)) - { - // Populate the token instance. - $this->term = implode($spacer, $term); - $this->stem = implode($spacer, array_map(array(Helper::class, 'stem'), $term, array($lang))); - $this->numeric = false; - $this->common = false; - $this->phrase = true; - $this->length = StringHelper::strlen($this->term); - - /* - * Calculate the weight of the token. - * - * 1. Length of the token up to 30 and divide by 30, add 1. - * 2. Round weight to 4 decimal points. - */ - $this->weight = (($this->length >= 30 ? 30 : $this->length) / 30) + 1; - $this->weight = round($this->weight, 4); - } - else - { - // Populate the token instance. - $this->term = $term; - $this->stem = Helper::stem($this->term, $lang); - $this->numeric = (is_numeric($this->term) || (bool) preg_match('#^[0-9,.\-\+]+$#', $this->term)); - $this->common = $this->numeric ? false : Helper::isCommon($this->term, $lang); - $this->phrase = false; - $this->length = StringHelper::strlen($this->term); - - /* - * Calculate the weight of the token. - * - * 1. Length of the token up to 15 and divide by 15. - * 2. If common term, divide weight by 8. - * 3. If numeric, multiply weight by 1.5. - * 4. Round weight to 4 decimal points. - */ - $this->weight = ($this->length >= 15 ? 15 : $this->length) / 15; - $this->weight = $this->common === true ? $this->weight / 8 : $this->weight; - $this->weight = $this->numeric === true ? $this->weight * 1.5 : $this->weight; - $this->weight = round($this->weight, 4); - } - } + /** + * This is the term that will be referenced in the terms table and the + * mapping tables. + * + * @var string + * @since 2.5 + */ + public $term; + + /** + * The stem is used to match the root term and produce more potential + * matches when searching the index. + * + * @var string + * @since 2.5 + */ + public $stem; + + /** + * If the token is numeric, it is likely to be short and uncommon so the + * weight is adjusted to compensate for that situation. + * + * @var boolean + * @since 2.5 + */ + public $numeric; + + /** + * If the token is a common term, the weight is adjusted to compensate for + * the higher frequency of the term in relation to other terms. + * + * @var boolean + * @since 2.5 + */ + public $common; + + /** + * Flag for phrase tokens. + * + * @var boolean + * @since 2.5 + */ + public $phrase; + + /** + * The length is used to calculate the weight of the token. + * + * @var integer + * @since 2.5 + */ + public $length; + + /** + * The weight is calculated based on token size and whether the token is + * considered a common term. + * + * @var integer + * @since 2.5 + */ + public $weight; + + /** + * The simple language identifier for the token. + * + * @var string + * @since 2.5 + */ + public $language; + + /** + * The container for matches. + * + * @var array + * @since 3.8.12 + */ + public $matches = array(); + + /** + * Is derived token (from individual words) + * + * @var boolean + * @since 3.8.12 + */ + public $derived; + + /** + * The suggested term + * + * @var string + * @since 3.8.12 + */ + public $suggestion; + + /** + * Method to construct the token object. + * + * @param mixed $term The term as a string for words or an array for phrases. + * @param string $lang The simple language identifier. + * @param string $spacer The space separator for phrases. [optional] + * + * @since 2.5 + */ + public function __construct($term, $lang, $spacer = ' ') + { + if (!$lang) { + $this->language = '*'; + } else { + $this->language = $lang; + } + + // Tokens can be a single word or an array of words representing a phrase. + if (is_array($term)) { + // Populate the token instance. + $this->term = implode($spacer, $term); + $this->stem = implode($spacer, array_map(array(Helper::class, 'stem'), $term, array($lang))); + $this->numeric = false; + $this->common = false; + $this->phrase = true; + $this->length = StringHelper::strlen($this->term); + + /* + * Calculate the weight of the token. + * + * 1. Length of the token up to 30 and divide by 30, add 1. + * 2. Round weight to 4 decimal points. + */ + $this->weight = (($this->length >= 30 ? 30 : $this->length) / 30) + 1; + $this->weight = round($this->weight, 4); + } else { + // Populate the token instance. + $this->term = $term; + $this->stem = Helper::stem($this->term, $lang); + $this->numeric = (is_numeric($this->term) || (bool) preg_match('#^[0-9,.\-\+]+$#', $this->term)); + $this->common = $this->numeric ? false : Helper::isCommon($this->term, $lang); + $this->phrase = false; + $this->length = StringHelper::strlen($this->term); + + /* + * Calculate the weight of the token. + * + * 1. Length of the token up to 15 and divide by 15. + * 2. If common term, divide weight by 8. + * 3. If numeric, multiply weight by 1.5. + * 4. Round weight to 4 decimal points. + */ + $this->weight = ($this->length >= 15 ? 15 : $this->length) / 15; + $this->weight = $this->common === true ? $this->weight / 8 : $this->weight; + $this->weight = $this->numeric === true ? $this->weight * 1.5 : $this->weight; + $this->weight = round($this->weight, 4); + } + } } diff --git a/code/administrator/components/com_finder/src/Model/FilterModel.php b/code/administrator/components/com_finder/src/Model/FilterModel.php index a559e2e5..f21598a2 100644 --- a/code/administrator/components/com_finder/src/Model/FilterModel.php +++ b/code/administrator/components/com_finder/src/Model/FilterModel.php @@ -1,4 +1,5 @@ getState('filter.id'); - - // Get a FinderTableFilter instance. - $filter = $this->getTable(); - - // Attempt to load the row. - $return = $filter->load($filter_id); - - // Check for a database error. - if ($return === false && $filter->getError()) - { - $this->setError($filter->getError()); - - return false; - } - - // Process the filter data. - if (!empty($filter->data)) - { - $filter->data = explode(',', $filter->data); - } - elseif (empty($filter->data)) - { - $filter->data = array(); - } - - return $filter; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. [optional] - * @param boolean $loadData True if the form is to load its own data (default case), false if not. [optional] - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 2.5 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_finder.filter', 'filter', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 2.5 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_finder.edit.filter.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_finder.filter', $data); - - return $data; - } - - /** - * Method to get the total indexed items - * - * @return number the number of indexed items - * - * @since 3.5 - */ - public function getTotal() - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('MAX(link_id)') - ->from('#__finder_links'); - - return $db->setQuery($query)->loadResult(); - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 2.5 + */ + protected $text_prefix = 'COM_FINDER'; + + /** + * Model context string. + * + * @var string + * @since 2.5 + */ + protected $context = 'com_finder.filter'; + + /** + * Custom clean cache method. + * + * @param string $group The component name. [optional] + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 2.5 + */ + protected function cleanCache($group = 'com_finder', $clientId = 0) + { + parent::cleanCache($group); + } + + /** + * Method to get the filter data. + * + * @return FilterTable|boolean The filter data or false on a failure. + * + * @since 2.5 + */ + public function getFilter() + { + $filter_id = (int) $this->getState('filter.id'); + + // Get a FinderTableFilter instance. + $filter = $this->getTable(); + + // Attempt to load the row. + $return = $filter->load($filter_id); + + // Check for a database error. + if ($return === false && $filter->getError()) { + $this->setError($filter->getError()); + + return false; + } + + // Process the filter data. + if (!empty($filter->data)) { + $filter->data = explode(',', $filter->data); + } elseif (empty($filter->data)) { + $filter->data = array(); + } + + return $filter; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. [optional] + * @param boolean $loadData True if the form is to load its own data (default case), false if not. [optional] + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 2.5 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_finder.filter', 'filter', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 2.5 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_finder.edit.filter.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_finder.filter', $data); + + return $data; + } + + /** + * Method to get the total indexed items + * + * @return number the number of indexed items + * + * @since 3.5 + */ + public function getTotal() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('MAX(link_id)') + ->from('#__finder_links'); + + return $db->setQuery($query)->loadResult(); + } } diff --git a/code/administrator/components/com_finder/src/Model/FiltersModel.php b/code/administrator/components/com_finder/src/Model/FiltersModel.php index f930fb26..69948730 100644 --- a/code/administrator/components/com_finder/src/Model/FiltersModel.php +++ b/code/administrator/components/com_finder/src/Model/FiltersModel.php @@ -1,4 +1,5 @@ getDbo(); - $query = $db->getQuery(true); - - // Select all fields from the table. - $query->select('a.*') - ->from($db->quoteName('#__finder_filters', 'a')); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join('LEFT', $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Join over the users for the author. - $query->select($db->quoteName('ua.name', 'user_name')) - ->join('LEFT', $db->quoteName('#__users', 'ua') . ' ON ' . $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')); - - // Check for a search filter. - if ($search = $this->getState('filter.search')) - { - $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); - $query->where($db->quoteName('a.title') . ' LIKE ' . $search); - } - - // If the model is set to check item state, add to the query. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('a.state') . ' = ' . (int) $state); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.title') . ' ' . $db->escape($this->getState('list.direction', 'ASC')))); - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. [optional] - * @param string $direction An optional direction. [optional] - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = 'a.title', $direction = 'asc') - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_finder'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.7 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'filter_id', 'a.filter_id', + 'title', 'a.title', + 'state', 'a.state', + 'created_by_alias', 'a.created_by_alias', + 'created', 'a.created', + 'map_count', 'a.map_count' + ); + } + + parent::__construct($config, $factory); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 2.5 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select all fields from the table. + $query->select('a.*') + ->from($db->quoteName('#__finder_filters', 'a')); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join('LEFT', $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Join over the users for the author. + $query->select($db->quoteName('ua.name', 'user_name')) + ->join('LEFT', $db->quoteName('#__users', 'ua') . ' ON ' . $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')); + + // Check for a search filter. + if ($search = $this->getState('filter.search')) { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $query->where($db->quoteName('a.title') . ' LIKE ' . $search); + } + + // If the model is set to check item state, add to the query. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $query->where($db->quoteName('a.state') . ' = ' . (int) $state); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.title') . ' ' . $db->escape($this->getState('list.direction', 'ASC')))); + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. [optional] + * @param string $direction An optional direction. [optional] + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = 'a.title', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_finder'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } } diff --git a/code/administrator/components/com_finder/src/Model/IndexModel.php b/code/administrator/components/com_finder/src/Model/IndexModel.php index 91d5cf41..3ba8a5d0 100644 --- a/code/administrator/components/com_finder/src/Model/IndexModel.php +++ b/code/administrator/components/com_finder/src/Model/IndexModel.php @@ -1,4 +1,5 @@ authorise('core.delete', $this->option); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. - * - * @since 2.5 - */ - protected function canEditState($record) - { - return Factory::getUser()->authorise('core.edit.state', $this->option); - } - - /** - * Method to delete one or more records. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 2.5 - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - - // Include the content plugins for the on delete events. - PluginHelper::importPlugin('content'); - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ($this->canDelete($table)) - { - $context = $this->option . '.' . $this->name; - - // Trigger the onContentBeforeDelete event. - $result = Factory::getApplication()->triggerEvent($this->event_before_delete, array($context, $table)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the onContentAfterDelete event. - Factory::getApplication()->triggerEvent($this->event_after_delete, array($context, $table)); - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - $error = $this->getError(); - - if ($error) - { - $this->setError($error); - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); - } - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 2.5 - */ - protected function getListQuery() - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('l.*') - ->select($db->quoteName('t.title', 't_title')) - ->from($db->quoteName('#__finder_links', 'l')) - ->join('INNER', $db->quoteName('#__finder_types', 't') . ' ON ' . $db->quoteName('t.id') . ' = ' . $db->quoteName('l.type_id')); - - // Check the type filter. - $type = $this->getState('filter.type'); - - // Join over the language - $query->select('la.title AS language_title, la.image AS language_image') - ->join('LEFT', $db->quoteName('#__languages') . ' AS la ON la.lang_code = l.language'); - - if (is_numeric($type)) - { - $query->where($db->quoteName('l.type_id') . ' = ' . (int) $type); - } - - // Check the map filter. - $contentMapId = $this->getState('filter.content_map'); - - if (is_numeric($contentMapId)) - { - $query->join('INNER', $db->quoteName('#__finder_taxonomy_map', 'm') . ' ON ' . $db->quoteName('m.link_id') . ' = ' . $db->quoteName('l.link_id')) - ->where($db->quoteName('m.node_id') . ' = ' . (int) $contentMapId); - } - - // Check for state filter. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('l.published') . ' = ' . (int) $state); - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('l.language') . ' = ' . $db->quote($language)); - } - - // Check the search phrase. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); - $orSearchSql = $db->quoteName('l.title') . ' LIKE ' . $search . ' OR ' . $db->quoteName('l.url') . ' LIKE ' . $search; - - // Filter by indexdate only if $search doesn't contains non-ascii characters - if (!preg_match('/[^\x00-\x7F]/', $search)) - { - $orSearchSql .= ' OR ' . $query->castAsChar($db->quoteName('l.indexdate')) . ' LIKE ' . $search; - } - - $query->where('(' . $orSearchSql . ')'); - } - - // Handle the list ordering. - $listOrder = $this->getState('list.ordering', 'l.title'); - $listDir = $this->getState('list.direction', 'ASC'); - - if ($listOrder === 't.title') - { - $ordering = $db->quoteName('t.title') . ' ' . $db->escape($listDir) . ', ' . $db->quoteName('l.title') . ' ' . $db->escape($listDir); - } - else - { - $ordering = $db->escape($listOrder) . ' ' . $db->escape($listDir); - } - - $query->order($ordering); - - return $query; - } - - /** - * Method to get the state of the Smart Search Plugins. - * - * @return array Array of relevant plugins and whether they are enabled or not. - * - * @since 2.5 - */ - public function getPluginState() - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('name, enabled') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' IN (' . $db->quote('system') . ',' . $db->quote('content') . ')') - ->where($db->quoteName('element') . ' = ' . $db->quote('finder')); - $db->setQuery($query); - - return $db->loadObjectList('name'); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.type'); - $id .= ':' . $this->getState('filter.content_map'); - - return parent::getStoreId($id); - } - - /** - * Gets the total of indexed items. - * - * @return integer The total of indexed items. - * - * @since 3.6.0 - */ - public function getTotalIndexed() - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('COUNT(link_id)') - ->from($db->quoteName('#__finder_links')); - $db->setQuery($query); - - return (int) $db->loadResult(); - } - - /** - * Returns a Table object, always creating it. - * - * @param string $type The table type to instantiate. [optional] - * @param string $prefix A prefix for the table class name. [optional] - * @param array $config Configuration array for model. [optional] - * - * @return \Joomla\CMS\Table\Table A database object - * - * @since 2.5 - */ - public function getTable($type = 'Link', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Method to purge the index, deleting all links. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - * @throws \Exception on database error - */ - public function purge() - { - $db = $this->getDbo(); - - // Truncate the links table. - $db->truncateTable('#__finder_links'); - - // Truncate the links terms tables. - $db->truncateTable('#__finder_links_terms'); - - // Truncate the terms table. - $db->truncateTable('#__finder_terms'); - - // Truncate the taxonomy map table. - $db->truncateTable('#__finder_taxonomy_map'); - - // Truncate the taxonomy table and insert the root node. - $db->truncateTable('#__finder_taxonomy'); - $root = (object) array( - 'id' => 1, - 'parent_id' => 0, - 'lft' => 0, - 'rgt' => 1, - 'level' => 0, - 'path' => '', - 'title' => 'ROOT', - 'alias' => 'root', - 'state' => 1, - 'access' => 1, - 'language' => '*' - ); - $db->insertObject('#__finder_taxonomy', $root); - - // Truncate the tokens tables. - $db->truncateTable('#__finder_tokens'); - - // Truncate the tokens aggregate table. - $db->truncateTable('#__finder_tokens_aggregate'); - - // Include the finder plugins for the on purge events. - PluginHelper::importPlugin('finder'); - Factory::getApplication()->triggerEvent($this->event_after_purge); - - return true; - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. [optional] - * @param string $direction An optional direction. [optional] - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = 'l.title', $direction = 'asc') - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'cmd')); - $this->setState('filter.content_map', $this->getUserStateFromRequest($this->context . '.filter.content_map', 'filter_content_map', '', 'cmd')); - $this->setState('filter.language', $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_finder'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to change the published state of one or more records. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the published state. [optional] - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function publish(&$pks, $value = 1) - { - $user = Factory::getUser(); - $table = $this->getTable(); - $pks = (array) $pks; - - // Include the content plugins for the change of state event. - PluginHelper::importPlugin('content'); - - // Access checks. - foreach ($pks as $i => $pk) - { - $table->reset(); - - if ($table->load($pk) && !$this->canEditState($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - - return false; - } - } - - // Attempt to change the state of the records. - if (!$table->publish($pks, $value, $user->get('id'))) - { - $this->setError($table->getError()); - - return false; - } - - $context = $this->option . '.' . $this->name; - - // Trigger the onContentChangeState event. - $result = Factory::getApplication()->triggerEvent('onContentChangeState', array($context, $pks, $value)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } + /** + * The event to trigger after deleting the data. + * + * @var string + * @since 2.5 + */ + protected $event_after_delete = 'onContentAfterDelete'; + + /** + * The event to trigger before deleting the data. + * + * @var string + * @since 2.5 + */ + protected $event_before_delete = 'onContentBeforeDelete'; + + /** + * The event to trigger after purging the data. + * + * @var string + * @since 4.0.0 + */ + protected $event_after_purge = 'onFinderIndexAfterPurge'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.7 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'state', 'published', 'l.published', + 'title', 'l.title', + 'type', 'type_id', 'l.type_id', + 't.title', 't_title', + 'url', 'l.url', + 'language', 'l.language', + 'indexdate', 'l.indexdate', + 'content_map', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 2.5 + */ + protected function canDelete($record) + { + return Factory::getUser()->authorise('core.delete', $this->option); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. + * + * @since 2.5 + */ + protected function canEditState($record) + { + return Factory::getUser()->authorise('core.edit.state', $this->option); + } + + /** + * Method to delete one or more records. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 2.5 + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + + // Include the content plugins for the on delete events. + PluginHelper::importPlugin('content'); + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ($this->canDelete($table)) { + $context = $this->option . '.' . $this->name; + + // Trigger the onContentBeforeDelete event. + $result = Factory::getApplication()->triggerEvent($this->event_before_delete, array($context, $table)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the onContentAfterDelete event. + Factory::getApplication()->triggerEvent($this->event_after_delete, array($context, $table)); + } else { + // Prune items that you can't change. + unset($pks[$i]); + $error = $this->getError(); + + if ($error) { + $this->setError($error); + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); + } + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 2.5 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('l.*') + ->select($db->quoteName('t.title', 't_title')) + ->from($db->quoteName('#__finder_links', 'l')) + ->join('INNER', $db->quoteName('#__finder_types', 't') . ' ON ' . $db->quoteName('t.id') . ' = ' . $db->quoteName('l.type_id')); + + // Check the type filter. + $type = $this->getState('filter.type'); + + // Join over the language + $query->select('la.title AS language_title, la.image AS language_image') + ->join('LEFT', $db->quoteName('#__languages') . ' AS la ON la.lang_code = l.language'); + + if (is_numeric($type)) { + $query->where($db->quoteName('l.type_id') . ' = ' . (int) $type); + } + + // Check the map filter. + $contentMapId = $this->getState('filter.content_map'); + + if (is_numeric($contentMapId)) { + $query->join('INNER', $db->quoteName('#__finder_taxonomy_map', 'm') . ' ON ' . $db->quoteName('m.link_id') . ' = ' . $db->quoteName('l.link_id')) + ->where($db->quoteName('m.node_id') . ' = ' . (int) $contentMapId); + } + + // Check for state filter. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $query->where($db->quoteName('l.published') . ' = ' . (int) $state); + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('l.language') . ' = ' . $db->quote($language)); + } + + // Check the search phrase. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $orSearchSql = $db->quoteName('l.title') . ' LIKE ' . $search . ' OR ' . $db->quoteName('l.url') . ' LIKE ' . $search; + + // Filter by indexdate only if $search doesn't contains non-ascii characters + if (!preg_match('/[^\x00-\x7F]/', $search)) { + $orSearchSql .= ' OR ' . $query->castAsChar($db->quoteName('l.indexdate')) . ' LIKE ' . $search; + } + + $query->where('(' . $orSearchSql . ')'); + } + + // Handle the list ordering. + $listOrder = $this->getState('list.ordering', 'l.title'); + $listDir = $this->getState('list.direction', 'ASC'); + + if ($listOrder === 't.title') { + $ordering = $db->quoteName('t.title') . ' ' . $db->escape($listDir) . ', ' . $db->quoteName('l.title') . ' ' . $db->escape($listDir); + } else { + $ordering = $db->escape($listOrder) . ' ' . $db->escape($listDir); + } + + $query->order($ordering); + + return $query; + } + + /** + * Method to get the state of the Smart Search Plugins. + * + * @return array Array of relevant plugins and whether they are enabled or not. + * + * @since 2.5 + */ + public function getPluginState() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('name, enabled') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' IN (' . $db->quote('system') . ',' . $db->quote('content') . ')') + ->where($db->quoteName('element') . ' = ' . $db->quote('finder')); + $db->setQuery($query); + + return $db->loadObjectList('name'); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.type'); + $id .= ':' . $this->getState('filter.content_map'); + + return parent::getStoreId($id); + } + + /** + * Gets the total of indexed items. + * + * @return integer The total of indexed items. + * + * @since 3.6.0 + */ + public function getTotalIndexed() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(link_id)') + ->from($db->quoteName('#__finder_links')); + $db->setQuery($query); + + return (int) $db->loadResult(); + } + + /** + * Returns a Table object, always creating it. + * + * @param string $type The table type to instantiate. [optional] + * @param string $prefix A prefix for the table class name. [optional] + * @param array $config Configuration array for model. [optional] + * + * @return \Joomla\CMS\Table\Table A database object + * + * @since 2.5 + */ + public function getTable($type = 'Link', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Method to purge the index, deleting all links. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + * @throws \Exception on database error + */ + public function purge() + { + $db = $this->getDatabase(); + + // Truncate the links table. + $db->truncateTable('#__finder_links'); + + // Truncate the links terms tables. + $db->truncateTable('#__finder_links_terms'); + + // Truncate the terms table. + $db->truncateTable('#__finder_terms'); + + // Truncate the taxonomy map table. + $db->truncateTable('#__finder_taxonomy_map'); + + // Truncate the taxonomy table and insert the root node. + $db->truncateTable('#__finder_taxonomy'); + $root = (object) array( + 'id' => 1, + 'parent_id' => 0, + 'lft' => 0, + 'rgt' => 1, + 'level' => 0, + 'path' => '', + 'title' => 'ROOT', + 'alias' => 'root', + 'state' => 1, + 'access' => 1, + 'language' => '*' + ); + $db->insertObject('#__finder_taxonomy', $root); + + // Truncate the tokens tables. + $db->truncateTable('#__finder_tokens'); + + // Truncate the tokens aggregate table. + $db->truncateTable('#__finder_tokens_aggregate'); + + // Include the finder plugins for the on purge events. + PluginHelper::importPlugin('finder'); + Factory::getApplication()->triggerEvent($this->event_after_purge); + + return true; + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. [optional] + * @param string $direction An optional direction. [optional] + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = 'l.title', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'cmd')); + $this->setState('filter.content_map', $this->getUserStateFromRequest($this->context . '.filter.content_map', 'filter_content_map', '', 'cmd')); + $this->setState('filter.language', $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_finder'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to change the published state of one or more records. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the published state. [optional] + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function publish(&$pks, $value = 1) + { + $user = Factory::getUser(); + $table = $this->getTable(); + $pks = (array) $pks; + + // Include the content plugins for the change of state event. + PluginHelper::importPlugin('content'); + + // Access checks. + foreach ($pks as $i => $pk) { + $table->reset(); + + if ($table->load($pk) && !$this->canEditState($table)) { + // Prune items that you can't change. + unset($pks[$i]); + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + + return false; + } + } + + // Attempt to change the state of the records. + if (!$table->publish($pks, $value, $user->get('id'))) { + $this->setError($table->getError()); + + return false; + } + + $context = $this->option . '.' . $this->name; + + // Trigger the onContentChangeState event. + $result = Factory::getApplication()->triggerEvent('onContentChangeState', array($context, $pks, $value)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } } diff --git a/code/administrator/components/com_finder/src/Model/IndexerModel.php b/code/administrator/components/com_finder/src/Model/IndexerModel.php index 34a9f023..46e8b6cd 100644 --- a/code/administrator/components/com_finder/src/Model/IndexerModel.php +++ b/code/administrator/components/com_finder/src/Model/IndexerModel.php @@ -1,4 +1,5 @@ authorise('core.delete', $this->option); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. - * - * @since 2.5 - */ - protected function canEditState($record) - { - return Factory::getUser()->authorise('core.edit.state', $this->option); - } - - /** - * Method to delete one or more records. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 2.5 - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - - // Include the content plugins for the on delete events. - PluginHelper::importPlugin('content'); - - // Iterate the items to check if all of them exist. - foreach ($pks as $i => $pk) - { - if (!$table->load($pk)) - { - // Item is not in the table. - $this->setError($table->getError()); - - return false; - } - } - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ($this->canDelete($table)) - { - $context = $this->option . '.' . $this->name; - - // Trigger the onContentBeforeDelete event. - $result = Factory::getApplication()->triggerEvent('onContentBeforeDelete', array($context, $table)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the onContentAfterDelete event. - Factory::getApplication()->triggerEvent('onContentAfterDelete', array($context, $table)); - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - $error = $this->getError(); - - if ($error) - { - $this->setError($error); - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); - } - } - } - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 2.5 - */ - protected function getListQuery() - { - $db = $this->getDbo(); - - // Select all fields from the table. - $query = $db->getQuery(true) - ->select('a.id, a.parent_id, a.lft, a.rgt, a.level, a.path, a.title, a.alias, a.state, a.access, a.language') - ->from($db->quoteName('#__finder_taxonomy', 'a')) - ->where('a.parent_id != 0'); - - // Join to get the branch title - $query->select([$db->quoteName('b.id', 'branch_id'), $db->quoteName('b.title', 'branch_title')]) - ->leftJoin($db->quoteName('#__finder_taxonomy', 'b') . ' ON b.level = 1 AND b.lft <= a.lft AND a.rgt <= b.rgt'); - - // Join to get the map links. - $stateQuery = $db->getQuery(true) - ->select('m.node_id') - ->select('COUNT(NULLIF(l.published, 0)) AS count_published') - ->select('COUNT(NULLIF(l.published, 1)) AS count_unpublished') - ->from($db->quoteName('#__finder_taxonomy_map', 'm')) - ->leftJoin($db->quoteName('#__finder_links', 'l') . ' ON l.link_id = m.link_id') - ->group('m.node_id'); - - $query->select('COALESCE(s.count_published, 0) AS count_published'); - $query->select('COALESCE(s.count_unpublished, 0) AS count_unpublished'); - $query->leftJoin('(' . $stateQuery . ') AS s ON s.node_id = a.id'); - - // If the model is set to check item state, add to the query. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $query->where('a.state = ' . (int) $state); - } - - // Filter over level. - $level = $this->getState('filter.level'); - - if (is_numeric($level) && (int) $level === 1) - { - $query->where('a.parent_id = 1'); - } - - // Filter the maps over the branch if set. - $branchId = $this->getState('filter.branch'); - - if (is_numeric($branchId)) - { - $query->where('a.parent_id = ' . (int) $branchId); - } - - // Filter the maps over the search string if set. - if ($search = $this->getState('filter.search')) - { - $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); - $query->where('a.title LIKE ' . $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'branch_title, a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Returns a record count for the query. - * - * @param \Joomla\Database\DatabaseQuery|string - * - * @return integer Number of rows for query. - * - * @since 3.0 - */ - protected function _getListCount($query) - { - $query = clone $query; - $query->clear('select')->clear('join')->clear('order')->clear('limit')->clear('offset')->select('COUNT(*)'); - - return (int) $this->getDbo()->setQuery($query)->loadResult(); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.branch'); - $id .= ':' . $this->getState('filter.level'); - - return parent::getStoreId($id); - } - - /** - * Returns a Table object, always creating it. - * - * @param string $type The table type to instantiate. [optional] - * @param string $prefix A prefix for the table class name. [optional] - * @param array $config Configuration array for model. [optional] - * - * @return \Joomla\CMS\Table\Table A database object - * - * @since 2.5 - */ - public function getTable($type = 'Map', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. [optional] - * @param string $direction An optional direction. [optional] - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = 'branch_title, a.lft', $direction = 'ASC') - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); - $this->setState('filter.branch', $this->getUserStateFromRequest($this->context . '.filter.branch', 'filter_branch', '', 'cmd')); - $this->setState('filter.level', $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level', '', 'cmd')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_finder'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to change the published state of one or more records. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the published state. [optional] - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function publish(&$pks, $value = 1) - { - $user = Factory::getUser(); - $table = $this->getTable(); - $pks = (array) $pks; - - // Include the content plugins for the change of state event. - PluginHelper::importPlugin('content'); - - // Access checks. - foreach ($pks as $i => $pk) - { - $table->reset(); - - if ($table->load($pk) && !$this->canEditState($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - - return false; - } - } - - // Attempt to change the state of the records. - if (!$table->publish($pks, $value, $user->get('id'))) - { - $this->setError($table->getError()); - - return false; - } - - $context = $this->option . '.' . $this->name; - - // Trigger the onContentChangeState event. - $result = Factory::getApplication()->triggerEvent('onContentChangeState', array($context, $pks, $value)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Method to purge all maps from the taxonomy. - * - * @return boolean Returns true on success, false on failure. - * - * @since 2.5 - */ - public function purge() - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' > 1'); - $db->setQuery($query); - $db->execute(); - - $query->clear() - ->delete($db->quoteName('#__finder_taxonomy_map')); - $db->setQuery($query); - $db->execute(); - - return true; - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $title = 'ROOT'; - - $query->where($this->_db->quoteName('title') . ' <> :title') - ->bind(':title', $title); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.7 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'state', 'a.state', + 'title', 'a.title', + 'branch', + 'branch_title', 'd.branch_title', + 'level', 'd.level', + 'language', 'a.language', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 2.5 + */ + protected function canDelete($record) + { + return Factory::getUser()->authorise('core.delete', $this->option); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. + * + * @since 2.5 + */ + protected function canEditState($record) + { + return Factory::getUser()->authorise('core.edit.state', $this->option); + } + + /** + * Method to delete one or more records. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 2.5 + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + + // Include the content plugins for the on delete events. + PluginHelper::importPlugin('content'); + + // Iterate the items to check if all of them exist. + foreach ($pks as $i => $pk) { + if (!$table->load($pk)) { + // Item is not in the table. + $this->setError($table->getError()); + + return false; + } + } + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ($this->canDelete($table)) { + $context = $this->option . '.' . $this->name; + + // Trigger the onContentBeforeDelete event. + $result = Factory::getApplication()->triggerEvent('onContentBeforeDelete', array($context, $table)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the onContentAfterDelete event. + Factory::getApplication()->triggerEvent('onContentAfterDelete', array($context, $table)); + } else { + // Prune items that you can't change. + unset($pks[$i]); + $error = $this->getError(); + + if ($error) { + $this->setError($error); + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); + } + } + } + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 2.5 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + + // Select all fields from the table. + $query = $db->getQuery(true) + ->select('a.id, a.parent_id, a.lft, a.rgt, a.level, a.path, a.title, a.alias, a.state, a.access, a.language') + ->from($db->quoteName('#__finder_taxonomy', 'a')) + ->where('a.parent_id != 0'); + + // Join to get the branch title + $query->select([$db->quoteName('b.id', 'branch_id'), $db->quoteName('b.title', 'branch_title')]) + ->leftJoin($db->quoteName('#__finder_taxonomy', 'b') . ' ON b.level = 1 AND b.lft <= a.lft AND a.rgt <= b.rgt'); + + // Join to get the map links. + $stateQuery = $db->getQuery(true) + ->select('m.node_id') + ->select('COUNT(NULLIF(l.published, 0)) AS count_published') + ->select('COUNT(NULLIF(l.published, 1)) AS count_unpublished') + ->from($db->quoteName('#__finder_taxonomy_map', 'm')) + ->leftJoin($db->quoteName('#__finder_links', 'l') . ' ON l.link_id = m.link_id') + ->group('m.node_id'); + + $query->select('COALESCE(s.count_published, 0) AS count_published'); + $query->select('COALESCE(s.count_unpublished, 0) AS count_unpublished'); + $query->leftJoin('(' . $stateQuery . ') AS s ON s.node_id = a.id'); + + // If the model is set to check item state, add to the query. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $query->where('a.state = ' . (int) $state); + } + + // Filter over level. + $level = $this->getState('filter.level'); + + if (is_numeric($level) && (int) $level === 1) { + $query->where('a.parent_id = 1'); + } + + // Filter the maps over the branch if set. + $branchId = $this->getState('filter.branch'); + + if (is_numeric($branchId)) { + $query->where('a.parent_id = ' . (int) $branchId); + } + + // Filter the maps over the search string if set. + if ($search = $this->getState('filter.search')) { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $query->where('a.title LIKE ' . $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'branch_title, a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Returns a record count for the query. + * + * @param \Joomla\Database\DatabaseQuery|string + * + * @return integer Number of rows for query. + * + * @since 3.0 + */ + protected function _getListCount($query) + { + $query = clone $query; + $query->clear('select')->clear('join')->clear('order')->clear('limit')->clear('offset')->select('COUNT(*)'); + + return (int) $this->getDatabase()->setQuery($query)->loadResult(); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.branch'); + $id .= ':' . $this->getState('filter.level'); + + return parent::getStoreId($id); + } + + /** + * Returns a Table object, always creating it. + * + * @param string $type The table type to instantiate. [optional] + * @param string $prefix A prefix for the table class name. [optional] + * @param array $config Configuration array for model. [optional] + * + * @return \Joomla\CMS\Table\Table A database object + * + * @since 2.5 + */ + public function getTable($type = 'Map', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. [optional] + * @param string $direction An optional direction. [optional] + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = 'branch_title, a.lft', $direction = 'ASC') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); + $this->setState('filter.branch', $this->getUserStateFromRequest($this->context . '.filter.branch', 'filter_branch', '', 'cmd')); + $this->setState('filter.level', $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level', '', 'cmd')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_finder'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to change the published state of one or more records. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the published state. [optional] + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function publish(&$pks, $value = 1) + { + $user = Factory::getUser(); + $table = $this->getTable(); + $pks = (array) $pks; + + // Include the content plugins for the change of state event. + PluginHelper::importPlugin('content'); + + // Access checks. + foreach ($pks as $i => $pk) { + $table->reset(); + + if ($table->load($pk) && !$this->canEditState($table)) { + // Prune items that you can't change. + unset($pks[$i]); + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + + return false; + } + } + + // Attempt to change the state of the records. + if (!$table->publish($pks, $value, $user->get('id'))) { + $this->setError($table->getError()); + + return false; + } + + $context = $this->option . '.' . $this->name; + + // Trigger the onContentChangeState event. + $result = Factory::getApplication()->triggerEvent('onContentChangeState', array($context, $pks, $value)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Method to purge all maps from the taxonomy. + * + * @return boolean Returns true on success, false on failure. + * + * @since 2.5 + */ + public function purge() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' > 1'); + $db->setQuery($query); + $db->execute(); + + $query->clear() + ->delete($db->quoteName('#__finder_taxonomy_map')); + $db->setQuery($query); + $db->execute(); + + return true; + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $title = 'ROOT'; + + $query->where($this->getDatabase()->quoteName('title') . ' <> :title') + ->bind(':title', $title); + + return $query; + } } diff --git a/code/administrator/components/com_finder/src/Model/SearchesModel.php b/code/administrator/components/com_finder/src/Model/SearchesModel.php index f8c83349..68e4c02f 100644 --- a/code/administrator/components/com_finder/src/Model/SearchesModel.php +++ b/code/administrator/components/com_finder/src/Model/SearchesModel.php @@ -1,4 +1,5 @@ setState('show_results', $this->getUserStateFromRequest($this->context . '.show_results', 'show_results', 1, 'int')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_finder'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 4.0.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('show_results'); - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 4.0.0 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - $query->from($db->quoteName('#__finder_logging', 'a')); - - // Filter by search in title - if ($search = $this->getState('filter.search')) - { - $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); - $query->where($db->quoteName('a.searchterm') . ' LIKE ' . $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.hits')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Override the parent getItems to inject optional data. - * - * @return mixed An array of objects on success, false on failure. - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - - foreach ($items as $item) - { - if (is_resource($item->query)) - { - $item->query = unserialize(stream_get_contents($item->query)); - } - else - { - $item->query = unserialize($item->query); - } - } - - return $items; - } - - /** - * Method to reset the search log table. - * - * @return boolean - * - * @since 4.0.0 - */ - public function reset() - { - $db = $this->getDbo(); - - try - { - $db->truncateTable('#__finder_logging'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 4.0.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'searchterm', 'a.searchterm', + 'hits', 'a.hits', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = 'a.hits', $direction = 'asc') + { + // Special state for toggle results button. + $this->setState('show_results', $this->getUserStateFromRequest($this->context . '.show_results', 'show_results', 1, 'int')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_finder'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 4.0.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('show_results'); + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 4.0.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__finder_logging', 'a')); + + // Filter by search in title + if ($search = $this->getState('filter.search')) { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $query->where($db->quoteName('a.searchterm') . ' LIKE ' . $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.hits')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Override the parent getItems to inject optional data. + * + * @return mixed An array of objects on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + foreach ($items as $item) { + if (is_resource($item->query)) { + $item->query = unserialize(stream_get_contents($item->query)); + } else { + $item->query = unserialize($item->query); + } + } + + return $items; + } + + /** + * Method to reset the search log table. + * + * @return boolean + * + * @since 4.0.0 + */ + public function reset() + { + $db = $this->getDatabase(); + + try { + $db->truncateTable('#__finder_logging'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } } diff --git a/code/administrator/components/com_finder/src/Model/StatisticsModel.php b/code/administrator/components/com_finder/src/Model/StatisticsModel.php index 32b15100..e5dd06af 100644 --- a/code/administrator/components/com_finder/src/Model/StatisticsModel.php +++ b/code/administrator/components/com_finder/src/Model/StatisticsModel.php @@ -1,4 +1,5 @@ getDbo(); - $query = $db->getQuery(true); - $data = new CMSObject; + /** + * Method to get the component statistics + * + * @return CMSObject The component statistics + * + * @since 2.5 + */ + public function getData() + { + // Initialise + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $data = new CMSObject(); - $query->select('COUNT(term_id)') - ->from($db->quoteName('#__finder_terms')); - $db->setQuery($query); - $data->term_count = $db->loadResult(); + $query->select('COUNT(term_id)') + ->from($db->quoteName('#__finder_terms')); + $db->setQuery($query); + $data->term_count = $db->loadResult(); - $query->clear() - ->select('COUNT(link_id)') - ->from($db->quoteName('#__finder_links')); - $db->setQuery($query); - $data->link_count = $db->loadResult(); + $query->clear() + ->select('COUNT(link_id)') + ->from($db->quoteName('#__finder_links')); + $db->setQuery($query); + $data->link_count = $db->loadResult(); - $query->clear() - ->select('COUNT(id)') - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' = 1'); - $db->setQuery($query); - $data->taxonomy_branch_count = $db->loadResult(); + $query->clear() + ->select('COUNT(id)') + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' = 1'); + $db->setQuery($query); + $data->taxonomy_branch_count = $db->loadResult(); - $query->clear() - ->select('COUNT(id)') - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' > 1'); - $db->setQuery($query); - $data->taxonomy_node_count = $db->loadResult(); + $query->clear() + ->select('COUNT(id)') + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' > 1'); + $db->setQuery($query); + $data->taxonomy_node_count = $db->loadResult(); - $query->clear() - ->select('t.title AS type_title, COUNT(a.link_id) AS link_count') - ->from($db->quoteName('#__finder_links') . ' AS a') - ->join('INNER', $db->quoteName('#__finder_types') . ' AS t ON t.id = a.type_id') - ->group('a.type_id, t.title') - ->order($db->quoteName('type_title') . ' ASC'); - $db->setQuery($query); - $data->type_list = $db->loadObjectList(); + $query->clear() + ->select('t.title AS type_title, COUNT(a.link_id) AS link_count') + ->from($db->quoteName('#__finder_links') . ' AS a') + ->join('INNER', $db->quoteName('#__finder_types') . ' AS t ON t.id = a.type_id') + ->group('a.type_id, t.title') + ->order($db->quoteName('type_title') . ' ASC'); + $db->setQuery($query); + $data->type_list = $db->loadObjectList(); - $lang = Factory::getLanguage(); - $plugins = PluginHelper::getPlugin('finder'); + $lang = Factory::getLanguage(); + $plugins = PluginHelper::getPlugin('finder'); - foreach ($plugins as $plugin) - { - $lang->load('plg_finder_' . $plugin->name . '.sys', JPATH_ADMINISTRATOR) - || $lang->load('plg_finder_' . $plugin->name . '.sys', JPATH_PLUGINS . '/finder/' . $plugin->name); - } + foreach ($plugins as $plugin) { + $lang->load('plg_finder_' . $plugin->name . '.sys', JPATH_ADMINISTRATOR) + || $lang->load('plg_finder_' . $plugin->name . '.sys', JPATH_PLUGINS . '/finder/' . $plugin->name); + } - return $data; - } + return $data; + } } diff --git a/code/administrator/components/com_finder/src/Response/Response.php b/code/administrator/components/com_finder/src/Response/Response.php index d45a7f40..c59acdce 100644 --- a/code/administrator/components/com_finder/src/Response/Response.php +++ b/code/administrator/components/com_finder/src/Response/Response.php @@ -1,4 +1,5 @@ get('enable_logging', '0')) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'indexer.php'; - Log::addLogger($options); - } + if ($params->get('enable_logging', '0')) { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'indexer.php'; + Log::addLogger($options); + } - // Check if we are dealing with an error. - if ($state instanceof \Exception) - { - // Log the error - try - { - Log::add($state->getMessage(), Log::ERROR); - } - catch (\RuntimeException $exception) - { - // Informational log only - } + // Check if we are dealing with an error. + if ($state instanceof \Exception) { + // Log the error + try { + Log::add($state->getMessage(), Log::ERROR); + } catch (\RuntimeException $exception) { + // Informational log only + } - // Prepare the error response. - $this->error = true; - $this->header = Text::_('COM_FINDER_INDEXER_HEADER_ERROR'); - $this->message = $state->getMessage(); - } - else - { - // Prepare the response data. - $this->batchSize = (int) $state->batchSize; - $this->batchOffset = (int) $state->batchOffset; - $this->totalItems = (int) $state->totalItems; - $this->pluginState = $state->pluginState; + // Prepare the error response. + $this->error = true; + $this->header = Text::_('COM_FINDER_INDEXER_HEADER_ERROR'); + $this->message = $state->getMessage(); + } else { + // Prepare the response data. + $this->batchSize = (int) $state->batchSize; + $this->batchOffset = (int) $state->batchOffset; + $this->totalItems = (int) $state->totalItems; + $this->pluginState = $state->pluginState; - $this->startTime = $state->startTime; - $this->endTime = Factory::getDate()->toSql(); + $this->startTime = $state->startTime; + $this->endTime = Factory::getDate()->toSql(); - $this->start = !empty($state->start) ? (int) $state->start : 0; - $this->complete = !empty($state->complete) ? (int) $state->complete : 0; + $this->start = !empty($state->start) ? (int) $state->start : 0; + $this->complete = !empty($state->complete) ? (int) $state->complete : 0; - // Set the appropriate messages. - if ($this->totalItems <= 0 && $this->complete) - { - $this->header = Text::_('COM_FINDER_INDEXER_HEADER_COMPLETE'); - $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_COMPLETE'); - } - elseif ($this->totalItems <= 0) - { - $this->header = Text::_('COM_FINDER_INDEXER_HEADER_OPTIMIZE'); - $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_OPTIMIZE'); - } - else - { - $this->header = Text::_('COM_FINDER_INDEXER_HEADER_RUNNING'); - $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_RUNNING'); - } - } - } + // Set the appropriate messages. + if ($this->totalItems <= 0 && $this->complete) { + $this->header = Text::_('COM_FINDER_INDEXER_HEADER_COMPLETE'); + $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_COMPLETE'); + } elseif ($this->totalItems <= 0) { + $this->header = Text::_('COM_FINDER_INDEXER_HEADER_OPTIMIZE'); + $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_OPTIMIZE'); + } else { + $this->header = Text::_('COM_FINDER_INDEXER_HEADER_RUNNING'); + $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_RUNNING'); + } + } + } } diff --git a/code/administrator/components/com_finder/src/Service/HTML/Filter.php b/code/administrator/components/com_finder/src/Service/HTML/Filter.php index f9de3fcc..87e9687b 100644 --- a/code/administrator/components/com_finder/src/Service/HTML/Filter.php +++ b/code/administrator/components/com_finder/src/Service/HTML/Filter.php @@ -1,4 +1,5 @@ getQuery(true); - $user = Factory::getUser(); - $groups = implode(',', $user->getAuthorisedViewLevels()); - $html = ''; - $filter = null; - - // Get the configuration options. - $filterId = $options['filter_id'] ?? null; - $activeNodes = array_key_exists('selected_nodes', $options) ? $options['selected_nodes'] : array(); - $classSuffix = array_key_exists('class_suffix', $options) ? $options['class_suffix'] : ''; - - // Load the predefined filter if specified. - if (!empty($filterId)) - { - $query->select('f.data, f.params') - ->from($db->quoteName('#__finder_filters') . ' AS f') - ->where('f.filter_id = ' . (int) $filterId); - - // Load the filter data. - $db->setQuery($query); - - try - { - $filter = $db->loadObject(); - } - catch (\RuntimeException $e) - { - return null; - } - - // Initialize the filter parameters. - if ($filter) - { - $filter->params = new Registry($filter->params); - } - } - - // Build the query to get the branch data and the number of child nodes. - $query->clear() - ->select('t.*, count(c.id) AS children') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS c ON c.parent_id = t.id') - ->where('t.parent_id = 1') - ->where('t.state = 1') - ->where('t.access IN (' . $groups . ')') - ->group('t.id, t.parent_id, t.state, t.access, t.title, c.parent_id') - ->order('t.lft, t.title'); - - // Limit the branch children to a predefined filter. - if ($filter) - { - $query->where('c.id IN(' . $filter->data . ')'); - } - - // Load the branches. - $db->setQuery($query); - - try - { - $branches = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - return null; - } - - // Check that we have at least one branch. - if (count($branches) === 0) - { - return null; - } - - $branch_keys = array_keys($branches); - $html .= HTMLHelper::_('bootstrap.startAccordion', 'accordion', array('active' => 'accordion-' . $branch_keys[0]) - ); - - // Load plugin language files. - LanguageHelper::loadPluginLanguage(); - - // Iterate through the branches and build the branch groups. - foreach ($branches as $bk => $bv) - { - // If the multi-lang plugin is enabled then drop the language branch. - if ($bv->title === 'Language' && Multilanguage::isEnabled()) - { - continue; - } - - // Build the query to get the child nodes for this branch. - $query->clear() - ->select('t.*') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->where('t.lft > ' . (int) $bv->lft) - ->where('t.rgt < ' . (int) $bv->rgt) - ->where('t.state = 1') - ->where('t.access IN (' . $groups . ')') - ->order('t.lft, t.title'); - - // Self-join to get the parent title. - $query->select('e.title AS parent_title') - ->join('LEFT', $db->quoteName('#__finder_taxonomy', 'e') . ' ON ' . $db->quoteName('e.id') . ' = ' . $db->quoteName('t.parent_id')); - - // Load the branches. - $db->setQuery($query); - - try - { - $nodes = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - return null; - } - - // Translate node titles if possible. - $lang = Factory::getLanguage(); - - foreach ($nodes as $nk => $nv) - { - if (trim($nv->parent_title, '*') === 'Language') - { - $title = LanguageHelper::branchLanguageTitle($nv->title); - } - else - { - $key = LanguageHelper::branchPlural($nv->title); - $title = $lang->hasKey($key) ? Text::_($key) : $nv->title; - } - - $nodes[$nk]->title = $title; - } - - // Adding slides - $html .= HTMLHelper::_('bootstrap.addSlide', - 'accordion', - Text::sprintf('COM_FINDER_FILTER_BRANCH_LABEL', - Text::_(LanguageHelper::branchSingular($bv->title)) . ' - ' . count($nodes) - ), - 'accordion-' . $bk - ); - - // Populate the toggle button. - $html .= '
'; - - // Populate the group with nodes. - foreach ($nodes as $nk => $nv) - { - // Determine if the node should be checked. - $checked = in_array($nk, $activeNodes) ? ' checked="checked"' : ''; - - // Build a node. - $html .= '
'; - $html .= ''; - $html .= '
'; - } - - $html .= HTMLHelper::_('bootstrap.endSlide'); - } - - $html .= HTMLHelper::_('bootstrap.endAccordion'); - - return $html; - } - - /** - * Method to generate filters using select box dropdown controls. - * - * @param Query $idxQuery A Query object. - * @param array $options An array of options. - * - * @return mixed A rendered HTML widget on success, null otherwise. - * - * @since 2.5 - */ - public function select($idxQuery, $options) - { - $user = Factory::getUser(); - $groups = implode(',', $user->getAuthorisedViewLevels()); - $filter = null; - - // Get the configuration options. - $classSuffix = $options->get('class_suffix', null); - $showDates = $options->get('show_date_filters', false); - - // Try to load the results from cache. - $cache = Factory::getCache('com_finder', ''); - $cacheId = 'filter_select_' . serialize(array($idxQuery->filter, $options, $groups, Factory::getLanguage()->getTag())); - - // Check the cached results. - if ($cache->contains($cacheId)) - { - $branches = $cache->get($cacheId); - } - else - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - // Load the predefined filter if specified. - if (!empty($idxQuery->filter)) - { - $query->select('f.data, ' . $db->quoteName('f.params')) - ->from($db->quoteName('#__finder_filters') . ' AS f') - ->where('f.filter_id = ' . (int) $idxQuery->filter); - - // Load the filter data. - $db->setQuery($query); - - try - { - $filter = $db->loadObject(); - } - catch (\RuntimeException $e) - { - return null; - } - - // Initialize the filter parameters. - if ($filter) - { - $filter->params = new Registry($filter->params); - } - } - - // Build the query to get the branch data and the number of child nodes. - $query->clear() - ->select('t.*, count(c.id) AS children') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS c ON c.parent_id = t.id') - ->where('t.parent_id = 1') - ->where('t.state = 1') - ->where('t.access IN (' . $groups . ')') - ->where('c.state = 1') - ->where('c.access IN (' . $groups . ')') - ->group($db->quoteName('t.id')) - ->group($db->quoteName('t.parent_id')) - ->group('t.title, t.state, t.access, t.lft') - ->order('t.lft, t.title'); - - // Limit the branch children to a predefined filter. - if (!empty($filter->data)) - { - $query->where('c.id IN(' . $filter->data . ')'); - } - - // Load the branches. - $db->setQuery($query); - - try - { - $branches = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - return null; - } - - // Check that we have at least one branch. - if (count($branches) === 0) - { - return null; - } - - // Iterate through the branches and build the branch groups. - foreach ($branches as $bk => $bv) - { - // If the multi-lang plugin is enabled then drop the language branch. - if ($bv->title === 'Language' && Multilanguage::isEnabled()) - { - continue; - } - - // Build the query to get the child nodes for this branch. - $query->clear() - ->select('t.*') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->where('t.lft > ' . (int) $bv->lft) - ->where('t.rgt < ' . (int) $bv->rgt) - ->where('t.state = 1') - ->where('t.access IN (' . $groups . ')') - ->order('t.title'); - - // Self-join to get the parent title. - $query->select('e.title AS parent_title') - ->join('LEFT', $db->quoteName('#__finder_taxonomy', 'e') . ' ON ' . $db->quoteName('e.id') . ' = ' . $db->quoteName('t.parent_id')); - - // Limit the nodes to a predefined filter. - if (!empty($filter->data)) - { - $query->where('t.id IN(' . $filter->data . ')'); - } - - // Load the branches. - $db->setQuery($query); - - try - { - $branches[$bk]->nodes = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - return null; - } - - // Translate branch nodes if possible. - $language = Factory::getLanguage(); - - foreach ($branches[$bk]->nodes as $node_id => $node) - { - if (trim($node->parent_title, '*') === 'Language') - { - $title = LanguageHelper::branchLanguageTitle($node->title); - } - else - { - $key = LanguageHelper::branchPlural($node->title); - $title = $language->hasKey($key) ? Text::_($key) : $node->title; - } - - if ($node->level > 2) - { - $branches[$bk]->nodes[$node_id]->title = str_repeat('-', $node->level - 2) . $title; - } - else - { - $branches[$bk]->nodes[$node_id]->title = $title; - } - } - - // Add the Search All option to the branch. - array_unshift($branches[$bk]->nodes, array('id' => null, 'title' => Text::_('COM_FINDER_FILTER_SELECT_ALL_LABEL'))); - } - - // Store the data in cache. - $cache->store($branches, $cacheId); - } - - $html = ''; - - // Add the dates if enabled. - if ($showDates) - { - $html .= HTMLHelper::_('filter.dates', $idxQuery, $options); - } - - $html .= '
'; - - // Iterate through all branches and build code. - foreach ($branches as $bk => $bv) - { - // If the multi-lang plugin is enabled then drop the language branch. - if ($bv->title === 'Language' && Multilanguage::isEnabled()) - { - continue; - } - - $active = null; - - // Check if the branch is in the filter. - if (array_key_exists($bv->title, $idxQuery->filters)) - { - // Get the request filters. - $temp = Factory::getApplication()->input->request->get('t', array(), 'array'); - - // Search for active nodes in the branch and get the active node. - $active = array_intersect($temp, $idxQuery->filters[$bv->title]); - $active = count($active) === 1 ? array_shift($active) : null; - } - - // Build a node. - $html .= '
'; - $html .= '
'; - $html .= ''; - $html .= '
'; - $html .= '
'; - $html .= HTMLHelper::_( - 'select.genericlist', - $branches[$bk]->nodes, 't[]', 'class="form-select advancedSelect"', 'id', 'title', $active, - 'tax-' . OutputFilter::stringURLSafe($bv->title) - ); - $html .= '
'; - $html .= '
'; - } - - $html .= '
'; - - return $html; - } - - /** - * Method to generate fields for filtering dates - * - * @param Query $idxQuery A Query object. - * @param array $options An array of options. - * - * @return mixed A rendered HTML widget on success, null otherwise. - * - * @since 2.5 - */ - public function dates($idxQuery, $options) - { - $html = ''; - - // Get the configuration options. - $classSuffix = $options->get('class_suffix', null); - $loadMedia = $options->get('load_media', true); - $showDates = $options->get('show_date_filters', false); - - if (!empty($showDates)) - { - // Build the date operators options. - $operators = array(); - $operators[] = HTMLHelper::_('select.option', 'before', Text::_('COM_FINDER_FILTER_DATE_BEFORE')); - $operators[] = HTMLHelper::_('select.option', 'exact', Text::_('COM_FINDER_FILTER_DATE_EXACTLY')); - $operators[] = HTMLHelper::_('select.option', 'after', Text::_('COM_FINDER_FILTER_DATE_AFTER')); - - // Load the CSS/JS resources. - if ($loadMedia) - { - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - $wa->useStyle('com_finder.dates'); - } - - // Open the widget. - $html .= '
    '; - - // Start date filter. - $attribs['class'] = 'input-medium'; - $html .= '
  • '; - $html .= ''; - $html .= '
    '; - $html .= HTMLHelper::_( - 'select.genericlist', - $operators, 'w1', 'class="inputbox filter-date-operator advancedSelect form-select w-auto mb-2"', 'value', 'text', $idxQuery->when1, 'finder-filter-w1' - ); - $html .= HTMLHelper::_('calendar', $idxQuery->date1, 'd1', 'filter_date1', '%Y-%m-%d', $attribs); - $html .= '
  • '; - - // End date filter. - $html .= '
  • '; - $html .= ''; - $html .= '
    '; - $html .= HTMLHelper::_( - 'select.genericlist', - $operators, 'w2', 'class="inputbox filter-date-operator advancedSelect form-select w-auto mb-2"', 'value', 'text', $idxQuery->when2, 'finder-filter-w2' - ); - $html .= HTMLHelper::_('calendar', $idxQuery->date2, 'd2', 'filter_date2', '%Y-%m-%d', $attribs); - $html .= '
  • '; - - // Close the widget. - $html .= '
'; - } - - return $html; - } + use DatabaseAwareTrait; + + /** + * Method to generate filters using the slider widget and decorated + * with the FinderFilter JavaScript behaviors. + * + * @param array $options An array of configuration options. [optional] + * + * @return mixed A rendered HTML widget on success, null otherwise. + * + * @since 2.5 + */ + public function slider($options = array()) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + $html = ''; + $filter = null; + + // Get the configuration options. + $filterId = $options['filter_id'] ?? null; + $activeNodes = array_key_exists('selected_nodes', $options) ? $options['selected_nodes'] : array(); + $classSuffix = array_key_exists('class_suffix', $options) ? $options['class_suffix'] : ''; + + // Load the predefined filter if specified. + if (!empty($filterId)) { + $query->select('f.data, f.params') + ->from($db->quoteName('#__finder_filters') . ' AS f') + ->where('f.filter_id = ' . (int) $filterId); + + // Load the filter data. + $db->setQuery($query); + + try { + $filter = $db->loadObject(); + } catch (\RuntimeException $e) { + return null; + } + + // Initialize the filter parameters. + if ($filter) { + $filter->params = new Registry($filter->params); + } + } + + // Build the query to get the branch data and the number of child nodes. + $query->clear() + ->select('t.*, count(c.id) AS children') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS c ON c.parent_id = t.id') + ->where('t.parent_id = 1') + ->where('t.state = 1') + ->where('t.access IN (' . $groups . ')') + ->group('t.id, t.parent_id, t.state, t.access, t.title, c.parent_id') + ->order('t.lft, t.title'); + + // Limit the branch children to a predefined filter. + if ($filter) { + $query->where('c.id IN(' . $filter->data . ')'); + } + + // Load the branches. + $db->setQuery($query); + + try { + $branches = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + return null; + } + + // Check that we have at least one branch. + if (count($branches) === 0) { + return null; + } + + $branch_keys = array_keys($branches); + $html .= HTMLHelper::_('bootstrap.startAccordion', 'accordion', array('active' => 'accordion-' . $branch_keys[0])); + + // Load plugin language files. + LanguageHelper::loadPluginLanguage(); + + // Iterate through the branches and build the branch groups. + foreach ($branches as $bk => $bv) { + // If the multi-lang plugin is enabled then drop the language branch. + if ($bv->title === 'Language' && Multilanguage::isEnabled()) { + continue; + } + + // Build the query to get the child nodes for this branch. + $query->clear() + ->select('t.*') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->where('t.lft > ' . (int) $bv->lft) + ->where('t.rgt < ' . (int) $bv->rgt) + ->where('t.state = 1') + ->where('t.access IN (' . $groups . ')') + ->order('t.lft, t.title'); + + // Self-join to get the parent title. + $query->select('e.title AS parent_title') + ->join('LEFT', $db->quoteName('#__finder_taxonomy', 'e') . ' ON ' . $db->quoteName('e.id') . ' = ' . $db->quoteName('t.parent_id')); + + // Load the branches. + $db->setQuery($query); + + try { + $nodes = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + return null; + } + + // Translate node titles if possible. + $lang = Factory::getLanguage(); + + foreach ($nodes as $nk => $nv) { + if (trim($nv->parent_title, '*') === 'Language') { + $title = LanguageHelper::branchLanguageTitle($nv->title); + } else { + $key = LanguageHelper::branchPlural($nv->title); + $title = $lang->hasKey($key) ? Text::_($key) : $nv->title; + } + + $nodes[$nk]->title = $title; + } + + // Adding slides + $html .= HTMLHelper::_( + 'bootstrap.addSlide', + 'accordion', + Text::sprintf( + 'COM_FINDER_FILTER_BRANCH_LABEL', + Text::_(LanguageHelper::branchSingular($bv->title)) . ' - ' . count($nodes) + ), + 'accordion-' . $bk + ); + + // Populate the toggle button. + $html .= '
'; + + // Populate the group with nodes. + foreach ($nodes as $nk => $nv) { + // Determine if the node should be checked. + $checked = in_array($nk, $activeNodes) ? ' checked="checked"' : ''; + + // Build a node. + $html .= '
'; + $html .= ''; + $html .= '
'; + } + + $html .= HTMLHelper::_('bootstrap.endSlide'); + } + + $html .= HTMLHelper::_('bootstrap.endAccordion'); + + return $html; + } + + /** + * Method to generate filters using select box dropdown controls. + * + * @param Query $idxQuery A Query object. + * @param array $options An array of options. + * + * @return mixed A rendered HTML widget on success, null otherwise. + * + * @since 2.5 + */ + public function select($idxQuery, $options) + { + $user = Factory::getUser(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + $filter = null; + + // Get the configuration options. + $classSuffix = $options->get('class_suffix', null); + $showDates = $options->get('show_date_filters', false); + + // Try to load the results from cache. + $cache = Factory::getCache('com_finder', ''); + $cacheId = 'filter_select_' . serialize(array($idxQuery->filter, $options, $groups, Factory::getLanguage()->getTag())); + + // Check the cached results. + if ($cache->contains($cacheId)) { + $branches = $cache->get($cacheId); + } else { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Load the predefined filter if specified. + if (!empty($idxQuery->filter)) { + $query->select('f.data, ' . $db->quoteName('f.params')) + ->from($db->quoteName('#__finder_filters') . ' AS f') + ->where('f.filter_id = ' . (int) $idxQuery->filter); + + // Load the filter data. + $db->setQuery($query); + + try { + $filter = $db->loadObject(); + } catch (\RuntimeException $e) { + return null; + } + + // Initialize the filter parameters. + if ($filter) { + $filter->params = new Registry($filter->params); + } + } + + // Build the query to get the branch data and the number of child nodes. + $query->clear() + ->select('t.*, count(c.id) AS children') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS c ON c.parent_id = t.id') + ->where('t.parent_id = 1') + ->where('t.state = 1') + ->where('t.access IN (' . $groups . ')') + ->where('c.state = 1') + ->where('c.access IN (' . $groups . ')') + ->group($db->quoteName('t.id')) + ->group($db->quoteName('t.parent_id')) + ->group('t.title, t.state, t.access, t.lft') + ->order('t.lft, t.title'); + + // Limit the branch children to a predefined filter. + if (!empty($filter->data)) { + $query->where('c.id IN(' . $filter->data . ')'); + } + + // Load the branches. + $db->setQuery($query); + + try { + $branches = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + return null; + } + + // Check that we have at least one branch. + if (count($branches) === 0) { + return null; + } + + // Iterate through the branches and build the branch groups. + foreach ($branches as $bk => $bv) { + // If the multi-lang plugin is enabled then drop the language branch. + if ($bv->title === 'Language' && Multilanguage::isEnabled()) { + continue; + } + + // Build the query to get the child nodes for this branch. + $query->clear() + ->select('t.*') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->where('t.lft > ' . (int) $bv->lft) + ->where('t.rgt < ' . (int) $bv->rgt) + ->where('t.state = 1') + ->where('t.access IN (' . $groups . ')') + ->order('t.title'); + + // Self-join to get the parent title. + $query->select('e.title AS parent_title') + ->join('LEFT', $db->quoteName('#__finder_taxonomy', 'e') . ' ON ' . $db->quoteName('e.id') . ' = ' . $db->quoteName('t.parent_id')); + + // Limit the nodes to a predefined filter. + if (!empty($filter->data)) { + $query->where('t.id IN(' . $filter->data . ')'); + } + + // Load the branches. + $db->setQuery($query); + + try { + $branches[$bk]->nodes = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + return null; + } + + // Translate branch nodes if possible. + $language = Factory::getLanguage(); + + foreach ($branches[$bk]->nodes as $node_id => $node) { + if (trim($node->parent_title, '*') === 'Language') { + $title = LanguageHelper::branchLanguageTitle($node->title); + } else { + $key = LanguageHelper::branchPlural($node->title); + $title = $language->hasKey($key) ? Text::_($key) : $node->title; + } + + if ($node->level > 2) { + $branches[$bk]->nodes[$node_id]->title = str_repeat('-', $node->level - 2) . $title; + } else { + $branches[$bk]->nodes[$node_id]->title = $title; + } + } + + // Add the Search All option to the branch. + array_unshift($branches[$bk]->nodes, array('id' => null, 'title' => Text::_('COM_FINDER_FILTER_SELECT_ALL_LABEL'))); + } + + // Store the data in cache. + $cache->store($branches, $cacheId); + } + + $html = ''; + + // Add the dates if enabled. + if ($showDates) { + $html .= HTMLHelper::_('filter.dates', $idxQuery, $options); + } + + $html .= '
'; + + // Iterate through all branches and build code. + foreach ($branches as $bk => $bv) { + // If the multi-lang plugin is enabled then drop the language branch. + if ($bv->title === 'Language' && Multilanguage::isEnabled()) { + continue; + } + + $active = null; + + // Check if the branch is in the filter. + if (array_key_exists($bv->title, $idxQuery->filters)) { + // Get the request filters. + $temp = Factory::getApplication()->input->request->get('t', array(), 'array'); + + // Search for active nodes in the branch and get the active node. + $active = array_intersect($temp, $idxQuery->filters[$bv->title]); + $active = count($active) === 1 ? array_shift($active) : null; + } + + // Build a node. + $html .= '
'; + $html .= '
'; + $html .= ''; + $html .= '
'; + $html .= '
'; + $html .= HTMLHelper::_( + 'select.genericlist', + $branches[$bk]->nodes, + 't[]', + 'class="form-select advancedSelect"', + 'id', + 'title', + $active, + 'tax-' . OutputFilter::stringURLSafe($bv->title) + ); + $html .= '
'; + $html .= '
'; + } + + $html .= '
'; + + return $html; + } + + /** + * Method to generate fields for filtering dates + * + * @param Query $idxQuery A Query object. + * @param array $options An array of options. + * + * @return mixed A rendered HTML widget on success, null otherwise. + * + * @since 2.5 + */ + public function dates($idxQuery, $options) + { + $html = ''; + + // Get the configuration options. + $classSuffix = $options->get('class_suffix', null); + $loadMedia = $options->get('load_media', true); + $showDates = $options->get('show_date_filters', false); + + if (!empty($showDates)) { + // Build the date operators options. + $operators = array(); + $operators[] = HTMLHelper::_('select.option', 'before', Text::_('COM_FINDER_FILTER_DATE_BEFORE')); + $operators[] = HTMLHelper::_('select.option', 'exact', Text::_('COM_FINDER_FILTER_DATE_EXACTLY')); + $operators[] = HTMLHelper::_('select.option', 'after', Text::_('COM_FINDER_FILTER_DATE_AFTER')); + + // Load the CSS/JS resources. + if ($loadMedia) { + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + $wa->useStyle('com_finder.dates'); + } + + // Open the widget. + $html .= '
    '; + + // Start date filter. + $attribs['class'] = 'input-medium'; + $html .= '
  • '; + $html .= ''; + $html .= '
    '; + $html .= HTMLHelper::_( + 'select.genericlist', + $operators, + 'w1', + 'class="inputbox filter-date-operator advancedSelect form-select w-auto mb-2"', + 'value', + 'text', + $idxQuery->when1, + 'finder-filter-w1' + ); + $html .= HTMLHelper::_('calendar', $idxQuery->date1, 'd1', 'filter_date1', '%Y-%m-%d', $attribs); + $html .= '
  • '; + + // End date filter. + $html .= '
  • '; + $html .= ''; + $html .= '
    '; + $html .= HTMLHelper::_( + 'select.genericlist', + $operators, + 'w2', + 'class="inputbox filter-date-operator advancedSelect form-select w-auto mb-2"', + 'value', + 'text', + $idxQuery->when2, + 'finder-filter-w2' + ); + $html .= HTMLHelper::_('calendar', $idxQuery->date2, 'd2', 'filter_date2', '%Y-%m-%d', $attribs); + $html .= '
  • '; + + // Close the widget. + $html .= '
'; + } + + return $html; + } } diff --git a/code/administrator/components/com_finder/src/Service/HTML/Finder.php b/code/administrator/components/com_finder/src/Service/HTML/Finder.php index a0dc52d1..1061212d 100644 --- a/code/administrator/components/com_finder/src/Service/HTML/Finder.php +++ b/code/administrator/components/com_finder/src/Service/HTML/Finder.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT t.title AS text, t.id AS value') - ->from($db->quoteName('#__finder_types') . ' AS t') - ->join('LEFT', $db->quoteName('#__finder_links') . ' AS l ON l.type_id = t.id') - ->order('t.title ASC'); - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - return array(); - } - - // Compile the options. - $options = array(); - - $lang = Factory::getLanguage(); - - foreach ($rows as $row) - { - $key = $lang->hasKey(LanguageHelper::branchPlural($row->text)) ? LanguageHelper::branchPlural($row->text) : $row->text; - $options[] = HTMLHelper::_('select.option', $row->value, Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_($key))); - } - - return $options; - } - - /** - * Creates a list of maps. - * - * @return array An array containing the maps that can be selected. - * - * @since 2.5 - */ - public function mapslist() - { - // Load the finder types. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title', 'text')) - ->select($db->quoteName('id', 'value')) - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' = 1'); - $db->setQuery($query); - - try - { - $branches = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Translate. - $lang = Factory::getLanguage(); - - foreach ($branches as $branch) - { - $key = LanguageHelper::branchPlural($branch->text); - $branch->translatedText = $lang->hasKey($key) ? Text::_($key) : $branch->text; - } - - // Order by title. - $branches = ArrayHelper::sortObjects($branches, 'translatedText', 1, true, true); - - // Compile the options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '', Text::_('COM_FINDER_MAPS_SELECT_BRANCH')); - - // Convert the values to options. - foreach ($branches as $branch) - { - $options[] = HTMLHelper::_('select.option', $branch->value, $branch->translatedText); - } - - return $options; - } - - /** - * Creates a list of published states. - * - * @return array An array containing the states that can be selected. - * - * @since 2.5 - */ - public static function statelist() - { - return array( - HTMLHelper::_('select.option', '1', Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_('JPUBLISHED'))), - HTMLHelper::_('select.option', '0', Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_('JUNPUBLISHED'))) - ); - } + use DatabaseAwareTrait; + + /** + * Creates a list of types to filter on. + * + * @return array An array containing the types that can be selected. + * + * @since 2.5 + */ + public function typeslist() + { + // Load the finder types. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('DISTINCT t.title AS text, t.id AS value') + ->from($db->quoteName('#__finder_types') . ' AS t') + ->join('LEFT', $db->quoteName('#__finder_links') . ' AS l ON l.type_id = t.id') + ->order('t.title ASC'); + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + return array(); + } + + // Compile the options. + $options = array(); + + $lang = Factory::getLanguage(); + + foreach ($rows as $row) { + $key = $lang->hasKey(LanguageHelper::branchPlural($row->text)) ? LanguageHelper::branchPlural($row->text) : $row->text; + $options[] = HTMLHelper::_('select.option', $row->value, Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_($key))); + } + + return $options; + } + + /** + * Creates a list of maps. + * + * @return array An array containing the maps that can be selected. + * + * @since 2.5 + */ + public function mapslist() + { + // Load the finder types. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title', 'text')) + ->select($db->quoteName('id', 'value')) + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' = 1'); + $db->setQuery($query); + + try { + $branches = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Translate. + $lang = Factory::getLanguage(); + + foreach ($branches as $branch) { + $key = LanguageHelper::branchPlural($branch->text); + $branch->translatedText = $lang->hasKey($key) ? Text::_($key) : $branch->text; + } + + // Order by title. + $branches = ArrayHelper::sortObjects($branches, 'translatedText', 1, true, true); + + // Compile the options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '', Text::_('COM_FINDER_MAPS_SELECT_BRANCH')); + + // Convert the values to options. + foreach ($branches as $branch) { + $options[] = HTMLHelper::_('select.option', $branch->value, $branch->translatedText); + } + + return $options; + } + + /** + * Creates a list of published states. + * + * @return array An array containing the states that can be selected. + * + * @since 2.5 + */ + public static function statelist() + { + return array( + HTMLHelper::_('select.option', '1', Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_('JPUBLISHED'))), + HTMLHelper::_('select.option', '0', Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_('JUNPUBLISHED'))) + ); + } } diff --git a/code/administrator/components/com_finder/src/Service/HTML/Query.php b/code/administrator/components/com_finder/src/Service/HTML/Query.php index 47540a83..b6c7346f 100644 --- a/code/administrator/components/com_finder/src/Service/HTML/Query.php +++ b/code/administrator/components/com_finder/src/Service/HTML/Query.php @@ -1,4 +1,5 @@ included as $token) - { - if ($token->required && (!isset($token->derived) || $token->derived == false)) - { - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_REQUIRED', $token->term) . ''; - } - } - - // Process the optional tokens. - foreach ($query->included as $token) - { - if (!$token->required && (!isset($token->derived) || $token->derived == false)) - { - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_OPTIONAL', $token->term) . ''; - } - } - - // Process the excluded tokens. - foreach ($query->excluded as $token) - { - if (!isset($token->derived) || $token->derived === false) - { - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_EXCLUDED', $token->term) . ''; - } - } - - // Process the start date. - if ($query->date1) - { - $date = Factory::getDate($query->date1)->format(Text::_('DATE_FORMAT_LC')); - $datecondition = Text::_('COM_FINDER_QUERY_DATE_CONDITION_' . strtoupper($query->when1)); - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_START_DATE', $datecondition, $date) . ''; - } - - // Process the end date. - if ($query->date2) - { - $date = Factory::getDate($query->date2)->format(Text::_('DATE_FORMAT_LC')); - $datecondition = Text::_('COM_FINDER_QUERY_DATE_CONDITION_' . strtoupper($query->when2)); - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_END_DATE', $datecondition, $date) . ''; - } - - // Process the taxonomy filters. - if (!empty($query->filters)) - { - // Get the filters in the request. - $t = Factory::getApplication()->input->request->get('t', array(), 'array'); - - // Process the taxonomy branches. - foreach ($query->filters as $branch => $nodes) - { - // Process the taxonomy nodes. - $lang = Factory::getLanguage(); - - foreach ($nodes as $title => $id) - { - // Translate the title for Types - $key = LanguageHelper::branchPlural($title); - - if ($lang->hasKey($key)) - { - $title = Text::_($key); - } - - // Don't include the node if it is not in the request. - if (!in_array($id, $t)) - { - continue; - } - - // Add the node to the explanation. - $parts[] = '' - . Text::sprintf('COM_FINDER_QUERY_TAXONOMY_NODE', $title, Text::_(LanguageHelper::branchSingular($branch))) - . ''; - } - } - } - - // Build the interpreted query. - return count($parts) ? implode(Text::_('COM_FINDER_QUERY_TOKEN_GLUE'), $parts) : null; - } - - /** - * Method to get the suggested search query. - * - * @param IndexerQuery $query A IndexerQuery object. - * - * @return mixed String if there is a suggestion, false otherwise. - * - * @since 2.5 - */ - public static function suggested(IndexerQuery $query) - { - $suggested = false; - - // Check if the query input is empty. - if (empty($query->input)) - { - return $suggested; - } - - // Check if there were any ignored or included keywords. - if (count($query->ignored) || count($query->included)) - { - $suggested = $query->input; - - // Replace the ignored keyword suggestions. - foreach (array_reverse($query->ignored) as $token) - { - if (isset($token->suggestion)) - { - $suggested = str_ireplace($token->term, $token->suggestion, $suggested); - } - } - - // Replace the included keyword suggestions. - foreach (array_reverse($query->included) as $token) - { - if (isset($token->suggestion)) - { - $suggested = str_ireplace($token->term, $token->suggestion, $suggested); - } - } - - // Check if we made any changes. - if ($suggested == $query->input) - { - $suggested = false; - } - } - - return $suggested; - } + /** + * Method to get the explained (human-readable) search query. + * + * @param IndexerQuery $query A IndexerQuery object to explain. + * + * @return mixed String if there is data to explain, null otherwise. + * + * @since 2.5 + */ + public static function explained(IndexerQuery $query) + { + $parts = array(); + + // Process the required tokens. + foreach ($query->included as $token) { + if ($token->required && (!isset($token->derived) || $token->derived == false)) { + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_REQUIRED', $token->term) . ''; + } + } + + // Process the optional tokens. + foreach ($query->included as $token) { + if (!$token->required && (!isset($token->derived) || $token->derived == false)) { + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_OPTIONAL', $token->term) . ''; + } + } + + // Process the excluded tokens. + foreach ($query->excluded as $token) { + if (!isset($token->derived) || $token->derived === false) { + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_EXCLUDED', $token->term) . ''; + } + } + + // Process the start date. + if ($query->date1) { + $date = Factory::getDate($query->date1)->format(Text::_('DATE_FORMAT_LC')); + $datecondition = Text::_('COM_FINDER_QUERY_DATE_CONDITION_' . strtoupper($query->when1)); + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_START_DATE', $datecondition, $date) . ''; + } + + // Process the end date. + if ($query->date2) { + $date = Factory::getDate($query->date2)->format(Text::_('DATE_FORMAT_LC')); + $datecondition = Text::_('COM_FINDER_QUERY_DATE_CONDITION_' . strtoupper($query->when2)); + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_END_DATE', $datecondition, $date) . ''; + } + + // Process the taxonomy filters. + if (!empty($query->filters)) { + // Get the filters in the request. + $t = Factory::getApplication()->input->request->get('t', array(), 'array'); + + // Process the taxonomy branches. + foreach ($query->filters as $branch => $nodes) { + // Process the taxonomy nodes. + $lang = Factory::getLanguage(); + + foreach ($nodes as $title => $id) { + // Translate the title for Types + $key = LanguageHelper::branchPlural($title); + + if ($lang->hasKey($key)) { + $title = Text::_($key); + } + + // Don't include the node if it is not in the request. + if (!in_array($id, $t)) { + continue; + } + + // Add the node to the explanation. + $parts[] = '' + . Text::sprintf('COM_FINDER_QUERY_TAXONOMY_NODE', $title, Text::_(LanguageHelper::branchSingular($branch))) + . ''; + } + } + } + + // Build the interpreted query. + return count($parts) ? implode(Text::_('COM_FINDER_QUERY_TOKEN_GLUE'), $parts) : null; + } + + /** + * Method to get the suggested search query. + * + * @param IndexerQuery $query A IndexerQuery object. + * + * @return mixed String if there is a suggestion, false otherwise. + * + * @since 2.5 + */ + public static function suggested(IndexerQuery $query) + { + $suggested = false; + + // Check if the query input is empty. + if (empty($query->input)) { + return $suggested; + } + + // Check if there were any ignored or included keywords. + if (count($query->ignored) || count($query->included)) { + $suggested = $query->input; + + // Replace the ignored keyword suggestions. + foreach (array_reverse($query->ignored) as $token) { + if (isset($token->suggestion)) { + $suggested = str_ireplace($token->term, $token->suggestion, $suggested); + } + } + + // Replace the included keyword suggestions. + foreach (array_reverse($query->included) as $token) { + if (isset($token->suggestion)) { + $suggested = str_ireplace($token->term, $token->suggestion, $suggested); + } + } + + // Check if we made any changes. + if ($suggested == $query->input) { + $suggested = false; + } + } + + return $suggested; + } } diff --git a/code/administrator/components/com_finder/src/Table/FilterTable.php b/code/administrator/components/com_finder/src/Table/FilterTable.php index 2e12e043..621076fa 100644 --- a/code/administrator/components/com_finder/src/Table/FilterTable.php +++ b/code/administrator/components/com_finder/src/Table/FilterTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - } - - /** - * Method to perform sanity checks on the \JTable instance properties to ensure - * they are safe to store in the database. Child classes should override this - * method to make sure the data they are storing in the database is safe and - * as expected before storage. - * - * @return boolean True if the instance is sane and able to be stored in the database. - * - * @since 2.5 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (trim($this->alias) === '') - { - $this->alias = $this->title; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias); - - if (trim(str_replace('-', '', $this->alias)) === '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - $params = new Registry($this->params); - - $d1 = $params->get('d1', ''); - $d2 = $params->get('d2', ''); - - // Check the end date is not earlier than the start date. - if (!empty($d1) && !empty($d2) && $d2 < $d1) - { - // Swap the dates. - $params->set('d1', $d2); - $params->set('d2', $d1); - $this->params = (string) $params; - } - - return true; - } - - /** - * Method to store a row in the database from the \JTable instance properties. - * If a primary key value is set the row with that primary key value will be - * updated with the instance property values. If no primary key value is set - * a new row will be inserted into the database with the properties from the - * \JTable instance. - * - * @param boolean $updateNulls True to update fields even if they are null. [optional] - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate()->toSql(); - $userId = Factory::getUser()->id; - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = $date; - } - - if ($this->filter_id) - { - // Existing item - $this->modified_by = $userId; - $this->modified = $date; - } - else - { - if (empty($this->created_by)) - { - $this->created_by = $userId; - } - - if (!(int) $this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - } - - if (is_array($this->data)) - { - $this->map_count = count($this->data); - $this->data = implode(',', $this->data); - } - else - { - $this->map_count = 0; - $this->data = implode(',', array()); - } - - // Verify that the alias is unique - $table = new static($this->getDbo()); - - if ($table->load(array('alias' => $this->alias)) && ($table->filter_id != $this->filter_id || $this->filter_id == 0)) - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_ARTICLE_UNIQUE_ALIAS')); - - return false; - } - - return parent::store($updateNulls); - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Ensure the params are json encoded in the bind method + * + * @var array + * @since 4.0.0 + */ + protected $_jsonEncode = array('params'); + + /** + * Constructor + * + * @param DatabaseDriver $db Database Driver connector object. + * + * @since 2.5 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__finder_filters', 'filter_id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Method to perform sanity checks on the \JTable instance properties to ensure + * they are safe to store in the database. Child classes should override this + * method to make sure the data they are storing in the database is safe and + * as expected before storage. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @since 2.5 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (trim($this->alias) === '') { + $this->alias = $this->title; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias); + + if (trim(str_replace('-', '', $this->alias)) === '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + $params = new Registry($this->params); + + $d1 = $params->get('d1', ''); + $d2 = $params->get('d2', ''); + + // Check the end date is not earlier than the start date. + if (!empty($d1) && !empty($d2) && $d2 < $d1) { + // Swap the dates. + $params->set('d1', $d2); + $params->set('d2', $d1); + $this->params = (string) $params; + } + + return true; + } + + /** + * Method to store a row in the database from the \JTable instance properties. + * If a primary key value is set the row with that primary key value will be + * updated with the instance property values. If no primary key value is set + * a new row will be inserted into the database with the properties from the + * \JTable instance. + * + * @param boolean $updateNulls True to update fields even if they are null. [optional] + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate()->toSql(); + $userId = Factory::getUser()->id; + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = $date; + } + + if ($this->filter_id) { + // Existing item + $this->modified_by = $userId; + $this->modified = $date; + } else { + if (empty($this->created_by)) { + $this->created_by = $userId; + } + + if (!(int) $this->modified) { + $this->modified = $this->created; + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + } + + if (is_array($this->data)) { + $this->map_count = count($this->data); + $this->data = implode(',', $this->data); + } else { + $this->map_count = 0; + $this->data = implode(',', array()); + } + + // Verify that the alias is unique + $table = new static($this->getDbo()); + + if ($table->load(array('alias' => $this->alias)) && ($table->filter_id != $this->filter_id || $this->filter_id == 0)) { + $this->setError(Text::_('JLIB_DATABASE_ERROR_ARTICLE_UNIQUE_ALIAS')); + + return false; + } + + return parent::store($updateNulls); + } } diff --git a/code/administrator/components/com_finder/src/Table/LinkTable.php b/code/administrator/components/com_finder/src/Table/LinkTable.php index 62f3c0e1..dc38656c 100644 --- a/code/administrator/components/com_finder/src/Table/LinkTable.php +++ b/code/administrator/components/com_finder/src/Table/LinkTable.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Finder\Administrator\Table; use Joomla\CMS\Table\Table; use Joomla\Database\DatabaseDriver; +// phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Link table class for the Finder package. @@ -20,38 +24,38 @@ */ class LinkTable extends Table { - /** - * Indicates that columns fully support the NULL value in the database - * - * @var boolean - * @since 4.0.0 - */ - protected $_supportNullValue = true; + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; - /** - * Constructor - * - * @param DatabaseDriver $db Database Driver connector object. - * - * @since 2.5 - */ - public function __construct(DatabaseDriver $db) - { - parent::__construct('#__finder_links', 'link_id', $db); - } + /** + * Constructor + * + * @param DatabaseDriver $db Database Driver connector object. + * + * @since 2.5 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__finder_links', 'link_id', $db); + } - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - return parent::store($updateNulls); - } + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + return parent::store($updateNulls); + } } diff --git a/code/administrator/components/com_finder/src/Table/MapTable.php b/code/administrator/components/com_finder/src/Table/MapTable.php index 26bc302c..f823f8f4 100644 --- a/code/administrator/components/com_finder/src/Table/MapTable.php +++ b/code/administrator/components/com_finder/src/Table/MapTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - $this->access = (int) Factory::getApplication()->get('access'); - } + $this->setColumnAlias('published', 'state'); + $this->access = (int) Factory::getApplication()->get('access'); + } - /** - * Override check function - * - * @return boolean - * - * @see Table::check() - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); + /** + * Override check function + * + * @return boolean + * + * @see Table::check() + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); - return false; - } + return false; + } - // Check for a title. - if (trim($this->title) == '') - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_CATEGORY')); + // Check for a title. + if (trim($this->title) == '') { + $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_CATEGORY')); - return false; - } + return false; + } - $this->alias = ApplicationHelper::stringURLSafe($this->title, $this->language); + $this->alias = ApplicationHelper::stringURLSafe($this->title, $this->language); - if (trim($this->alias) == '') - { - $this->alias = md5(serialize($this->getProperties())); - } + if (trim($this->alias) == '') { + $this->alias = md5(serialize($this->getProperties())); + } - return true; - } + return true; + } } diff --git a/code/administrator/components/com_finder/src/View/Filter/HtmlView.php b/code/administrator/components/com_finder/src/View/Filter/HtmlView.php index 43dcfe64..384a9d74 100644 --- a/code/administrator/components/com_finder/src/View/Filter/HtmlView.php +++ b/code/administrator/components/com_finder/src/View/Filter/HtmlView.php @@ -1,4 +1,5 @@ filter = $this->get('Filter'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Configure the toolbar. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Method to configure the toolbar for this view. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->filter_id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == Factory::getUser()->id); - $canDo = ContentHelper::getActions('com_finder'); - - // Configure the toolbar. - ToolbarHelper::title( - $isNew ? Text::_('COM_FINDER_FILTER_NEW_TOOLBAR_TITLE') : Text::_('COM_FINDER_FILTER_EDIT_TOOLBAR_TITLE'), - 'zoom-in finder' - ); - - // Set the actions for new and existing records. - if ($isNew) - { - // For new records, check the create permission. - if ($canDo->get('core.create')) - { - ToolbarHelper::apply('filter.apply'); - - ToolbarHelper::saveGroup( - [ - ['save', 'filter.save'], - ['save2new', 'filter.save2new'] - ], - 'btn-success' - ); - } - - ToolbarHelper::cancel('filter.cancel'); - } - else - { - $toolbarButtons = []; - - // Can't save the record if it's checked out. - // Since it's an existing record, check the edit permission. - if (!$checkedOut && $canDo->get('core.edit')) - { - ToolbarHelper::apply('filter.apply'); - - $toolbarButtons[] = ['save', 'filter.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'filter.save2new']; - } - } - - // If an existing item, can save as a copy - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'filter.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('filter.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Smart_Search:_New_or_Edit_Filter'); - } + /** + * The filter object + * + * @var \Joomla\Component\Finder\Administrator\Table\FilterTable + * + * @since 3.6.2 + */ + protected $filter; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + * + * @since 3.6.2 + */ + protected $form; + + /** + * The active item + * + * @var CMSObject|boolean + * + * @since 3.6.2 + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + * + * @since 3.6.2 + */ + protected $state; + + /** + * The total indexed items + * + * @var integer + * + * @since 3.8.0 + */ + protected $total; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load the view data. + $this->filter = $this->get('Filter'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Configure the toolbar. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Method to configure the toolbar for this view. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->filter_id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $this->getCurrentUser()->id); + $canDo = ContentHelper::getActions('com_finder'); + + // Configure the toolbar. + ToolbarHelper::title( + $isNew ? Text::_('COM_FINDER_FILTER_NEW_TOOLBAR_TITLE') : Text::_('COM_FINDER_FILTER_EDIT_TOOLBAR_TITLE'), + 'zoom-in finder' + ); + + // Set the actions for new and existing records. + if ($isNew) { + // For new records, check the create permission. + if ($canDo->get('core.create')) { + ToolbarHelper::apply('filter.apply'); + + ToolbarHelper::saveGroup( + [ + ['save', 'filter.save'], + ['save2new', 'filter.save2new'] + ], + 'btn-success' + ); + } + + ToolbarHelper::cancel('filter.cancel'); + } else { + $toolbarButtons = []; + + // Can't save the record if it's checked out. + // Since it's an existing record, check the edit permission. + if (!$checkedOut && $canDo->get('core.edit')) { + ToolbarHelper::apply('filter.apply'); + + $toolbarButtons[] = ['save', 'filter.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'filter.save2new']; + } + } + + // If an existing item, can save as a copy + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'filter.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('filter.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Smart_Search:_New_or_Edit_Filter'); + } } diff --git a/code/administrator/components/com_finder/src/View/Filters/HtmlView.php b/code/administrator/components/com_finder/src/View/Filters/HtmlView.php index f935a72e..7e4cfd3a 100644 --- a/code/administrator/components/com_finder/src/View/Filters/HtmlView.php +++ b/code/administrator/components/com_finder/src/View/Filters/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->total = $this->get('Total'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (\count($this->items) === 0 && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Configure the toolbar. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Method to configure the toolbar for this view. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_finder'); - - ToolbarHelper::title(Text::_('COM_FINDER_FILTERS_TOOLBAR_TITLE'), 'search-plus finder'); - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('filter.add'); - ToolbarHelper::divider(); - } - - if ($this->isEmptyState === false) - { - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('filters.publish')->listCheck(true); - $childBar->unpublish('filters.unpublish')->listCheck(true); - $childBar->checkin('filters.checkin')->listCheck(true); - } - - ToolbarHelper::divider(); - $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); - ToolbarHelper::divider(); - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('', 'filters.delete'); - ToolbarHelper::divider(); - } - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_finder'); - } - - ToolbarHelper::help('Smart_Search:_Search_Filters'); - } + /** + * An array of items + * + * @var array + * + * @since 3.6.1 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.6.1 + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.6.1 + */ + protected $state; + + /** + * The total number of items + * + * @var integer + * + * @since 3.6.1 + */ + protected $total; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load the view data. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->total = $this->get('Total'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (\count($this->items) === 0 && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Configure the toolbar. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Method to configure the toolbar for this view. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_finder'); + + ToolbarHelper::title(Text::_('COM_FINDER_FILTERS_TOOLBAR_TITLE'), 'search-plus finder'); + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('filter.add'); + ToolbarHelper::divider(); + } + + if ($this->isEmptyState === false) { + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('filters.publish')->listCheck(true); + $childBar->unpublish('filters.unpublish')->listCheck(true); + $childBar->checkin('filters.checkin')->listCheck(true); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('', 'filters.delete'); + ToolbarHelper::divider(); + } + + ToolbarHelper::divider(); + $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_finder'); + } + + ToolbarHelper::help('Smart_Search:_Search_Filters'); + } } diff --git a/code/administrator/components/com_finder/src/View/Index/HtmlView.php b/code/administrator/components/com_finder/src/View/Index/HtmlView.php index fc096769..b89769b7 100644 --- a/code/administrator/components/com_finder/src/View/Index/HtmlView.php +++ b/code/administrator/components/com_finder/src/View/Index/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->total = $this->get('Total'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->pluginState = $this->get('pluginState'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if ($this->get('TotalIndexed') === 0 && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check that the content - finder plugin is enabled - if (!PluginHelper::isEnabled('content', 'finder')) - { - $this->finderPluginId = FinderHelper::getFinderPluginId(); - } - - // Configure the toolbar. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Method to configure the toolbar for this view. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_finder'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_FINDER_INDEX_TOOLBAR_TITLE'), 'search-plus finder'); - - $toolbar->appendButton( - 'Popup', 'archive', 'COM_FINDER_INDEX', 'index.php?option=com_finder&view=indexer&tmpl=component', 500, 210, 0, 0, - 'window.parent.location.reload()', Text::_('COM_FINDER_HEADING_INDEXER') - ); - - if (!$this->isEmptyState) - { - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('index.publish')->listCheck(true); - $childBar->unpublish('index.unpublish')->listCheck(true); - } - - $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('', 'index.delete'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.edit.state')) - { - ToolbarHelper::trash('index.purge', 'COM_FINDER_INDEX_TOOLBAR_PURGE', false); - } - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_finder'); - } - - ToolbarHelper::help('Smart_Search:_Indexed_Content'); - } + /** + * An array of items + * + * @var array + * + * @since 3.6.1 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.6.1 + */ + protected $pagination; + + /** + * The state of core Smart Search plugins + * + * @var array + * + * @since 3.6.1 + */ + protected $pluginState; + + /** + * The id of the content - finder plugin in mysql + * + * @var integer + * + * @since 4.0.0 + */ + protected $finderPluginId = 0; + + /** + * The model state + * + * @var mixed + * + * @since 3.6.1 + */ + protected $state; + + /** + * The total number of items + * + * @var integer + * + * @since 3.6.1 + */ + protected $total; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * @var mixed + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load plugin language files. + LanguageHelper::loadPluginLanguage(); + + $this->items = $this->get('Items'); + $this->total = $this->get('Total'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->pluginState = $this->get('pluginState'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if ($this->get('TotalIndexed') === 0 && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check that the content - finder plugin is enabled + if (!PluginHelper::isEnabled('content', 'finder')) { + $this->finderPluginId = FinderHelper::getFinderPluginId(); + } + + // Configure the toolbar. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Method to configure the toolbar for this view. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_finder'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_FINDER_INDEX_TOOLBAR_TITLE'), 'search-plus finder'); + + $toolbar->appendButton( + 'Popup', + 'archive', + 'COM_FINDER_INDEX', + 'index.php?option=com_finder&view=indexer&tmpl=component', + 500, + 210, + 0, + 0, + 'window.parent.location.reload()', + Text::_('COM_FINDER_HEADING_INDEXER') + ); + + if (!$this->isEmptyState) { + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('index.publish')->listCheck(true); + $childBar->unpublish('index.unpublish')->listCheck(true); + } + + if ($canDo->get('core.delete')) { + $toolbar->confirmButton('', 'JTOOLBAR_DELETE', 'index.delete') + ->icon('icon-delete') + ->message('COM_FINDER_INDEX_CONFIRM_DELETE_PROMPT') + ->listCheck(true); + $toolbar->divider(); + } + + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('maintenance-group'); + $dropdown->text('COM_FINDER_INDEX_TOOLBAR_MAINTENANCE') + ->toggleSplit(false) + ->icon('icon-wrench') + ->buttonClass('btn btn-action'); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->standardButton('cog', 'COM_FINDER_INDEX_TOOLBAR_OPTIMISE', 'index.optimise', false); + $childBar->confirmButton('index.purge', 'COM_FINDER_INDEX_TOOLBAR_PURGE', 'index.purge') + ->icon('icon-trash') + ->message('COM_FINDER_INDEX_CONFIRM_PURGE_PROMPT'); + } + + $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_finder'); + } + + ToolbarHelper::help('Smart_Search:_Indexed_Content'); + } } diff --git a/code/administrator/components/com_finder/src/View/Indexer/HtmlView.php b/code/administrator/components/com_finder/src/View/Indexer/HtmlView.php index 981ffbbf..e13214d3 100644 --- a/code/administrator/components/com_finder/src/View/Indexer/HtmlView.php +++ b/code/administrator/components/com_finder/src/View/Indexer/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->total = $this->get('Total'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if ($this->total === 0 && $this->isEmptyState = $this->get('isEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Prepare the view. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Method to configure the toolbar for this view. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_finder'); - - ToolbarHelper::title(Text::_('COM_FINDER_MAPS_TOOLBAR_TITLE'), 'search-plus finder'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if (!$this->isEmptyState) - { - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('maps.publish')->listCheck(true); - $childBar->unpublish('maps.unpublish')->listCheck(true); - } - - ToolbarHelper::divider(); - $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); - ToolbarHelper::divider(); - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('', 'maps.delete'); - ToolbarHelper::divider(); - } - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_finder'); - } - - ToolbarHelper::help('Smart_Search:_Content_Maps'); - } + /** + * An array of items + * + * @var array + * + * @since 3.6.1 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.6.1 + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.6.1 + */ + protected $state; + + /** + * The total number of items + * + * @var integer + * + * @since 3.6.1 + */ + protected $total; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load plugin language files. + LanguageHelper::loadPluginLanguage(); + + // Load the view data. + $this->items = $this->get('Items'); + $this->total = $this->get('Total'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if ($this->total === 0 && $this->isEmptyState = $this->get('isEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Prepare the view. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Method to configure the toolbar for this view. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_finder'); + + ToolbarHelper::title(Text::_('COM_FINDER_MAPS_TOOLBAR_TITLE'), 'search-plus finder'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if (!$this->isEmptyState) { + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('maps.publish')->listCheck(true); + $childBar->unpublish('maps.unpublish')->listCheck(true); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('', 'maps.delete'); + ToolbarHelper::divider(); + } + + ToolbarHelper::divider(); + $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_finder'); + } + + ToolbarHelper::help('Smart_Search:_Content_Maps'); + } } diff --git a/code/administrator/components/com_finder/src/View/Searches/HtmlView.php b/code/administrator/components/com_finder/src/View/Searches/HtmlView.php index b8029582..57697391 100644 --- a/code/administrator/components/com_finder/src/View/Searches/HtmlView.php +++ b/code/administrator/components/com_finder/src/View/Searches/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->enabled = $this->state->params->get('gather_search_statistics', 0); - $this->canDo = ContentHelper::getActions('com_finder'); - $uri = Uri::getInstance(); - $link = 'index.php?option=com_config&view=component&component=com_finder&return=' . base64_encode($uri); - $output = HTMLHelper::_('link', Route::_($link), Text::_('JOPTIONS')); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check if component is enabled - if (!$this->enabled) - { - $app->enqueueMessage(Text::sprintf('COM_FINDER_LOGGING_DISABLED', $output), 'warning'); - } - - // Prepare the view. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = $this->canDo; - - ToolbarHelper::title(Text::_('COM_FINDER_MANAGER_SEARCHES'), 'search'); - - if (!$this->isEmptyState) - { - if ($canDo->get('core.edit.state')) - { - ToolbarHelper::custom('searches.reset', 'refresh', '', 'JSEARCH_RESET', false); - } - - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_finder'); - } - - ToolbarHelper::help('Smart_Search:_Search_Term_Analysis'); - } + /** + * True if gathering search statistics is enabled + * + * @var boolean + */ + protected $enabled; + + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * The actions the user is authorised to perform + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->enabled = $this->state->params->get('gather_search_statistics', 0); + $this->canDo = ContentHelper::getActions('com_finder'); + $uri = Uri::getInstance(); + $link = 'index.php?option=com_config&view=component&component=com_finder&return=' . base64_encode($uri); + $output = HTMLHelper::_('link', Route::_($link), Text::_('JOPTIONS')); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check if component is enabled + if (!$this->enabled) { + $app->enqueueMessage(Text::sprintf('COM_FINDER_LOGGING_DISABLED', $output), 'warning'); + } + + // Prepare the view. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = $this->canDo; + + ToolbarHelper::title(Text::_('COM_FINDER_MANAGER_SEARCHES'), 'search'); + + if (!$this->isEmptyState) { + if ($canDo->get('core.edit.state')) { + ToolbarHelper::custom('searches.reset', 'refresh', '', 'JSEARCH_RESET', false); + } + + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_finder'); + } + + ToolbarHelper::help('Smart_Search:_Search_Term_Analysis'); + } } diff --git a/code/administrator/components/com_finder/src/View/Statistics/HtmlView.php b/code/administrator/components/com_finder/src/View/Statistics/HtmlView.php index 1ccaf2fc..84f9f255 100644 --- a/code/administrator/components/com_finder/src/View/Statistics/HtmlView.php +++ b/code/administrator/components/com_finder/src/View/Statistics/HtmlView.php @@ -1,4 +1,5 @@ data = $this->get('Data'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - } + /** + * The index statistics + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.6.1 + */ + protected $data; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load the view data. + $this->data = $this->get('Data'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + } } diff --git a/code/administrator/components/com_finder/tmpl/filter/edit.php b/code/administrator/components/com_finder/tmpl/filter/edit.php index 7cec87a0..1dd6f47b 100644 --- a/code/administrator/components/com_finder/tmpl/filter/edit.php +++ b/code/administrator/components/com_finder/tmpl/filter/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_finder.finder-edit'); + ->useScript('form.validate') + ->useScript('com_finder.finder-edit'); ?>
- - -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
-
- total > 0) : ?> -
- form->renderField('map_count'); ?> -
- - - -
- - - $this->filter->data)); ?> -
-
- -
-
- - - -
-
-
- -
- -
-
-
-
-
- -
- form->renderFieldset('jbasic'); ?> -
-
-
-
- - - - - - - - - -
+ + +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+
+ total > 0) : ?> +
+ form->renderField('map_count'); ?> +
+ + + +
+ + + $this->filter->data)); ?> +
+
+ +
+
+ + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ form->renderFieldset('jbasic'); ?> +
+
+
+
+ + + + + + + + + +
diff --git a/code/administrator/components/com_finder/tmpl/filters/default.php b/code/administrator/components/com_finder/tmpl/filters/default.php index 47a06806..3be3fc9f 100644 --- a/code/administrator/components/com_finder/tmpl/filters/default.php +++ b/code/administrator/components/com_finder/tmpl/filters/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect') - ->useScript('com_finder.filters'); +$wa->useScript('com_finder.filters') + ->useScript('table.columns') + ->useScript('multiselect'); ?>
-
-
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - authorise('core.create', 'com_finder'); - $canEdit = $user->authorise('core.edit', 'com_finder'); - $userAuthoriseCoreManage = $user->authorise('core.manage', 'com_checkin'); - $userAuthoriseCoreEditState = $user->authorise('core.edit.state', 'com_finder'); - $userId = $user->id; - foreach ($this->items as $i => $item) : - $canCheckIn = $userAuthoriseCoreManage || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $userAuthoriseCoreEditState && $canCheckIn; - $escapedTitle = $this->escape($item->title); - ?> - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - -
- filter_id, false, 'cid', 'cb', $item->title); ?> - - state, $i, 'filters.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'filters.', $canCheckIn); ?> - - - - - - - - - created_by_alias ?: $item->user_name; ?> - - created, Text::_('DATE_FORMAT_LC4')); ?> - - map_count; ?> - - filter_id; ?> -
+
+
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + authorise('core.create', 'com_finder'); + $canEdit = $user->authorise('core.edit', 'com_finder'); + $userAuthoriseCoreManage = $user->authorise('core.manage', 'com_checkin'); + $userAuthoriseCoreEditState = $user->authorise('core.edit.state', 'com_finder'); + $userId = $user->id; + foreach ($this->items as $i => $item) : + $canCheckIn = $userAuthoriseCoreManage || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $userAuthoriseCoreEditState && $canCheckIn; + $escapedTitle = $this->escape($item->title); + ?> + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + +
+ filter_id, false, 'cid', 'cb', $item->title); ?> + + state, $i, 'filters.', $canChange); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'filters.', $canCheckIn); ?> + + + + + + + + + created_by_alias ?: $item->user_name; ?> + + created, Text::_('DATE_FORMAT_LC4')); ?> + + map_count; ?> + + filter_id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + +
+
+
diff --git a/code/administrator/components/com_finder/tmpl/filters/emptystate.php b/code/administrator/components/com_finder/tmpl/filters/emptystate.php index aaa26c47..bea0eab7 100644 --- a/code/administrator/components/com_finder/tmpl/filters/emptystate.php +++ b/code/administrator/components/com_finder/tmpl/filters/emptystate.php @@ -1,4 +1,5 @@ 'COM_FINDER', - 'formURL' => 'index.php?option=com_finder&view=filters', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Smart_Search_quickstart_guide', - 'icon' => 'icon-search-plus finder', - 'btnadd' => Text::_('COM_FINDER_FILTERS_EMPTYSTATE_BUTTON_ADD'), - 'content' => Text::_('COM_FINDER_FILTERS_EMPTYSTATE_CONTENT'), - 'title' => Text::_('COM_FINDER_FILTERS_TOOLBAR_TITLE'), + 'textPrefix' => 'COM_FINDER', + 'formURL' => 'index.php?option=com_finder&view=filters', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Smart_Search_quickstart_guide', + 'icon' => 'icon-search-plus finder', + 'btnadd' => Text::_('COM_FINDER_FILTERS_EMPTYSTATE_BUTTON_ADD'), + 'content' => Text::_('COM_FINDER_FILTERS_EMPTYSTATE_CONTENT'), + 'title' => Text::_('COM_FINDER_FILTERS_TOOLBAR_TITLE'), ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_finder')) -{ - $displayData['createURL'] = "index.php?option=com_finder&task=filter.add"; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_finder')) { + $displayData['createURL'] = "index.php?option=com_finder&task=filter.add"; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_finder/tmpl/index/default.php b/code/administrator/components/com_finder/tmpl/index/default.php index b0013d19..bc5d1c12 100644 --- a/code/administrator/components/com_finder/tmpl/index/default.php +++ b/code/administrator/components/com_finder/tmpl/index/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); $lang = Factory::getLanguage(); -Text::script('COM_FINDER_INDEX_CONFIRM_PURGE_PROMPT'); -Text::script('COM_FINDER_INDEX_CONFIRM_DELETE_PROMPT'); - /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('multiselect') - ->useScript('com_finder.index'); + ->useScript('table.columns'); ?>
-
-
-
- $this)); ?> - finderPluginId) : ?> - finderPluginId . '&tmpl=component&layout=modal'); ?> - finderPluginId . 'Modal', - array( - 'url' => $link, - 'title' => Text::_('COM_FINDER_EDIT_PLUGIN_SETTINGS'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'closeButton' => false, - 'backdrop' => 'static', - 'keyboard' => false, - 'footer' => '' - . '' - . '' - ) - ); ?> - - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - authorise('core.manage', 'com_finder'); ?> - items as $i => $item) : ?> - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - -
- link_id, false, 'cid', 'cb', $item->title); ?> - - published, $i, 'index.', $canChange, 'cb'); ?> - - escape($item->title); ?> - - t_title); - echo $lang->hasKey($key) ? Text::_($key) : $item->t_title; - ?> - - indexdate, Text::_('DATE_FORMAT_LC4')); ?> - - - - publish_start_date or (int) $item->publish_end_date or (int) $item->start_date or (int) $item->end_date) : ?> - - - - - - - - url) > 80) ? substr($item->url, 0, 70) . '...' : $item->url; ?> -
+
+
+
+ $this)); ?> + finderPluginId) : ?> + finderPluginId . '&tmpl=component&layout=modal'); ?> + finderPluginId . 'Modal', + array( + 'url' => $link, + 'title' => Text::_('COM_FINDER_EDIT_PLUGIN_SETTINGS'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'closeButton' => false, + 'backdrop' => 'static', + 'keyboard' => false, + 'footer' => '' + . '' + . '' + ) + ); ?> + + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + authorise('core.manage', 'com_finder'); ?> + items as $i => $item) : ?> + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + +
+ link_id, false, 'cid', 'cb', $item->title); ?> + + published, $i, 'index.', $canChange, 'cb'); ?> + + escape($item->title); ?> + + t_title); + echo $lang->hasKey($key) ? Text::_($key) : $item->t_title; + ?> + + indexdate, Text::_('DATE_FORMAT_LC4')); ?> + + + + publish_start_date or (int) $item->publish_end_date or (int) $item->start_date or (int) $item->end_date) : ?> + + + + + + + + url) > 80) ? substr($item->url, 0, 70) . '...' : $item->url; ?> +
- - pagination->getListFooter(); ?> - + + pagination->getListFooter(); ?> + - - - -
-
-
+ + + +
+
+
diff --git a/code/administrator/components/com_finder/tmpl/index/emptystate.php b/code/administrator/components/com_finder/tmpl/index/emptystate.php index dea524c6..e6d54ce7 100644 --- a/code/administrator/components/com_finder/tmpl/index/emptystate.php +++ b/code/administrator/components/com_finder/tmpl/index/emptystate.php @@ -1,4 +1,5 @@ 'COM_FINDER', - 'formURL' => 'index.php?option=com_finder&view=index', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Smart_Search_quickstart_guide', - 'icon' => 'icon-search-plus finder', - 'content' => Text::_('COM_FINDER_INDEX_NO_DATA') . '
' . Text::_('COM_FINDER_INDEX_TIP'), - 'title' => Text::_('COM_FINDER_HEADING_INDEXER'), - 'createURL' => "javascript:document.getElementsByClassName('button-archive')[0].click();", + 'textPrefix' => 'COM_FINDER', + 'formURL' => 'index.php?option=com_finder&view=index', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Smart_Search_quickstart_guide', + 'icon' => 'icon-search-plus finder', + 'content' => Text::_('COM_FINDER_INDEX_NO_DATA') . '
' . Text::_('COM_FINDER_INDEX_TIP'), + 'title' => Text::_('COM_FINDER_HEADING_INDEXER'), + 'createURL' => "javascript:document.getElementsByClassName('button-archive')[0].click();", ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); if ($this->finderPluginId) : ?> - finderPluginId . '&tmpl=component&layout=modal'); ?> - finderPluginId . 'Modal', - array( - 'url' => $link, - 'title' => Text::_('COM_FINDER_EDIT_PLUGIN_SETTINGS'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'closeButton' => false, - 'backdrop' => 'static', - 'keyboard' => false, - 'footer' => '' - . '' - . '' - ) - ); ?> - + finderPluginId . '&tmpl=component&layout=modal'); ?> + finderPluginId . 'Modal', + array( + 'url' => $link, + 'title' => Text::_('COM_FINDER_EDIT_PLUGIN_SETTINGS'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'closeButton' => false, + 'backdrop' => 'static', + 'keyboard' => false, + 'footer' => '' + . '' + . '' + ) + ); ?> +document->getWebAssetManager(); $wa->useScript('keepalive') - ->useStyle('com_finder.indexer') - ->useScript('com_finder.indexer'); + ->useStyle('com_finder.indexer') + ->useScript('com_finder.indexer'); ?>
-

-

-
-
-
- -
-
- - +

+

+
+
+
+ +
+
+ +
diff --git a/code/administrator/components/com_finder/tmpl/maps/default.php b/code/administrator/components/com_finder/tmpl/maps/default.php index de3c55e0..9466adc8 100644 --- a/code/administrator/components/com_finder/tmpl/maps/default.php +++ b/code/administrator/components/com_finder/tmpl/maps/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect') - ->useScript('com_finder.maps'); +$wa->useScript('com_finder.maps') + ->useScript('table.columns') + ->useScript('multiselect'); ?>
-
-
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - getIdentity()->authorise('core.manage', 'com_finder'); ?> - items as $i => $item) : ?> - - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->title); ?> - - state, $i, 'maps.', $canChange, 'cb'); ?> - - branch_title, '*') === 'Language') - { - $title = LanguageHelper::branchLanguageTitle($item->title); - } - else - { - $key = LanguageHelper::branchSingular($item->title); - $title = $lang->hasKey($key) ? Text::_($key) : $item->title; - } - ?> - —', $item->level - 1); ?> - escape($title); ?> - escape(trim($title, '*')) === 'Language' && Multilanguage::isEnabled()) : ?> -
- -
- -
- rgt - $item->lft > 1) : ?> - - rgt - $item->lft) / 2); ?> - - - - - - - - level > 1) : ?> - - count_published; ?> - - - - - - - - level > 1) : ?> - - count_unpublished; ?> - - - - - - - - language; ?> -
+
+
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + + getIdentity()->authorise('core.manage', 'com_finder'); ?> + items as $i => $item) : ?> + + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + state, $i, 'maps.', $canChange, 'cb'); ?> + + branch_title, '*') === 'Language') { + $title = LanguageHelper::branchLanguageTitle($item->title); + } else { + $key = LanguageHelper::branchSingular($item->title); + $title = $lang->hasKey($key) ? Text::_($key) : $item->title; + } + ?> + —', $item->level - 1); ?> + escape($title); ?> + escape(trim($title, '*')) === 'Language' && Multilanguage::isEnabled()) : ?> +
+ +
+ +
+ rgt - $item->lft > 1) : ?> + + rgt - $item->lft) / 2); ?> + + + + - + + + level > 1) : ?> + + count_published; ?> + + + + - + + + level > 1) : ?> + + count_unpublished; ?> + + + + - + + + language; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - -
+ +
- - - -
-
+ + + +
+
diff --git a/code/administrator/components/com_finder/tmpl/maps/emptystate.php b/code/administrator/components/com_finder/tmpl/maps/emptystate.php index 3675b05b..301df7df 100644 --- a/code/administrator/components/com_finder/tmpl/maps/emptystate.php +++ b/code/administrator/components/com_finder/tmpl/maps/emptystate.php @@ -1,4 +1,5 @@ 'COM_FINDER', - 'formURL' => 'index.php?option=com_finder&view=maps', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Smart_Search:_Content_Maps', - 'icon' => 'icon-search-plus finder', - 'title' => Text::_('COM_FINDER_MAPS_TOOLBAR_TITLE') + 'textPrefix' => 'COM_FINDER', + 'formURL' => 'index.php?option=com_finder&view=maps', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Smart_Search:_Content_Maps', + 'icon' => 'icon-search-plus finder', + 'title' => Text::_('COM_FINDER_MAPS_TOOLBAR_TITLE') ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_finder/tmpl/searches/default.php b/code/administrator/components/com_finder/tmpl/searches/default.php index b0557ece..81dc67ee 100644 --- a/code/administrator/components/com_finder/tmpl/searches/default.php +++ b/code/administrator/components/com_finder/tmpl/searches/default.php @@ -1,4 +1,5 @@
-
-
-
- $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - -
- , - , - -
- - - - - -
- escape($item->searchterm); ?> - - hits; ?> - - results; ?> -
+
+
+
+ $this, 'options' => array('filterButton' => false))); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + +
+ , + , + +
+ + + + + +
+ escape($item->searchterm); ?> + + hits; ?> + + results; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + +
+
+
diff --git a/code/administrator/components/com_finder/tmpl/searches/emptystate.php b/code/administrator/components/com_finder/tmpl/searches/emptystate.php index 27c52319..f43471bc 100644 --- a/code/administrator/components/com_finder/tmpl/searches/emptystate.php +++ b/code/administrator/components/com_finder/tmpl/searches/emptystate.php @@ -1,4 +1,5 @@ 'COM_FINDER', - 'formURL' => 'index.php?option=com_finder&view=searches', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Smart_Search:_Search_Term_Analysis', - 'icon' => 'icon-search', - 'title' => Text::_('COM_FINDER_MANAGER_SEARCHES'), - 'content' => Text::_('COM_FINDER_EMPTYSTATE_SEARCHES_CONTENT'), + 'textPrefix' => 'COM_FINDER', + 'formURL' => 'index.php?option=com_finder&view=searches', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Smart_Search:_Search_Term_Analysis', + 'icon' => 'icon-search', + 'title' => Text::_('COM_FINDER_MANAGER_SEARCHES'), + 'content' => Text::_('COM_FINDER_EMPTYSTATE_SEARCHES_CONTENT'), ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_finder/tmpl/statistics/default.php b/code/administrator/components/com_finder/tmpl/statistics/default.php index 2726038b..4c2e2444 100644 --- a/code/administrator/components/com_finder/tmpl/statistics/default.php +++ b/code/administrator/components/com_finder/tmpl/statistics/default.php @@ -1,4 +1,5 @@
- - - - - - - - - - data->type_list as $type) : ?> - - - - - - - - - - -
data->term_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->taxonomy_node_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->taxonomy_branch_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR'))); ?>
- - - -
- type_title); - $lang_string = Text::_($lang_key); - echo $lang_string === $lang_key ? $type->type_title : $lang_string; - ?> - - link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> -
- - - data->link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> -
+ + + + + + + + + + data->type_list as $type) : ?> + + + + + + + + + + +
data->term_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->taxonomy_node_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->taxonomy_branch_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR'))); ?>
+ + + +
+ type_title); + $lang_string = Text::_($lang_key); + echo $lang_string === $lang_key ? $type->type_title : $lang_string; + ?> + + link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> +
+ + + data->link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> +
diff --git a/code/administrator/components/com_installer/forms/filter_manage.xml b/code/administrator/components/com_installer/forms/filter_manage.xml index 950370ea..5a57bc85 100644 --- a/code/administrator/components/com_installer/forms/filter_manage.xml +++ b/code/administrator/components/com_installer/forms/filter_manage.xml @@ -46,6 +46,16 @@
+ + + + COM_INSTALLER_HEADING_LOCATION_DESC + + diff --git a/code/administrator/components/com_installer/helpers/installer.php b/code/administrator/components/com_installer/helpers/installer.php index 59ccf41a..b1bf493d 100644 --- a/code/administrator/components/com_installer/helpers/installer.php +++ b/code/administrator/components/com_installer/helpers/installer.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Installer helper. @@ -18,5 +23,4 @@ */ class InstallerHelper extends \Joomla\Component\Installer\Administrator\Helper\InstallerHelper { - } diff --git a/code/administrator/components/com_installer/installer.xml b/code/administrator/components/com_installer/installer.xml index 3608acc2..15b412a7 100644 --- a/code/administrator/components/com_installer/installer.xml +++ b/code/administrator/components/com_installer/installer.xml @@ -2,7 +2,7 @@ com_installer Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_installer/services/provider.php b/code/administrator/components/com_installer/services/provider.php index f619ea55..0cc60d9f 100644 --- a/code/administrator/components/com_installer/services/provider.php +++ b/code/administrator/components/com_installer/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Installer')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Installer')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Installer')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Installer')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new InstallerComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new InstallerComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_installer/src/Controller/DatabaseController.php b/code/administrator/components/com_installer/src/Controller/DatabaseController.php index 8b54fc5e..06e7ced1 100644 --- a/code/administrator/components/com_installer/src/Controller/DatabaseController.php +++ b/code/administrator/components/com_installer/src/Controller/DatabaseController.php @@ -1,4 +1,5 @@ checkToken(); - - // Get items to fix the database. - $cid = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - $this->app->getLogger()->warning( - Text::_( - 'COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED' - ), array('category' => 'jerror') - ); - } - else - { - /** @var DatabaseModel $model */ - $model = $this->getModel('Database'); - $model->fix($cid); - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $updateModel */ - $updateModel = $this->app->bootComponent('com_joomlaupdate') - ->getMVCFactory()->createModel('Update', 'Administrator', ['ignore_request' => true]); - $updateModel->purge(); - - // Refresh versionable assets cache - $this->app->flushAssets(); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=database', false)); - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Database'); - - $changeSet = $model->getItems(); - - $changeSetCount = 0; - - foreach ($changeSet as $item) - { - $changeSetCount += $item['errorsCount']; - } - - echo new JsonResponse($changeSetCount); - } + /** + * Tries to fix missing database updates + * + * @return void + * + * @throws \Exception + * + * @since 2.5 + * @todo Purge updates has to be replaced with an events system + */ + public function fix() + { + // Check for request forgeries. + $this->checkToken(); + + // Get items to fix the database. + $cid = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + $this->app->getLogger()->warning( + Text::_( + 'COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED' + ), + array('category' => 'jerror') + ); + } else { + /** @var DatabaseModel $model */ + $model = $this->getModel('Database'); + $model->fix($cid); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $updateModel */ + $updateModel = $this->app->bootComponent('com_joomlaupdate') + ->getMVCFactory()->createModel('Update', 'Administrator', ['ignore_request' => true]); + $updateModel->purge(); + + // Refresh versionable assets cache + $this->app->flushAssets(); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=database', false)); + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Database'); + + $changeSet = $model->getItems(); + + $changeSetCount = 0; + + foreach ($changeSet as $item) { + $changeSetCount += $item['errorsCount']; + } + + echo new JsonResponse($changeSetCount); + } } diff --git a/code/administrator/components/com_installer/src/Controller/DiscoverController.php b/code/administrator/components/com_installer/src/Controller/DiscoverController.php index 65a74580..86c0a786 100644 --- a/code/administrator/components/com_installer/src/Controller/DiscoverController.php +++ b/code/administrator/components/com_installer/src/Controller/DiscoverController.php @@ -1,4 +1,5 @@ checkToken('request'); + /** + * Refreshes the cache of discovered extensions. + * + * @return void + * + * @since 1.6 + */ + public function refresh() + { + $this->checkToken('request'); - /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ - $model = $this->getModel('discover'); - $model->discover(); + /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ + $model = $this->getModel('discover'); + $model->discover(); - if (!$model->getTotal()) - { - $this->setMessage(Text::_('COM_INSTALLER_ERROR_NO_EXTENSIONS_DISCOVERED'), 'info'); - } + if (!$model->getTotal()) { + $this->setMessage(Text::_('COM_INSTALLER_ERROR_NO_EXTENSIONS_DISCOVERED'), 'info'); + } - $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false)); - } + $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false)); + } - /** - * Install a discovered extension. - * - * @return void - * - * @since 1.6 - */ - public function install() - { - $this->checkToken(); + /** + * Install a discovered extension. + * + * @return void + * + * @since 1.6 + */ + public function install() + { + $this->checkToken(); - /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ - $model = $this->getModel('discover'); - $model->discover_install(); - $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false)); - } + /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ + $model = $this->getModel('discover'); + $model->discover_install(); + $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false)); + } - /** - * Clean out the discovered extension cache. - * - * @return void - * - * @since 1.6 - */ - public function purge() - { - $this->checkToken('request'); + /** + * Clean out the discovered extension cache. + * + * @return void + * + * @since 1.6 + */ + public function purge() + { + $this->checkToken('request'); - /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ - $model = $this->getModel('discover'); - $model->purge(); - $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false), $model->_message); - } + /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ + $model = $this->getModel('discover'); + $model->purge(); + $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false), $model->_message); + } - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } - $model = $this->getModel('Discover'); + $model = $this->getModel('Discover'); - echo new JsonResponse($model->getTotal()); - } + echo new JsonResponse($model->getTotal()); + } } diff --git a/code/administrator/components/com_installer/src/Controller/DisplayController.php b/code/administrator/components/com_installer/src/Controller/DisplayController.php index 9584877b..25c306f2 100644 --- a/code/administrator/components/com_installer/src/Controller/DisplayController.php +++ b/code/administrator/components/com_installer/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'install'); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - $id = $this->input->getInt('update_site_id'); - - // Check for edit form. - if ($vName === 'updatesite' && $lName === 'edit' && !$this->checkEditId('com_installer.edit.updatesite', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); - - $this->redirect(); - } - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - // Get the model for the view. - $model = $this->getModel($vName); - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - - $view->display(); - } - - return $this; - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Warnings'); - - echo new JsonResponse(count($model->getItems())); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'install'); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + $id = $this->input->getInt('update_site_id'); + + // Check for edit form. + if ($vName === 'updatesite' && $lName === 'edit' && !$this->checkEditId('com_installer.edit.updatesite', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); + + $this->redirect(); + } + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + // Get the model for the view. + $model = $this->getModel($vName); + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + + $view->display(); + } + + return $this; + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Warnings'); + + echo new JsonResponse(count($model->getItems())); + } } diff --git a/code/administrator/components/com_installer/src/Controller/InstallController.php b/code/administrator/components/com_installer/src/Controller/InstallController.php index 3382e5e6..585cd99b 100644 --- a/code/administrator/components/com_installer/src/Controller/InstallController.php +++ b/code/administrator/components/com_installer/src/Controller/InstallController.php @@ -1,4 +1,5 @@ checkToken(); - - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $model */ - $model = $this->getModel('install'); - - // @todo: Reset the users acl here as well to kill off any missing bits. - $result = $model->install(); - - $app = $this->app; - $redirect_url = $app->getUserState('com_installer.redirect_url'); - $return = $this->input->getBase64('return'); - - if (!$redirect_url && $return) - { - $redirect_url = base64_decode($return); - } - - // Don't redirect to an external URL. - if ($redirect_url && !Uri::isInternal($redirect_url)) - { - $redirect_url = ''; - } - - if (empty($redirect_url)) - { - $redirect_url = Route::_('index.php?option=com_installer&view=install', false); - } - else - { - // Wipe out the user state when we're going to redirect. - $app->setUserState('com_installer.redirect_url', ''); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - } - - $this->setRedirect($redirect_url); - - return $result; - } - - /** - * Install an extension from drag & drop ajax upload. - * - * @return void - * - * @since 3.7.0 - */ - public function ajax_upload() - { - // Check for request forgeries. - Session::checkToken() or jexit(Text::_('JINVALID_TOKEN')); - - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $message = $this->app->getUserState('com_installer.message'); - - // Do install - $result = $this->install(); - - // Get redirect URL - $redirect = $this->redirect; - - // Push message queue to session because we will redirect page by \Javascript, not $app->redirect(). - // The "application.queue" is only set in redirect() method, so we must manually store it. - $this->app->getSession()->set('application.queue', $this->app->getMessageQueue()); - - header('Content-Type: application/json'); - - echo new JsonResponse(array('redirect' => $redirect), $message, !$result); - - $this->app->close(); - } + /** + * Install an extension. + * + * @return mixed + * + * @since 1.5 + */ + public function install() + { + // Check for request forgeries. + $this->checkToken(); + + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $model */ + $model = $this->getModel('install'); + + // @todo: Reset the users acl here as well to kill off any missing bits. + $result = $model->install(); + + $app = $this->app; + $redirect_url = $app->getUserState('com_installer.redirect_url'); + $return = $this->input->getBase64('return'); + + if (!$redirect_url && $return) { + $redirect_url = base64_decode($return); + } + + // Don't redirect to an external URL. + if ($redirect_url && !Uri::isInternal($redirect_url)) { + $redirect_url = ''; + } + + if (empty($redirect_url)) { + $redirect_url = Route::_('index.php?option=com_installer&view=install', false); + } else { + // Wipe out the user state when we're going to redirect. + $app->setUserState('com_installer.redirect_url', ''); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + } + + $this->setRedirect($redirect_url); + + return $result; + } + + /** + * Install an extension from drag & drop ajax upload. + * + * @return void + * + * @since 3.7.0 + */ + public function ajax_upload() + { + // Check for request forgeries. + Session::checkToken() or jexit(Text::_('JINVALID_TOKEN')); + + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $message = $this->app->getUserState('com_installer.message'); + + // Do install + $result = $this->install(); + + // Get redirect URL + $redirect = $this->redirect; + + // Push message queue to session because we will redirect page by \Javascript, not $app->redirect(). + // The "application.queue" is only set in redirect() method, so we must manually store it. + $this->app->getSession()->set('application.queue', $this->app->getMessageQueue()); + + header('Content-Type: application/json'); + + echo new JsonResponse(array('redirect' => $redirect), $message, !$result); + + $this->app->close(); + } } diff --git a/code/administrator/components/com_installer/src/Controller/ManageController.php b/code/administrator/components/com_installer/src/Controller/ManageController.php index 3496706a..7c6cb5d3 100644 --- a/code/administrator/components/com_installer/src/Controller/ManageController.php +++ b/code/administrator/components/com_installer/src/Controller/ManageController.php @@ -1,4 +1,5 @@ registerTask('unpublish', 'publish'); - $this->registerTask('publish', 'publish'); - } - - /** - * Enable/Disable an extension (if supported). - * - * @return void - * - * @throws \Exception - * - * @since 1.6 - */ - public function publish() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('publish' => 1, 'unpublish' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->setMessage(Text::_('COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED'), 'warning'); - } - else - { - /** @var ManageModel $model */ - $model = $this->getModel('manage'); - - // Change the state of the records. - if (!$model->publish($ids, $value)) - { - $this->setMessage(implode('
', $model->getErrors()), 'warning'); - } - else - { - if ($value == 1) - { - $ntext = 'COM_INSTALLER_N_EXTENSIONS_PUBLISHED'; - } - else - { - $ntext = 'COM_INSTALLER_N_EXTENSIONS_UNPUBLISHED'; - } - - $this->setMessage(Text::plural($ntext, count($ids))); - } - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); - } - - /** - * Remove an extension (Uninstall). - * - * @return void - * - * @throws \Exception - * - * @since 1.5 - */ - public function remove() - { - // Check for request forgeries. - $this->checkToken(); - - $eid = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $eid = array_filter($eid); - - if (!empty($eid)) - { - /** @var ManageModel $model */ - $model = $this->getModel('manage'); - - $model->remove($eid); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); - } - - /** - * Refreshes the cached metadata about an extension. - * - * Useful for debugging and testing purposes when the XML file might change. - * - * @return void - * - * @since 1.6 - */ - public function refresh() - { - // Check for request forgeries. - $this->checkToken(); - - $uid = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $uid = array_filter($uid); - - if (!empty($uid)) - { - /** @var ManageModel $model */ - $model = $this->getModel('manage'); - - $model->refresh($uid); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); - } - - /** - * Load the changelog for a given extension. - * - * @return void - * - * @since 4.0.0 - */ - public function loadChangelog() - { - /** @var ManageModel $model */ - $model = $this->getModel('manage'); - - $eid = $this->input->get('eid', 0, 'int'); - $source = $this->input->get('source', 'manage', 'string'); - - if (!$eid) - { - return; - } - - $output = $model->loadChangelog($eid, $source); - - echo (new JsonResponse($output)); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('unpublish', 'publish'); + $this->registerTask('publish', 'publish'); + } + + /** + * Enable/Disable an extension (if supported). + * + * @return void + * + * @throws \Exception + * + * @since 1.6 + */ + public function publish() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('publish' => 1, 'unpublish' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED'), 'warning'); + } else { + /** @var ManageModel $model */ + $model = $this->getModel('manage'); + + // Change the state of the records. + if (!$model->publish($ids, $value)) { + $this->setMessage(implode('
', $model->getErrors()), 'warning'); + } else { + if ($value == 1) { + $ntext = 'COM_INSTALLER_N_EXTENSIONS_PUBLISHED'; + } else { + $ntext = 'COM_INSTALLER_N_EXTENSIONS_UNPUBLISHED'; + } + + $this->setMessage(Text::plural($ntext, count($ids))); + } + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); + } + + /** + * Remove an extension (Uninstall). + * + * @return void + * + * @throws \Exception + * + * @since 1.5 + */ + public function remove() + { + // Check for request forgeries. + $this->checkToken(); + + $eid = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $eid = array_filter($eid); + + if (!empty($eid)) { + /** @var ManageModel $model */ + $model = $this->getModel('manage'); + + $model->remove($eid); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); + } + + /** + * Refreshes the cached metadata about an extension. + * + * Useful for debugging and testing purposes when the XML file might change. + * + * @return void + * + * @since 1.6 + */ + public function refresh() + { + // Check for request forgeries. + $this->checkToken(); + + $uid = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $uid = array_filter($uid); + + if (!empty($uid)) { + /** @var ManageModel $model */ + $model = $this->getModel('manage'); + + $model->refresh($uid); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); + } + + /** + * Load the changelog for a given extension. + * + * @return void + * + * @since 4.0.0 + */ + public function loadChangelog() + { + /** @var ManageModel $model */ + $model = $this->getModel('manage'); + + $eid = $this->input->get('eid', 0, 'int'); + $source = $this->input->get('source', 'manage', 'string'); + + if (!$eid) { + return; + } + + $output = $model->loadChangelog($eid, $source); + + echo (new JsonResponse($output)); + } } diff --git a/code/administrator/components/com_installer/src/Controller/UpdateController.php b/code/administrator/components/com_installer/src/Controller/UpdateController.php index cc3a6e89..5738e937 100644 --- a/code/administrator/components/com_installer/src/Controller/UpdateController.php +++ b/code/administrator/components/com_installer/src/Controller/UpdateController.php @@ -1,4 +1,5 @@ checkToken(); - - /** @var UpdateModel $model */ - $model = $this->getModel('update'); - - $uid = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $uid = array_filter($uid); - - // Get the minimum stability. - $params = ComponentHelper::getComponent('com_installer')->getParams(); - $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); - - $model->update($uid, $minimum_stability); - - $app = $this->app; - $redirect_url = $app->getUserState('com_installer.redirect_url'); - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect_url)) - { - $redirect_url = ''; - } - - if (empty($redirect_url)) - { - $redirect_url = Route::_('index.php?option=com_installer&view=update', false); - } - else - { - // Wipe out the user state when we're going to redirect. - $app->setUserState('com_installer.redirect_url', ''); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - } - - $this->setRedirect($redirect_url); - } - - /** - * Find new updates. - * - * @return void - * - * @since 1.6 - */ - public function find() - { - $this->checkToken('request'); - - // Get the caching duration. - $params = ComponentHelper::getComponent('com_installer')->getParams(); - $cache_timeout = (int) $params->get('cachetimeout', 6); - $cache_timeout = 3600 * $cache_timeout; - - // Get the minimum stability. - $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); - - // Find updates. - /** @var UpdateModel $model */ - $model = $this->getModel('update'); - - // Purge the table before checking again - $model->purge(); - - $disabledUpdateSites = $model->getDisabledUpdateSites(); - - if ($disabledUpdateSites) - { - $updateSitesUrl = Route::_('index.php?option=com_installer&view=updatesites'); - $this->app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATE_SITES_COUNT_CHECK', $updateSitesUrl), 'warning'); - } - - $model->findUpdates(0, $cache_timeout, $minimum_stability); - - if (0 === $model->getTotal()) - { - $this->app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATE_NOUPDATES'), 'info'); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=update', false)); - } - - /** - * Fetch and report updates in \JSON format, for AJAX requests - * - * @return void - * - * @since 2.5 - */ - public function ajax() - { - $app = $this->app; - - if (!Session::checkToken('get')) - { - $app->setHeader('status', 403, true); - $app->sendHeaders(); - echo Text::_('JINVALID_TOKEN_NOTICE'); - $app->close(); - } - - // Close the session before we make a long running request - $app->getSession()->abort(); - - $eid = $this->input->getInt('eid', 0); - $skip = $this->input->get('skip', array(), 'array'); - $cache_timeout = $this->input->getInt('cache_timeout', 0); - $minimum_stability = $this->input->getInt('minimum_stability', -1); - - $params = ComponentHelper::getComponent('com_installer')->getParams(); - - if ($cache_timeout == 0) - { - $cache_timeout = (int) $params->get('cachetimeout', 6); - $cache_timeout = 3600 * $cache_timeout; - } - - if ($minimum_stability < 0) - { - $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); - } - - /** @var UpdateModel $model */ - $model = $this->getModel('update'); - $model->findUpdates($eid, $cache_timeout, $minimum_stability); - - $model->setState('list.start', 0); - $model->setState('list.limit', 0); - - if ($eid != 0) - { - $model->setState('filter.extension_id', $eid); - } - - $updates = $model->getItems(); - - if (!empty($skip)) - { - $unfiltered_updates = $updates; - $updates = array(); - - foreach ($unfiltered_updates as $update) - { - if (!in_array($update->extension_id, $skip)) - { - $updates[] = $update; - } - } - } - - echo json_encode($updates); - - $app->close(); - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Update'); - - echo new JsonResponse($model->getTotal()); - } + /** + * Update a set of extensions. + * + * @return void + * + * @since 1.6 + */ + public function update() + { + // Check for request forgeries. + $this->checkToken(); + + /** @var UpdateModel $model */ + $model = $this->getModel('update'); + + $uid = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $uid = array_filter($uid); + + // Get the minimum stability. + $params = ComponentHelper::getComponent('com_installer')->getParams(); + $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); + + $model->update($uid, $minimum_stability); + + $app = $this->app; + $redirect_url = $app->getUserState('com_installer.redirect_url'); + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect_url)) { + $redirect_url = ''; + } + + if (empty($redirect_url)) { + $redirect_url = Route::_('index.php?option=com_installer&view=update', false); + } else { + // Wipe out the user state when we're going to redirect. + $app->setUserState('com_installer.redirect_url', ''); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + } + + $this->setRedirect($redirect_url); + } + + /** + * Find new updates. + * + * @return void + * + * @since 1.6 + */ + public function find() + { + $this->checkToken('request'); + + // Get the caching duration. + $params = ComponentHelper::getComponent('com_installer')->getParams(); + $cache_timeout = (int) $params->get('cachetimeout', 6); + $cache_timeout = 3600 * $cache_timeout; + + // Get the minimum stability. + $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); + + // Find updates. + /** @var UpdateModel $model */ + $model = $this->getModel('update'); + + // Purge the table before checking again + $model->purge(); + + $disabledUpdateSites = $model->getDisabledUpdateSites(); + + if ($disabledUpdateSites) { + $updateSitesUrl = Route::_('index.php?option=com_installer&view=updatesites'); + $this->app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATE_SITES_COUNT_CHECK', $updateSitesUrl), 'warning'); + } + + $model->findUpdates(0, $cache_timeout, $minimum_stability); + + if (0 === $model->getTotal()) { + $this->app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATE_NOUPDATES'), 'info'); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=update', false)); + } + + /** + * Fetch and report updates in \JSON format, for AJAX requests + * + * @return void + * + * @since 2.5 + */ + public function ajax() + { + $app = $this->app; + + if (!Session::checkToken('get')) { + $app->setHeader('status', 403, true); + $app->sendHeaders(); + echo Text::_('JINVALID_TOKEN_NOTICE'); + $app->close(); + } + + // Close the session before we make a long running request + $app->getSession()->abort(); + + $eid = $this->input->getInt('eid', 0); + $skip = $this->input->get('skip', array(), 'array'); + $cache_timeout = $this->input->getInt('cache_timeout', 0); + $minimum_stability = $this->input->getInt('minimum_stability', -1); + + $params = ComponentHelper::getComponent('com_installer')->getParams(); + + if ($cache_timeout == 0) { + $cache_timeout = (int) $params->get('cachetimeout', 6); + $cache_timeout = 3600 * $cache_timeout; + } + + if ($minimum_stability < 0) { + $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); + } + + /** @var UpdateModel $model */ + $model = $this->getModel('update'); + $model->findUpdates($eid, $cache_timeout, $minimum_stability); + + $model->setState('list.start', 0); + $model->setState('list.limit', 0); + + if ($eid != 0) { + $model->setState('filter.extension_id', $eid); + } + + $updates = $model->getItems(); + + if (!empty($skip)) { + $unfiltered_updates = $updates; + $updates = array(); + + foreach ($unfiltered_updates as $update) { + if (!in_array($update->extension_id, $skip)) { + $updates[] = $update; + } + } + } + + echo json_encode($updates); + + $app->close(); + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Update'); + + echo new JsonResponse($model->getTotal()); + } } diff --git a/code/administrator/components/com_installer/src/Controller/UpdatesiteController.php b/code/administrator/components/com_installer/src/Controller/UpdatesiteController.php index 1ce791f7..ffbba59b 100644 --- a/code/administrator/components/com_installer/src/Controller/UpdatesiteController.php +++ b/code/administrator/components/com_installer/src/Controller/UpdatesiteController.php @@ -1,4 +1,5 @@ registerTask('unpublish', 'publish'); - $this->registerTask('publish', 'publish'); - $this->registerTask('delete', 'delete'); - $this->registerTask('rebuild', 'rebuild'); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel - * - * @since 4.0.0 - */ - public function getModel($name = 'Updatesite', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Enable/Disable an extension (if supported). - * - * @return void - * - * @since 3.4 - * - * @throws \Exception on error - */ - public function publish() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('publish' => 1, 'unpublish' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - throw new \Exception(Text::_('COM_INSTALLER_ERROR_NO_UPDATESITES_SELECTED'), 500); - } - - // Get the model. - /** @var \Joomla\Component\Installer\Administrator\Model\UpdatesitesModel $model */ - $model = $this->getModel('Updatesites'); - - // Change the state of the records. - if (!$model->publish($ids, $value)) - { - throw new \Exception(implode('
', $model->getErrors()), 500); - } - - $ntext = ($value == 0) ? 'COM_INSTALLER_N_UPDATESITES_UNPUBLISHED' : 'COM_INSTALLER_N_UPDATESITES_PUBLISHED'; - - $this->setMessage(Text::plural($ntext, count($ids))); - - $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); - } - - /** - * Deletes an update site (if supported). - * - * @return void - * - * @since 3.6 - * - * @throws \Exception on error - */ - public function delete() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - throw new \Exception(Text::_('COM_INSTALLER_ERROR_NO_UPDATESITES_SELECTED'), 500); - } - - // Delete the records. - $this->getModel('Updatesites')->delete($ids); - - $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); - } - - /** - * Rebuild update sites tables. - * - * @return void - * - * @since 3.6 - */ - public function rebuild() - { - // Check for request forgeries. - $this->checkToken(); - - // Rebuild the update sites. - $this->getModel('Updatesites')->rebuild(); - - $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 4.0.0 + */ + protected $text_prefix = 'COM_INSTALLER_UPDATESITES'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('unpublish', 'publish'); + $this->registerTask('publish', 'publish'); + $this->registerTask('delete', 'delete'); + $this->registerTask('rebuild', 'rebuild'); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 4.0.0 + */ + public function getModel($name = 'Updatesite', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Enable/Disable an extension (if supported). + * + * @return void + * + * @since 3.4 + * + * @throws \Exception on error + */ + public function publish() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('publish' => 1, 'unpublish' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + throw new \Exception(Text::_('COM_INSTALLER_ERROR_NO_UPDATESITES_SELECTED'), 500); + } + + // Get the model. + /** @var \Joomla\Component\Installer\Administrator\Model\UpdatesitesModel $model */ + $model = $this->getModel('Updatesites'); + + // Change the state of the records. + if (!$model->publish($ids, $value)) { + throw new \Exception(implode('
', $model->getErrors()), 500); + } + + $ntext = ($value == 0) ? 'COM_INSTALLER_N_UPDATESITES_UNPUBLISHED' : 'COM_INSTALLER_N_UPDATESITES_PUBLISHED'; + + $this->setMessage(Text::plural($ntext, count($ids))); + + $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); + } + + /** + * Deletes an update site (if supported). + * + * @return void + * + * @since 3.6 + * + * @throws \Exception on error + */ + public function delete() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + throw new \Exception(Text::_('COM_INSTALLER_ERROR_NO_UPDATESITES_SELECTED'), 500); + } + + // Delete the records. + $this->getModel('Updatesites')->delete($ids); + + $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); + } + + /** + * Rebuild update sites tables. + * + * @return void + * + * @since 3.6 + */ + public function rebuild() + { + // Check for request forgeries. + $this->checkToken(); + + // Rebuild the update sites. + $this->getModel('Updatesites')->rebuild(); + + $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); + } } diff --git a/code/administrator/components/com_installer/src/Extension/InstallerComponent.php b/code/administrator/components/com_installer/src/Extension/InstallerComponent.php index 2a48011a..e422e860 100644 --- a/code/administrator/components/com_installer/src/Extension/InstallerComponent.php +++ b/code/administrator/components/com_installer/src/Extension/InstallerComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('manage', new Manage); - $this->getRegistry()->register('updatesites', new Updatesites); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('manage', new Manage()); + $this->getRegistry()->register('updatesites', new Updatesites()); + } } diff --git a/code/administrator/components/com_installer/src/Field/ExtensionstatusField.php b/code/administrator/components/com_installer/src/Field/ExtensionstatusField.php index 5a418cb5..e660d995 100644 --- a/code/administrator/components/com_installer/src/Field/ExtensionstatusField.php +++ b/code/administrator/components/com_installer/src/Field/ExtensionstatusField.php @@ -1,4 +1,5 @@ + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Installer\Administrator\Field; + +use Joomla\CMS\Form\Field\ListField; +use Joomla\Component\Installer\Administrator\Helper\InstallerHelper; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Package field. + * + * Selects the extension ID of an extension of the "package" type. + * + * @since 4.2.0 + */ +class PackageField extends ListField +{ + /** + * Method to get the field options. + * + * @return array The field option objects. + * @since 4.2.0 + */ + protected function getOptions() + { + $options = InstallerHelper::getPackageOptions(); + + return array_merge(parent::getOptions(), $options); + } +} diff --git a/code/administrator/components/com_installer/src/Field/TypeField.php b/code/administrator/components/com_installer/src/Field/TypeField.php index 9ca31c9f..6329b959 100644 --- a/code/administrator/components/com_installer/src/Field/TypeField.php +++ b/code/administrator/components/com_installer/src/Field/TypeField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT ' . $db->quoteName('type')) - ->from($db->quoteName('#__extensions')); - $db->setQuery($query); - $types = $db->loadColumn(); - - $options = array(); - - foreach ($types as $type) - { - $options[] = HTMLHelper::_('select.option', $type, Text::_('COM_INSTALLER_TYPE_' . strtoupper($type))); - } - - return $options; - } - - /** - * Get a list of filter options for the extension types. - * - * @return array An array of \stdClass objects. - * - * @since 3.0 - */ - public static function getExtensionGroups() - { - $nofolder = ''; - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('folder')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' != :folder') - ->bind(':folder', $nofolder) - ->order($db->quoteName('folder')); - $db->setQuery($query); - $folders = $db->loadColumn(); - - $options = array(); - - foreach ($folders as $folder) - { - $options[] = HTMLHelper::_('select.option', $folder, $folder); - } - - return $options; - } - - /** - * Get a list of filter options for the application clients. - * - * @return array An array of \JHtmlOption elements. - * - * @since 3.5 - */ - public static function getClientOptions() - { - // Build the filter options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE')); - $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR')); - $options[] = HTMLHelper::_('select.option', '3', Text::_('JAPI')); - - return $options; - } - - /** - * Get a list of filter options for the application statuses. - * - * @return array An array of \JHtmlOption elements. - * - * @since 3.5 - */ - public static function getStateOptions() - { - // Build the filter options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '0', Text::_('JDISABLED')); - $options[] = HTMLHelper::_('select.option', '1', Text::_('JENABLED')); - $options[] = HTMLHelper::_('select.option', '2', Text::_('JPROTECTED')); - $options[] = HTMLHelper::_('select.option', '3', Text::_('JUNPROTECTED')); - - return $options; - } - - /** - * Get a list of filter options for the application statuses. - * - * @param string $element element of an extension - * @param string $type type of an extension - * @param integer $clientId client_id of an extension - * @param string $folder folder of an extension - * - * @return SimpleXMLElement - * - * @since 4.0.0 - */ - public static function getInstallationXML(string $element, string $type, int $clientId = 1, - ?string $folder = null - ): ?SimpleXMLElement - { - $path = [0 => JPATH_SITE, 1 => JPATH_ADMINISTRATOR, 3 => JPATH_API][$clientId] ?? JPATH_SITE; - - switch ($type) - { - case 'component': - $path .= '/components/' . $element . '/' . substr($element, 4) . '.xml'; - break; - case 'plugin': - $path .= '/plugins/' . $folder . '/' . $element . '/' . $element . '.xml'; - break; - case 'module': - $path .= '/modules/' . $element . '/' . $element . '.xml'; - break; - case 'template': - $path .= '/templates/' . $element . '/templateDetails.xml'; - break; - case 'library': - $path = JPATH_ADMINISTRATOR . '/manifests/libraries/' . $element . '.xml'; - break; - case 'file': - $path = JPATH_ADMINISTRATOR . '/manifests/files/' . $element . '.xml'; - break; - case 'package': - $path = JPATH_ADMINISTRATOR . '/manifests/packages/' . $element . '.xml'; - break; - case 'language': - $path .= '/language/' . $element . '/install.xml'; - } - - if (file_exists($path) === false) - { - return null; - } - - $xmlElement = simplexml_load_file($path); - - return ($xmlElement !== false) ? $xmlElement : null; - } - - /** - * Get the download key of an extension going through their installation xml - * - * @param CMSObject $extension element of an extension - * - * @return array An array with the prefix, suffix and value of the download key - * - * @since 4.0.0 - */ - public static function getDownloadKey(CMSObject $extension): array - { - $installXmlFile = self::getInstallationXML( - $extension->get('element'), - $extension->get('type'), - $extension->get('client_id'), - $extension->get('folder') - ); - - if (!$installXmlFile) - { - return [ - 'supported' => false, - 'valid' => false, - ]; - } - - if (!isset($installXmlFile->dlid)) - { - return [ - 'supported' => false, - 'valid' => false, - ]; - } - - $prefix = (string) $installXmlFile->dlid['prefix']; - $suffix = (string) $installXmlFile->dlid['suffix']; - $value = substr($extension->get('extra_query'), strlen($prefix)); - - if ($suffix) - { - $value = substr($value, 0, -strlen($suffix)); - } - - $downloadKey = [ - 'supported' => true, - 'valid' => $value ? true : false, - 'prefix' => $prefix, - 'suffix' => $suffix, - 'value' => $value - ]; - - return $downloadKey; - } - - /** - * Get the download key of an extension given enough information to locate it in the #__extensions table - * - * @param string $element Name of the extension, e.g. com_foo - * @param string $type The type of the extension, e.g. component - * @param int $clientId [optional] Joomla client for the extension, see the #__extensions table - * @param string|null $folder Extension folder, only applies for 'plugin' type - * - * @return array - * - * @since 4.0.0 - */ - public static function getExtensionDownloadKey(string $element, string $type, int $clientId = 1, - ?string $folder = null - ): array - { - // Get the database driver. If it fails we cannot report whether the extension supports download keys. - try - { - $db = Factory::getDbo(); - } - catch (Exception $e) - { - return [ - 'supported' => false, - 'valid' => false, - ]; - } - - // Try to retrieve the extension information as a CMSObject - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = :type') - ->where($db->quoteName('element') . ' = :element') - ->where($db->quoteName('folder') . ' = :folder') - ->where($db->quoteName('client_id') . ' = :client_id'); - $query->bind(':type', $type, ParameterType::STRING); - $query->bind(':element', $element, ParameterType::STRING); - $query->bind(':client_id', $clientId, ParameterType::INTEGER); - $query->bind(':folder', $folder, ParameterType::STRING); - - try - { - $extension = new CMSObject($db->setQuery($query)->loadAssoc()); - } - catch (Exception $e) - { - return [ - 'supported' => false, - 'valid' => false, - ]; - } - - // Use the getDownloadKey() method to return the download key information - return self::getDownloadKey($extension); - } - - /** - * Returns a list of update site IDs which support download keys. By default this returns all qualifying update - * sites, even if they are not enabled. - * - * - * @param bool $onlyEnabled [optional] Set true to only returned enabled update sites. - * - * @return int[] - * @since 4.0.0 - */ - public static function getDownloadKeySupportedSites($onlyEnabled = false): array - { - /** - * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually - * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less - * readable but works around that issue. - */ - - $extensions = self::getUpdateSitesInformation($onlyEnabled); - - $filterClosure = function (CMSObject $extension) { - $dlidInfo = self::getDownloadKey($extension); - - return $dlidInfo['supported']; - }; - $extensions = array_filter($extensions, $filterClosure); - - $mapClosure = function (CMSObject $extension) { - return $extension->get('update_site_id'); - }; - - return array_map($mapClosure, $extensions); - } - - /** - * Returns a list of update site IDs which are missing download keys. By default this returns all qualifying update - * sites, even if they are not enabled. - * - * @param bool $exists [optional] If true, returns update sites with a valid download key. When false, - * returns update sites with an invalid / missing download key. - * @param bool $onlyEnabled [optional] Set true to only returned enabled update sites. - * - * @return int[] - * @since 4.0.0 - */ - public static function getDownloadKeyExistsSites(bool $exists = true, $onlyEnabled = false): array - { - /** - * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually - * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less - * readable but works around that issue. - */ - - $extensions = self::getUpdateSitesInformation($onlyEnabled); - - // Filter the extensions by what supports Download Keys - $filterClosure = function (CMSObject $extension) use ($exists) { - $dlidInfo = self::getDownloadKey($extension); - - if (!$dlidInfo['supported']) - { - return false; - } - - return $exists ? $dlidInfo['valid'] : !$dlidInfo['valid']; - }; - $extensions = array_filter($extensions, $filterClosure); - - // Return only the update site IDs - $mapClosure = function (CMSObject $extension) { - return $extension->get('update_site_id'); - }; - - return array_map($mapClosure, $extensions); - } - - - /** - * Get information about the update sites - * - * @param bool $onlyEnabled Only return enabled update sites - * - * @return CMSObject[] List of update site and linked extension information - * @since 4.0.0 - */ - protected static function getUpdateSitesInformation(bool $onlyEnabled): array - { - try - { - $db = Factory::getDbo(); - } - catch (Exception $e) - { - return []; - } - - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 's.update_site_id', - 's.enabled', - 's.extra_query', - 'e.extension_id', - 'e.type', - 'e.element', - 'e.folder', - 'e.client_id', - 'e.manifest_cache', - ], - [ - 'update_site_id', - 'enabled', - 'extra_query', - 'extension_id', - 'type', - 'element', - 'folder', - 'client_id', - 'manifest_cache', - ] - ) - ) - ->from($db->quoteName('#__update_sites', 's')) - ->innerJoin( - $db->quoteName('#__update_sites_extensions', 'se'), - $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id') - ) - ->innerJoin( - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id') - ) - ->where($db->quoteName('state') . ' = 0'); - - if ($onlyEnabled) - { - $enabled = 1; - $query->where($db->quoteName('s.enabled') . ' = :enabled') - ->bind(':enabled', $enabled, ParameterType::INTEGER); - } - - // Try to get all of the update sites, including related extension information - try - { - $items = []; - $db->setQuery($query); - - foreach ($db->getIterator() as $item) - { - $items[] = new CMSObject($item); - } - - return $items; - } - catch (Exception $e) - { - return []; - } - } + /** + * Get a list of filter options for the extension types. + * + * @return array An array of \stdClass objects. + * + * @since 3.0 + */ + public static function getExtensionTypes() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('type')) + ->from($db->quoteName('#__extensions')); + $db->setQuery($query); + $types = $db->loadColumn(); + + $options = array(); + + foreach ($types as $type) { + $options[] = HTMLHelper::_('select.option', $type, Text::_('COM_INSTALLER_TYPE_' . strtoupper($type))); + } + + return $options; + } + + /** + * Get a list of filter options for the extension types. + * + * @return array An array of \stdClass objects. + * + * @since 3.0 + */ + public static function getExtensionGroups() + { + $nofolder = ''; + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('folder')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' != :folder') + ->bind(':folder', $nofolder) + ->order($db->quoteName('folder')); + $db->setQuery($query); + $folders = $db->loadColumn(); + + $options = array(); + + foreach ($folders as $folder) { + $options[] = HTMLHelper::_('select.option', $folder, $folder); + } + + return $options; + } + + /** + * Get a list of filter options for the application clients. + * + * @return array An array of \JHtmlOption elements. + * + * @since 3.5 + */ + public static function getClientOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR')); + $options[] = HTMLHelper::_('select.option', '3', Text::_('JAPI')); + + return $options; + } + + /** + * Get a list of filter options for the application statuses. + * + * @return array An array of \JHtmlOption elements. + * + * @since 3.5 + */ + public static function getStateOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JDISABLED')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JENABLED')); + $options[] = HTMLHelper::_('select.option', '2', Text::_('JPROTECTED')); + $options[] = HTMLHelper::_('select.option', '3', Text::_('JUNPROTECTED')); + + return $options; + } + + /** + * Get a list of filter options for extensions of the "package" type. + * + * @return array + * @since 4.2.0 + */ + public static function getPackageOptions(): array + { + $options = []; + + /** @var DatabaseDriver $db The application's database driver object */ + $db = Factory::getContainer()->get(DatabaseDriver::class); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'extension_id', + 'name', + 'element', + ] + ) + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('package')); + $extensions = $db->setQuery($query)->loadObjectList() ?: []; + + if (empty($extensions)) { + return $options; + } + + $language = Factory::getApplication()->getLanguage(); + $arrayKeys = array_map( + function (object $entry) use ($language): string { + $language->load($entry->element, JPATH_ADMINISTRATOR); + + return Text::_($entry->name); + }, + $extensions + ); + $arrayValues = array_map( + function (object $entry): int { + return $entry->extension_id; + }, + $extensions + ); + + $extensions = array_combine($arrayKeys, $arrayValues); + ksort($extensions); + + foreach ($extensions as $label => $id) { + $options[] = HTMLHelper::_('select.option', $id, $label); + } + + return $options; + } + + /** + * Get a list of filter options for the application statuses. + * + * @param string $element element of an extension + * @param string $type type of an extension + * @param integer $clientId client_id of an extension + * @param string $folder folder of an extension + * + * @return SimpleXMLElement + * + * @since 4.0.0 + */ + public static function getInstallationXML( + string $element, + string $type, + int $clientId = 1, + ?string $folder = null + ): ?SimpleXMLElement { + $path = [0 => JPATH_SITE, 1 => JPATH_ADMINISTRATOR, 3 => JPATH_API][$clientId] ?? JPATH_SITE; + + switch ($type) { + case 'component': + $path .= '/components/' . $element . '/' . substr($element, 4) . '.xml'; + break; + case 'plugin': + $path .= '/plugins/' . $folder . '/' . $element . '/' . $element . '.xml'; + break; + case 'module': + $path .= '/modules/' . $element . '/' . $element . '.xml'; + break; + case 'template': + $path .= '/templates/' . $element . '/templateDetails.xml'; + break; + case 'library': + $path = JPATH_ADMINISTRATOR . '/manifests/libraries/' . $element . '.xml'; + break; + case 'file': + $path = JPATH_ADMINISTRATOR . '/manifests/files/' . $element . '.xml'; + break; + case 'package': + $path = JPATH_ADMINISTRATOR . '/manifests/packages/' . $element . '.xml'; + break; + case 'language': + $path .= '/language/' . $element . '/install.xml'; + } + + if (file_exists($path) === false) { + return null; + } + + $xmlElement = simplexml_load_file($path); + + return ($xmlElement !== false) ? $xmlElement : null; + } + + /** + * Get the download key of an extension going through their installation xml + * + * @param CMSObject $extension element of an extension + * + * @return array An array with the prefix, suffix and value of the download key + * + * @since 4.0.0 + */ + public static function getDownloadKey(CMSObject $extension): array + { + $installXmlFile = self::getInstallationXML( + $extension->get('element'), + $extension->get('type'), + $extension->get('client_id'), + $extension->get('folder') + ); + + if (!$installXmlFile) { + return [ + 'supported' => false, + 'valid' => false, + ]; + } + + if (!isset($installXmlFile->dlid)) { + return [ + 'supported' => false, + 'valid' => false, + ]; + } + + $prefix = (string) $installXmlFile->dlid['prefix']; + $suffix = (string) $installXmlFile->dlid['suffix']; + $value = substr($extension->get('extra_query'), strlen($prefix)); + + if ($suffix) { + $value = substr($value, 0, -strlen($suffix)); + } + + $downloadKey = [ + 'supported' => true, + 'valid' => $value ? true : false, + 'prefix' => $prefix, + 'suffix' => $suffix, + 'value' => $value + ]; + + return $downloadKey; + } + + /** + * Get the download key of an extension given enough information to locate it in the #__extensions table + * + * @param string $element Name of the extension, e.g. com_foo + * @param string $type The type of the extension, e.g. component + * @param int $clientId [optional] Joomla client for the extension, see the #__extensions table + * @param string|null $folder Extension folder, only applies for 'plugin' type + * + * @return array + * + * @since 4.0.0 + */ + public static function getExtensionDownloadKey( + string $element, + string $type, + int $clientId = 1, + ?string $folder = null + ): array { + // Get the database driver. If it fails we cannot report whether the extension supports download keys. + try { + $db = Factory::getDbo(); + } catch (Exception $e) { + return [ + 'supported' => false, + 'valid' => false, + ]; + } + + // Try to retrieve the extension information as a CMSObject + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = :type') + ->where($db->quoteName('element') . ' = :element') + ->where($db->quoteName('folder') . ' = :folder') + ->where($db->quoteName('client_id') . ' = :client_id'); + $query->bind(':type', $type, ParameterType::STRING); + $query->bind(':element', $element, ParameterType::STRING); + $query->bind(':client_id', $clientId, ParameterType::INTEGER); + $query->bind(':folder', $folder, ParameterType::STRING); + + try { + $extension = new CMSObject($db->setQuery($query)->loadAssoc()); + } catch (Exception $e) { + return [ + 'supported' => false, + 'valid' => false, + ]; + } + + // Use the getDownloadKey() method to return the download key information + return self::getDownloadKey($extension); + } + + /** + * Returns a list of update site IDs which support download keys. By default this returns all qualifying update + * sites, even if they are not enabled. + * + * + * @param bool $onlyEnabled [optional] Set true to only returned enabled update sites. + * + * @return int[] + * @since 4.0.0 + */ + public static function getDownloadKeySupportedSites($onlyEnabled = false): array + { + /** + * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually + * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less + * readable but works around that issue. + */ + + $extensions = self::getUpdateSitesInformation($onlyEnabled); + + $filterClosure = function (CMSObject $extension) { + $dlidInfo = self::getDownloadKey($extension); + + return $dlidInfo['supported']; + }; + $extensions = array_filter($extensions, $filterClosure); + + $mapClosure = function (CMSObject $extension) { + return $extension->get('update_site_id'); + }; + + return array_map($mapClosure, $extensions); + } + + /** + * Returns a list of update site IDs which are missing download keys. By default this returns all qualifying update + * sites, even if they are not enabled. + * + * @param bool $exists [optional] If true, returns update sites with a valid download key. When false, + * returns update sites with an invalid / missing download key. + * @param bool $onlyEnabled [optional] Set true to only returned enabled update sites. + * + * @return int[] + * @since 4.0.0 + */ + public static function getDownloadKeyExistsSites(bool $exists = true, $onlyEnabled = false): array + { + /** + * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually + * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less + * readable but works around that issue. + */ + + $extensions = self::getUpdateSitesInformation($onlyEnabled); + + // Filter the extensions by what supports Download Keys + $filterClosure = function (CMSObject $extension) use ($exists) { + $dlidInfo = self::getDownloadKey($extension); + + if (!$dlidInfo['supported']) { + return false; + } + + return $exists ? $dlidInfo['valid'] : !$dlidInfo['valid']; + }; + $extensions = array_filter($extensions, $filterClosure); + + // Return only the update site IDs + $mapClosure = function (CMSObject $extension) { + return $extension->get('update_site_id'); + }; + + return array_map($mapClosure, $extensions); + } + + + /** + * Get information about the update sites + * + * @param bool $onlyEnabled Only return enabled update sites + * + * @return CMSObject[] List of update site and linked extension information + * @since 4.0.0 + */ + protected static function getUpdateSitesInformation(bool $onlyEnabled): array + { + try { + $db = Factory::getDbo(); + } catch (Exception $e) { + return []; + } + + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 's.update_site_id', + 's.enabled', + 's.extra_query', + 'e.extension_id', + 'e.type', + 'e.element', + 'e.folder', + 'e.client_id', + 'e.manifest_cache', + ], + [ + 'update_site_id', + 'enabled', + 'extra_query', + 'extension_id', + 'type', + 'element', + 'folder', + 'client_id', + 'manifest_cache', + ] + ) + ) + ->from($db->quoteName('#__update_sites', 's')) + ->innerJoin( + $db->quoteName('#__update_sites_extensions', 'se'), + $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id') + ) + ->innerJoin( + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id') + ) + ->where($db->quoteName('state') . ' = 0'); + + if ($onlyEnabled) { + $enabled = 1; + $query->where($db->quoteName('s.enabled') . ' = :enabled') + ->bind(':enabled', $enabled, ParameterType::INTEGER); + } + + // Try to get all of the update sites, including related extension information + try { + $items = []; + $db->setQuery($query); + + foreach ($db->getIterator() as $item) { + $items[] = new CMSObject($item); + } + + return $items; + } catch (Exception $e) { + return []; + } + } } diff --git a/code/administrator/components/com_installer/src/Model/DatabaseModel.php b/code/administrator/components/com_installer/src/Model/DatabaseModel.php index 069f165b..11085d58 100644 --- a/code/administrator/components/com_installer/src/Model/DatabaseModel.php +++ b/code/administrator/components/com_installer/src/Model/DatabaseModel.php @@ -1,4 +1,5 @@ errorCount; - } - - /** - * Method to populate the schema cache. - * - * @param integer $cid The extension ID to get the schema for - * - * @return void - * - * @throws \Exception - * - * @since 4.0.0 - */ - private function fetchSchemaCache($cid = 0) - { - // We already have it - if (array_key_exists($cid, $this->changeSetList)) - { - return; - } - - // Add the ID to the state so it can be used for filtering - if ($cid) - { - $this->setState('filter.extension_id', $cid); - } - - // With the parent::save it can get the limit and we need to make sure it gets all extensions - $results = $this->_getList($this->getListQuery()); - - foreach ($results as $result) - { - $errorMessages = array(); - $errorCount = 0; - - if (strcmp($result->element, 'joomla') === 0) - { - $result->element = 'com_admin'; - - if (!$this->getDefaultTextFilters()) - { - $errorMessages[] = Text::_('COM_INSTALLER_MSG_DATABASE_FILTER_ERROR'); - $errorCount++; - } - } - - $db = $this->getDbo(); - - if ($result->type === 'component') - { - $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; - } - elseif ($result->type === 'plugin') - { - $basePath = JPATH_PLUGINS . '/' . $result->folder . '/' . $result->element; - } - elseif ($result->type === 'module') - { - // Typehint to integer to normalise some DBs returning strings and others integers - if ((int) $result->client_id === 1) - { - $basePath = JPATH_ADMINISTRATOR . '/modules/' . $result->element; - } - elseif ((int) $result->client_id === 0) - { - $basePath = JPATH_SITE . '/modules/' . $result->element; - } - else - { - // Module with unknown client id!? - bail - continue; - } - } - // Specific bodge for the Joomla CMS special database check which points to com_admin - elseif ($result->type === 'file' && $result->element === 'com_admin') - { - $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; - } - else - { - // Unknown extension type (library, files etc which don't have known SQL paths right now) - continue; - } - - // Search the standard SQL Path for the SQL Updates and then if not there check the configuration of the XML - // file. This just gives us a small performance win of not parsing the XML every time. - $folderTmp = $basePath . '/sql/updates/'; - - if (!file_exists($folderTmp)) - { - $installationXML = InstallerHelper::getInstallationXML( - $result->element, - $result->type, - $result->client_id, - $result->type === 'plugin' ? $result->folder : null - ); - - if ($installationXML !== null) - { - $folderTmp = (string) $installationXML->update->schemas->schemapath[0]; - $a = explode('/', $folderTmp); - array_pop($a); - $folderTmp = $basePath . '/' . implode('/', $a); - } - } - - // Can't find the folder still - give up now and move on. - if (!file_exists($folderTmp)) - { - continue; - } - - $changeSet = new ChangeSet($db, $folderTmp); - - // If the version in the #__schemas is different - // than the update files, add to problems message - $schema = $changeSet->getSchema(); - - // If the schema is empty we couldn't find any update files. Just ignore the extension. - if (empty($schema)) - { - continue; - } - - if ($result->version_id !== $schema) - { - $errorMessages[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SCHEMA_ERROR', $result->version_id, $schema); - $errorCount++; - } - - // If the version in the manifest_cache is different than the - // version in the installation xml, add to problems message - $compareUpdateMessage = $this->compareUpdateVersion($result); - - if ($compareUpdateMessage) - { - $errorMessages[] = $compareUpdateMessage; - $errorCount++; - } - - // If there are errors in the database, add to the problems message - $errors = $changeSet->check(); - - $errorsMessage = $this->getErrorsMessage($errors); - - if ($errorsMessage) - { - $errorMessages = array_merge($errorMessages, $errorsMessage); - $errorCount++; - } - - // Number of database tables Checked and Skipped - $errorMessages = array_merge($errorMessages, $this->getOtherInformationMessage($changeSet->getStatus())); - - // Set the total number of errors - $this->errorCount += $errorCount; - - // Collect the extension details - $this->changeSetList[$result->extension_id] = array( - 'folderTmp' => $folderTmp, - 'errorsMessage' => $errorMessages, - 'errorsCount' => $errorCount, - 'results' => $changeSet->getStatus(), - 'schema' => $schema, - 'extension' => $result - ); - } - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'name', $direction = 'asc') - { - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - - parent::populateState($ordering, $direction); - } - - /** - * Fixes database problems. - * - * @param array $cids List of the selected extensions to fix - * - * @return void|boolean - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function fix($cids = array()) - { - $db = $this->getDbo(); - - foreach ($cids as $i => $cid) - { - // Load the database issues - $this->fetchSchemaCache($cid); - - $changeSet = $this->changeSetList[$cid]; - $changeSet['changeset'] = new ChangeSet($db, $changeSet['folderTmp']); - $changeSet['changeset']->fix(); - - $this->fixSchemaVersion($changeSet['changeset'], $changeSet['extension']->extension_id); - $this->fixUpdateVersion($changeSet['extension']->extension_id); - - if ($changeSet['extension']->element === 'com_admin') - { - $installer = new \JoomlaInstallerScript; - $installer->deleteUnexistingFiles(); - $this->fixDefaultTextFilters(); - - /* - * Finally, if the schema updates succeeded, make sure the database table is - * converted to utf8mb4 or, if not supported by the server, compatible to it. - */ - $statusArray = $changeSet['changeset']->getStatus(); - - if (count($statusArray['error']) == 0) - { - $installer->convertTablesToUtf8mb4(false); - } - } - } - } - - /** - * Gets the changeset array. - * - * @return array Array with the information of the versions problems, errors and the extensions itself - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function getItems() - { - $this->fetchSchemaCache(); - - $results = parent::getItems(); - $results = $this->mergeSchemaCache($results); - - return $results; - } - - /** - * Method to get the database query - * - * @return DatabaseQuery The database query - * - * @since 4.0.0 - */ - protected function getListQuery() - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'extensions.client_id', - 'extensions.element', - 'extensions.extension_id', - 'extensions.folder', - 'extensions.manifest_cache', - 'extensions.name', - 'extensions.type', - 'schemas.version_id' - ] - ) - ) - ->from( - $db->quoteName( - '#__schemas', - 'schemas' - ) - ) - ->join( - 'INNER', - $db->quoteName('#__extensions', 'extensions'), - $db->quoteName('schemas.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') - ); - - $type = $this->getState('filter.type'); - $clientId = $this->getState('filter.client_id'); - $extensionId = $this->getState('filter.extension_id'); - $folder = $this->getState('filter.folder'); - - if ($type) - { - $query->where($db->quoteName('extensions.type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId != '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('extensions.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($extensionId != '') - { - $extensionId = (int) $extensionId; - $query->where($db->quoteName('extensions.extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - } - - if ($folder != '' && in_array($type, array('plugin', 'library', ''))) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('extensions.folder') . ' = :folder') - ->bind(':folder', $folder); - } - - // Process search filter (update site id). - $search = $this->getState('filter.search'); - - if (!empty($search) && stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('schemas.extension_id') . ' = :eid') - ->bind(':eid', $ids, ParameterType::INTEGER); - } - - return $query; - } - - /** - * Merge the items that will be visible with the changeSet information in cache - * - * @param array $results extensions returned from parent::getItems(). - * - * @return array the changeSetList of the merged items - * - * @since 4.0.0 - */ - protected function mergeSchemaCache($results) - { - $changeSetList = $this->changeSetList; - $finalResults = array(); - - foreach ($results as $result) - { - if (array_key_exists($result->extension_id, $changeSetList) && $changeSetList[$result->extension_id]) - { - $finalResults[] = $changeSetList[$result->extension_id]; - } - } - - return $finalResults; - } - - /** - * Get version from #__schemas table. - * - * @param integer $extensionId id of the extensions. - * - * @return mixed the return value from the query, or null if the query fails. - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function getSchemaVersion($extensionId) - { - $db = $this->getDbo(); - $extensionId = (int) $extensionId; - $query = $db->getQuery(true) - ->select($db->quoteName('version_id')) - ->from($db->quoteName('#__schemas')) - ->where($db->quoteName('extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Fix schema version if wrong. - * - * @param ChangeSet $changeSet Schema change set. - * @param integer $extensionId ID of the extensions. - * - * @return mixed string schema version if success, false if fail. - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function fixSchemaVersion($changeSet, $extensionId) - { - // Get correct schema version -- last file in array. - $schema = $changeSet->getSchema(); - - // Check value. If ok, don't do update. - if ($schema == $this->getSchemaVersion($extensionId)) - { - return $schema; - } - - // Delete old row. - $extensionId = (int) $extensionId; - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__schemas')) - ->where($db->quoteName('extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Add new row. - $query->clear() - ->insert($db->quoteName('#__schemas')) - ->columns($db->quoteName('extension_id') . ',' . $db->quoteName('version_id')) - ->values(':extensionid, :schema') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER) - ->bind(':schema', $schema); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - return false; - } - - return $schema; - } - - /** - * Get current version from #__extensions table. - * - * @param object $extension data from #__extensions of a single extension. - * - * @return mixed string message with the errors with the update version or null if none - * - * @since 4.0.0 - */ - public function compareUpdateVersion($extension) - { - $updateVersion = json_decode($extension->manifest_cache)->version; - - if ($extension->element === 'com_admin') - { - $extensionVersion = JVERSION; - } - else - { - $installationXML = InstallerHelper::getInstallationXML( - $extension->element, - $extension->type, - $extension->client_id, - $extension->type === 'plugin' ? $extension->folder : null - ); - - $extensionVersion = (string) $installationXML->version; - } - - if (version_compare($extensionVersion, $updateVersion) != 0) - { - return Text::sprintf('COM_INSTALLER_MSG_DATABASE_UPDATEVERSION_ERROR', $updateVersion, $extension->name, $extensionVersion); - } - - return null; - } - - /** - * Get a message of the tables skipped and checked - * - * @param array $status status of of the update files - * - * @return array Messages with the errors with the update version - * - * @since 4.0.0 - */ - private function getOtherInformationMessage($status) - { - $problemsMessage = array(); - $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_CHECKED_OK', count($status['ok'])); - $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SKIPPED', count($status['skipped'])); - - return $problemsMessage; - } - - /** - * Get a message with all errors found in a given extension - * - * @param array $errors data from #__extensions of a single extension. - * - * @return array List of messages with the errors in the database - * - * @since 4.0.0 - */ - private function getErrorsMessage($errors) - { - $errorMessages = array(); - - foreach ($errors as $line => $error) - { - $key = 'COM_INSTALLER_MSG_DATABASE_' . $error->queryType; - $messages = $error->msgElements; - $file = basename($error->file); - $message0 = isset($messages[0]) ? $messages[0] : ' '; - $message1 = isset($messages[1]) ? $messages[1] : ' '; - $message2 = isset($messages[2]) ? $messages[2] : ' '; - $errorMessages[] = Text::sprintf($key, $file, $message0, $message1, $message2); - } - - return $errorMessages; - } - - /** - * Fix Joomla version in #__extensions table if wrong (doesn't equal \JVersion short version). - * - * @param integer $extensionId id of the extension - * - * @return mixed string update version if success, false if fail. - * - * @since 4.0.0 - */ - public function fixUpdateVersion($extensionId) - { - $table = new Extension($this->getDbo()); - $table->load($extensionId); - $cache = new Registry($table->manifest_cache); - $updateVersion = $cache->get('version'); - - if ($table->get('type') === 'file' && $table->get('element') === 'joomla') - { - $extensionVersion = new Version; - $extensionVersion = $extensionVersion->getShortVersion(); - } - else - { - $installationXML = InstallerHelper::getInstallationXML( - $table->get('element'), - $table->get('type'), - $table->get('client_id'), - $table->get('type') === 'plugin' ? $table->get('folder') : null - ); - $extensionVersion = (string) $installationXML->version; - } - - if ($updateVersion === $extensionVersion) - { - return $updateVersion; - } - - $cache->set('version', $extensionVersion); - $table->set('manifest_cache', $cache->toString()); - - if ($table->store()) - { - return $extensionVersion; - } - - return false; - } - - /** - * For version 2.5.x only - * Check if com_config parameters are blank. - * - * @return string default text filters (if any). - * - * @since 4.0.0 - */ - public function getDefaultTextFilters() - { - $table = new Extension($this->getDbo()); - $table->load($table->find(array('name' => 'com_config'))); - - return $table->params; - } - - /** - * For version 2.5.x only - * Check if com_config parameters are blank. If so, populate with com_content text filters. - * - * @return void - * - * @since 4.0.0 - */ - private function fixDefaultTextFilters() - { - $table = new Extension($this->getDbo()); - $table->load($table->find(array('name' => 'com_config'))); - - // Check for empty $config and non-empty content filters. - if (!$table->params) - { - // Get filters from com_content and store if you find them. - $contentParams = ComponentHelper::getComponent('com_content')->getParams(); - - if ($contentParams->get('filters')) - { - $newParams = new Registry; - $newParams->set('filters', $contentParams->get('filters')); - $table->params = (string) $newParams; - $table->store(); - } - } - } + /** + * Set the model context + * + * @var string + * + * @since 4.0.0 + */ + protected $_context = 'com_installer.discover'; + + /** + * ChangeSet of all extensions + * + * @var array + * + * @since 4.0.0 + */ + private $changeSetList = array(); + + /** + * Total of errors + * + * @var integer + * + * @since 4.0.0 + */ + private $errorCount = 0; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see ListModel + * @since 4.0.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'update_site_name', + 'name', + 'client_id', + 'client', 'client_translated', + 'status', + 'type', 'type_translated', + 'folder', 'folder_translated', + 'extension_id' + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to return the total number of errors in all the extensions, saved in cache. + * + * @return integer + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getErrorCount() + { + return $this->errorCount; + } + + /** + * Method to populate the schema cache. + * + * @param integer $cid The extension ID to get the schema for + * + * @return void + * + * @throws \Exception + * + * @since 4.0.0 + */ + private function fetchSchemaCache($cid = 0) + { + // We already have it + if (array_key_exists($cid, $this->changeSetList)) { + return; + } + + // Add the ID to the state so it can be used for filtering + if ($cid) { + $this->setState('filter.extension_id', $cid); + } + + // With the parent::save it can get the limit and we need to make sure it gets all extensions + $results = $this->_getList($this->getListQuery()); + + foreach ($results as $result) { + $errorMessages = array(); + $errorCount = 0; + + if (strcmp($result->element, 'joomla') === 0) { + $result->element = 'com_admin'; + + if (!$this->getDefaultTextFilters()) { + $errorMessages[] = Text::_('COM_INSTALLER_MSG_DATABASE_FILTER_ERROR'); + $errorCount++; + } + } + + $db = $this->getDatabase(); + + if ($result->type === 'component') { + $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; + } elseif ($result->type === 'plugin') { + $basePath = JPATH_PLUGINS . '/' . $result->folder . '/' . $result->element; + } elseif ($result->type === 'module') { + // Typehint to integer to normalise some DBs returning strings and others integers + if ((int) $result->client_id === 1) { + $basePath = JPATH_ADMINISTRATOR . '/modules/' . $result->element; + } elseif ((int) $result->client_id === 0) { + $basePath = JPATH_SITE . '/modules/' . $result->element; + } else { + // Module with unknown client id!? - bail + continue; + } + } elseif ($result->type === 'file' && $result->element === 'com_admin') { + // Specific bodge for the Joomla CMS special database check which points to com_admin + $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; + } else { + // Unknown extension type (library, files etc which don't have known SQL paths right now) + continue; + } + + // Search the standard SQL Path for the SQL Updates and then if not there check the configuration of the XML + // file. This just gives us a small performance win of not parsing the XML every time. + $folderTmp = $basePath . '/sql/updates/'; + + if (!file_exists($folderTmp)) { + $installationXML = InstallerHelper::getInstallationXML( + $result->element, + $result->type, + $result->client_id, + $result->type === 'plugin' ? $result->folder : null + ); + + if ($installationXML !== null) { + $folderTmp = (string) $installationXML->update->schemas->schemapath[0]; + $a = explode('/', $folderTmp); + array_pop($a); + $folderTmp = $basePath . '/' . implode('/', $a); + } + } + + // Can't find the folder still - give up now and move on. + if (!file_exists($folderTmp)) { + continue; + } + + $changeSet = new ChangeSet($db, $folderTmp); + + // If the version in the #__schemas is different + // than the update files, add to problems message + $schema = $changeSet->getSchema(); + + // If the schema is empty we couldn't find any update files. Just ignore the extension. + if (empty($schema)) { + continue; + } + + if ($result->version_id !== $schema) { + $errorMessages[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SCHEMA_ERROR', $result->version_id, $schema); + $errorCount++; + } + + // If the version in the manifest_cache is different than the + // version in the installation xml, add to problems message + $compareUpdateMessage = $this->compareUpdateVersion($result); + + if ($compareUpdateMessage) { + $errorMessages[] = $compareUpdateMessage; + $errorCount++; + } + + // If there are errors in the database, add to the problems message + $errors = $changeSet->check(); + + $errorsMessage = $this->getErrorsMessage($errors); + + if ($errorsMessage) { + $errorMessages = array_merge($errorMessages, $errorsMessage); + $errorCount++; + } + + // Number of database tables Checked and Skipped + $errorMessages = array_merge($errorMessages, $this->getOtherInformationMessage($changeSet->getStatus())); + + // Set the total number of errors + $this->errorCount += $errorCount; + + // Collect the extension details + $this->changeSetList[$result->extension_id] = array( + 'folderTmp' => $folderTmp, + 'errorsMessage' => $errorMessages, + 'errorsCount' => $errorCount, + 'results' => $changeSet->getStatus(), + 'schema' => $schema, + 'extension' => $result + ); + } + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + + parent::populateState($ordering, $direction); + } + + /** + * Fixes database problems. + * + * @param array $cids List of the selected extensions to fix + * + * @return void|boolean + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function fix($cids = array()) + { + $db = $this->getDatabase(); + + foreach ($cids as $i => $cid) { + // Load the database issues + $this->fetchSchemaCache($cid); + + $changeSet = $this->changeSetList[$cid]; + $changeSet['changeset'] = new ChangeSet($db, $changeSet['folderTmp']); + $changeSet['changeset']->fix(); + + $this->fixSchemaVersion($changeSet['changeset'], $changeSet['extension']->extension_id); + $this->fixUpdateVersion($changeSet['extension']->extension_id); + + if ($changeSet['extension']->element === 'com_admin') { + $installer = new \JoomlaInstallerScript(); + $installer->deleteUnexistingFiles(); + $this->fixDefaultTextFilters(); + + /* + * Finally, if the schema updates succeeded, make sure the database table is + * converted to utf8mb4 or, if not supported by the server, compatible to it. + */ + $statusArray = $changeSet['changeset']->getStatus(); + + if (count($statusArray['error']) == 0) { + $installer->convertTablesToUtf8mb4(false); + } + } + } + } + + /** + * Gets the changeset array. + * + * @return array Array with the information of the versions problems, errors and the extensions itself + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getItems() + { + $this->fetchSchemaCache(); + + $results = parent::getItems(); + $results = $this->mergeSchemaCache($results); + + return $results; + } + + /** + * Method to get the database query + * + * @return DatabaseQuery The database query + * + * @since 4.0.0 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'extensions.client_id', + 'extensions.element', + 'extensions.extension_id', + 'extensions.folder', + 'extensions.manifest_cache', + 'extensions.name', + 'extensions.type', + 'schemas.version_id' + ] + ) + ) + ->from( + $db->quoteName( + '#__schemas', + 'schemas' + ) + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'extensions'), + $db->quoteName('schemas.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') + ); + + $type = $this->getState('filter.type'); + $clientId = $this->getState('filter.client_id'); + $extensionId = $this->getState('filter.extension_id'); + $folder = $this->getState('filter.folder'); + + if ($type) { + $query->where($db->quoteName('extensions.type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId != '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('extensions.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($extensionId != '') { + $extensionId = (int) $extensionId; + $query->where($db->quoteName('extensions.extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + } + + if ($folder != '' && in_array($type, array('plugin', 'library', ''))) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('extensions.folder') . ' = :folder') + ->bind(':folder', $folder); + } + + // Process search filter (update site id). + $search = $this->getState('filter.search'); + + if (!empty($search) && stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('schemas.extension_id') . ' = :eid') + ->bind(':eid', $ids, ParameterType::INTEGER); + } + + return $query; + } + + /** + * Merge the items that will be visible with the changeSet information in cache + * + * @param array $results extensions returned from parent::getItems(). + * + * @return array the changeSetList of the merged items + * + * @since 4.0.0 + */ + protected function mergeSchemaCache($results) + { + $changeSetList = $this->changeSetList; + $finalResults = array(); + + foreach ($results as $result) { + if (array_key_exists($result->extension_id, $changeSetList) && $changeSetList[$result->extension_id]) { + $finalResults[] = $changeSetList[$result->extension_id]; + } + } + + return $finalResults; + } + + /** + * Get version from #__schemas table. + * + * @param integer $extensionId id of the extensions. + * + * @return mixed the return value from the query, or null if the query fails. + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getSchemaVersion($extensionId) + { + $db = $this->getDatabase(); + $extensionId = (int) $extensionId; + $query = $db->getQuery(true) + ->select($db->quoteName('version_id')) + ->from($db->quoteName('#__schemas')) + ->where($db->quoteName('extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Fix schema version if wrong. + * + * @param ChangeSet $changeSet Schema change set. + * @param integer $extensionId ID of the extensions. + * + * @return mixed string schema version if success, false if fail. + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function fixSchemaVersion($changeSet, $extensionId) + { + // Get correct schema version -- last file in array. + $schema = $changeSet->getSchema(); + + // Check value. If ok, don't do update. + if ($schema == $this->getSchemaVersion($extensionId)) { + return $schema; + } + + // Delete old row. + $extensionId = (int) $extensionId; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__schemas')) + ->where($db->quoteName('extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Add new row. + $query->clear() + ->insert($db->quoteName('#__schemas')) + ->columns($db->quoteName('extension_id') . ',' . $db->quoteName('version_id')) + ->values(':extensionid, :schema') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER) + ->bind(':schema', $schema); + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + return false; + } + + return $schema; + } + + /** + * Get current version from #__extensions table. + * + * @param object $extension data from #__extensions of a single extension. + * + * @return mixed string message with the errors with the update version or null if none + * + * @since 4.0.0 + */ + public function compareUpdateVersion($extension) + { + $updateVersion = json_decode($extension->manifest_cache)->version; + + if ($extension->element === 'com_admin') { + $extensionVersion = JVERSION; + } else { + $installationXML = InstallerHelper::getInstallationXML( + $extension->element, + $extension->type, + $extension->client_id, + $extension->type === 'plugin' ? $extension->folder : null + ); + + $extensionVersion = (string) $installationXML->version; + } + + if (version_compare($extensionVersion, $updateVersion) != 0) { + return Text::sprintf('COM_INSTALLER_MSG_DATABASE_UPDATEVERSION_ERROR', $updateVersion, $extension->name, $extensionVersion); + } + + return null; + } + + /** + * Get a message of the tables skipped and checked + * + * @param array $status status of of the update files + * + * @return array Messages with the errors with the update version + * + * @since 4.0.0 + */ + private function getOtherInformationMessage($status) + { + $problemsMessage = array(); + $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_CHECKED_OK', count($status['ok'])); + $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SKIPPED', count($status['skipped'])); + + return $problemsMessage; + } + + /** + * Get a message with all errors found in a given extension + * + * @param array $errors data from #__extensions of a single extension. + * + * @return array List of messages with the errors in the database + * + * @since 4.0.0 + */ + private function getErrorsMessage($errors) + { + $errorMessages = array(); + + foreach ($errors as $line => $error) { + $key = 'COM_INSTALLER_MSG_DATABASE_' . $error->queryType; + $messages = $error->msgElements; + $file = basename($error->file); + $message0 = isset($messages[0]) ? $messages[0] : ' '; + $message1 = isset($messages[1]) ? $messages[1] : ' '; + $message2 = isset($messages[2]) ? $messages[2] : ' '; + $errorMessages[] = Text::sprintf($key, $file, $message0, $message1, $message2); + } + + return $errorMessages; + } + + /** + * Fix Joomla version in #__extensions table if wrong (doesn't equal \JVersion short version). + * + * @param integer $extensionId id of the extension + * + * @return mixed string update version if success, false if fail. + * + * @since 4.0.0 + */ + public function fixUpdateVersion($extensionId) + { + $table = new Extension($this->getDatabase()); + $table->load($extensionId); + $cache = new Registry($table->manifest_cache); + $updateVersion = $cache->get('version'); + + if ($table->get('type') === 'file' && $table->get('element') === 'joomla') { + $extensionVersion = new Version(); + $extensionVersion = $extensionVersion->getShortVersion(); + } else { + $installationXML = InstallerHelper::getInstallationXML( + $table->get('element'), + $table->get('type'), + $table->get('client_id'), + $table->get('type') === 'plugin' ? $table->get('folder') : null + ); + $extensionVersion = (string) $installationXML->version; + } + + if ($updateVersion === $extensionVersion) { + return $updateVersion; + } + + $cache->set('version', $extensionVersion); + $table->set('manifest_cache', $cache->toString()); + + if ($table->store()) { + return $extensionVersion; + } + + return false; + } + + /** + * For version 2.5.x only + * Check if com_config parameters are blank. + * + * @return string default text filters (if any). + * + * @since 4.0.0 + */ + public function getDefaultTextFilters() + { + $table = new Extension($this->getDatabase()); + $table->load($table->find(array('name' => 'com_config'))); + + return $table->params; + } + + /** + * For version 2.5.x only + * Check if com_config parameters are blank. If so, populate with com_content text filters. + * + * @return void + * + * @since 4.0.0 + */ + private function fixDefaultTextFilters() + { + $table = new Extension($this->getDatabase()); + $table->load($table->find(array('name' => 'com_config'))); + + // Check for empty $config and non-empty content filters. + if (!$table->params) { + // Get filters from com_content and store if you find them. + $contentParams = ComponentHelper::getComponent('com_content')->getParams(); + + if ($contentParams->get('filters')) { + $newParams = new Registry(); + $newParams->set('filters', $contentParams->get('filters')); + $table->params = (string) $newParams; + $table->store(); + } + } + } } diff --git a/code/administrator/components/com_installer/src/Model/DiscoverModel.php b/code/administrator/components/com_installer/src/Model/DiscoverModel.php index 681874a9..7c91cee1 100644 --- a/code/administrator/components/com_installer/src/Model/DiscoverModel.php +++ b/code/administrator/components/com_installer/src/Model/DiscoverModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - - $this->setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get the database query. - * - * @return DatabaseQuery The database query - * - * @since 3.1 - */ - protected function getListQuery() - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('state') . ' = -1'); - - // Process select filters. - $type = $this->getState('filter.type'); - $clientId = $this->getState('filter.client_id'); - $folder = $this->getState('filter.folder'); - - if ($type) - { - $query->where($db->quoteName('type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId != '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($folder != '' && in_array($type, array('plugin', 'library', ''))) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('folder') . ' = :folder') - ->bind(':folder', $folder); - } - - // Process search filter. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $ids, ParameterType::INTEGER); - } - } - - // Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php). - - return $query; - } - - /** - * Discover extensions. - * - * Finds uninstalled extensions - * - * @return int The count of discovered extensions - * - * @since 1.6 - */ - public function discover() - { - // Purge the list of discovered extensions and fetch them again. - $this->purge(); - $results = Installer::getInstance()->discover(); - - // Get all templates, including discovered ones - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName(['extension_id', 'element', 'folder', 'client_id', 'type'])) - ->from($db->quoteName('#__extensions')); - $db->setQuery($query); - $installedtmp = $db->loadObjectList(); - - $extensions = array(); - - foreach ($installedtmp as $install) - { - $key = implode(':', - [ - $install->type, - str_replace('\\', '/', $install->element), - $install->folder, - $install->client_id - ] - ); - $extensions[$key] = $install; - } - - $count = 0; - - foreach ($results as $result) - { - // Check if we have a match on the element - $key = implode(':', - [ - $result->type, - str_replace('\\', '/', $result->element), - $result->folder, - $result->client_id - ] - ); - - if (!array_key_exists($key, $extensions)) - { - // Put it into the table - $result->check(); - $result->store(); - $count++; - } - } - - return $count; - } - - /** - * Installs a discovered extension. - * - * @return void - * - * @since 1.6 - */ - public function discover_install() - { - $app = Factory::getApplication(); - $input = $app->input; - $eid = $input->get('cid', 0, 'array'); - - if (is_array($eid) || $eid) - { - if (!is_array($eid)) - { - $eid = array($eid); - } - - $eid = ArrayHelper::toInteger($eid); - $failed = false; - - foreach ($eid as $id) - { - $installer = new Installer; - - $result = $installer->discover_install($id); - - if (!$result) - { - $failed = true; - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLFAILED') . ': ' . $id); - } - } - - // @todo - We are only receiving the message for the last Installer instance - $this->setState('action', 'remove'); - $this->setState('name', $installer->get('name')); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - - if (!$failed) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLSUCCESSFUL')); - } - } - else - { - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_NOEXTENSIONSELECTED')); - } - } - - /** - * Cleans out the list of discovered extensions. - * - * @return boolean True on success - * - * @since 1.6 - */ - public function purge() - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__extensions')) - ->where($db->quoteName('state') . ' = -1'); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_FAILEDTOPURGEEXTENSIONS'); - - return false; - } - - $this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_PURGEDDISCOVEREDEXTENSIONS'); - - return true; - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $query->where($this->_db->quoteName('state') . ' = -1'); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', + 'client_id', + 'client', 'client_translated', + 'type', 'type_translated', + 'folder', 'folder_translated', + 'extension_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.1 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get the database query. + * + * @return DatabaseQuery The database query + * + * @since 3.1 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('state') . ' = -1'); + + // Process select filters. + $type = $this->getState('filter.type'); + $clientId = $this->getState('filter.client_id'); + $folder = $this->getState('filter.folder'); + + if ($type) { + $query->where($db->quoteName('type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId != '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($folder != '' && in_array($type, array('plugin', 'library', ''))) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('folder') . ' = :folder') + ->bind(':folder', $folder); + } + + // Process search filter. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $ids, ParameterType::INTEGER); + } + } + + // Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php). + + return $query; + } + + /** + * Discover extensions. + * + * Finds uninstalled extensions + * + * @return int The count of discovered extensions + * + * @since 1.6 + */ + public function discover() + { + // Purge the list of discovered extensions and fetch them again. + $this->purge(); + $results = Installer::getInstance()->discover(); + + // Get all templates, including discovered ones + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['extension_id', 'element', 'folder', 'client_id', 'type'])) + ->from($db->quoteName('#__extensions')); + $db->setQuery($query); + $installedtmp = $db->loadObjectList(); + + $extensions = array(); + + foreach ($installedtmp as $install) { + $key = implode( + ':', + [ + $install->type, + str_replace('\\', '/', $install->element), + $install->folder, + $install->client_id + ] + ); + $extensions[$key] = $install; + } + + $count = 0; + + foreach ($results as $result) { + // Check if we have a match on the element + $key = implode( + ':', + [ + $result->type, + str_replace('\\', '/', $result->element), + $result->folder, + $result->client_id + ] + ); + + if (!array_key_exists($key, $extensions)) { + // Put it into the table + $result->check(); + $result->store(); + $count++; + } + } + + return $count; + } + + /** + * Installs a discovered extension. + * + * @return void + * + * @since 1.6 + */ + public function discover_install() + { + $app = Factory::getApplication(); + $input = $app->input; + $eid = $input->get('cid', 0, 'array'); + + if (is_array($eid) || $eid) { + if (!is_array($eid)) { + $eid = array($eid); + } + + $eid = ArrayHelper::toInteger($eid); + $failed = false; + + foreach ($eid as $id) { + $installer = new Installer(); + $installer->setDatabase($this->getDatabase()); + + $result = $installer->discover_install($id); + + if (!$result) { + $failed = true; + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLFAILED') . ': ' . $id); + } + } + + // @todo - We are only receiving the message for the last Installer instance + $this->setState('action', 'remove'); + $this->setState('name', $installer->get('name')); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + + if (!$failed) { + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLSUCCESSFUL')); + } + } else { + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_NOEXTENSIONSELECTED')); + } + } + + /** + * Cleans out the list of discovered extensions. + * + * @return boolean True on success + * + * @since 1.6 + */ + public function purge() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__extensions')) + ->where($db->quoteName('state') . ' = -1'); + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + $this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_FAILEDTOPURGEEXTENSIONS'); + + return false; + } + + $this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_PURGEDDISCOVEREDEXTENSIONS'); + + return true; + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $query->where($this->getDatabase()->quoteName('state') . ' = -1'); + + return $query; + } + + /** + * Checks for not installed extensions in extensions table. + * + * @return boolean True if there are discovered extensions in the database. + * + * @since 4.2.0 + */ + public function checkExtensions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('state') . ' = -1'); + $db->setQuery($query); + $discoveredExtensions = $db->loadObjectList(); + + return count($discoveredExtensions) > 0; + } } diff --git a/code/administrator/components/com_installer/src/Model/InstallModel.php b/code/administrator/components/com_installer/src/Model/InstallModel.php index e3ea81eb..727257b4 100644 --- a/code/administrator/components/com_installer/src/Model/InstallModel.php +++ b/code/administrator/components/com_installer/src/Model/InstallModel.php @@ -1,4 +1,5 @@ setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState(); - } - - /** - * Install an extension from either folder, URL or upload. - * - * @return boolean - * - * @since 1.5 - */ - public function install() - { - $this->setState('action', 'install'); - - $app = Factory::getApplication(); - - // Load installer plugins for assistance if required: - PluginHelper::importPlugin('installer'); - - $package = null; - - // This event allows an input pre-treatment, a custom pre-packing or custom installation. - // (e.g. from a \JSON description). - $results = $app->triggerEvent('onInstallerBeforeInstallation', array($this, &$package)); - - if (in_array(true, $results, true)) - { - return true; - } - - if (in_array(false, $results, true)) - { - return false; - } - - $installType = $app->input->getWord('installtype'); - $installLang = $app->input->getWord('package'); - - if ($package === null) - { - switch ($installType) - { - case 'folder': - // Remember the 'Install from Directory' path. - $app->getUserStateFromRequest($this->_context . '.install_directory', 'install_directory'); - $package = $this->_getPackageFromFolder(); - break; - - case 'upload': - $package = $this->_getPackageFromUpload(); - break; - - case 'url': - $package = $this->_getPackageFromUrl(); - break; - - default: - $app->setUserState('com_installer.message', Text::_('COM_INSTALLER_NO_INSTALL_TYPE_FOUND')); - - return false; - } - } - - // This event allows a custom installation of the package or a customization of the package: - $results = $app->triggerEvent('onInstallerBeforeInstaller', array($this, &$package)); - - if (in_array(true, $results, true)) - { - return true; - } - - if (in_array(false, $results, true)) - { - if (in_array($installType, array('upload', 'url'))) - { - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - } - - return false; - } - - // Check if package was uploaded successfully. - if (!\is_array($package)) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_UNABLE_TO_FIND_INSTALL_PACKAGE'), 'error'); - - return false; - } - - // Get an installer instance. - $installer = Installer::getInstance(); - - /* - * Check for a Joomla core package. - * To do this we need to set the source path to find the manifest (the same first step as Installer::install()) - * - * This must be done before the unpacked check because InstallerHelper::detectType() returns a boolean false since the manifest - * can't be found in the expected location. - */ - if (isset($package['dir']) && is_dir($package['dir'])) - { - $installer->setPath('source', $package['dir']); - - if (!$installer->findManifest()) - { - // If a manifest isn't found at the source, this may be a Joomla package; check the package directory for the Joomla manifest - if (file_exists($package['dir'] . '/administrator/manifests/files/joomla.xml')) - { - // We have a Joomla package - if (in_array($installType, array('upload', 'url'))) - { - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - } - - $app->enqueueMessage( - Text::sprintf('COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE', Route::_('index.php?option=com_joomlaupdate')), - 'warning' - ); - - return false; - } - } - } - - // Was the package unpacked? - if (empty($package['type'])) - { - if (in_array($installType, array('upload', 'url'))) - { - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - } - - $app->enqueueMessage(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'), 'error'); - - return false; - } - - // Install the package. - if (!$installer->install($package['dir'])) - { - // There was an error installing the package. - $msg = Text::sprintf('COM_INSTALLER_INSTALL_ERROR', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type']))); - $result = false; - $msgType = 'error'; - } - else - { - // Package installed successfully. - $msg = Text::sprintf('COM_INSTALLER_INSTALL_SUCCESS', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($installLang . $package['type']))); - $result = true; - $msgType = 'message'; - } - - // This event allows a custom a post-flight: - $app->triggerEvent('onInstallerAfterInstaller', array($this, &$package, $installer, &$result, &$msg)); - - // Set some model state values. - $app->enqueueMessage($msg, $msgType); - $this->setState('name', $installer->get('name')); - $this->setState('result', $result); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - $app->setUserState('com_installer.redirect_url', $installer->get('redirect_url')); - - // Cleanup the install files. - if (!is_file($package['packagefile'])) - { - $package['packagefile'] = $app->get('tmp_path') . '/' . $package['packagefile']; - } - - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('com_plugins'); - $this->cleanCache('mod_menu'); - - return $result; - } - - /** - * Works out an installation package from a HTTP upload. - * - * @return mixed Package definition or false on failure. - */ - protected function _getPackageFromUpload() - { - // Get the uploaded file information. - $input = Factory::getApplication()->input; - - // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. - $userfile = $input->files->get('install_package', null, 'raw'); - - // Make sure that file uploads are enabled in php. - if (!(bool) ini_get('file_uploads')) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 'error'); - - return false; - } - - // Make sure that zlib is loaded so that the package can be unpacked. - if (!extension_loaded('zlib')) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 'error'); - - return false; - } - - // If there is no uploaded file, we have a problem... - if (!is_array($userfile)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 'error'); - - return false; - } - - // Is the PHP tmp directory missing? - if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) - { - Factory::getApplication()->enqueueMessage( - Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), - 'error' - ); - - return false; - } - - // Is the max upload size too small in php.ini? - if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) - { - Factory::getApplication()->enqueueMessage( - Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), - 'error' - ); - - return false; - } - - // Check if there was a different problem uploading the file. - if ($userfile['error'] || $userfile['size'] < 1) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 'error'); - - return false; - } - - // Build the appropriate paths. - $config = Factory::getApplication()->getConfig(); - $tmp_dest = $config->get('tmp_path') . '/' . $userfile['name']; - $tmp_src = $userfile['tmp_name']; - - // Move uploaded file. - File::upload($tmp_src, $tmp_dest, false, true); - - // Unpack the downloaded package file. - $package = InstallerHelper::unpack($tmp_dest, true); - - return $package; - } - - /** - * Install an extension from a directory - * - * @return array Package details or false on failure - * - * @since 1.5 - */ - protected function _getPackageFromFolder() - { - $input = Factory::getApplication()->input; - - // Get the path to the package to install. - $p_dir = $input->getString('install_directory'); - $p_dir = Path::clean($p_dir); - - // Did you give us a valid directory? - if (!is_dir($p_dir)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PLEASE_ENTER_A_PACKAGE_DIRECTORY'), 'error'); - - return false; - } - - // Detect the package type - $type = InstallerHelper::detectType($p_dir); - - // Did you give us a valid package? - if (!$type) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PATH_DOES_NOT_HAVE_A_VALID_PACKAGE'), 'error'); - } - - $package['packagefile'] = null; - $package['extractdir'] = null; - $package['dir'] = $p_dir; - $package['type'] = $type; - - return $package; - } - - /** - * Install an extension from a URL. - * - * @return bool|array Package details or false on failure. - * - * @since 1.5 - */ - protected function _getPackageFromUrl() - { - $input = Factory::getApplication()->input; - - // Get the URL of the package to install. - $url = $input->getString('install_url'); - - // Did you give us a URL? - if (!$url) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_ENTER_A_URL'), 'error'); - - return false; - } - - // We only allow http & https here - $uri = new Uri($url); - - if (!in_array($uri->getScheme(), ['http', 'https'])) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL_SCHEME'), 'error'); - - return false; - } - - // Handle updater XML file case: - if (preg_match('/\.xml\s*$/', $url)) - { - $update = new Update; - $update->loadFromXml($url); - $package_url = trim($update->get('downloadurl', false)->_data); - - if ($package_url) - { - $url = $package_url; - } - - unset($update); - } - - // Download the package at the URL given. - $p_file = InstallerHelper::downloadPackage($url); - - // Was the package downloaded? - if (!$p_file) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL'), 'error'); - - return false; - } - - $tmp_dest = Factory::getApplication()->get('tmp_path'); - - // Unpack the downloaded package file. - $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file, true); - - return $package; - } + /** + * @var \Joomla\CMS\Table\Table Table object + */ + protected $_table = null; + + /** + * @var string URL + */ + protected $_url = null; + + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_installer.install'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState(); + } + + /** + * Install an extension from either folder, URL or upload. + * + * @return boolean + * + * @since 1.5 + */ + public function install() + { + $this->setState('action', 'install'); + + $app = Factory::getApplication(); + + // Load installer plugins for assistance if required: + PluginHelper::importPlugin('installer'); + + $package = null; + + // This event allows an input pre-treatment, a custom pre-packing or custom installation. + // (e.g. from a \JSON description). + $results = $app->triggerEvent('onInstallerBeforeInstallation', array($this, &$package)); + + if (in_array(true, $results, true)) { + return true; + } + + if (in_array(false, $results, true)) { + return false; + } + + $installType = $app->input->getWord('installtype'); + $installLang = $app->input->getWord('package'); + + if ($package === null) { + switch ($installType) { + case 'folder': + // Remember the 'Install from Directory' path. + $app->getUserStateFromRequest($this->_context . '.install_directory', 'install_directory'); + $package = $this->_getPackageFromFolder(); + break; + + case 'upload': + $package = $this->_getPackageFromUpload(); + break; + + case 'url': + $package = $this->_getPackageFromUrl(); + break; + + default: + $app->setUserState('com_installer.message', Text::_('COM_INSTALLER_NO_INSTALL_TYPE_FOUND')); + + return false; + } + } + + // This event allows a custom installation of the package or a customization of the package: + $results = $app->triggerEvent('onInstallerBeforeInstaller', array($this, &$package)); + + if (in_array(true, $results, true)) { + return true; + } + + if (in_array(false, $results, true)) { + if (in_array($installType, array('upload', 'url'))) { + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + } + + return false; + } + + // Check if package was uploaded successfully. + if (!\is_array($package)) { + $app->enqueueMessage(Text::_('COM_INSTALLER_UNABLE_TO_FIND_INSTALL_PACKAGE'), 'error'); + + return false; + } + + // Get an installer instance. + $installer = Installer::getInstance(); + + /* + * Check for a Joomla core package. + * To do this we need to set the source path to find the manifest (the same first step as Installer::install()) + * + * This must be done before the unpacked check because InstallerHelper::detectType() returns a boolean false since the manifest + * can't be found in the expected location. + */ + if (isset($package['dir']) && is_dir($package['dir'])) { + $installer->setPath('source', $package['dir']); + + if (!$installer->findManifest()) { + // If a manifest isn't found at the source, this may be a Joomla package; check the package directory for the Joomla manifest + if (file_exists($package['dir'] . '/administrator/manifests/files/joomla.xml')) { + // We have a Joomla package + if (in_array($installType, array('upload', 'url'))) { + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + } + + $app->enqueueMessage( + Text::sprintf('COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE', Route::_('index.php?option=com_joomlaupdate')), + 'warning' + ); + + return false; + } + } + } + + // Was the package unpacked? + if (empty($package['type'])) { + if (in_array($installType, array('upload', 'url'))) { + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + } + + $app->enqueueMessage(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'), 'error'); + + return false; + } + + // Install the package. + if (!$installer->install($package['dir'])) { + // There was an error installing the package. + $msg = Text::sprintf('COM_INSTALLER_INSTALL_ERROR', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type']))); + $result = false; + $msgType = 'error'; + } else { + // Package installed successfully. + $msg = Text::sprintf('COM_INSTALLER_INSTALL_SUCCESS', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($installLang . $package['type']))); + $result = true; + $msgType = 'message'; + } + + // This event allows a custom a post-flight: + $app->triggerEvent('onInstallerAfterInstaller', array($this, &$package, $installer, &$result, &$msg)); + + // Set some model state values. + $app->enqueueMessage($msg, $msgType); + $this->setState('name', $installer->get('name')); + $this->setState('result', $result); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + $app->setUserState('com_installer.redirect_url', $installer->get('redirect_url')); + + // Cleanup the install files. + if (!is_file($package['packagefile'])) { + $package['packagefile'] = $app->get('tmp_path') . '/' . $package['packagefile']; + } + + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('com_plugins'); + $this->cleanCache('mod_menu'); + + return $result; + } + + /** + * Works out an installation package from a HTTP upload. + * + * @return mixed Package definition or false on failure. + */ + protected function _getPackageFromUpload() + { + // Get the uploaded file information. + $input = Factory::getApplication()->input; + + // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. + $userfile = $input->files->get('install_package', null, 'raw'); + + // Make sure that file uploads are enabled in php. + if (!(bool) ini_get('file_uploads')) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 'error'); + + return false; + } + + // Make sure that zlib is loaded so that the package can be unpacked. + if (!extension_loaded('zlib')) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 'error'); + + return false; + } + + // If there is no uploaded file, we have a problem... + if (!is_array($userfile)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 'error'); + + return false; + } + + // Is the PHP tmp directory missing? + if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) { + Factory::getApplication()->enqueueMessage( + Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), + 'error' + ); + + return false; + } + + // Is the max upload size too small in php.ini? + if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) { + Factory::getApplication()->enqueueMessage( + Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), + 'error' + ); + + return false; + } + + // Check if there was a different problem uploading the file. + if ($userfile['error'] || $userfile['size'] < 1) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 'error'); + + return false; + } + + // Build the appropriate paths. + $config = Factory::getApplication()->getConfig(); + $tmp_dest = $config->get('tmp_path') . '/' . $userfile['name']; + $tmp_src = $userfile['tmp_name']; + + // Move uploaded file. + File::upload($tmp_src, $tmp_dest, false, true); + + // Unpack the downloaded package file. + $package = InstallerHelper::unpack($tmp_dest, true); + + return $package; + } + + /** + * Install an extension from a directory + * + * @return array Package details or false on failure + * + * @since 1.5 + */ + protected function _getPackageFromFolder() + { + $input = Factory::getApplication()->input; + + // Get the path to the package to install. + $p_dir = $input->getString('install_directory'); + $p_dir = Path::clean($p_dir); + + // Did you give us a valid directory? + if (!is_dir($p_dir)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PLEASE_ENTER_A_PACKAGE_DIRECTORY'), 'error'); + + return false; + } + + // Detect the package type + $type = InstallerHelper::detectType($p_dir); + + // Did you give us a valid package? + if (!$type) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PATH_DOES_NOT_HAVE_A_VALID_PACKAGE'), 'error'); + } + + $package['packagefile'] = null; + $package['extractdir'] = null; + $package['dir'] = $p_dir; + $package['type'] = $type; + + return $package; + } + + /** + * Install an extension from a URL. + * + * @return bool|array Package details or false on failure. + * + * @since 1.5 + */ + protected function _getPackageFromUrl() + { + $input = Factory::getApplication()->input; + + // Get the URL of the package to install. + $url = $input->getString('install_url'); + + // Did you give us a URL? + if (!$url) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_ENTER_A_URL'), 'error'); + + return false; + } + + // We only allow http & https here + $uri = new Uri($url); + + if (!in_array($uri->getScheme(), ['http', 'https'])) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL_SCHEME'), 'error'); + + return false; + } + + // Handle updater XML file case: + if (preg_match('/\.xml\s*$/', $url)) { + $update = new Update(); + $update->loadFromXml($url); + $package_url = trim($update->get('downloadurl', false)->_data); + + if ($package_url) { + $url = $package_url; + } + + unset($update); + } + + // Download the package at the URL given. + $p_file = InstallerHelper::downloadPackage($url); + + // Was the package downloaded? + if (!$p_file) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL'), 'error'); + + return false; + } + + $tmp_dest = Factory::getApplication()->get('tmp_path'); + + // Unpack the downloaded package file. + $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file, true); + + return $package; + } } diff --git a/code/administrator/components/com_installer/src/Model/InstallerModel.php b/code/administrator/components/com_installer/src/Model/InstallerModel.php index 1d7da712..24940a30 100644 --- a/code/administrator/components/com_installer/src/Model/InstallerModel.php +++ b/code/administrator/components/com_installer/src/Model/InstallerModel.php @@ -1,4 +1,5 @@ getState('list.ordering', 'name'); - $listDirn = $this->getState('list.direction', 'asc'); - - // Replace slashes so preg_match will work - $search = $this->getState('filter.search'); - $search = str_replace('/', ' ', $search); - $db = $this->getDbo(); - - // Define which fields have to be processed in a custom way because of translation. - $customOrderFields = array('name', 'client_translated', 'type_translated', 'folder_translated'); - - // Process searching, ordering and pagination for fields that need to be translated. - if (in_array($listOrder, $customOrderFields) || (!empty($search) && stripos($search, 'id:') !== 0)) - { - // Get results from database and translate them. - $db->setQuery($query); - $result = $db->loadObjectList(); - $this->translate($result); - - // Process searching. - if (!empty($search) && stripos($search, 'id:') !== 0) - { - $escapedSearchString = $this->refineSearchStringToRegex($search, '/'); - - // By default search only the extension name field. - $searchFields = array('name'); - - // If in update sites view search also in the update site name field. - if ($this instanceof UpdatesitesModel) - { - $searchFields[] = 'update_site_name'; - } - - foreach ($result as $i => $item) - { - // Check if search string exists in any of the fields to be searched. - $found = 0; - - foreach ($searchFields as $key => $field) - { - if (!$found && preg_match('/' . $escapedSearchString . '/i', $item->{$field})) - { - $found = 1; - } - } - - // If search string was not found in any of the fields searched remove it from results array. - if (!$found) - { - unset($result[$i]); - } - } - } - - // Process ordering. - // Sort array object by selected ordering and selected direction. Sort is case insensitive and using locale sorting. - $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, false, true); - - // Process pagination. - $total = count($result); - $this->cache[$this->getStoreId('getTotal')] = $total; - - if ($total <= $limitstart) - { - $limitstart = 0; - $this->setState('list.limitstart', 0); - } - - return array_slice($result, $limitstart, $limit ?: null); - } - - // Process searching, ordering and pagination for regular database fields. - $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); - $result = parent::_getList($query, $limitstart, $limit); - $this->translate($result); - - return $result; - } - - /** - * Translate a list of objects - * - * @param array $items The array of objects - * - * @return array The array of translated objects - */ - protected function translate(&$items) - { - $lang = Factory::getLanguage(); - - foreach ($items as &$item) - { - if (strlen($item->manifest_cache) && $data = json_decode($item->manifest_cache)) - { - foreach ($data as $key => $value) - { - if ($key == 'type') - { - // Ignore the type field - continue; - } - - $item->$key = $value; - } - } - - $item->author_info = @$item->authorEmail . '
' . @$item->authorUrl; - $item->client = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); - $item->client_translated = $item->client; - $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); - $item->folder_translated = @$item->folder ? $item->folder : Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); - - $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; - - switch ($item->type) - { - case 'component': - $extension = $item->element; - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); - break; - case 'file': - $extension = 'files_' . $item->element; - $lang->load("$extension.sys", JPATH_SITE); - break; - case 'library': - $parts = explode('/', $item->element); - $vendor = (isset($parts[1]) ? $parts[0] : null); - $extension = 'lib_' . ($vendor ? implode('_', $parts) : $item->element); - - if (!$lang->load("$extension.sys", $path)) - { - $source = $path . '/libraries/' . ($vendor ? $vendor . '/' . $parts[1] : $item->element); - $lang->load("$extension.sys", $source); - } - break; - case 'module': - $extension = $item->element; - $source = $path . '/modules/' . $extension; - $lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source); - break; - case 'plugin': - $extension = 'plg_' . $item->folder . '_' . $item->element; - $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); - break; - case 'template': - $extension = 'tpl_' . $item->element; - $source = $path . '/templates/' . $item->element; - $lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source); - break; - case 'package': - default: - $extension = $item->element; - $lang->load("$extension.sys", JPATH_SITE); - break; - } - - // Translate the extension name if possible - $item->name = Text::_($item->name); - - settype($item->description, 'string'); - - if (!in_array($item->type, array('language'))) - { - $item->description = Text::_($item->description); - } - } - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', + 'client_id', + 'client', 'client_translated', + 'enabled', + 'type', 'type_translated', + 'folder', 'folder_translated', + 'extension_id', + 'creationDate', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Returns an object list + * + * @param DatabaseQuery $query The query + * @param int $limitstart Offset + * @param int $limit The number of records + * + * @return array + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $listOrder = $this->getState('list.ordering', 'name'); + $listDirn = $this->getState('list.direction', 'asc'); + + // Replace slashes so preg_match will work + $search = $this->getState('filter.search'); + $search = str_replace('/', ' ', $search); + $db = $this->getDatabase(); + + // Define which fields have to be processed in a custom way because of translation. + $customOrderFields = array('name', 'client_translated', 'type_translated', 'folder_translated', 'creationDate'); + + // Process searching, ordering and pagination for fields that need to be translated. + if (in_array($listOrder, $customOrderFields) || (!empty($search) && stripos($search, 'id:') !== 0)) { + // Get results from database and translate them. + $db->setQuery($query); + $result = $db->loadObjectList(); + $this->translate($result); + + // Process searching. + if (!empty($search) && stripos($search, 'id:') !== 0) { + $escapedSearchString = $this->refineSearchStringToRegex($search, '/'); + + // By default search only the extension name field. + $searchFields = array('name'); + + // If in update sites view search also in the update site name field. + if ($this instanceof UpdatesitesModel) { + $searchFields[] = 'update_site_name'; + } + + foreach ($result as $i => $item) { + // Check if search string exists in any of the fields to be searched. + $found = 0; + + foreach ($searchFields as $key => $field) { + if (!$found && preg_match('/' . $escapedSearchString . '/i', $item->{$field})) { + $found = 1; + } + } + + // If search string was not found in any of the fields searched remove it from results array. + if (!$found) { + unset($result[$i]); + } + } + } + + // Process ordering. + // Sort array object by selected ordering and selected direction. Sort is case insensitive and using locale sorting. + $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, false, true); + + // Process pagination. + $total = count($result); + $this->cache[$this->getStoreId('getTotal')] = $total; + + if ($total <= $limitstart) { + $limitstart = 0; + $this->setState('list.limitstart', 0); + } + + return array_slice($result, $limitstart, $limit ?: null); + } + + // Process searching, ordering and pagination for regular database fields. + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + $result = parent::_getList($query, $limitstart, $limit); + $this->translate($result); + + return $result; + } + + /** + * Translate a list of objects + * + * @param array $items The array of objects + * + * @return array The array of translated objects + */ + protected function translate(&$items) + { + $lang = Factory::getLanguage(); + + foreach ($items as &$item) { + if (strlen($item->manifest_cache) && $data = json_decode($item->manifest_cache)) { + foreach ($data as $key => $value) { + if ($key == 'type') { + // Ignore the type field + continue; + } + + $item->$key = $value; + } + } + + $item->author_info = @$item->authorEmail . '
' . @$item->authorUrl; + $item->client = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); + $item->client_translated = $item->client; + $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); + $item->folder_translated = @$item->folder ? $item->folder : Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); + + $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; + + switch ($item->type) { + case 'component': + $extension = $item->element; + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); + break; + case 'file': + $extension = 'files_' . $item->element; + $lang->load("$extension.sys", JPATH_SITE); + break; + case 'library': + $parts = explode('/', $item->element); + $vendor = (isset($parts[1]) ? $parts[0] : null); + $extension = 'lib_' . ($vendor ? implode('_', $parts) : $item->element); + + if (!$lang->load("$extension.sys", $path)) { + $source = $path . '/libraries/' . ($vendor ? $vendor . '/' . $parts[1] : $item->element); + $lang->load("$extension.sys", $source); + } + break; + case 'module': + $extension = $item->element; + $source = $path . '/modules/' . $extension; + $lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source); + break; + case 'plugin': + $extension = 'plg_' . $item->folder . '_' . $item->element; + $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); + break; + case 'template': + $extension = 'tpl_' . $item->element; + $source = $path . '/templates/' . $item->element; + $lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source); + break; + case 'package': + default: + $extension = $item->element; + $lang->load("$extension.sys", JPATH_SITE); + break; + } + + // Translate the extension name if possible + $item->name = Text::_($item->name); + + settype($item->description, 'string'); + + if (!in_array($item->type, array('language'))) { + $item->description = Text::_($item->description); + } + } + } } diff --git a/code/administrator/components/com_installer/src/Model/LanguagesModel.php b/code/administrator/components/com_installer/src/Model/LanguagesModel.php index 2e3d7b11..d6252a93 100644 --- a/code/administrator/components/com_installer/src/Model/LanguagesModel.php +++ b/code/administrator/components/com_installer/src/Model/LanguagesModel.php @@ -1,4 +1,5 @@ getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('us.location')) - ->from($db->quoteName('#__extensions', 'e')) - ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) - ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_en-GB')) - ->where($db->quoteName('e.client_id') . ' = 0') - ->join( - 'LEFT', $db->quoteName('#__update_sites_extensions', 'use') - . ' ON ' . $db->quoteName('use.extension_id') . ' = ' . $db->quoteName('e.extension_id') - ) - ->join( - 'LEFT', $db->quoteName('#__update_sites', 'us') - . ' ON ' . $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id') - ); - - return $db->setQuery($query)->loadResult(); - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.7.0 - */ - public function getItems() - { - // Get a storage key. - $store = $this->getStoreId(); - - // Try to load the data from internal storage. - if (isset($this->cache[$store])) - { - return $this->cache[$store]; - } - - try - { - // Load the list items and add the items to the internal cache. - $this->cache[$store] = $this->getLanguages(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $this->cache[$store]; - } - - /** - * Gets an array of objects from the updatesite. - * - * @return object[] An array of results. - * - * @since 3.0 - * @throws \RuntimeException - */ - protected function getLanguages() - { - $updateSite = $this->getUpdateSite(); - - // Check whether the updateserver is found - if (empty($updateSite)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNING_NO_LANGUAGES_UPDATESERVER'), 'warning'); - - return; - } - - try - { - $response = HttpFactory::getHttp()->get($updateSite); - } - catch (\RuntimeException $e) - { - $response = null; - } - - if ($response === null || $response->code !== 200) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_CONNECT_TO_UPDATESERVER', $updateSite), 'error'); - - return; - } - - $updateSiteXML = simplexml_load_string($response->body); - $languages = array(); - $search = strtolower($this->getState('filter.search')); - - foreach ($updateSiteXML->extension as $extension) - { - $language = new \stdClass; - - foreach ($extension->attributes() as $key => $value) - { - $language->$key = (string) $value; - } - - if ($search) - { - if (strpos(strtolower($language->name), $search) === false - && strpos(strtolower($language->element), $search) === false) - { - continue; - } - } - - $languages[$language->name] = $language; - } - - // Workaround for php 5.3 - $that = $this; - - // Sort the array by value of subarray - usort( - $languages, - function ($a, $b) use ($that) - { - $ordering = $that->getState('list.ordering'); - - if (strtolower($that->getState('list.direction')) === 'asc') - { - return StringHelper::strcmp($a->$ordering, $b->$ordering); - } - else - { - return StringHelper::strcmp($b->$ordering, $a->$ordering); - } - } - ); - - // Count the non-paginated list - $this->languageCount = count($languages); - $limit = ($this->getState('list.limit') > 0) ? $this->getState('list.limit') : $this->languageCount; - - return array_slice($languages, $this->getStart(), $limit); - } - - /** - * Returns a record count for the updatesite. - * - * @param \Joomla\Database\DatabaseQuery|string $query The query. - * - * @return integer Number of rows for query. - * - * @since 3.7.0 - */ - protected function _getListCount($query) - { - return $this->languageCount; - } - - /** - * Method to get a store id based on model configuration state. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 2.5.7 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering list order - * @param string $direction direction in the list - * - * @return void - * - * @since 2.5.7 - */ - protected function populateState($ordering = 'name', $direction = 'asc') - { - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - - $this->setState('extension_message', Factory::getApplication()->getUserState('com_installer.extension_message')); - - parent::populateState($ordering, $direction); - } - - /** - * Method to compare two languages in order to sort them. - * - * @param object $lang1 The first language. - * @param object $lang2 The second language. - * - * @return integer - * - * @since 3.7.0 - */ - protected function compareLanguages($lang1, $lang2) - { - return strcmp($lang1->name, $lang2->name); - } + /** + * Language count + * + * @var integer + * @since 3.7.0 + */ + private $languageCount; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', + 'element', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Get the Update Site + * + * @since 3.7.0 + * + * @return string The URL of the Accredited Languagepack Updatesite XML + */ + private function getUpdateSite() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('us.location')) + ->from($db->quoteName('#__extensions', 'e')) + ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_en-GB')) + ->where($db->quoteName('e.client_id') . ' = 0') + ->join( + 'LEFT', + $db->quoteName('#__update_sites_extensions', 'use') + . ' ON ' . $db->quoteName('use.extension_id') . ' = ' . $db->quoteName('e.extension_id') + ) + ->join( + 'LEFT', + $db->quoteName('#__update_sites', 'us') + . ' ON ' . $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id') + ); + + return $db->setQuery($query)->loadResult(); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.7.0 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId(); + + // Try to load the data from internal storage. + if (isset($this->cache[$store])) { + return $this->cache[$store]; + } + + try { + // Load the list items and add the items to the internal cache. + $this->cache[$store] = $this->getLanguages(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $this->cache[$store]; + } + + /** + * Gets an array of objects from the updatesite. + * + * @return object[] An array of results. + * + * @since 3.0 + * @throws \RuntimeException + */ + protected function getLanguages() + { + $updateSite = $this->getUpdateSite(); + + // Check whether the updateserver is found + if (empty($updateSite)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNING_NO_LANGUAGES_UPDATESERVER'), 'warning'); + + return; + } + + try { + $response = HttpFactory::getHttp()->get($updateSite); + } catch (\RuntimeException $e) { + $response = null; + } + + if ($response === null || $response->code !== 200) { + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_CONNECT_TO_UPDATESERVER', $updateSite), 'error'); + + return; + } + + $updateSiteXML = simplexml_load_string($response->body); + + if (!$updateSiteXML) { + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_RETRIEVE_XML', $updateSite), 'error'); + + return; + } + + $languages = array(); + $search = strtolower($this->getState('filter.search')); + + foreach ($updateSiteXML->extension as $extension) { + $language = new \stdClass(); + + foreach ($extension->attributes() as $key => $value) { + $language->$key = (string) $value; + } + + if ($search) { + if ( + strpos(strtolower($language->name), $search) === false + && strpos(strtolower($language->element), $search) === false + ) { + continue; + } + } + + $languages[$language->name] = $language; + } + + // Workaround for php 5.3 + $that = $this; + + // Sort the array by value of subarray + usort( + $languages, + function ($a, $b) use ($that) { + $ordering = $that->getState('list.ordering'); + + if (strtolower($that->getState('list.direction')) === 'asc') { + return StringHelper::strcmp($a->$ordering, $b->$ordering); + } else { + return StringHelper::strcmp($b->$ordering, $a->$ordering); + } + } + ); + + // Count the non-paginated list + $this->languageCount = count($languages); + $limit = ($this->getState('list.limit') > 0) ? $this->getState('list.limit') : $this->languageCount; + + return array_slice($languages, $this->getStart(), $limit); + } + + /** + * Returns a record count for the updatesite. + * + * @param \Joomla\Database\DatabaseQuery|string $query The query. + * + * @return integer Number of rows for query. + * + * @since 3.7.0 + */ + protected function _getListCount($query) + { + return $this->languageCount; + } + + /** + * Method to get a store id based on model configuration state. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 2.5.7 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering list order + * @param string $direction direction in the list + * + * @return void + * + * @since 2.5.7 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + + $this->setState('extension_message', Factory::getApplication()->getUserState('com_installer.extension_message')); + + parent::populateState($ordering, $direction); + } + + /** + * Method to compare two languages in order to sort them. + * + * @param object $lang1 The first language. + * @param object $lang2 The second language. + * + * @return integer + * + * @since 3.7.0 + */ + protected function compareLanguages($lang1, $lang2) + { + return strcmp($lang1->name, $lang2->name); + } } diff --git a/code/administrator/components/com_installer/src/Model/ManageModel.php b/code/administrator/components/com_installer/src/Model/ManageModel.php index 7d43ecc2..b5e25a52 100644 --- a/code/administrator/components/com_installer/src/Model/ManageModel.php +++ b/code/administrator/components/com_installer/src/Model/ManageModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.status', $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'string')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - $this->setState('filter.core', $this->getUserStateFromRequest($this->context . '.filter.core', 'filter_core', '', 'string')); - - $this->setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState($ordering, $direction); - } - - /** - * Enable/Disable an extension. - * - * @param array $eid Extension ids to un/publish - * @param int $value Publish value - * - * @return boolean True on success - * - * @throws \Exception - * - * @since 1.5 - */ - public function publish(&$eid = array(), $value = 1) - { - if (!Factory::getUser()->authorise('core.edit.state', 'com_installer')) - { - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - - return false; - } - - $result = true; - - /* - * Ensure eid is an array of extension ids - * @todo: If it isn't an array do we want to set an error and fail? - */ - if (!is_array($eid)) - { - $eid = array($eid); - } - - // Get a table object for the extension type - $table = new Extension($this->getDbo()); - - // Enable the extension in the table and store it in the database - foreach ($eid as $i => $id) - { - $table->load($id); - - if ($table->type == 'template') - { - $style = new StyleTable($this->getDbo()); - - if ($style->load(array('template' => $table->element, 'client_id' => $table->client_id, 'home' => 1))) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_DEFAULT_TEMPLATE_NOT_PERMITTED'), 'notice'); - unset($eid[$i]); - continue; - } - - // Parent template cannot be disabled if there are children - if ($style->load(['parent' => $table->element, 'client_id' => $table->client_id])) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_PARENT_TEMPLATE_NOT_PERMITTED'), 'notice'); - unset($eid[$i]); - continue; - } - } - - if ($table->protected == 1) - { - $result = false; - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - else - { - $table->enabled = $value; - } - - $context = $this->option . '.' . $this->name; - - PluginHelper::importPlugin('extension'); - Factory::getApplication()->triggerEvent('onExtensionChangeState', array($context, $eid, $value)); - - if (!$table->store()) - { - $this->setError($table->getError()); - $result = false; - } - } - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('mod_menu'); - - return $result; - } - - /** - * Refreshes the cached manifest information for an extension. - * - * @param int|int[] $eid extension identifier (key in #__extensions) - * - * @return boolean result of refresh - * - * @since 1.6 - */ - public function refresh($eid) - { - if (!is_array($eid)) - { - $eid = array($eid => 0); - } - - // Get an installer object for the extension type - $installer = Installer::getInstance(); - $result = 0; - - // Uninstall the chosen extensions - foreach ($eid as $id) - { - $result |= $installer->refreshManifestCache($id); - } - - return $result; - } - - /** - * Remove (uninstall) an extension - * - * @param array $eid An array of identifiers - * - * @return boolean True on success - * - * @throws \Exception - * - * @since 1.5 - */ - public function remove($eid = array()) - { - if (!Factory::getUser()->authorise('core.delete', 'com_installer')) - { - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - - return false; - } - - /* - * Ensure eid is an array of extension ids in the form id => client_id - * @todo: If it isn't an array do we want to set an error and fail? - */ - if (!is_array($eid)) - { - $eid = array($eid => 0); - } - - // Get an installer object for the extension type - $installer = Installer::getInstance(); - $row = new \Joomla\CMS\Table\Extension($this->getDbo()); - - // Uninstall the chosen extensions - $msgs = array(); - $result = false; - - foreach ($eid as $id) - { - $id = trim($id); - $row->load($id); - $result = false; - - // Do not allow to uninstall locked extensions. - if ((int) $row->locked === 1) - { - $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $id); - - continue; - } - - $langstring = 'COM_INSTALLER_TYPE_TYPE_' . strtoupper($row->type); - $rowtype = Text::_($langstring); - - if (strpos($rowtype, $langstring) !== false) - { - $rowtype = $row->type; - } - - if ($row->type) - { - $result = $installer->uninstall($row->type, $id); - - // Build an array of extensions that failed to uninstall - if ($result === false) - { - // There was an error in uninstalling the package - $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype); - - continue; - } - - // Package uninstalled successfully - $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_SUCCESS', $rowtype); - $result = true; - - continue; - } - - // There was an error in uninstalling the package - $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype); - } - - $msg = implode('
', $msgs); - $app = Factory::getApplication(); - $app->enqueueMessage($msg); - $this->setState('action', 'remove'); - $this->setState('name', $installer->get('name')); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('com_plugins'); - $this->cleanCache('mod_menu'); - - return $result; - } - - /** - * Method to get the database query - * - * @return DatabaseQuery The database query - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->select('2*protected+(1-protected)*enabled AS status') - ->from('#__extensions') - ->where('state = 0'); - - // Process select filters. - $status = $this->getState('filter.status', ''); - $type = $this->getState('filter.type'); - $clientId = $this->getState('filter.client_id', ''); - $folder = $this->getState('filter.folder'); - $core = $this->getState('filter.core', ''); - - if ($status !== '') - { - if ($status === '2') - { - $query->where('protected = 1'); - } - elseif ($status === '3') - { - $query->where('protected = 0'); - } - else - { - $status = (int) $status; - $query->where($db->quoteName('protected') . ' = 0') - ->where($db->quoteName('enabled') . ' = :status') - ->bind(':status', $status, ParameterType::INTEGER); - } - } - - if ($type) - { - $query->where($db->quoteName('type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId !== '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($folder) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('folder') . ' = :folder') - ->bind(':folder', $folder); - } - - // Filter by core extensions. - if ($core === '1' || $core === '0') - { - $coreExtensionIds = ExtensionHelper::getCoreExtensionIds(); - $method = $core === '1' ? 'whereIn' : 'whereNotIn'; - $query->$method($db->quoteName('extension_id'), $coreExtensionIds); - } - - // Process search filter (extension id). - $search = $this->getState('filter.search'); - - if (!empty($search) && stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $ids, ParameterType::INTEGER); - } - - // Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php). - - return $query; - } - - /** - * Load the changelog details for a given extension. - * - * @param integer $eid The extension ID - * @param string $source The view the changelog is for, this is used to determine which version number to show - * - * @return string The output to show in the modal. - * - * @since 4.0.0 - */ - public function loadChangelog($eid, $source) - { - // Get the changelog URL - $eid = (int) $eid; - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'extensions.element', - 'extensions.type', - 'extensions.folder', - 'extensions.changelogurl', - 'extensions.manifest_cache', - 'extensions.client_id' - ] - ) - ) - ->select($db->quoteName('updates.version', 'updateVersion')) - ->from($db->quoteName('#__extensions', 'extensions')) - ->join( - 'LEFT', - $db->quoteName('#__updates', 'updates'), - $db->quoteName('updates.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') - ) - ->where($db->quoteName('extensions.extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - $db->setQuery($query); - - $extensions = $db->loadObjectList(); - $this->translate($extensions); - $extension = array_shift($extensions); - - if (!$extension->changelogurl) - { - return ''; - } - - $changelog = new Changelog; - $changelog->setVersion($source === 'manage' ? $extension->version : $extension->updateVersion); - $changelog->loadFromXml($extension->changelogurl); - - // Read all the entries - $entries = array( - 'security' => array(), - 'fix' => array(), - 'addition' => array(), - 'change' => array(), - 'remove' => array(), - 'language' => array(), - 'note' => array() - ); - - array_walk( - $entries, - function (&$value, $name) use ($changelog) { - if ($field = $changelog->get($name)) - { - $value = $changelog->get($name)->data; - } - } - ); - - $layout = new FileLayout('joomla.installer.changelog'); - $output = $layout->render($entries); - - return $output; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'status', + 'name', + 'client_id', + 'client', 'client_translated', + 'type', 'type_translated', + 'folder', 'folder_translated', + 'package_id', + 'extension_id', + 'creationDate', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @throws \Exception + * + * @since 1.6 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.package_id', $this->getUserStateFromRequest($this->context . '.filter.package_id', 'filter_package_id', null, 'int')); + $this->setState('filter.status', $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'string')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + $this->setState('filter.core', $this->getUserStateFromRequest($this->context . '.filter.core', 'filter_core', '', 'string')); + + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState($ordering, $direction); + } + + /** + * Enable/Disable an extension. + * + * @param array $eid Extension ids to un/publish + * @param int $value Publish value + * + * @return boolean True on success + * + * @throws \Exception + * + * @since 1.5 + */ + public function publish(&$eid = array(), $value = 1) + { + if (!Factory::getUser()->authorise('core.edit.state', 'com_installer')) { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + + return false; + } + + $result = true; + + /* + * Ensure eid is an array of extension ids + * @todo: If it isn't an array do we want to set an error and fail? + */ + if (!is_array($eid)) { + $eid = array($eid); + } + + // Get a table object for the extension type + $table = new Extension($this->getDatabase()); + + // Enable the extension in the table and store it in the database + foreach ($eid as $i => $id) { + $table->load($id); + + if ($table->type == 'template') { + $style = new StyleTable($this->getDatabase()); + + if ($style->load(array('template' => $table->element, 'client_id' => $table->client_id, 'home' => 1))) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_DEFAULT_TEMPLATE_NOT_PERMITTED'), 'notice'); + unset($eid[$i]); + continue; + } + + // Parent template cannot be disabled if there are children + if ($style->load(['parent' => $table->element, 'client_id' => $table->client_id])) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_PARENT_TEMPLATE_NOT_PERMITTED'), 'notice'); + unset($eid[$i]); + continue; + } + } + + if ($table->protected == 1) { + $result = false; + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } else { + $table->enabled = $value; + } + + $context = $this->option . '.' . $this->name; + + PluginHelper::importPlugin('extension'); + Factory::getApplication()->triggerEvent('onExtensionChangeState', array($context, $eid, $value)); + + if (!$table->store()) { + $this->setError($table->getError()); + $result = false; + } + } + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('mod_menu'); + + return $result; + } + + /** + * Refreshes the cached manifest information for an extension. + * + * @param int|int[] $eid extension identifier (key in #__extensions) + * + * @return boolean result of refresh + * + * @since 1.6 + */ + public function refresh($eid) + { + if (!is_array($eid)) { + $eid = array($eid => 0); + } + + // Get an installer object for the extension type + $installer = Installer::getInstance(); + $result = 0; + + // Uninstall the chosen extensions + foreach ($eid as $id) { + $result |= $installer->refreshManifestCache($id); + } + + return $result; + } + + /** + * Remove (uninstall) an extension + * + * @param array $eid An array of identifiers + * + * @return boolean True on success + * + * @throws \Exception + * + * @since 1.5 + */ + public function remove($eid = array()) + { + if (!Factory::getUser()->authorise('core.delete', 'com_installer')) { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + + return false; + } + + /* + * Ensure eid is an array of extension ids in the form id => client_id + * @todo: If it isn't an array do we want to set an error and fail? + */ + if (!is_array($eid)) { + $eid = array($eid => 0); + } + + // Get an installer object for the extension type + $installer = Installer::getInstance(); + $row = new \Joomla\CMS\Table\Extension($this->getDatabase()); + + // Uninstall the chosen extensions + $msgs = array(); + $result = false; + + foreach ($eid as $id) { + $id = trim($id); + $row->load($id); + $result = false; + + // Do not allow to uninstall locked extensions. + if ((int) $row->locked === 1) { + $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $id); + + continue; + } + + $langstring = 'COM_INSTALLER_TYPE_TYPE_' . strtoupper($row->type); + $rowtype = Text::_($langstring); + + if (strpos($rowtype, $langstring) !== false) { + $rowtype = $row->type; + } + + if ($row->type) { + $result = $installer->uninstall($row->type, $id); + + // Build an array of extensions that failed to uninstall + if ($result === false) { + // There was an error in uninstalling the package + $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype); + + continue; + } + + // Package uninstalled successfully + $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_SUCCESS', $rowtype); + $result = true; + + continue; + } + + // There was an error in uninstalling the package + $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype); + } + + $msg = implode('
', $msgs); + $app = Factory::getApplication(); + $app->enqueueMessage($msg); + $this->setState('action', 'remove'); + $this->setState('name', $installer->get('name')); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('com_plugins'); + $this->cleanCache('mod_menu'); + + return $result; + } + + /** + * Method to get the database query + * + * @return DatabaseQuery The database query + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->select('2*protected+(1-protected)*enabled AS status') + ->from('#__extensions') + ->where('state = 0'); + + // Process select filters. + $status = $this->getState('filter.status', ''); + $type = $this->getState('filter.type'); + $clientId = $this->getState('filter.client_id', ''); + $folder = $this->getState('filter.folder'); + $core = $this->getState('filter.core', ''); + $packageId = $this->getState('filter.package_id', ''); + + if ($status !== '') { + if ($status === '2') { + $query->where('protected = 1'); + } elseif ($status === '3') { + $query->where('protected = 0'); + } else { + $status = (int) $status; + $query->where($db->quoteName('protected') . ' = 0') + ->where($db->quoteName('enabled') . ' = :status') + ->bind(':status', $status, ParameterType::INTEGER); + } + } + + if ($type) { + $query->where($db->quoteName('type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId !== '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($packageId !== '') { + $packageId = (int) $packageId; + $query->where( + '((' . $db->quoteName('package_id') . ' = :packageId1) OR ' + . '(' . $db->quoteName('extension_id') . ' = :packageId2))' + ) + ->bind([':packageId1',':packageId2'], $packageId, ParameterType::INTEGER); + } + + if ($folder) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('folder') . ' = :folder') + ->bind(':folder', $folder); + } + + // Filter by core extensions. + if ($core === '1' || $core === '0') { + $coreExtensionIds = ExtensionHelper::getCoreExtensionIds(); + $method = $core === '1' ? 'whereIn' : 'whereNotIn'; + $query->$method($db->quoteName('extension_id'), $coreExtensionIds); + } + + // Process search filter (extension id). + $search = $this->getState('filter.search'); + + if (!empty($search) && stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $ids, ParameterType::INTEGER); + } + + // Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php). + + return $query; + } + + /** + * Load the changelog details for a given extension. + * + * @param integer $eid The extension ID + * @param string $source The view the changelog is for, this is used to determine which version number to show + * + * @return string The output to show in the modal. + * + * @since 4.0.0 + */ + public function loadChangelog($eid, $source) + { + // Get the changelog URL + $eid = (int) $eid; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'extensions.element', + 'extensions.type', + 'extensions.folder', + 'extensions.changelogurl', + 'extensions.manifest_cache', + 'extensions.client_id' + ] + ) + ) + ->select($db->quoteName('updates.version', 'updateVersion')) + ->from($db->quoteName('#__extensions', 'extensions')) + ->join( + 'LEFT', + $db->quoteName('#__updates', 'updates'), + $db->quoteName('updates.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') + ) + ->where($db->quoteName('extensions.extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + $db->setQuery($query); + + $extensions = $db->loadObjectList(); + $this->translate($extensions); + $extension = array_shift($extensions); + + if (!$extension->changelogurl) { + return ''; + } + + $changelog = new Changelog(); + $changelog->setVersion($source === 'manage' ? $extension->version : $extension->updateVersion); + $changelog->loadFromXml($extension->changelogurl); + + // Read all the entries + $entries = array( + 'security' => array(), + 'fix' => array(), + 'addition' => array(), + 'change' => array(), + 'remove' => array(), + 'language' => array(), + 'note' => array() + ); + + array_walk( + $entries, + function (&$value, $name) use ($changelog) { + if ($field = $changelog->get($name)) { + $value = $changelog->get($name)->data; + } + } + ); + + $layout = new FileLayout('joomla.installer.changelog'); + $output = $layout->render($entries); + + return $output; + } } diff --git a/code/administrator/components/com_installer/src/Model/UpdateModel.php b/code/administrator/components/com_installer/src/Model/UpdateModel.php index 15e0e17c..87ed0e3c 100644 --- a/code/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/code/administrator/components/com_installer/src/Model/UpdateModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - - $app = Factory::getApplication(); - $this->setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get the database query - * - * @return \Joomla\Database\DatabaseQuery The database query - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDbo(); - - // Grab updates ignoring new installs - $query = $db->getQuery(true) - ->select('u.*') - ->select($db->quoteName('e.manifest_cache')) - ->from($db->quoteName('#__updates', 'u')) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('u.extension_id') - ) - ->where($db->quoteName('u.extension_id') . ' != 0'); - - // Process select filters. - $clientId = $this->getState('filter.client_id'); - $type = $this->getState('filter.type'); - $folder = $this->getState('filter.folder'); - $extensionId = $this->getState('filter.extension_id'); - - if ($type) - { - $query->where($db->quoteName('u.type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId != '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('u.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($folder != '' && in_array($type, array('plugin', 'library', ''))) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('u.folder') . ' = :folder') - ->bind(':folder', $folder); - } - - if ($extensionId) - { - $extensionId = (int) $extensionId; - $query->where($db->quoteName('u.extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - } - else - { - $eid = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - $query->where($db->quoteName('u.extension_id') . ' != 0') - ->where($db->quoteName('u.extension_id') . ' != :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - } - - // Process search filter. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'eid:') !== false) - { - $sid = (int) substr($search, 4); - $query->where($db->quoteName('u.extension_id') . ' = :sid') - ->bind(':sid', $sid, ParameterType::INTEGER); - } - else - { - if (stripos($search, 'uid:') !== false) - { - $suid = (int) substr($search, 4); - $query->where($db->quoteName('u.update_site_id') . ' = :suid') - ->bind(':suid', $suid, ParameterType::INTEGER); - } - elseif (stripos($search, 'id:') !== false) - { - $uid = (int) substr($search, 3); - $query->where($db->quoteName('u.update_id') . ' = :uid') - ->bind(':uid', $uid, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where($db->quoteName('u.name') . ' LIKE :search') - ->bind(':search', $search); - } - } - } - - return $query; - } - - /** - * Translate a list of objects - * - * @param array $items The array of objects - * - * @return array The array of translated objects - * - * @since 3.5 - */ - protected function translate(&$items) - { - foreach ($items as &$item) - { - $item->client_translated = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); - $manifest = json_decode($item->manifest_cache); - $item->current_version = $manifest->version ?? Text::_('JLIB_UNKNOWN'); - $item->description = $item->description !== '' ? $item->description : Text::_('COM_INSTALLER_MSG_UPDATE_NODESC'); - $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); - $item->folder_translated = $item->folder ?: Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); - $item->install_type = $item->extension_id ? Text::_('COM_INSTALLER_MSG_UPDATE_UPDATE') : Text::_('COM_INSTALLER_NEW_INSTALL'); - } - - return $items; - } - - /** - * Returns an object list - * - * @param DatabaseQuery $query The query - * @param int $limitstart Offset - * @param int $limit The number of records - * - * @return array - * - * @since 3.5 - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $db = $this->getDbo(); - $listOrder = $this->getState('list.ordering', 'u.name'); - $listDirn = $this->getState('list.direction', 'asc'); - - // Process ordering. - if (in_array($listOrder, array('client_translated', 'folder_translated', 'type_translated'))) - { - $db->setQuery($query); - $result = $db->loadObjectList(); - $this->translate($result); - $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); - $total = count($result); - - if ($total < $limitstart) - { - $limitstart = 0; - $this->setState('list.start', 0); - } - - return array_slice($result, $limitstart, $limit ?: null); - } - else - { - $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); - - $result = parent::_getList($query, $limitstart, $limit); - $this->translate($result); - - return $result; - } - } - - /** - * Get the count of disabled update sites - * - * @return integer - * - * @since 3.4 - */ - public function getDisabledUpdateSites() - { - $db = $this->getDbo(); - - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__update_sites')) - ->where($db->quoteName('enabled') . ' = 0'); - - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Finds updates for an extension. - * - * @param int $eid Extension identifier to look for - * @param int $cacheTimeout Cache timeout - * @param int $minimumStability Minimum stability for updates {@see Updater} (0=dev, 1=alpha, 2=beta, 3=rc, 4=stable) - * - * @return boolean Result - * - * @since 1.6 - */ - public function findUpdates($eid = 0, $cacheTimeout = 0, $minimumStability = Updater::STABILITY_STABLE) - { - Updater::getInstance()->findUpdates($eid, $cacheTimeout, $minimumStability); - - return true; - } - - /** - * Removes all of the updates from the table. - * - * @return boolean result of operation - * - * @since 1.6 - */ - public function purge() - { - $db = $this->getDbo(); - - try - { - $db->truncateTable('#__updates'); - } - catch (ExecutionFailureException $e) - { - $this->_message = Text::_('JLIB_INSTALLER_FAILED_TO_PURGE_UPDATES'); - - return false; - } - - // Reset the last update check timestamp - $query = $db->getQuery(true) - ->update($db->quoteName('#__update_sites')) - ->set($db->quoteName('last_check_timestamp') . ' = ' . $db->quote(0)); - $db->setQuery($query); - $db->execute(); - - // Clear the administrator cache - $this->cleanCache('_system'); - - $this->_message = Text::_('JLIB_INSTALLER_PURGED_UPDATES'); - - return true; - } - - /** - * Update function. - * - * Sets the "result" state with the result of the operation. - * - * @param int[] $uids List of updates to apply - * @param int $minimumStability The minimum allowed stability for installed updates {@see Updater} - * - * @return void - * - * @since 1.6 - */ - public function update($uids, $minimumStability = Updater::STABILITY_STABLE) - { - $result = true; - - foreach ($uids as $uid) - { - $update = new Update; - $instance = new \Joomla\CMS\Table\Update($this->getDbo()); - - if (!$instance->load($uid)) - { - // Update no longer available, maybe already updated by a package. - continue; - } - - $update->loadFromXml($instance->detailsurl, $minimumStability); - - // Find and use extra_query from update_site if available - $updateSiteInstance = new \Joomla\CMS\Table\UpdateSite($this->getDbo()); - $updateSiteInstance->load($instance->update_site_id); - - if ($updateSiteInstance->extra_query) - { - $update->set('extra_query', $updateSiteInstance->extra_query); - } - - $this->preparePreUpdate($update, $instance); - - // Install sets state and enqueues messages - $res = $this->install($update); - - if ($res) - { - $instance->delete($uid); - } - - $result = $res & $result; - } - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('com_plugins'); - $this->cleanCache('mod_menu'); - - // Set the final state - $this->setState('result', $result); - } - - /** - * Handles the actual update installation. - * - * @param Update $update An update definition - * - * @return boolean Result of install - * - * @since 1.6 - */ - private function install($update) - { - // Load overrides plugin. - PluginHelper::importPlugin('installer'); - - $app = Factory::getApplication(); - - if (!isset($update->get('downloadurl')->_data)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_INVALID_EXTENSION_UPDATE'), 'error'); - - return false; - } - - $url = trim($update->downloadurl->_data); - $sources = $update->get('downloadSources', array()); - - if ($extra_query = $update->get('extra_query')) - { - $url .= (strpos($url, '?') === false) ? '?' : '&'; - $url .= $extra_query; - } - - $mirror = 0; - - while (!($p_file = InstallerHelper::downloadPackage($url)) && isset($sources[$mirror])) - { - $name = $sources[$mirror]; - $url = trim($name->url); - - if ($extra_query) - { - $url .= (strpos($url, '?') === false) ? '?' : '&'; - $url .= $extra_query; - } - - $mirror++; - } - - // Was the package downloaded? - if (!$p_file) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); - - return false; - } - - $config = $app->getConfig(); - $tmp_dest = $config->get('tmp_path'); - - // Unpack the downloaded package file - $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file); - - if (empty($package)) - { - $app->enqueueMessage(Text::sprintf('COM_INSTALLER_UNPACK_ERROR', $p_file), 'error'); - - return false; - } - - // Get an installer instance - $installer = Installer::getInstance(); - $update->set('type', $package['type']); - - // Check the package - $check = InstallerHelper::isChecksumValid($package['packagefile'], $update); - - if ($check === InstallerHelper::HASH_NOT_VALIDATED) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WRONG'), 'error'); - - return false; - } - - if ($check === InstallerHelper::HASH_NOT_PROVIDED) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WARNING'), 'warning'); - } - - // Install the package - if (!$installer->update($package['dir'])) - { - // There was an error updating the package - $app->enqueueMessage( - Text::sprintf('COM_INSTALLER_MSG_UPDATE_ERROR', - Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) - ), 'error' - ); - $result = false; - } - else - { - // Package updated successfully - $app->enqueueMessage( - Text::sprintf('COM_INSTALLER_MSG_UPDATE_SUCCESS', - Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) - ), 'success' - ); - $result = true; - } - - // Quick change - $this->type = $package['type']; - - // @todo: Reconfigure this code when you have more battery life left - $this->setState('name', $installer->get('name')); - $this->setState('result', $result); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - - // Cleanup the install files - if (!is_file($package['packagefile'])) - { - $package['packagefile'] = $config->get('tmp_path') . '/' . $package['packagefile']; - } - - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - - return $result; - } - - /** - * Method to get the row form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 2.5.2 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - Form::addFormPath(JPATH_COMPONENT . '/models/forms'); - Form::addFieldPath(JPATH_COMPONENT . '/models/fields'); - $form = Form::getInstance('com_installer.update', 'update', array('load_data' => $loadData)); - - // Check for an error. - if ($form == false) - { - $this->setError($form->getMessage()); - - return false; - } - - // Check the session for previously entered form data. - $data = $this->loadFormData(); - - // Bind the form data if present. - if (!empty($data)) - { - $form->bind($data); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 2.5.2 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState($this->context, array()); - - return $data; - } - - /** - * Method to add parameters to the update - * - * @param Update $update An update definition - * @param \Joomla\CMS\Table\Update $table The update instance from the database - * - * @return void - * - * @since 3.7.0 - */ - protected function preparePreUpdate($update, $table) - { - switch ($table->type) - { - // Components could have a helper which adds additional data - case 'component': - $ename = str_replace('com_', '', $table->element); - $fname = $ename . '.php'; - $cname = ucfirst($ename) . 'Helper'; - - $path = JPATH_ADMINISTRATOR . '/components/' . $table->element . '/helpers/' . $fname; - - if (File::exists($path)) - { - require_once $path; - - if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) - { - call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); - } - } - - break; - - // Modules could have a helper which adds additional data - case 'module': - $cname = str_replace('_', '', $table->element) . 'Helper'; - $path = ($table->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE) . '/modules/' . $table->element . '/helper.php'; - - if (File::exists($path)) - { - require_once $path; - - if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) - { - call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); - } - } - - break; - - // If we have a plugin, we can use the plugin trigger "onInstallerBeforePackageDownload" - // But we should make sure, that our plugin is loaded, so we don't need a second "installer" plugin - case 'plugin': - $cname = str_replace('plg_', '', $table->element); - PluginHelper::importPlugin($table->folder, $cname); - break; - } - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $query->where($this->_db->quoteName('extension_id') . ' != 0'); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', 'u.name', + 'client_id', 'u.client_id', 'client_translated', + 'type', 'u.type', 'type_translated', + 'folder', 'u.folder', 'folder_translated', + 'extension_id', 'u.extension_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'u.name', $direction = 'asc') + { + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + + $app = Factory::getApplication(); + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get the database query + * + * @return \Joomla\Database\DatabaseQuery The database query + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + + // Grab updates ignoring new installs + $query = $db->getQuery(true) + ->select('u.*') + ->select($db->quoteName('e.manifest_cache')) + ->from($db->quoteName('#__updates', 'u')) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('u.extension_id') + ) + ->where($db->quoteName('u.extension_id') . ' != 0'); + + // Process select filters. + $clientId = $this->getState('filter.client_id'); + $type = $this->getState('filter.type'); + $folder = $this->getState('filter.folder'); + $extensionId = $this->getState('filter.extension_id'); + + if ($type) { + $query->where($db->quoteName('u.type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId != '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('u.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($folder != '' && in_array($type, array('plugin', 'library', ''))) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('u.folder') . ' = :folder') + ->bind(':folder', $folder); + } + + if ($extensionId) { + $extensionId = (int) $extensionId; + $query->where($db->quoteName('u.extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + } else { + $eid = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + $query->where($db->quoteName('u.extension_id') . ' != 0') + ->where($db->quoteName('u.extension_id') . ' != :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + } + + // Process search filter. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'eid:') !== false) { + $sid = (int) substr($search, 4); + $query->where($db->quoteName('u.extension_id') . ' = :sid') + ->bind(':sid', $sid, ParameterType::INTEGER); + } else { + if (stripos($search, 'uid:') !== false) { + $suid = (int) substr($search, 4); + $query->where($db->quoteName('u.update_site_id') . ' = :suid') + ->bind(':suid', $suid, ParameterType::INTEGER); + } elseif (stripos($search, 'id:') !== false) { + $uid = (int) substr($search, 3); + $query->where($db->quoteName('u.update_id') . ' = :uid') + ->bind(':uid', $uid, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where($db->quoteName('u.name') . ' LIKE :search') + ->bind(':search', $search); + } + } + } + + return $query; + } + + /** + * Translate a list of objects + * + * @param array $items The array of objects + * + * @return array The array of translated objects + * + * @since 3.5 + */ + protected function translate(&$items) + { + foreach ($items as &$item) { + $item->client_translated = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); + $manifest = json_decode($item->manifest_cache); + $item->current_version = $manifest->version ?? Text::_('JLIB_UNKNOWN'); + $item->description = $item->description !== '' ? $item->description : Text::_('COM_INSTALLER_MSG_UPDATE_NODESC'); + $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); + $item->folder_translated = $item->folder ?: Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); + $item->install_type = $item->extension_id ? Text::_('COM_INSTALLER_MSG_UPDATE_UPDATE') : Text::_('COM_INSTALLER_NEW_INSTALL'); + } + + return $items; + } + + /** + * Returns an object list + * + * @param DatabaseQuery $query The query + * @param int $limitstart Offset + * @param int $limit The number of records + * + * @return array + * + * @since 3.5 + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $db = $this->getDatabase(); + $listOrder = $this->getState('list.ordering', 'u.name'); + $listDirn = $this->getState('list.direction', 'asc'); + + // Process ordering. + if (in_array($listOrder, array('client_translated', 'folder_translated', 'type_translated'))) { + $db->setQuery($query); + $result = $db->loadObjectList(); + $this->translate($result); + $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); + $total = count($result); + + if ($total < $limitstart) { + $limitstart = 0; + $this->setState('list.start', 0); + } + + return array_slice($result, $limitstart, $limit ?: null); + } else { + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + + $result = parent::_getList($query, $limitstart, $limit); + $this->translate($result); + + return $result; + } + } + + /** + * Get the count of disabled update sites + * + * @return integer + * + * @since 3.4 + */ + public function getDisabledUpdateSites() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__update_sites')) + ->where($db->quoteName('enabled') . ' = 0'); + + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Finds updates for an extension. + * + * @param int $eid Extension identifier to look for + * @param int $cacheTimeout Cache timeout + * @param int $minimumStability Minimum stability for updates {@see Updater} (0=dev, 1=alpha, 2=beta, 3=rc, 4=stable) + * + * @return boolean Result + * + * @since 1.6 + */ + public function findUpdates($eid = 0, $cacheTimeout = 0, $minimumStability = Updater::STABILITY_STABLE) + { + Updater::getInstance()->findUpdates($eid, $cacheTimeout, $minimumStability); + + return true; + } + + /** + * Removes all of the updates from the table. + * + * @return boolean result of operation + * + * @since 1.6 + */ + public function purge() + { + $db = $this->getDatabase(); + + try { + $db->truncateTable('#__updates'); + } catch (ExecutionFailureException $e) { + $this->_message = Text::_('JLIB_INSTALLER_FAILED_TO_PURGE_UPDATES'); + + return false; + } + + // Reset the last update check timestamp + $query = $db->getQuery(true) + ->update($db->quoteName('#__update_sites')) + ->set($db->quoteName('last_check_timestamp') . ' = ' . $db->quote(0)); + $db->setQuery($query); + $db->execute(); + + // Clear the administrator cache + $this->cleanCache('_system'); + + $this->_message = Text::_('JLIB_INSTALLER_PURGED_UPDATES'); + + return true; + } + + /** + * Update function. + * + * Sets the "result" state with the result of the operation. + * + * @param int[] $uids List of updates to apply + * @param int $minimumStability The minimum allowed stability for installed updates {@see Updater} + * + * @return void + * + * @since 1.6 + */ + public function update($uids, $minimumStability = Updater::STABILITY_STABLE) + { + $result = true; + + foreach ($uids as $uid) { + $update = new Update(); + $instance = new \Joomla\CMS\Table\Update($this->getDatabase()); + + if (!$instance->load($uid)) { + // Update no longer available, maybe already updated by a package. + continue; + } + + $update->loadFromXml($instance->detailsurl, $minimumStability); + + // Find and use extra_query from update_site if available + $updateSiteInstance = new \Joomla\CMS\Table\UpdateSite($this->getDatabase()); + $updateSiteInstance->load($instance->update_site_id); + + if ($updateSiteInstance->extra_query) { + $update->set('extra_query', $updateSiteInstance->extra_query); + } + + $this->preparePreUpdate($update, $instance); + + // Install sets state and enqueues messages + $res = $this->install($update); + + if ($res) { + $instance->delete($uid); + } + + $result = $res & $result; + } + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('com_plugins'); + $this->cleanCache('mod_menu'); + + // Set the final state + $this->setState('result', $result); + } + + /** + * Handles the actual update installation. + * + * @param Update $update An update definition + * + * @return boolean Result of install + * + * @since 1.6 + */ + private function install($update) + { + // Load overrides plugin. + PluginHelper::importPlugin('installer'); + + $app = Factory::getApplication(); + + if (!isset($update->get('downloadurl')->_data)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_INVALID_EXTENSION_UPDATE'), 'error'); + + return false; + } + + $url = trim($update->downloadurl->_data); + $sources = $update->get('downloadSources', array()); + + if ($extra_query = $update->get('extra_query')) { + $url .= (strpos($url, '?') === false) ? '?' : '&'; + $url .= $extra_query; + } + + $mirror = 0; + + while (!($p_file = InstallerHelper::downloadPackage($url)) && isset($sources[$mirror])) { + $name = $sources[$mirror]; + $url = trim($name->url); + + if ($extra_query) { + $url .= (strpos($url, '?') === false) ? '?' : '&'; + $url .= $extra_query; + } + + $mirror++; + } + + // Was the package downloaded? + if (!$p_file) { + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); + + return false; + } + + $config = $app->getConfig(); + $tmp_dest = $config->get('tmp_path'); + + // Unpack the downloaded package file + $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file); + + if (empty($package)) { + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_UNPACK_ERROR', $p_file), 'error'); + + return false; + } + + // Get an installer instance + $installer = Installer::getInstance(); + $update->set('type', $package['type']); + + // Check the package + $check = InstallerHelper::isChecksumValid($package['packagefile'], $update); + + if ($check === InstallerHelper::HASH_NOT_VALIDATED) { + $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WRONG'), 'error'); + + return false; + } + + if ($check === InstallerHelper::HASH_NOT_PROVIDED) { + $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WARNING'), 'warning'); + } + + // Install the package + if (!$installer->update($package['dir'])) { + // There was an error updating the package + $app->enqueueMessage( + Text::sprintf( + 'COM_INSTALLER_MSG_UPDATE_ERROR', + Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) + ), + 'error' + ); + $result = false; + } else { + // Package updated successfully + $app->enqueueMessage( + Text::sprintf( + 'COM_INSTALLER_MSG_UPDATE_SUCCESS', + Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) + ), + 'success' + ); + $result = true; + } + + // Quick change + $this->type = $package['type']; + + // @todo: Reconfigure this code when you have more battery life left + $this->setState('name', $installer->get('name')); + $this->setState('result', $result); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + + // Cleanup the install files + if (!is_file($package['packagefile'])) { + $package['packagefile'] = $config->get('tmp_path') . '/' . $package['packagefile']; + } + + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + + return $result; + } + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 2.5.2 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + Form::addFormPath(JPATH_COMPONENT . '/models/forms'); + Form::addFieldPath(JPATH_COMPONENT . '/models/fields'); + $form = Form::getInstance('com_installer.update', 'update', array('load_data' => $loadData)); + + // Check for an error. + if ($form == false) { + $this->setError($form->getMessage()); + + return false; + } + + // Check the session for previously entered form data. + $data = $this->loadFormData(); + + // Bind the form data if present. + if (!empty($data)) { + $form->bind($data); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 2.5.2 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState($this->context, array()); + + return $data; + } + + /** + * Method to add parameters to the update + * + * @param Update $update An update definition + * @param \Joomla\CMS\Table\Update $table The update instance from the database + * + * @return void + * + * @since 3.7.0 + */ + protected function preparePreUpdate($update, $table) + { + switch ($table->type) { + // Components could have a helper which adds additional data + case 'component': + $ename = str_replace('com_', '', $table->element); + $fname = $ename . '.php'; + $cname = ucfirst($ename) . 'Helper'; + + $path = JPATH_ADMINISTRATOR . '/components/' . $table->element . '/helpers/' . $fname; + + if (File::exists($path)) { + require_once $path; + + if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) { + call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); + } + } + + break; + + // Modules could have a helper which adds additional data + case 'module': + $cname = str_replace('_', '', $table->element) . 'Helper'; + $path = ($table->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE) . '/modules/' . $table->element . '/helper.php'; + + if (File::exists($path)) { + require_once $path; + + if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) { + call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); + } + } + + break; + + // If we have a plugin, we can use the plugin trigger "onInstallerBeforePackageDownload" + // But we should make sure, that our plugin is loaded, so we don't need a second "installer" plugin + case 'plugin': + $cname = str_replace('plg_', '', $table->element); + PluginHelper::importPlugin($table->folder, $cname); + break; + } + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $query->where($this->getDatabase()->quoteName('extension_id') . ' != 0'); + + return $query; + } } diff --git a/code/administrator/components/com_installer/src/Model/UpdatesiteModel.php b/code/administrator/components/com_installer/src/Model/UpdatesiteModel.php index f59cc38c..a2b2fc8a 100644 --- a/code/administrator/components/com_installer/src/Model/UpdatesiteModel.php +++ b/code/administrator/components/com_installer/src/Model/UpdatesiteModel.php @@ -1,4 +1,5 @@ loadForm('com_installer.updatesite', 'updatesite', ['control' => 'jform', 'load_data' => $loadData]); - - if (empty($form)) - { - return false; - } - - return $form; - } - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - $data = $this->getItem(); - $this->preprocessData('com_installer.updatesite', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return CMSObject|boolean Object on success, false on failure. - * - * @since 4.0.0 - */ - public function getItem($pk = null) - { - $item = parent::getItem($pk); - - $db = $this->getDbo(); - $updateSiteId = (int) $item->get('update_site_id'); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'update_sites.extra_query', - 'extensions.type', - 'extensions.element', - 'extensions.folder', - 'extensions.client_id', - 'extensions.checked_out' - ] - ) - ) - ->from($db->quoteName('#__update_sites', 'update_sites')) - ->join( - 'INNER', - $db->quoteName('#__update_sites_extensions', 'update_sites_extensions'), - $db->quoteName('update_sites_extensions.update_site_id') . ' = ' . $db->quoteName('update_sites.update_site_id') - ) - ->join( - 'INNER', - $db->quoteName('#__extensions', 'extensions'), - $db->quoteName('extensions.extension_id') . ' = ' . $db->quoteName('update_sites_extensions.extension_id') - ) - ->where($db->quoteName('update_sites.update_site_id') . ' = :updatesiteid') - ->bind(':updatesiteid', $updateSiteId, ParameterType::INTEGER); - - $db->setQuery($query); - $extension = new CMSObject($db->loadAssoc()); - - $downloadKey = InstallerHelper::getDownloadKey($extension); - - $item->set('extra_query', $downloadKey['value'] ?? ''); - $item->set('downloadIdPrefix', $downloadKey['prefix'] ?? ''); - $item->set('downloadIdSuffix', $downloadKey['suffix'] ?? ''); - - return $item; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 4.0.0 - */ - public function save($data): bool - { - // Apply the extra_query. Always empty when saving a free extension's update site. - if (isset($data['extra_query'])) - { - $data['extra_query'] = $data['downloadIdPrefix'] . $data['extra_query'] . $data['downloadIdSuffix']; - } - - // Force Joomla to recheck for updates - $data['last_check_timestamp'] = 0; - - $result = parent::save($data); - - if (!$result) - { - return $result; - } - - // Delete update records forcing Joomla to fetch them again, applying the new extra_query. - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__updates')) - ->where($db->quoteName('update_site_id') . ' = :updateSiteId'); - $query->bind(':updateSiteId', $data['update_site_id'], ParameterType::INTEGER); - - try - { - $db->setQuery($query)->execute(); - } - catch (Exception $e) - { - // No problem if this fails for any reason. - } - - return true; - } + /** + * The type alias for this content type. + * + * @var string + * @since 4.0.0 + */ + public $typeAlias = 'com_installer.updatesite'; + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @throws Exception + * + * @since 4.0.0 + */ + public function getForm($data = [], $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_installer.updatesite', 'updatesite', ['control' => 'jform', 'load_data' => $loadData]); + + if (empty($form)) { + return false; + } + + return $form; + } + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + $data = $this->getItem(); + $this->preprocessData('com_installer.updatesite', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return CMSObject|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getItem($pk = null) + { + $item = parent::getItem($pk); + + $db = $this->getDatabase(); + $updateSiteId = (int) $item->get('update_site_id'); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'update_sites.extra_query', + 'extensions.type', + 'extensions.element', + 'extensions.folder', + 'extensions.client_id', + 'extensions.checked_out' + ] + ) + ) + ->from($db->quoteName('#__update_sites', 'update_sites')) + ->join( + 'INNER', + $db->quoteName('#__update_sites_extensions', 'update_sites_extensions'), + $db->quoteName('update_sites_extensions.update_site_id') . ' = ' . $db->quoteName('update_sites.update_site_id') + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'extensions'), + $db->quoteName('extensions.extension_id') . ' = ' . $db->quoteName('update_sites_extensions.extension_id') + ) + ->where($db->quoteName('update_sites.update_site_id') . ' = :updatesiteid') + ->bind(':updatesiteid', $updateSiteId, ParameterType::INTEGER); + + $db->setQuery($query); + $extension = new CMSObject($db->loadAssoc()); + + $downloadKey = InstallerHelper::getDownloadKey($extension); + + $item->set('extra_query', $downloadKey['value'] ?? ''); + $item->set('downloadIdPrefix', $downloadKey['prefix'] ?? ''); + $item->set('downloadIdSuffix', $downloadKey['suffix'] ?? ''); + + return $item; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 4.0.0 + */ + public function save($data): bool + { + // Apply the extra_query. Always empty when saving a free extension's update site. + if (isset($data['extra_query'])) { + $data['extra_query'] = $data['downloadIdPrefix'] . $data['extra_query'] . $data['downloadIdSuffix']; + } + + // Force Joomla to recheck for updates + $data['last_check_timestamp'] = 0; + + $result = parent::save($data); + + if (!$result) { + return $result; + } + + // Delete update records forcing Joomla to fetch them again, applying the new extra_query. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__updates')) + ->where($db->quoteName('update_site_id') . ' = :updateSiteId'); + $query->bind(':updateSiteId', $data['update_site_id'], ParameterType::INTEGER); + + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + // No problem if this fails for any reason. + } + + return true; + } } diff --git a/code/administrator/components/com_installer/src/Model/UpdatesitesModel.php b/code/administrator/components/com_installer/src/Model/UpdatesitesModel.php index 45ef7bc8..591be21e 100644 --- a/code/administrator/components/com_installer/src/Model/UpdatesitesModel.php +++ b/code/administrator/components/com_installer/src/Model/UpdatesitesModel.php @@ -1,4 +1,5 @@ authorise('core.edit.state', 'com_installer')) - { - throw new Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403); - } - - $result = true; - - // Ensure eid is an array of extension ids - if (!is_array($eid)) - { - $eid = [$eid]; - } - - // Get a table object for the extension type - $table = new UpdateSiteTable($this->getDbo()); - - // Enable the update site in the table and store it in the database - foreach ($eid as $i => $id) - { - $table->load($id); - $table->enabled = $value; - - if (!$table->store()) - { - $this->setError($table->getError()); - $result = false; - } - } - - return $result; - } - - /** - * Deletes an update site. - * - * @param array $ids Extension ids to delete. - * - * @return void - * - * @throws Exception on ACL error - * @since 3.6 - * - */ - public function delete($ids = []) - { - if (!Factory::getUser()->authorise('core.delete', 'com_installer')) - { - throw new Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); - } - - // Ensure eid is an array of extension ids - if (!is_array($ids)) - { - $ids = [$ids]; - } - - $db = $this->getDbo(); - $app = Factory::getApplication(); - - $count = 0; - - // Gets the update site names. - $query = $db->getQuery(true) - ->select($db->quoteName(['update_site_id', 'name'])) - ->from($db->quoteName('#__update_sites')) - ->whereIn($db->quoteName('update_site_id'), $ids); - $db->setQuery($query); - $updateSitesNames = $db->loadObjectList('update_site_id'); - - // Gets Joomla core update sites Ids. - $joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0); - - // Enable the update site in the table and store it in the database - foreach ($ids as $i => $id) - { - // Don't allow to delete Joomla Core update sites. - if (in_array((int) $id, $joomlaUpdateSitesIds)) - { - $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_DELETE_CANNOT_DELETE', $updateSitesNames[$id]->name), 'error'); - continue; - } - - // Delete the update site from all tables. - try - { - $id = (int) $id; - $query = $db->getQuery(true) - ->delete($db->quoteName('#__update_sites')) - ->where($db->quoteName('update_site_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__update_sites_extensions')) - ->where($db->quoteName('update_site_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__updates')) - ->where($db->quoteName('update_site_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - $count++; - } - catch (RuntimeException $e) - { - $app->enqueueMessage( - Text::sprintf( - 'COM_INSTALLER_MSG_UPDATESITES_DELETE_ERROR', - $updateSitesNames[$id]->name, $e->getMessage() - ), 'error' - ); - } - } - - if ($count > 0) - { - $app->enqueueMessage(Text::plural('COM_INSTALLER_MSG_UPDATESITES_N_DELETE_UPDATESITES_DELETED', $count), 'message'); - } - } - - /** - * Fetch the Joomla update sites ids. - * - * @param integer $column Column to return. 0 for update site ids, 1 for extension ids. - * - * @return array Array with joomla core update site ids. - * - * @since 3.6.0 - */ - protected function getJoomlaUpdateSitesIds($column = 0) - { - $db = $this->getDbo(); - - // Fetch the Joomla core update sites ids and their extension ids. We search for all except the core joomla extension with update sites. - $query = $db->getQuery(true) - ->select($db->quoteName(['use.update_site_id', 'e.extension_id'])) - ->from($db->quoteName('#__update_sites_extensions', 'use')) - ->join( - 'LEFT', - $db->quoteName('#__update_sites', 'us'), - $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id') - ) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') - ) - ->where('(' - . '(' . $db->quoteName('e.type') . ' = ' . $db->quote('file') . - ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('joomla') . ')' - . ' OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('package') . ' AND ' . $db->quoteName('e.element') - . ' = ' . $db->quote('pkg_en-GB') . ') OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('component') - . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('com_joomlaupdate') . ')' - . ')' - ); - - $db->setQuery($query); - - return $db->loadColumn($column); - } - - /** - * Rebuild update sites tables. - * - * @return void - * - * @throws Exception on ACL error - * @since 3.6 - * - */ - public function rebuild(): void - { - if (!Factory::getUser()->authorise('core.admin', 'com_installer')) - { - throw new Exception(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_NOT_PERMITTED'), 403); - } - - $db = $this->getDbo(); - $app = Factory::getApplication(); - - // Check if Joomla Extension plugin is enabled. - if (!PluginHelper::isEnabled('extension', 'joomla')) - { - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('extension')); - $db->setQuery($query); - - $pluginId = (int) $db->loadResult(); - - $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $pluginId); - $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_REBUILD_EXTENSION_PLUGIN_NOT_ENABLED', $link), 'error'); - - return; - } - - $clients = [JPATH_SITE, JPATH_ADMINISTRATOR, JPATH_API]; - $extensionGroupFolders = ['components', 'modules', 'plugins', 'templates', 'language', 'manifests']; - - $pathsToSearch = []; - - // Identifies which folders to search for manifest files. - foreach ($clients as $clientPath) - { - foreach ($extensionGroupFolders as $extensionGroupFolderName) - { - // Components, modules, plugins, templates, languages and manifest (files, libraries, etc) - if ($extensionGroupFolderName !== 'plugins') - { - foreach (glob($clientPath . '/' . $extensionGroupFolderName . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) - { - $pathsToSearch[] = $extensionFolderPath; - } - } - else - { - // Plugins (another directory level is needed) - foreach (glob($clientPath . '/' . $extensionGroupFolderName . '/*', - GLOB_NOSORT | GLOB_ONLYDIR - ) as $pluginGroupFolderPath) - { - foreach (glob($pluginGroupFolderPath . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) - { - $pathsToSearch[] = $extensionFolderPath; - } - } - } - } - } - - // Gets Joomla core update sites Ids. - $joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0); - - // First backup any custom extra_query for the sites - $query = $db->getQuery(true) - ->select('TRIM(' . $db->quoteName('location') . ') AS ' . $db->quoteName('location') . ', ' . $db->quoteName('extra_query')) - ->from($db->quoteName('#__update_sites')); - $db->setQuery($query); - $backupExtraQuerys = $db->loadAssocList('location'); - - // Delete from all tables (except joomla core update sites). - $query = $db->getQuery(true) - ->delete($db->quoteName('#__update_sites')) - ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); - $db->setQuery($query); - $db->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__update_sites_extensions')) - ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); - $db->setQuery($query); - $db->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__updates')) - ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); - $db->setQuery($query); - $db->execute(); - - $count = 0; - - // Gets Joomla core extension Ids. - $joomlaCoreExtensionIds = $this->getJoomlaUpdateSitesIds(1); - - // Search for updateservers in manifest files inside the folders to search. - foreach ($pathsToSearch as $extensionFolderPath) - { - $tmpInstaller = new Installer; - - $tmpInstaller->setPath('source', $extensionFolderPath); - - // Main folder manifests (higher priority) - $parentXmlfiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', false, true); - - // Search for children manifests (lower priority) - $allXmlFiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', 1, true); - - // Create a unique array of files ordered by priority - $xmlfiles = array_unique(array_merge($parentXmlfiles, $allXmlFiles)); - - if (!empty($xmlfiles)) - { - foreach ($xmlfiles as $file) - { - // Is it a valid Joomla installation manifest file? - $manifest = $tmpInstaller->isManifest($file); - - if ($manifest !== null) - { - /** - * Search if the extension exists in the extensions table. Excluding Joomla - * core extensions and discovered but not yet installed extensions. - */ - - $name = (string) $manifest->name; - $pkgName = (string) $manifest->packagename; - $type = (string) $manifest['type']; - - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where( - [ - $db->quoteName('type') . ' = :type', - $db->quoteName('state') . ' != -1', - ] - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('name') . ' = :name', - $db->quoteName('name') . ' = :pkgname', - ], - 'OR' - ) - ->whereNotIn($db->quoteName('extension_id'), $joomlaCoreExtensionIds) - ->bind(':name', $name) - ->bind(':pkgname', $pkgName) - ->bind(':type', $type); - $db->setQuery($query); - - $eid = (int) $db->loadResult(); - - if ($eid && $manifest->updateservers) - { - // Set the manifest object and path - $tmpInstaller->manifest = $manifest; - $tmpInstaller->setPath('manifest', $file); - - // Remove last extra_query as we are in a foreach - $tmpInstaller->extraQuery = ''; - - if ($tmpInstaller->manifest->updateservers - && $tmpInstaller->manifest->updateservers->server - && isset($backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)])) - { - $tmpInstaller->extraQuery = $backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)]['extra_query']; - } - - // Load the extension plugin (if not loaded yet). - PluginHelper::importPlugin('extension', 'joomla'); - - // Fire the onExtensionAfterUpdate - $app->triggerEvent('onExtensionAfterUpdate', ['installer' => $tmpInstaller, 'eid' => $eid]); - - $count++; - } - } - } - } - } - - if ($count > 0) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_SUCCESS'), 'message'); - } - else - { - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_MESSAGE'), 'message'); - } - - // Flush the system cache to ensure extra_query is correctly loaded next time. - $this->cleanCache('_system'); - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - - array_walk($items, - static function ($item) { - $data = new CMSObject($item); - $item->downloadKey = InstallerHelper::getDownloadKey($data); - } - ); - - return $items; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.4 - */ - protected function populateState($ordering = 'name', $direction = 'asc') - { - // Load the filter state. - $stateKeys = [ - 'search' => 'string', - 'client_id' => 'int', - 'enabled' => 'string', - 'type' => 'string', - 'folder' => 'string', - 'supported' => 'int', - ]; - - foreach ($stateKeys as $key => $filterType) - { - $stateKey = 'filter.' . $key; - - switch ($filterType) - { - case 'int': - case 'bool': - $default = null; - break; - - default: - $default = ''; - break; - } - - $stateValue = $this->getUserStateFromRequest( - $this->context . '.' . $stateKey, 'filter_' . $key, $default, $filterType - ); - - $this->setState($stateKey, $stateValue); - } - - parent::populateState($ordering, $direction); - } - - protected function getStoreId($id = '') - { - $id .= ':' . $this->getState('search'); - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('enabled'); - $id .= ':' . $this->getState('type'); - $id .= ':' . $this->getState('folder'); - $id .= ':' . $this->getState('supported'); - - return parent::getStoreId($id); - } - - /** - * Method to get the database query - * - * @return \Joomla\Database\DatabaseQuery The database query - * - * @since 3.4 - */ - protected function getListQuery() - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 's.update_site_id', - 's.name', - 's.type', - 's.location', - 's.enabled', - 's.checked_out', - 's.checked_out_time', - 's.extra_query', - 'e.extension_id', - 'e.name', - 'e.type', - 'e.element', - 'e.folder', - 'e.client_id', - 'e.state', - 'e.manifest_cache', - 'u.name' - ], - [ - 'update_site_id', - 'update_site_name', - 'update_site_type', - 'location', - 'enabled', - 'checked_out', - 'checked_out_time', - 'extra_query', - 'extension_id', - 'name', - 'type', - 'element', - 'folder', - 'client_id', - 'state', - 'manifest_cache', - 'editor' - ] - ) - ) - ->from($db->quoteName('#__update_sites', 's')) - ->join( - 'INNER', - $db->quoteName('#__update_sites_extensions', 'se'), - $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id') - ) - ->join( - 'INNER', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id') - ) - ->join( - 'LEFT', - $db->quoteName('#__users', 'u'), - $db->quoteName('s.checked_out') . ' = ' . $db->quoteName('u.id') - ) - ->where($db->quoteName('state') . ' = 0'); - - // Process select filters. - $supported = $this->getState('filter.supported'); - $enabled = $this->getState('filter.enabled'); - $type = $this->getState('filter.type'); - $clientId = $this->getState('filter.client_id'); - $folder = $this->getState('filter.folder'); - - if ($enabled !== '') - { - $enabled = (int) $enabled; - $query->where($db->quoteName('s.enabled') . ' = :enabled') - ->bind(':enabled', $enabled, ParameterType::INTEGER); - } - - if ($type) - { - $query->where($db->quoteName('e.type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId !== null && $clientId !== '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('e.client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - if ($folder !== '' && in_array($type, ['plugin', 'library', ''], true)) - { - $folderForBinding = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('e.folder') . ' = :folder') - ->bind(':folder', $folderForBinding); - } - - // Process search filter (update site id). - $search = $this->getState('filter.search'); - - if (!empty($search) && stripos($search, 'id:') === 0) - { - $uid = (int) substr($search, 3); - $query->where($db->quoteName('s.update_site_id') . ' = :siteId') - ->bind(':siteId', $uid, ParameterType::INTEGER); - } - - if (is_numeric($supported)) - { - switch ($supported) - { - // Show Update Sites which support Download Keys - case 1: - $supportedIDs = InstallerHelper::getDownloadKeySupportedSites($enabled); - break; - - // Show Update Sites which are missing Download Keys - case -1: - $supportedIDs = InstallerHelper::getDownloadKeyExistsSites(false, $enabled); - break; - - // Show Update Sites which have valid Download Keys - case 2: - $supportedIDs = InstallerHelper::getDownloadKeyExistsSites(true, $enabled); - break; - } - - if (!empty($supportedIDs)) - { - // Don't remove array_values(). whereIn expect a zero-based array. - $query->whereIn($db->qn('s.update_site_id'), array_values($supportedIDs)); - } - else - { - // In case of an empty list of IDs we apply a fake filter to effectively return no data - $query->where($db->qn('s.update_site_id') . ' = 0'); - } - } - - /** - * Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in - * extension.php). - */ - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @since 1.6 + * @see \Joomla\CMS\MVC\Model\ListModel + */ + public function __construct($config = [], MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'update_site_name', + 'name', + 'client_id', + 'client', + 'client_translated', + 'status', + 'type', + 'type_translated', + 'folder', + 'folder_translated', + 'update_site_id', + 'enabled', + 'supported' + ]; + } + + parent::__construct($config, $factory); + } + + /** + * Enable/Disable an extension. + * + * @param array $eid Extension ids to un/publish + * @param int $value Publish value + * + * @return boolean True on success + * + * @throws Exception on ACL error + * @since 3.4 + * + */ + public function publish(&$eid = [], $value = 1) + { + if (!Factory::getUser()->authorise('core.edit.state', 'com_installer')) { + throw new Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403); + } + + $result = true; + + // Ensure eid is an array of extension ids + if (!is_array($eid)) { + $eid = [$eid]; + } + + // Get a table object for the extension type + $table = new UpdateSiteTable($this->getDatabase()); + + // Enable the update site in the table and store it in the database + foreach ($eid as $i => $id) { + $table->load($id); + $table->enabled = $value; + + if (!$table->store()) { + $this->setError($table->getError()); + $result = false; + } + } + + return $result; + } + + /** + * Deletes an update site. + * + * @param array $ids Extension ids to delete. + * + * @return void + * + * @throws Exception on ACL error + * @since 3.6 + * + */ + public function delete($ids = []) + { + if (!Factory::getUser()->authorise('core.delete', 'com_installer')) { + throw new Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); + } + + // Ensure eid is an array of extension ids + if (!is_array($ids)) { + $ids = [$ids]; + } + + $db = $this->getDatabase(); + $app = Factory::getApplication(); + + $count = 0; + + // Gets the update site names. + $query = $db->getQuery(true) + ->select($db->quoteName(['update_site_id', 'name'])) + ->from($db->quoteName('#__update_sites')) + ->whereIn($db->quoteName('update_site_id'), $ids); + $db->setQuery($query); + $updateSitesNames = $db->loadObjectList('update_site_id'); + + // Gets Joomla core update sites Ids. + $joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0); + + // Enable the update site in the table and store it in the database + foreach ($ids as $i => $id) { + // Don't allow to delete Joomla Core update sites. + if (in_array((int) $id, $joomlaUpdateSitesIds)) { + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_DELETE_CANNOT_DELETE', $updateSitesNames[$id]->name), 'error'); + continue; + } + + // Delete the update site from all tables. + try { + $id = (int) $id; + $query = $db->getQuery(true) + ->delete($db->quoteName('#__update_sites')) + ->where($db->quoteName('update_site_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__update_sites_extensions')) + ->where($db->quoteName('update_site_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__updates')) + ->where($db->quoteName('update_site_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + $count++; + } catch (RuntimeException $e) { + $app->enqueueMessage( + Text::sprintf( + 'COM_INSTALLER_MSG_UPDATESITES_DELETE_ERROR', + $updateSitesNames[$id]->name, + $e->getMessage() + ), + 'error' + ); + } + } + + if ($count > 0) { + $app->enqueueMessage(Text::plural('COM_INSTALLER_MSG_UPDATESITES_N_DELETE_UPDATESITES_DELETED', $count), 'message'); + } + } + + /** + * Fetch the Joomla update sites ids. + * + * @param integer $column Column to return. 0 for update site ids, 1 for extension ids. + * + * @return array Array with joomla core update site ids. + * + * @since 3.6.0 + */ + protected function getJoomlaUpdateSitesIds($column = 0) + { + $db = $this->getDatabase(); + + // Fetch the Joomla core update sites ids and their extension ids. We search for all except the core joomla extension with update sites. + $query = $db->getQuery(true) + ->select($db->quoteName(['use.update_site_id', 'e.extension_id'])) + ->from($db->quoteName('#__update_sites_extensions', 'use')) + ->join( + 'LEFT', + $db->quoteName('#__update_sites', 'us'), + $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id') + ) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') + ) + ->where('(' + . '(' . $db->quoteName('e.type') . ' = ' . $db->quote('file') . + ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('joomla') . ')' + . ' OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('package') . ' AND ' . $db->quoteName('e.element') + . ' = ' . $db->quote('pkg_en-GB') . ') OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('component') + . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('com_joomlaupdate') . ')' + . ')'); + + $db->setQuery($query); + + return $db->loadColumn($column); + } + + /** + * Rebuild update sites tables. + * + * @return void + * + * @throws Exception on ACL error + * @since 3.6 + * + */ + public function rebuild(): void + { + if (!Factory::getUser()->authorise('core.admin', 'com_installer')) { + throw new Exception(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_NOT_PERMITTED'), 403); + } + + $db = $this->getDatabase(); + $app = Factory::getApplication(); + + // Check if Joomla Extension plugin is enabled. + if (!PluginHelper::isEnabled('extension', 'joomla')) { + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('extension')); + $db->setQuery($query); + + $pluginId = (int) $db->loadResult(); + + $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $pluginId); + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_REBUILD_EXTENSION_PLUGIN_NOT_ENABLED', $link), 'error'); + + return; + } + + $clients = [JPATH_SITE, JPATH_ADMINISTRATOR, JPATH_API]; + $extensionGroupFolders = ['components', 'modules', 'plugins', 'templates', 'language', 'manifests']; + + $pathsToSearch = []; + + // Identifies which folders to search for manifest files. + foreach ($clients as $clientPath) { + foreach ($extensionGroupFolders as $extensionGroupFolderName) { + // Components, modules, plugins, templates, languages and manifest (files, libraries, etc) + if ($extensionGroupFolderName !== 'plugins') { + foreach (glob($clientPath . '/' . $extensionGroupFolderName . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) { + $pathsToSearch[] = $extensionFolderPath; + } + } else { + // Plugins (another directory level is needed) + foreach ( + glob( + $clientPath . '/' . $extensionGroupFolderName . '/*', + GLOB_NOSORT | GLOB_ONLYDIR + ) as $pluginGroupFolderPath + ) { + foreach (glob($pluginGroupFolderPath . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) { + $pathsToSearch[] = $extensionFolderPath; + } + } + } + } + } + + // Gets Joomla core update sites Ids. + $joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0); + + // First backup any custom extra_query for the sites + $query = $db->getQuery(true) + ->select('TRIM(' . $db->quoteName('location') . ') AS ' . $db->quoteName('location') . ', ' . $db->quoteName('extra_query')) + ->from($db->quoteName('#__update_sites')); + $db->setQuery($query); + $backupExtraQuerys = $db->loadAssocList('location'); + + // Delete from all tables (except joomla core update sites). + $query = $db->getQuery(true) + ->delete($db->quoteName('#__update_sites')) + ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__update_sites_extensions')) + ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__updates')) + ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); + $db->setQuery($query); + $db->execute(); + + $count = 0; + + // Gets Joomla core extension Ids. + $joomlaCoreExtensionIds = $this->getJoomlaUpdateSitesIds(1); + + // Search for updateservers in manifest files inside the folders to search. + foreach ($pathsToSearch as $extensionFolderPath) { + $tmpInstaller = new Installer(); + $tmpInstaller->setDatabase($this->getDatabase()); + + $tmpInstaller->setPath('source', $extensionFolderPath); + + // Main folder manifests (higher priority) + $parentXmlfiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', false, true); + + // Search for children manifests (lower priority) + $allXmlFiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', 1, true); + + // Create a unique array of files ordered by priority + $xmlfiles = array_unique(array_merge($parentXmlfiles, $allXmlFiles)); + + if (!empty($xmlfiles)) { + foreach ($xmlfiles as $file) { + // Is it a valid Joomla installation manifest file? + $manifest = $tmpInstaller->isManifest($file); + + if ($manifest !== null) { + /** + * Search if the extension exists in the extensions table. Excluding Joomla + * core extensions and discovered but not yet installed extensions. + */ + + $name = (string) $manifest->name; + $pkgName = (string) $manifest->packagename; + $type = (string) $manifest['type']; + + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where( + [ + $db->quoteName('type') . ' = :type', + $db->quoteName('state') . ' != -1', + ] + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('name') . ' = :name', + $db->quoteName('name') . ' = :pkgname', + ], + 'OR' + ) + ->whereNotIn($db->quoteName('extension_id'), $joomlaCoreExtensionIds) + ->bind(':name', $name) + ->bind(':pkgname', $pkgName) + ->bind(':type', $type); + $db->setQuery($query); + + $eid = (int) $db->loadResult(); + + if ($eid && $manifest->updateservers) { + // Set the manifest object and path + $tmpInstaller->manifest = $manifest; + $tmpInstaller->setPath('manifest', $file); + + // Remove last extra_query as we are in a foreach + $tmpInstaller->extraQuery = ''; + + if ( + $tmpInstaller->manifest->updateservers + && $tmpInstaller->manifest->updateservers->server + && isset($backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)]) + ) { + $tmpInstaller->extraQuery = $backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)]['extra_query']; + } + + // Load the extension plugin (if not loaded yet). + PluginHelper::importPlugin('extension', 'joomla'); + + // Fire the onExtensionAfterUpdate + $app->triggerEvent('onExtensionAfterUpdate', ['installer' => $tmpInstaller, 'eid' => $eid]); + + $count++; + } + } + } + } + } + + if ($count > 0) { + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_SUCCESS'), 'message'); + } else { + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_MESSAGE'), 'message'); + } + + // Flush the system cache to ensure extra_query is correctly loaded next time. + $this->cleanCache('_system'); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + array_walk( + $items, + static function ($item) { + $data = new CMSObject($item); + $item->downloadKey = InstallerHelper::getDownloadKey($data); + } + ); + + return $items; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.4 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + // Load the filter state. + $stateKeys = [ + 'search' => 'string', + 'client_id' => 'int', + 'enabled' => 'string', + 'type' => 'string', + 'folder' => 'string', + 'supported' => 'int', + ]; + + foreach ($stateKeys as $key => $filterType) { + $stateKey = 'filter.' . $key; + + switch ($filterType) { + case 'int': + case 'bool': + $default = null; + break; + + default: + $default = ''; + break; + } + + $stateValue = $this->getUserStateFromRequest( + $this->context . '.' . $stateKey, + 'filter_' . $key, + $default, + $filterType + ); + + $this->setState($stateKey, $stateValue); + } + + parent::populateState($ordering, $direction); + } + + protected function getStoreId($id = '') + { + $id .= ':' . $this->getState('search'); + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('enabled'); + $id .= ':' . $this->getState('type'); + $id .= ':' . $this->getState('folder'); + $id .= ':' . $this->getState('supported'); + + return parent::getStoreId($id); + } + + /** + * Method to get the database query + * + * @return \Joomla\Database\DatabaseQuery The database query + * + * @since 3.4 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 's.update_site_id', + 's.name', + 's.type', + 's.location', + 's.enabled', + 's.checked_out', + 's.checked_out_time', + 's.extra_query', + 'e.extension_id', + 'e.name', + 'e.type', + 'e.element', + 'e.folder', + 'e.client_id', + 'e.state', + 'e.manifest_cache', + 'u.name' + ], + [ + 'update_site_id', + 'update_site_name', + 'update_site_type', + 'location', + 'enabled', + 'checked_out', + 'checked_out_time', + 'extra_query', + 'extension_id', + 'name', + 'type', + 'element', + 'folder', + 'client_id', + 'state', + 'manifest_cache', + 'editor' + ] + ) + ) + ->from($db->quoteName('#__update_sites', 's')) + ->join( + 'INNER', + $db->quoteName('#__update_sites_extensions', 'se'), + $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id') + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id') + ) + ->join( + 'LEFT', + $db->quoteName('#__users', 'u'), + $db->quoteName('s.checked_out') . ' = ' . $db->quoteName('u.id') + ) + ->where($db->quoteName('state') . ' = 0'); + + // Process select filters. + $supported = $this->getState('filter.supported'); + $enabled = $this->getState('filter.enabled'); + $type = $this->getState('filter.type'); + $clientId = $this->getState('filter.client_id'); + $folder = $this->getState('filter.folder'); + + if ($enabled !== '') { + $enabled = (int) $enabled; + $query->where($db->quoteName('s.enabled') . ' = :enabled') + ->bind(':enabled', $enabled, ParameterType::INTEGER); + } + + if ($type) { + $query->where($db->quoteName('e.type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId !== null && $clientId !== '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('e.client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + if ($folder !== '' && in_array($type, ['plugin', 'library', ''], true)) { + $folderForBinding = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('e.folder') . ' = :folder') + ->bind(':folder', $folderForBinding); + } + + // Process search filter (update site id). + $search = $this->getState('filter.search'); + + if (!empty($search) && stripos($search, 'id:') === 0) { + $uid = (int) substr($search, 3); + $query->where($db->quoteName('s.update_site_id') . ' = :siteId') + ->bind(':siteId', $uid, ParameterType::INTEGER); + } + + if (is_numeric($supported)) { + switch ($supported) { + // Show Update Sites which support Download Keys + case 1: + $supportedIDs = InstallerHelper::getDownloadKeySupportedSites($enabled); + break; + + // Show Update Sites which are missing Download Keys + case -1: + $supportedIDs = InstallerHelper::getDownloadKeyExistsSites(false, $enabled); + break; + + // Show Update Sites which have valid Download Keys + case 2: + $supportedIDs = InstallerHelper::getDownloadKeyExistsSites(true, $enabled); + break; + } + + if (!empty($supportedIDs)) { + // Don't remove array_values(). whereIn expect a zero-based array. + $query->whereIn($db->qn('s.update_site_id'), array_values($supportedIDs)); + } else { + // In case of an empty list of IDs we apply a fake filter to effectively return no data + $query->where($db->qn('s.update_site_id') . ' = 0'); + } + } + + /** + * Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in + * extension.php). + */ + + return $query; + } } diff --git a/code/administrator/components/com_installer/src/Model/WarningsModel.php b/code/administrator/components/com_installer/src/Model/WarningsModel.php index ff3d20e5..5c5796ec 100644 --- a/code/administrator/components/com_installer/src/Model/WarningsModel.php +++ b/code/administrator/components/com_installer/src/Model/WarningsModel.php @@ -1,4 +1,5 @@ Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADSDISABLED'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADISDISABLEDDESC'), - ]; - } - - $upload_dir = ini_get('upload_tmp_dir'); - - if (!$upload_dir) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSETDESC'), - ]; - } - elseif (!is_writable($upload_dir)) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLE'), - 'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLEDESC', $upload_dir), - ]; - } - - $tmp_path = Factory::getApplication()->get('tmp_path'); - - if (!$tmp_path) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSETDESC'), - ]; - } - elseif (!is_writable($tmp_path)) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE'), - 'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLEDESC', $tmp_path), - ]; - } - - $memory_limit = $this->return_bytes(ini_get('memory_limit')); - - if ($memory_limit > -1) - { - if ($memory_limit < $minLimit) - { - // 16MB - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYWARN'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYDESC'), - ]; - } - elseif ($memory_limit < ($minLimit * 1.5)) - { - // 24MB - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYWARN'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYDESC'), - ]; - } - } - - $post_max_size = $this->return_bytes(ini_get('post_max_size')); - $upload_max_filesize = $this->return_bytes(ini_get('upload_max_filesize')); - - if ($post_max_size > 0 && $post_max_size < $upload_max_filesize) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOST'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOSTDESC'), - ]; - } - - if ($post_max_size > 0 && $post_max_size < $minLimit) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZE'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZEDESC'), - ]; - } - - if ($upload_max_filesize > 0 && $upload_max_filesize < $minLimit) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZEDESC'), - ]; - } - - return $messages; - } + /** + * Extension Type + * @var string + */ + public $type = 'warnings'; + + /** + * Return the byte value of a particular string. + * + * @param string $val String optionally with G, M or K suffix + * + * @return integer size in bytes + * + * @since 1.6 + */ + public function return_bytes($val) + { + if (empty($val)) { + return 0; + } + + $val = trim($val); + + preg_match('#([0-9]+)[\s]*([a-z]+)#i', $val, $matches); + + $last = ''; + + if (isset($matches[2])) { + $last = $matches[2]; + } + + if (isset($matches[1])) { + $val = (int) $matches[1]; + } + + switch (strtolower($last)) { + case 'g': + case 'gb': + $val *= (1024 * 1024 * 1024); + break; + case 'm': + case 'mb': + $val *= (1024 * 1024); + break; + case 'k': + case 'kb': + $val *= 1024; + break; + } + + return (int) $val; + } + + /** + * Load the data. + * + * @return array Messages + * + * @since 1.6 + */ + public function getItems() + { + static $messages; + + if ($messages) { + return $messages; + } + + $messages = []; + + // 16MB + $minLimit = 16 * 1024 * 1024; + + $file_uploads = ini_get('file_uploads'); + + if (!$file_uploads) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADSDISABLED'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADISDISABLEDDESC'), + ]; + } + + $upload_dir = ini_get('upload_tmp_dir'); + + if (!$upload_dir) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSETDESC'), + ]; + } elseif (!is_writable($upload_dir)) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLE'), + 'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLEDESC', $upload_dir), + ]; + } + + $tmp_path = Factory::getApplication()->get('tmp_path'); + + if (!$tmp_path) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSETDESC'), + ]; + } elseif (!is_writable($tmp_path)) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE'), + 'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLEDESC', $tmp_path), + ]; + } + + $memory_limit = $this->return_bytes(ini_get('memory_limit')); + + if ($memory_limit > -1) { + if ($memory_limit < $minLimit) { + // 16MB + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYWARN'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYDESC'), + ]; + } elseif ($memory_limit < ($minLimit * 1.5)) { + // 24MB + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYWARN'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYDESC'), + ]; + } + } + + $post_max_size = $this->return_bytes(ini_get('post_max_size')); + $upload_max_filesize = $this->return_bytes(ini_get('upload_max_filesize')); + + if ($post_max_size > 0 && $post_max_size < $upload_max_filesize) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOST'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOSTDESC'), + ]; + } + + if ($post_max_size > 0 && $post_max_size < $minLimit) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZE'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZEDESC'), + ]; + } + + if ($upload_max_filesize > 0 && $upload_max_filesize < $minLimit) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZEDESC'), + ]; + } + + return $messages; + } } diff --git a/code/administrator/components/com_installer/src/Service/HTML/Manage.php b/code/administrator/components/com_installer/src/Service/HTML/Manage.php index 35a10d69..d74e27a5 100644 --- a/code/administrator/components/com_installer/src/Service/HTML/Manage.php +++ b/code/administrator/components/com_installer/src/Service/HTML/Manage.php @@ -1,4 +1,5 @@ array( - '', - 'COM_INSTALLER_EXTENSION_PROTECTED', - '', - 'COM_INSTALLER_EXTENSION_PROTECTED', - true, - 'protected', - 'protected', - ), - 1 => array( - 'unpublish', - 'COM_INSTALLER_EXTENSION_ENABLED', - 'COM_INSTALLER_EXTENSION_DISABLE', - 'COM_INSTALLER_EXTENSION_ENABLED', - true, - 'publish', - 'publish', - ), - 0 => array( - 'publish', - 'COM_INSTALLER_EXTENSION_DISABLED', - 'COM_INSTALLER_EXTENSION_ENABLE', - 'COM_INSTALLER_EXTENSION_DISABLED', - true, - 'unpublish', - 'unpublish', - ), - ); + /** + * Returns a published state on a grid. + * + * @param integer $value The state value. + * @param integer $i The row index. + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * + * @return string The Html code + * + * @see JHtmlJGrid::state + * + * @since 2.5 + */ + public function state($value, $i, $enabled = true, $checkbox = 'cb') + { + $states = array( + 2 => array( + '', + 'COM_INSTALLER_EXTENSION_PROTECTED', + '', + 'COM_INSTALLER_EXTENSION_PROTECTED', + true, + 'protected', + 'protected', + ), + 1 => array( + 'unpublish', + 'COM_INSTALLER_EXTENSION_ENABLED', + 'COM_INSTALLER_EXTENSION_DISABLE', + 'COM_INSTALLER_EXTENSION_ENABLED', + true, + 'publish', + 'publish', + ), + 0 => array( + 'publish', + 'COM_INSTALLER_EXTENSION_DISABLED', + 'COM_INSTALLER_EXTENSION_ENABLE', + 'COM_INSTALLER_EXTENSION_DISABLED', + true, + 'unpublish', + 'unpublish', + ), + ); - return HTMLHelper::_('jgrid.state', $states, $value, $i, 'manage.', $enabled, true, $checkbox); - } + return HTMLHelper::_('jgrid.state', $states, $value, $i, 'manage.', $enabled, true, $checkbox); + } } diff --git a/code/administrator/components/com_installer/src/Service/HTML/Updatesites.php b/code/administrator/components/com_installer/src/Service/HTML/Updatesites.php index 8f917c2c..ca3a850b 100644 --- a/code/administrator/components/com_installer/src/Service/HTML/Updatesites.php +++ b/code/administrator/components/com_installer/src/Service/HTML/Updatesites.php @@ -1,4 +1,5 @@ array( - 'unpublish', - 'COM_INSTALLER_UPDATESITE_ENABLED', - 'COM_INSTALLER_UPDATESITE_DISABLE', - 'COM_INSTALLER_UPDATESITE_ENABLED', - true, - 'publish', - 'publish', - ), - 0 => array( - 'publish', - 'COM_INSTALLER_UPDATESITE_DISABLED', - 'COM_INSTALLER_UPDATESITE_ENABLE', - 'COM_INSTALLER_UPDATESITE_DISABLED', - true, - 'unpublish', - 'unpublish', - ), - ); + /** + * Returns a published state on a grid. + * + * @param integer $value The state value. + * @param integer $i The row index. + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * + * @return string The HTML code + * + * @see JHtmlJGrid::state() + * @since 3.5 + */ + public function state($value, $i, $enabled = true, $checkbox = 'cb') + { + $states = array( + 1 => array( + 'unpublish', + 'COM_INSTALLER_UPDATESITE_ENABLED', + 'COM_INSTALLER_UPDATESITE_DISABLE', + 'COM_INSTALLER_UPDATESITE_ENABLED', + true, + 'publish', + 'publish', + ), + 0 => array( + 'publish', + 'COM_INSTALLER_UPDATESITE_DISABLED', + 'COM_INSTALLER_UPDATESITE_ENABLE', + 'COM_INSTALLER_UPDATESITE_DISABLED', + true, + 'unpublish', + 'unpublish', + ), + ); - return HTMLHelper::_('jgrid.state', $states, $value, $i, 'updatesites.', $enabled, true, $checkbox); - } + return HTMLHelper::_('jgrid.state', $states, $value, $i, 'updatesites.', $enabled, true, $checkbox); + } } diff --git a/code/administrator/components/com_installer/src/Table/UpdatesiteTable.php b/code/administrator/components/com_installer/src/Table/UpdatesiteTable.php index 57a5f878..31b4102d 100644 --- a/code/administrator/components/com_installer/src/Table/UpdatesiteTable.php +++ b/code/administrator/components/com_installer/src/Table/UpdatesiteTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_installer.downloadkey'; + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 4.0.0 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_installer.downloadkey'; - parent::__construct('#__update_sites', 'update_site_id', $db); - } + parent::__construct('#__update_sites', 'update_site_id', $db); + } } diff --git a/code/administrator/components/com_installer/src/View/Database/HtmlView.php b/code/administrator/components/com_installer/src/View/Database/HtmlView.php index 772e86d4..8e803086 100644 --- a/code/administrator/components/com_installer/src/View/Database/HtmlView.php +++ b/code/administrator/components/com_installer/src/View/Database/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - - try - { - $this->changeSet = $model->getItems(); - } - catch (\Exception $exception) - { - $app->enqueueMessage($exception->getMessage(), 'error'); - } - - $this->errorCount = $model->getErrorCount(); - $this->pagination = $model->getPagination(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if ($this->changeSet) - { - ($this->errorCount === 0) - ? $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_OK'), 'info') - : $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_ERRORS'), 'warning'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - /* - * Set toolbar items for the page. - */ - ToolbarHelper::custom('database.fix', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DATABASE_FIX', true); - ToolbarHelper::divider(); - parent::addToolbar(); - ToolbarHelper::help('Information:_Database'); - } + /** + * List of change sets + * + * @var array + * @since 4.0.0 + */ + protected $changeSet = array(); + + /** + * The number of errors found + * + * @var integer + * @since 4.0.0 + */ + protected $errorCount = 0; + + /** + * List pagination. + * + * @var Pagination + * @since 4.0.0 + */ + protected $pagination; + + /** + * The filter form + * + * @var Form + * @since 4.0.0 + */ + public $filterForm; + + /** + * A list of form filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters = array(); + + /** + * Display the view. + * + * @param string $tpl Template + * + * @return void + * + * @throws \Exception + * + * @since 1.6 + */ + public function display($tpl = null) + { + // Get the application + $app = Factory::getApplication(); + + // Get data from the model. + /** @var DatabaseModel $model */ + $model = $this->getModel(); + + try { + $this->changeSet = $model->getItems(); + } catch (\Exception $exception) { + $app->enqueueMessage($exception->getMessage(), 'error'); + } + + $this->errorCount = $model->getErrorCount(); + $this->pagination = $model->getPagination(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if ($this->changeSet) { + ($this->errorCount === 0) + ? $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_OK'), 'info') + : $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_ERRORS'), 'warning'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + /* + * Set toolbar items for the page. + */ + ToolbarHelper::custom('database.fix', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DATABASE_FIX', true); + ToolbarHelper::divider(); + parent::addToolbar(); + ToolbarHelper::help('Information:_Database'); + } } diff --git a/code/administrator/components/com_installer/src/View/Discover/HtmlView.php b/code/administrator/components/com_installer/src/View/Discover/HtmlView.php index 95b841c2..dc41ec35 100644 --- a/code/administrator/components/com_installer/src/View/Discover/HtmlView.php +++ b/code/administrator/components/com_installer/src/View/Discover/HtmlView.php @@ -1,4 +1,5 @@ checkExtensions()) - { - $this->getModel('discover')->discover(); - } - - // Get data from the model. - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; - if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } + /** + * Display the view. + * + * @param string $tpl Template + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + // Run discover from the model. + if (!$this->getModel()->checkExtensions()) { + $this->getModel()->discover(); + } - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Get data from the model. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); - parent::display($tpl); - } + if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.1 - */ - protected function addToolbar() - { - /* - * Set toolbar items for the page. - */ - if (!$this->isEmptyState) - { - ToolbarHelper::custom('discover.install', 'upload', '', 'JTOOLBAR_INSTALL', true); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - ToolbarHelper::custom('discover.refresh', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DISCOVER', false); - ToolbarHelper::divider(); + parent::display($tpl); + } - parent::addToolbar(); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.1 + */ + protected function addToolbar() + { + /* + * Set toolbar items for the page. + */ + if (!$this->isEmptyState) { + ToolbarHelper::custom('discover.install', 'upload', '', 'JTOOLBAR_INSTALL', true); + } - ToolbarHelper::help('Extensions:_Discover'); - } + ToolbarHelper::custom('discover.refresh', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DISCOVER', false); + ToolbarHelper::divider(); - /** - * Check extensions. - * - * Checks uninstalled extensions in extensions table. - * - * @return boolean True if there are discovered extensions on the database. - * - * @since 3.5 - */ - public function checkExtensions() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('state') . ' = -1'); - $db->setQuery($query); - $discoveredExtensions = $db->loadObjectList(); + parent::addToolbar(); - return (count($discoveredExtensions) === 0) ? false : true; - } + ToolbarHelper::help('Extensions:_Discover'); + } } diff --git a/code/administrator/components/com_installer/src/View/Install/HtmlView.php b/code/administrator/components/com_installer/src/View/Install/HtmlView.php index 32cc41f0..29013969 100644 --- a/code/administrator/components/com_installer/src/View/Install/HtmlView.php +++ b/code/administrator/components/com_installer/src/View/Install/HtmlView.php @@ -1,4 +1,5 @@ authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Display the view + * + * @param string $tpl Template + * + * @return void + * + * @since 1.5 + */ + public function display($tpl = null) + { + if (!$this->getCurrentUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - $paths = new \stdClass; - $paths->first = ''; + $paths = new \stdClass(); + $paths->first = ''; - $this->paths = &$paths; + $this->paths = &$paths; - PluginHelper::importPlugin('installer'); + PluginHelper::importPlugin('installer'); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - if (ContentHelper::getActions('com_installer')->get('core.manage')) - { - ToolbarHelper::link('index.php?option=com_installer&view=manage', 'COM_INSTALLER_TOOLBAR_MANAGE', 'list'); - ToolbarHelper::divider(); - } + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + if (ContentHelper::getActions('com_installer')->get('core.manage')) { + ToolbarHelper::link('index.php?option=com_installer&view=manage', 'COM_INSTALLER_TOOLBAR_MANAGE', 'list'); + ToolbarHelper::divider(); + } - parent::addToolbar(); + parent::addToolbar(); - ToolbarHelper::help('Extensions:_Install'); - } + ToolbarHelper::help('Extensions:_Install'); + } } diff --git a/code/administrator/components/com_installer/src/View/Installer/HtmlView.php b/code/administrator/components/com_installer/src/View/Installer/HtmlView.php index bf3cbb9b..fa376a9b 100644 --- a/code/administrator/components/com_installer/src/View/Installer/HtmlView.php +++ b/code/administrator/components/com_installer/src/View/Installer/HtmlView.php @@ -1,4 +1,5 @@ _addPath('template', $this->_basePath . '/tmpl/installer'); - $this->_addPath('template', JPATH_THEMES . '/' . Factory::getApplication()->getTemplate() . '/html/com_installer/installer'); - } + $this->_addPath('template', $this->_basePath . '/tmpl/installer'); + $this->_addPath('template', JPATH_THEMES . '/' . Factory::getApplication()->getTemplate() . '/html/com_installer/installer'); + } - /** - * Display the view. - * - * @param string $tpl Template - * - * @return void - * - * @since 1.5 - */ - public function display($tpl = null) - { - // Get data from the model. - $state = $this->get('State'); + /** + * Display the view. + * + * @param string $tpl Template + * + * @return void + * + * @since 1.5 + */ + public function display($tpl = null) + { + // Get data from the model. + $state = $this->get('State'); - // Are there messages to display? - $showMessage = false; + // Are there messages to display? + $showMessage = false; - if (is_object($state)) - { - $message1 = $state->get('message'); - $message2 = $state->get('extension_message'); - $showMessage = ($message1 || $message2); - } + if (is_object($state)) { + $message1 = $state->get('message'); + $message2 = $state->get('extension_message'); + $showMessage = ($message1 || $message2); + } - $this->showMessage = $showMessage; - $this->state = &$state; + $this->showMessage = $showMessage; + $this->state = &$state; - $this->addToolbar(); - parent::display($tpl); - } + $this->addToolbar(); + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_installer'); - ToolbarHelper::title(Text::_('COM_INSTALLER_HEADER_' . strtoupper($this->getName())), 'puzzle-piece install'); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_installer'); + ToolbarHelper::title(Text::_('COM_INSTALLER_HEADER_' . strtoupper($this->getName())), 'puzzle-piece install'); - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_installer'); - ToolbarHelper::divider(); - } - } + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_installer'); + ToolbarHelper::divider(); + } + } } diff --git a/code/administrator/components/com_installer/src/View/Languages/HtmlView.php b/code/administrator/components/com_installer/src/View/Languages/HtmlView.php index 5cef7b65..5713211d 100644 --- a/code/administrator/components/com_installer/src/View/Languages/HtmlView.php +++ b/code/administrator/components/com_installer/src/View/Languages/HtmlView.php @@ -1,4 +1,5 @@ authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Display the view. + * + * @param null $tpl template to display + * + * @return mixed|void + */ + public function display($tpl = null) + { + if (!$this->getCurrentUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - // Get data from the model. - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->installedLang = LanguageHelper::getInstalledLanguages(); + // Get data from the model. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->installedLang = LanguageHelper::getInstalledLanguages(); - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_installer'); - ToolbarHelper::title(Text::_('COM_INSTALLER_HEADER_' . $this->getName()), 'puzzle-piece install'); + /** + * Add the page title and toolbar. + * + * @return void + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_installer'); + ToolbarHelper::title(Text::_('COM_INSTALLER_HEADER_' . $this->getName()), 'puzzle-piece install'); - if ($canDo->get('core.admin')) - { - parent::addToolbar(); + if ($canDo->get('core.admin')) { + parent::addToolbar(); - ToolbarHelper::help('Extensions:_Languages'); - } - } + ToolbarHelper::help('Extensions:_Languages'); + } + } } diff --git a/code/administrator/components/com_installer/src/View/Manage/HtmlView.php b/code/administrator/components/com_installer/src/View/Manage/HtmlView.php index 3262bae2..ea096693 100644 --- a/code/administrator/components/com_installer/src/View/Manage/HtmlView.php +++ b/code/administrator/components/com_installer/src/View/Manage/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Display the view. - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $toolbar = Toolbar::getInstance('toolbar'); - $canDo = ContentHelper::getActions('com_installer'); - - if ($canDo->get('core.edit.state')) - { - $toolbar->publish('manage.publish') - ->text('JTOOLBAR_ENABLE') - ->listCheck(true); - $toolbar->unpublish('manage.unpublish') - ->text('JTOOLBAR_DISABLE') - ->listCheck(true); - $toolbar->divider(); - } - - $toolbar->standardButton('refresh') - ->text('JTOOLBAR_REFRESH_CACHE') - ->task('manage.refresh') - ->listCheck(true); - $toolbar->divider(); - - if ($canDo->get('core.delete')) - { - $toolbar->delete('manage.remove') - ->text('JTOOLBAR_UNINSTALL') - ->message('COM_INSTALLER_CONFIRM_UNINSTALL') - ->listCheck(true); - $toolbar->divider(); - } - - if ($canDo->get('core.manage')) - { - ToolbarHelper::link('index.php?option=com_installer&view=install', 'COM_INSTALLER_TOOLBAR_INSTALL_EXTENSIONS', 'upload'); - $toolbar->divider(); - } - - parent::addToolbar(); - $toolbar->help('Extensions:_Manage'); - } + /** + * List of updatesites + * + * @var \stdClass[] + */ + protected $items; + + /** + * Pagination object + * + * @var Pagination + */ + protected $pagination; + + /** + * Form object + * + * @var Form + */ + protected $form; + + /** + * Display the view. + * + * @param string $tpl Template + * + * @return mixed|void + * + * @since 1.6 + */ + public function display($tpl = null) + { + // Get data from the model. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Display the view. + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $toolbar = Toolbar::getInstance('toolbar'); + $canDo = ContentHelper::getActions('com_installer'); + + if ($canDo->get('core.edit.state')) { + $toolbar->publish('manage.publish') + ->text('JTOOLBAR_ENABLE') + ->listCheck(true); + $toolbar->unpublish('manage.unpublish') + ->text('JTOOLBAR_DISABLE') + ->listCheck(true); + $toolbar->divider(); + } + + $toolbar->standardButton('refresh') + ->text('JTOOLBAR_REFRESH_CACHE') + ->task('manage.refresh') + ->listCheck(true); + $toolbar->divider(); + + if ($canDo->get('core.delete')) { + $toolbar->delete('manage.remove') + ->text('JTOOLBAR_UNINSTALL') + ->message('COM_INSTALLER_CONFIRM_UNINSTALL') + ->listCheck(true); + $toolbar->divider(); + } + + if ($canDo->get('core.manage')) { + ToolbarHelper::link('index.php?option=com_installer&view=install', 'COM_INSTALLER_TOOLBAR_INSTALL_EXTENSIONS', 'upload'); + $toolbar->divider(); + } + + parent::addToolbar(); + $toolbar->help('Extensions:_Manage'); + } } diff --git a/code/administrator/components/com_installer/src/View/Update/HtmlView.php b/code/administrator/components/com_installer/src/View/Update/HtmlView.php index 2f1b9915..62bb3705 100644 --- a/code/administrator/components/com_installer/src/View/Update/HtmlView.php +++ b/code/administrator/components/com_installer/src/View/Update/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - $paths = new \stdClass; - $paths->first = ''; - - $this->paths = &$paths; - - if (count($this->items) === 0 && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE'), 'warning'); - } - - // Find if there are any updates which require but are missing a Download Key - if (!class_exists('Joomla\Component\Installer\Administrator\Helper\InstallerHelper')) - { - require_once JPATH_COMPONENT_ADMINISTRATOR . '/Helper/InstallerHelper.php'; - } - - $mappingCallback = function ($item) { - $dlkeyInfo = CmsInstallerHelper::getDownloadKey(new CMSObject($item)); - $item->isMissingDownloadKey = $dlkeyInfo['supported'] && !$dlkeyInfo['valid']; - - if ($item->isMissingDownloadKey) - { - $this->missingDownloadKeys++; - } - - return $item; - }; - $this->items = array_map($mappingCallback, $this->items); - - if ($this->missingDownloadKeys) - { - $url = 'index.php?option=com_installer&view=updatesites&filter[supported]=-1'; - $msg = Text::plural('COM_INSTALLER_UPDATE_MISSING_DOWNLOADKEY_LABEL_N', $this->missingDownloadKeys, $url); - Factory::getApplication()->enqueueMessage($msg, CMSApplication::MSG_WARNING); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - if (false === $this->isEmptyState) - { - ToolbarHelper::custom('update.update', 'upload', '', 'COM_INSTALLER_TOOLBAR_UPDATE', true); - } - - ToolbarHelper::custom('update.find', 'refresh', '', 'COM_INSTALLER_TOOLBAR_FIND_UPDATES', false); - ToolbarHelper::divider(); - - parent::addToolbar(); - ToolbarHelper::help('Extensions:_Update'); - } + /** + * List of update items. + * + * @var array + */ + protected $items; + + /** + * List pagination. + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * How many updates require but are missing Download Keys + * + * @var integer + * @since 4.0.0 + */ + protected $missingDownloadKeys = 0; + + /** + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view. + * + * @param string $tpl Template + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + // Get data from the model. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + $paths = new \stdClass(); + $paths->first = ''; + + $this->paths = &$paths; + + if (count($this->items) === 0 && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } else { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE'), 'warning'); + } + + // Find if there are any updates which require but are missing a Download Key + if (!class_exists('Joomla\Component\Installer\Administrator\Helper\InstallerHelper')) { + require_once JPATH_COMPONENT_ADMINISTRATOR . '/Helper/InstallerHelper.php'; + } + + $mappingCallback = function ($item) { + $dlkeyInfo = CmsInstallerHelper::getDownloadKey(new CMSObject($item)); + $item->isMissingDownloadKey = $dlkeyInfo['supported'] && !$dlkeyInfo['valid']; + + if ($item->isMissingDownloadKey) { + $this->missingDownloadKeys++; + } + + return $item; + }; + $this->items = array_map($mappingCallback, $this->items); + + if ($this->missingDownloadKeys) { + $url = 'index.php?option=com_installer&view=updatesites&filter[supported]=-1'; + $msg = Text::plural('COM_INSTALLER_UPDATE_MISSING_DOWNLOADKEY_LABEL_N', $this->missingDownloadKeys, $url); + Factory::getApplication()->enqueueMessage($msg, CMSApplication::MSG_WARNING); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + if (false === $this->isEmptyState) { + ToolbarHelper::custom('update.update', 'upload', '', 'COM_INSTALLER_TOOLBAR_UPDATE', true); + } + + ToolbarHelper::custom('update.find', 'refresh', '', 'COM_INSTALLER_TOOLBAR_FIND_UPDATES', false); + ToolbarHelper::divider(); + + parent::addToolbar(); + ToolbarHelper::help('Extensions:_Update'); + } } diff --git a/code/administrator/components/com_installer/src/View/Updatesite/HtmlView.php b/code/administrator/components/com_installer/src/View/Updatesite/HtmlView.php index eff7ee4e..c7919898 100644 --- a/code/administrator/components/com_installer/src/View/Updatesite/HtmlView.php +++ b/code/administrator/components/com_installer/src/View/Updatesite/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->form = $model->getForm(); - $this->item = $model->getItem(); - - // Remove the extra_query field if it's a free download extension - $dlidSupportingSites = InstallerHelper::getDownloadKeySupportedSites(false); - $update_site_id = $this->item->get('update_site_id'); - - if (!in_array($update_site_id, $dlidSupportingSites)) - { - $this->form->removeField('extra_query'); - } - - // Check for errors. - if (count($errors = $model->getErrors())) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - * - * @throws \Exception - */ - protected function addToolbar(): void - { - $app = Factory::getApplication(); - $app->input->set('hidemainmenu', true); - - $user = $app->getIdentity(); - $userId = $user->id; - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out === $userId); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_installer', 'updatesite'); - - ToolbarHelper::title(Text::_('COM_INSTALLER_UPDATESITE_EDIT_TITLE'), 'address contact'); - - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit'); - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable && $this->form->getField('extra_query')) - { - $toolbarButtons[] = ['apply', 'updatesite.apply']; - $toolbarButtons[] = ['save', 'updatesite.save']; - } - - ToolbarHelper::saveGroup($toolbarButtons); - - ToolbarHelper::cancel('updatesite.cancel', 'JTOOLBAR_CLOSE'); - - ToolbarHelper::help('Edit_Update_Site'); - } + /** + * The Form object + * + * @var Form + * + * @since 4.0.0 + */ + protected $form; + + /** + * The active item + * + * @var object + * + * @since 4.0.0 + */ + protected $item; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + * + * @throws \Exception + */ + public function display($tpl = null): void + { + /** @var UpdatesiteModel $model */ + $model = $this->getModel(); + $this->form = $model->getForm(); + $this->item = $model->getItem(); + + // Remove the extra_query field if it's a free download extension + $dlidSupportingSites = InstallerHelper::getDownloadKeySupportedSites(false); + $update_site_id = $this->item->get('update_site_id'); + + if (!in_array($update_site_id, $dlidSupportingSites)) { + $this->form->removeField('extra_query'); + } + + // Check for errors. + if (count($errors = $model->getErrors())) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + * + * @throws \Exception + */ + protected function addToolbar(): void + { + $app = Factory::getApplication(); + $app->input->set('hidemainmenu', true); + + $user = $app->getIdentity(); + $userId = $user->id; + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out === $userId); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_installer', 'updatesite'); + + ToolbarHelper::title(Text::_('COM_INSTALLER_UPDATESITE_EDIT_TITLE'), 'address contact'); + + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit'); + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable && $this->form->getField('extra_query')) { + $toolbarButtons[] = ['apply', 'updatesite.apply']; + $toolbarButtons[] = ['save', 'updatesite.save']; + } + + ToolbarHelper::saveGroup($toolbarButtons); + + ToolbarHelper::cancel('updatesite.cancel', 'JTOOLBAR_CLOSE'); + + ToolbarHelper::help('Edit_Update_Site'); + } } diff --git a/code/administrator/components/com_installer/src/View/Updatesites/HtmlView.php b/code/administrator/components/com_installer/src/View/Updatesites/HtmlView.php index 3041f780..11318dbe 100644 --- a/code/administrator/components/com_installer/src/View/Updatesites/HtmlView.php +++ b/code/administrator/components/com_installer/src/View/Updatesites/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - // Check for errors. - if (count($errors = $model->getErrors())) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Display the view - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.4 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_installer'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('updatesites.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $childBar->unpublish('updatesites.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - - if ($canDo->get('core.delete')) - { - $childBar->delete('updatesites.delete')->listCheck(true); - } - - $childBar->checkin('updatesites.checkin')->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::custom('updatesites.rebuild', 'refresh', '', 'JTOOLBAR_REBUILD', false); - } - - parent::addToolbar(); - - ToolbarHelper::help('Extensions:_Update_Sites'); - } + /** + * The search tools form + * + * @var Form + * @since 3.4 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 3.4 + */ + public $activeFilters = []; + + /** + * List of updatesites + * + * @var \stdClass[] + * @since 3.4 + */ + protected $items; + + /** + * Pagination object + * + * @var Pagination + * @since 3.4 + */ + protected $pagination; + + /** + * Display the view + * + * @param string $tpl Template + * + * @return mixed|void + * + * @since 3.4 + * + * @throws \Exception on errors + */ + public function display($tpl = null): void + { + /** @var UpdatesitesModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + // Check for errors. + if (count($errors = $model->getErrors())) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Display the view + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.4 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_installer'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('updatesites.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $childBar->unpublish('updatesites.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + + if ($canDo->get('core.delete')) { + $childBar->delete('updatesites.delete')->listCheck(true); + } + + $childBar->checkin('updatesites.checkin')->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::custom('updatesites.rebuild', 'refresh', '', 'JTOOLBAR_REBUILD', false); + } + + parent::addToolbar(); + + ToolbarHelper::help('Extensions:_Update_Sites'); + } } diff --git a/code/administrator/components/com_installer/src/View/Warnings/HtmlView.php b/code/administrator/components/com_installer/src/View/Warnings/HtmlView.php index 64520408..3fb2f544 100644 --- a/code/administrator/components/com_installer/src/View/Warnings/HtmlView.php +++ b/code/administrator/components/com_installer/src/View/Warnings/HtmlView.php @@ -1,4 +1,5 @@ messages = $this->get('Items'); - - if (!\count($this->messages)) - { - $this->setLayout('emptystate'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - parent::addToolbar(); - - ToolbarHelper::help('Information:_Warnings'); - } + /** + * Display the view + * + * @param string $tpl Template + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->messages = $this->get('Items'); + + if (!\count($this->messages)) { + $this->setLayout('emptystate'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + parent::addToolbar(); + + ToolbarHelper::help('Information:_Warnings'); + } } diff --git a/code/administrator/components/com_installer/tmpl/database/default.php b/code/administrator/components/com_installer/tmpl/database/default.php index d6351455..ac7d87af 100644 --- a/code/administrator/components/com_installer/tmpl/database/default.php +++ b/code/administrator/components/com_installer/tmpl/database/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirection = $this->escape($this->state->get('list.direction')); ?>
-
-
-
-
- $this)); ?> - changeSet)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - changeSet as $i => $item) : ?> - - manifest_cache); ?> + +
+
+
+ $this)); ?> + changeSet)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + changeSet as $i => $item) : ?> + + manifest_cache); ?> - - - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + +
- extension_id, false, 'cid', 'cb', $extension->name); ?> - - name; ?> -
- description); ?> -
-
- client_translated; ?> - - type_translated; ?> - - - - - - - version_id; ?> - - version; ?> - - folder_translated; ?> - - extension_id; ?> -
+ + + extension_id, false, 'cid', 'cb', $extension->name); ?> + + + name; ?> +
+ description); ?> +
+ + + client_translated; ?> + + + type_translated; ?> + + + + + + + + + version_id; ?> + + + version; ?> + + + folder_translated; ?> + + + extension_id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
-
+ + + + +
+ + + diff --git a/code/administrator/components/com_installer/tmpl/discover/default.php b/code/administrator/components/com_installer/tmpl/discover/default.php index c01ea5f9..c2b850b8 100644 --- a/code/administrator/components/com_installer/tmpl/discover/default.php +++ b/code/administrator/components/com_installer/tmpl/discover/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
-
-
-
-
- showMessage) : ?> - loadTemplate('message'); ?> - - $this)); ?> - items)) : ?> -
- - -
-
- - -
- - - - - - - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - -
- extension_id, false, 'cid', 'cb', $item->name); ?> - - name; ?> -
description; ?>
-
- client_translated; ?> - - type_translated; ?> - - version) ? $item->version : ' '; ?> - - creationDate) ? $item->creationDate : ' '; ?> - - author) ? $item->author : ' '; ?> - - folder_translated; ?> - - extension_id; ?> -
+ +
+
+
+ showMessage) : ?> + loadTemplate('message'); ?> + + $this)); ?> + items)) : ?> +
+ + +
+
+ + +
+ + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + +
+ extension_id, false, 'cid', 'cb', $item->name); ?> + + name; ?> +
description; ?>
+
+ client_translated; ?> + + type_translated; ?> + + version) ? $item->version : ' '; ?> + + creationDate) ? $item->creationDate : ' '; ?> + + author) ? $item->author : ' '; ?> + + folder_translated; ?> + + extension_id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
- + + + + +
+
+
+
diff --git a/code/administrator/components/com_installer/tmpl/discover/emptystate.php b/code/administrator/components/com_installer/tmpl/discover/emptystate.php index 547c2b77..0d56305b 100644 --- a/code/administrator/components/com_installer/tmpl/discover/emptystate.php +++ b/code/administrator/components/com_installer/tmpl/discover/emptystate.php @@ -1,4 +1,5 @@ 'COM_INSTALLER', - 'formURL' => 'index.php?option=com_installer&task=discover.refresh', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Extensions:_Discover', - 'icon' => 'icon-puzzle-piece install', - 'createURL' => 'index.php?option=com_installer&task=discover.refresh&' . Session::getFormToken() . '=1', - 'content' => Text::_('COM_INSTALLER_MSG_DISCOVER_DESCRIPTION'), - 'title' => Text::_('COM_INSTALLER_EMPTYSTATE_DISCOVER_TITLE'), - 'btnadd' => Text::_('COM_INSTALLER_EMPTYSTATE_DISCOVER_BUTTON_ADD'), + 'textPrefix' => 'COM_INSTALLER', + 'formURL' => 'index.php?option=com_installer&task=discover.refresh', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Extensions:_Discover', + 'icon' => 'icon-puzzle-piece install', + 'createURL' => 'index.php?option=com_installer&task=discover.refresh&' . Session::getFormToken() . '=1', + 'content' => Text::_('COM_INSTALLER_MSG_DISCOVER_DESCRIPTION'), + 'title' => Text::_('COM_INSTALLER_EMPTYSTATE_DISCOVER_TITLE'), + 'btnadd' => Text::_('COM_INSTALLER_EMPTYSTATE_DISCOVER_BUTTON_ADD'), ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_installer/tmpl/install/default.php b/code/administrator/components/com_installer/tmpl/install/default.php index 151bf9fa..1c2b115a 100644 --- a/code/administrator/components/com_installer/tmpl/install/default.php +++ b/code/administrator/components/com_installer/tmpl/install/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->usePreset('com_installer.installer') - ->useScript('webcomponent.core-loader'); + ->usePreset('com_installer.installer') + ->useScript('webcomponent.core-loader'); $app = Factory::getApplication(); $tabs = $app->triggerEvent('onInstallerAddInstallationTab', []); @@ -34,42 +35,42 @@ ?>
-
- - showMessage) : ?> - loadTemplate('message'); ?> - + + + showMessage) : ?> + loadTemplate('message'); ?> + -
-
-
- -
- - -
- +
+
+
+ +
+ + +
+ - - $tabs[0]['name'] ?? '', 'recall' => true, 'breakpoint' => 768]); ?> - - - -
- -
- - + + $tabs[0]['name'] ?? '', 'recall' => true, 'breakpoint' => 768]); ?> + + + +
+ +
+ + - - + + - - - -
-
-
- + + + +
+
+
+
diff --git a/code/administrator/components/com_installer/tmpl/installer/default_message.php b/code/administrator/components/com_installer/tmpl/installer/default_message.php index b0e8170b..3d1c78d6 100644 --- a/code/administrator/components/com_installer/tmpl/installer/default_message.php +++ b/code/administrator/components/com_installer/tmpl/installer/default_message.php @@ -1,4 +1,5 @@ -
- -
+
+ +
-
- -
+
+ +
diff --git a/code/administrator/components/com_installer/tmpl/languages/default.php b/code/administrator/components/com_installer/tmpl/languages/default.php index bf329b4d..e17347af 100644 --- a/code/administrator/components/com_installer/tmpl/languages/default.php +++ b/code/administrator/components/com_installer/tmpl/languages/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
-
-
-
- $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - getShortVersion()); - $i = 0; - foreach ($this->items as $language) : - preg_match('#^pkg_([a-z]{2,3}-[A-Z]{2})$#', $language->element, $element); - $language->code = $element[1]; - ?> - - - - - - - - - - -
- , - , - -
- - - - - - - -
- installedLang[0][$language->code]) || isset($this->installedLang[1][$language->code])) ? 'REINSTALL' : 'INSTALL'; ?> - installedLang[0][$language->code]) || isset($this->installedLang[1][$language->code])) ? 'btn btn-success btn-sm' : 'btn btn-primary btn-sm'; ?> - detailsurl . '\'; Joomla.submitbutton(\'install.install\');'; ?> - - - name; ?> - - code; ?> - - - - version, $minorVersion) !== 0 || strpos($language->version, $currentShortVersion) !== 0) : ?> - version; ?> - - - - version; ?> - - - detailsurl; ?> -
+ +
+
+
+ $this, 'options' => array('filterButton' => false))); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + getShortVersion()); + $i = 0; + foreach ($this->items as $language) : + preg_match('#^pkg_([a-z]{2,3}-[A-Z]{2})$#', $language->element, $element); + $language->code = $element[1]; + ?> + + + + + + + + + + +
+ , + , + +
+ + + + + + + +
+ installedLang[0][$language->code]) || isset($this->installedLang[1][$language->code])) ? 'REINSTALL' : 'INSTALL'; ?> + installedLang[0][$language->code]) || isset($this->installedLang[1][$language->code])) ? 'btn btn-success btn-sm' : 'btn btn-primary btn-sm'; ?> + detailsurl . '\'; Joomla.submitbutton(\'install.install\');'; ?> + + + name; ?> + + code; ?> + + + + version, $minorVersion) !== 0 || strpos($language->version, $currentShortVersion) !== 0) : ?> + version; ?> + + + + version; ?> + + + detailsurl; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - - - - - -
-
-
- + + + + + + + + +
+
+
+
diff --git a/code/administrator/components/com_installer/tmpl/manage/default.php b/code/administrator/components/com_installer/tmpl/manage/default.php index a68c49b1..52fb8018 100644 --- a/code/administrator/components/com_installer/tmpl/manage/default.php +++ b/code/administrator/components/com_installer/tmpl/manage/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect') - ->useScript('com_installer.changelog'); +$wa->useScript('com_installer.changelog') + ->useScript('table.columns') + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
-
-
-
- showMessage) : ?> - loadTemplate('message'); ?> - - $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - - - - - - - -
- extension_id, false, 'cid', 'cb', $item->name); ?> - - element) : ?> - X - - status, $i, $item->status < 2, 'cb'); ?> - - - name; ?> - - - client_translated; ?> - - type_translated; ?> - - version)) : ?> - changelogurl)) : ?> - - version?> - - extension_id, - array( - 'title' => Text::sprintf('COM_INSTALLER_CHANGELOG_TITLE', $item->name, $item->version), - ), - '' - ); - ?> - - version; ?> - - - - creationDate) ? $item->creationDate : ' '; ?> - - author) ? $item->author : ' '; ?> - - folder_translated; ?> - - locked ? Text::_('JYES') : Text::_('JNO'); ?> - - package_id ?: ' '; ?> - - extension_id; ?> -
+ +
+
+
+ showMessage) : ?> + loadTemplate('message'); ?> + + $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ extension_id, false, 'cid', 'cb', $item->name); ?> + + element) : ?> + X + + status, $i, $item->status < 2, 'cb'); ?> + + + name; ?> + + + client_translated; ?> + + type_translated; ?> + + version)) : ?> + changelogurl)) : ?> + + version?> + + extension_id, + array( + 'title' => Text::sprintf('COM_INSTALLER_CHANGELOG_TITLE', $item->name, $item->version), + ), + '' + ); + ?> + + version; ?> + + + + creationDate)) : ?> + creationDate, $createdDateFormat); + } catch (Exception $e) { + echo $item->creationDate; + }?> + + + + + author) ? $item->author : ' '; ?> + + folder_translated; ?> + + locked ? Text::_('JYES') : Text::_('JNO'); ?> + + package_id ?: ' '; ?> + + extension_id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
- + + + + +
+
+
+
diff --git a/code/administrator/components/com_installer/tmpl/update/default.php b/code/administrator/components/com_installer/tmpl/update/default.php index a5444026..ea0fe568 100644 --- a/code/administrator/components/com_installer/tmpl/update/default.php +++ b/code/administrator/components/com_installer/tmpl/update/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('multiselect') - ->useScript('com_installer.changelog'); + ->useScript('com_installer.changelog'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
-
-
-
- showMessage) : ?> - loadTemplate('message'); ?> - - $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - items as $i => $item): ?> - - - - - - - - + + + + + +
- , - , - -
- - - - - - - - - - - - - - - - - -
- isMissingDownloadKey): ?> - - - update_id, false, 'cid', 'cb', $item->name); ?> - - - escape($item->name); ?> - -
- detailsurl; ?> - infourl)) : ?> -
- escape(trim($item->infourl)); ?> - -
- isMissingDownloadKey): ?> - update_site_id; ?> - - -
- client_translated; ?> - - type_translated; ?> - - current_version; ?> - - version; ?> - - changelogurl)) : ?> - - - - extension_id, - array( - 'title' => Text::sprintf('COM_INSTALLER_CHANGELOG_TITLE', $item->name, $item->version), - ), - '' - ); - ?> - - - - + +
+
+
+ showMessage) : ?> + loadTemplate('message'); ?> + + $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + +
+ isMissingDownloadKey) : ?> + + + update_id, false, 'cid', 'cb', $item->name); ?> + + + escape($item->name); ?> + +
+ detailsurl; ?> + infourl)) : ?> +
+ escape(trim($item->infourl)); ?> + +
+ isMissingDownloadKey) : ?> + update_site_id; ?> + + +
+ client_translated; ?> + + type_translated; ?> + + current_version; ?> + + version; ?> + + changelogurl)) : ?> + + + + extension_id, + array( + 'title' => Text::sprintf('COM_INSTALLER_CHANGELOG_TITLE', $item->name, $item->version), + ), + '' + ); + ?> + + + + - - - folder_translated; ?> - - install_type; ?> -
+ +
+ folder_translated; ?> + + install_type; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
-
+ + + + +
+ + + diff --git a/code/administrator/components/com_installer/tmpl/update/emptystate.php b/code/administrator/components/com_installer/tmpl/update/emptystate.php index 923e5613..172cfd66 100644 --- a/code/administrator/components/com_installer/tmpl/update/emptystate.php +++ b/code/administrator/components/com_installer/tmpl/update/emptystate.php @@ -1,4 +1,5 @@ 'COM_INSTALLER', - 'formURL' => 'index.php?option=com_installer&view=update', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Extensions:_Update', - 'icon' => 'icon-puzzle-piece install', + 'textPrefix' => 'COM_INSTALLER', + 'formURL' => 'index.php?option=com_installer&view=update', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Extensions:_Update', + 'icon' => 'icon-puzzle-piece install', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_installer&task=update.find&' . Session::getFormToken() . '=1'; +if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_installer&task=update.find&' . Session::getFormToken() . '=1'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_installer/tmpl/updatesite/edit.php b/code/administrator/components/com_installer/tmpl/updatesite/edit.php index 25bce987..0c252686 100644 --- a/code/administrator/components/com_installer/tmpl/updatesite/edit.php +++ b/code/administrator/components/com_installer/tmpl/updatesite/edit.php @@ -1,4 +1,5 @@

item->name; ?>

- form->renderFieldset('updateSite'); ?> - - + form->renderFieldset('updateSite'); ?> + +
diff --git a/code/administrator/components/com_installer/tmpl/updatesites/default.php b/code/administrator/components/com_installer/tmpl/updatesites/default.php index 40371a32..79e90252 100644 --- a/code/administrator/components/com_installer/tmpl/updatesites/default.php +++ b/code/administrator/components/com_installer/tmpl/updatesites/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getApplication()->getIdentity(); $userId = $user->get('id'); @@ -25,136 +27,139 @@ $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
-
-
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - items as $i => $item) : - $canCheckin = $user->authorise('core.manage', 'com_checkin') - || $item->checked_out === $userId - || is_null($item->checked_out); - $canEdit = $user->authorise('core.edit', 'com_installer'); - ?> - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - -
- update_site_id, false, 'cid', 'cb', $item->update_site_name); ?> - - element) : ?> - X - - enabled, $i, $item->enabled < 2, 'cb'); ?> - - - checked_out) : ?> - editor, $item->checked_out_time, 'updatesites.', $canCheckin); ?> - - - - update_site_name); ?> - - - update_site_name); ?> - - -
- downloadKey['valid']) : ?> - - - - downloadKey['value']; ?> - downloadKey['supported']) : ?> - - - - - -
-
- - name; ?> - - - - client_translated; ?> - - type_translated; ?> - - folder_translated; ?> - - update_site_id; ?> -
+ +
+
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + items as $i => $item) : + $canCheckin = $user->authorise('core.manage', 'com_checkin') + || $item->checked_out === $userId + || is_null($item->checked_out); + $canEdit = $user->authorise('core.edit', 'com_installer'); + ?> + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + +
+ update_site_id, false, 'cid', 'cb', $item->update_site_name); ?> + + element) : ?> + X + + enabled, $i, $item->enabled < 2, 'cb'); ?> + + + checked_out) : ?> + editor, $item->checked_out_time, 'updatesites.', $canCheckin); ?> + + + + update_site_name); ?> + + + update_site_name); ?> + + +
+ downloadKey['valid']) : ?> + + + + downloadKey['value']; ?> + downloadKey['supported']) : ?> + + + + + +
+
+ + name; ?> + + + + client_translated; ?> + + type_translated; ?> + + folder_translated; ?> + + update_site_id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
- + + + + +
+
+
+
diff --git a/code/administrator/components/com_installer/tmpl/warnings/default.php b/code/administrator/components/com_installer/tmpl/warnings/default.php index bcf4c662..8bd5bc8a 100644 --- a/code/administrator/components/com_installer/tmpl/warnings/default.php +++ b/code/administrator/components/com_installer/tmpl/warnings/default.php @@ -1,4 +1,5 @@
-
-
-
-
- messages)) : ?> - messages as $message) : ?> -
-

- - - -

-

-
- -
-

- - - -

-

-
- -
- - - -
- -
- - -
-
-
-
-
+
+
+
+
+ messages)) : ?> + messages as $message) : ?> +
+

+ + + +

+

+
+ +
+

+ + + +

+

+
+ +
+ + + +
+ +
+ + +
+
+
+
+
diff --git a/code/administrator/components/com_installer/tmpl/warnings/emptystate.php b/code/administrator/components/com_installer/tmpl/warnings/emptystate.php index 03ebbd1b..82397e9b 100644 --- a/code/administrator/components/com_installer/tmpl/warnings/emptystate.php +++ b/code/administrator/components/com_installer/tmpl/warnings/emptystate.php @@ -1,4 +1,5 @@ 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Information:_Warnings', - 'icon' => 'icon-puzzle-piece install', - 'title' => Text::_('COM_INSTALLER_MSG_WARNINGS_NONE'), - 'content' => '', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Information:_Warnings', + 'icon' => 'icon-puzzle-piece install', + 'title' => Text::_('COM_INSTALLER_MSG_WARNINGS_NONE'), + 'content' => '', ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_joomlaupdate/config.xml b/code/administrator/components/com_joomlaupdate/config.xml index 18fea2ce..938aed8b 100644 --- a/code/administrator/components/com_joomlaupdate/config.xml +++ b/code/administrator/components/com_joomlaupdate/config.xml @@ -49,5 +49,29 @@ showon="updatesource:custom" /> + + + + + + + + + +
diff --git a/code/administrator/components/com_joomlaupdate/extract.php b/code/administrator/components/com_joomlaupdate/extract.php index c59dc7b0..737e15a3 100644 --- a/code/administrator/components/com_joomlaupdate/extract.php +++ b/code/administrator/components/com_joomlaupdate/extract.php @@ -1,10 +1,13 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ /** @@ -42,1732 +45,1632 @@ */ class ZIPExtraction { - /** - * How much data to read at once when processing files - * - * @var int - * @since 4.0.4 - */ - private const CHUNK_SIZE = 524288; - - /** - * Maximum execution time (seconds). - * - * Each page load will take at most this much time. Please note that if the ZIP archive contains fairly large, - * compressed files we may overshoot this time since we can't interrupt the decompression. This should not be an - * issue in the context of updating Joomla as the ZIP archive contains fairly small files. - * - * If this is too low it will cause too many requests to hit the server, potentially triggering a DoS protection and - * causing the extraction to fail. If this is too big the extraction will not be as verbose and the user might think - * something is broken. A value between 3 and 7 seconds is, therefore, recommended. - * - * @var int - * @since 4.0.4 - */ - private const MAX_EXEC_TIME = 4; - - /** - * Run-time execution bias (percentage points). - * - * We evaluate the time remaining on the timer before processing each file on the ZIP archive. If we have already - * consumed at least this much percentage of the MAX_EXEC_TIME we will stop processing the archive in this page - * load, return the result to the client and wait for it to call us again so we can resume the extraction. - * - * This becomes important when the MAX_EXEC_TIME is close to the PHP, PHP-FPM or Apache timeout on the server - * (whichever is lowest) and there are fairly large files in the backup archive. If we start extracting a large, - * compressed file close to a hard server timeout it's possible that we will overshoot that hard timeout and see the - * extraction failing. - * - * Since Joomla Update is used to extract a ZIP archive with many small files we can keep at a fairly high 90% - * without much fear that something will break. - * - * Example: if MAX_EXEC_TIME is 10 seconds and RUNTIME_BIAS is 80 each page load will take between 80% and 100% of - * the MAX_EXEC_TIME, i.e. anywhere between 8 and 10 seconds. - * - * Lower values make it less likely to overshoot MAX_EXEC_TIME when extracting large files. - * - * @var int - * @since 4.0.4 - */ - private const RUNTIME_BIAS = 90; - - /** - * Minimum execution time (seconds). - * - * A request cannot take less than this many seconds. If it does, we add “dead time” (sleep) where the script does - * nothing except wait. This is essentially a rate limiting feature to avoid hitting a server-side DoS protection - * which could be triggered if we ended up sending too many requests in a limited amount of time. - * - * This should normally be less than MAX_EXEC * (RUNTIME_BIAS / 100). Values between that and MAX_EXEC_TIME have the - * effect of almost always adding dead time in each request, unless a really large file is being extracted from the - * ZIP archive. Values larger than MAX_EXEC will always add dead time to the request. This can be useful to - * artificially reduce the CPU usage limit. Some servers might kill the request if they see a sustained CPU usage - * spike over a short period of time. - * - * The chosen value of 3 seconds belongs to the first category, essentially making sure that we have a decent rate - * limiting without annoying the user too much but also catering for the most badly configured of shared - * hosting. It's a happy medium which works for the majority (~90%) of commercial servers out there. - * - * @var int - * @since 4.0.4 - */ - private const MIN_EXEC_TIME = 3; - - /** - * Internal state when extracting files: we need to be initialised - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_INITIALIZE = -1; - - /** - * Internal state when extracting files: no file currently being extracted - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_NOFILE = 0; - - /** - * Internal state when extracting files: reading the file header - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_HEADER = 1; - - /** - * Internal state when extracting files: reading file data - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_DATA = 2; - - /** - * Internal state when extracting files: file data has been read thoroughly - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_DATAREAD = 3; - - /** - * Internal state when extracting files: post-processing the file - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_POSTPROC = 4; - - /** - * Internal state when extracting files: done with this file - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_DONE = 5; - - /** - * Internal state when extracting files: finished extracting the ZIP file - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_FINISHED = 999; - - /** - * Internal logging level: debug - * - * @var int - * @since 4.0.4 - */ - private const LOG_DEBUG = 1; - - /** - * Internal logging level: information - * - * @var int - * @since 4.0.4 - */ - private const LOG_INFO = 10; - - /** - * Internal logging level: warning - * - * @var int - * @since 4.0.4 - */ - private const LOG_WARNING = 50; - - /** - * Internal logging level: error - * - * @var int - * @since 4.0.4 - */ - private const LOG_ERROR = 90; - - /** - * Singleton instance - * - * @var null|self - * @since 4.0.4 - */ - private static $instance = null; - - /** - * Debug log file pointer resource - * - * @var null|resource|boolean - * @since 4.0.4 - */ - private static $logFP = null; - - /** - * Debug log filename - * - * @var null|string - * @since 4.0.4 - */ - private static $logFilePath = null; - - /** - * The total size of the ZIP archive - * - * @var integer - * @since 4.0.4 - */ - public $totalSize = 0; - - /** - * Which files to skip - * - * @var array - * @since 4.0.4 - */ - public $skipFiles = []; - - /** - * Current tally of compressed size read - * - * @var integer - * @since 4.0.4 - */ - public $compressedTotal = 0; - - /** - * Current tally of bytes written to disk - * - * @var integer - * @since 4.0.4 - */ - public $uncompressedTotal = 0; - - /** - * Current tally of files extracted - * - * @var integer - * @since 4.0.4 - */ - public $filesProcessed = 0; - - /** - * Maximum execution time allowance per step - * - * @var integer - * @since 4.0.4 - */ - private $maxExecTime = null; - - /** - * Timestamp of execution start - * - * @var integer - * @since 4.0.4 - */ - private $startTime; - - /** - * The last error message - * - * @var string|null - * @since 4.0.4 - */ - private $lastErrorMessage = null; - - /** - * Archive filename - * - * @var string - * @since 4.0.4 - */ - private $filename = null; - - /** - * Current archive part number - * - * @var boolean - * @since 4.0.4 - */ - private $archiveFileIsBeingRead = false; - - /** - * The offset inside the current part - * - * @var integer - * @since 4.0.4 - */ - private $currentOffset = 0; - - /** - * Absolute path to prepend to extracted files - * - * @var string - * @since 4.0.4 - */ - private $addPath = ''; - - /** - * File pointer to the current archive part file - * - * @var resource|null - * @since 4.0.4 - */ - private $fp = null; - - /** - * Run state when processing the current archive file - * - * @var integer - * @since 4.0.4 - */ - private $runState = self::AK_STATE_INITIALIZE; - - /** - * File header data, as read by the readFileHeader() method - * - * @var stdClass - * @since 4.0.4 - */ - private $fileHeader = null; - - /** - * How much of the uncompressed data we've read so far - * - * @var integer - * @since 4.0.4 - */ - private $dataReadLength = 0; - - /** - * Unwritable files in these directories are always ignored and do not cause errors when not - * extracted. - * - * @var array - * @since 4.0.4 - */ - private $ignoreDirectories = []; - - /** - * Internal flag, set when the ZIP file has a data descriptor (which we will be ignoring) - * - * @var boolean - * @since 4.0.4 - */ - private $expectDataDescriptor = false; - - /** - * The UNIX last modification timestamp of the file last extracted - * - * @var integer - * @since 4.0.4 - */ - private $lastExtractedFileTimestamp = 0; - - /** - * The file path of the file last extracted - * - * @var string - * @since 4.0.4 - */ - private $lastExtractedFilename = null; - - /** - * Public constructor. - * - * Sets up the internal timer. - * - * @since 4.0.4 - */ - public function __construct() - { - $this->setupMaxExecTime(); - - // Initialize start time - $this->startTime = microtime(true); - } - - /** - * Singleton implementation. - * - * @return static - * @since 4.0.4 - */ - public static function getInstance(): self - { - if (is_null(self::$instance)) - { - self::$instance = new self; - } - - return self::$instance; - } - - /** - * Returns a serialised copy of the object. - * - * This is different to calling serialise() directly. This operates on a copy of the object which undergoes a - * call to shutdown() first so any open files are closed first. - * - * @return string The serialised data, potentially base64 encoded. - * @since 4.0.4 - */ - public static function getSerialised(): string - { - $clone = clone self::getInstance(); - $clone->shutdown(); - $serialized = serialize($clone); - - return (function_exists('base64_encode') && function_exists('base64_decode')) ? base64_encode($serialized) : $serialized; - } - - /** - * Restores a serialised instance into the singleton implementation and returns it. - * - * If the serialised data is corrupt it will return null. - * - * @param string $serialised The serialised data, potentially base64 encoded, to deserialize. - * - * @return static|null The instance of the object, NULL if it cannot be deserialised. - * @since 4.0.4 - */ - public static function unserialiseInstance(string $serialised): ?self - { - if (function_exists('base64_encode') && function_exists('base64_decode')) - { - $serialised = base64_decode($serialised); - } - - $instance = @unserialize($serialised, [ - 'allowed_classes' => [ - self::class, - stdClass::class, - ], - ] - ); - - if (($instance === false) || !is_object($instance) || !($instance instanceof self)) - { - return null; - } - - self::$instance = $instance; - - return self::$instance; - } - - /** - * Wakeup function, called whenever the class is deserialized. - * - * This method does the following: - * - Restart the timer. - * - Reopen the archive file, if one is defined. - * - Seek to the correct offset of the file. - * - * @return void - * @since 4.0.4 - * @internal - */ - public function __wakeup(): void - { - // Reset the timer when deserializing the object. - $this->startTime = microtime(true); - - if (!$this->archiveFileIsBeingRead) - { - return; - } - - $this->fp = @fopen($this->filename, 'rb'); - - if ((is_resource($this->fp)) && ($this->currentOffset > 0)) - { - @fseek($this->fp, $this->currentOffset); - } - } - - /** - * Enforce the minimum execution time. - * - * @return void - * @since 4.0.4 - */ - public function enforceMinimumExecutionTime() - { - $elapsed = $this->getRunningTime() * 1000; - $minExecTime = 1000.0 * min(1, (min(self::MIN_EXEC_TIME, $this->getPhpMaxExecTime()) - 1)); - - // Only run a sleep delay if we haven't reached the minimum execution time - if (($minExecTime <= $elapsed) || ($elapsed <= 0)) - { - return; - } - - $sleepMillisec = intval($minExecTime - $elapsed); - - /** - * If we need to sleep for more than 1 second we should be using sleep() or time_sleep_until() to prevent high - * CPU usage, also because some OS might not support sleeping for over 1 second using these functions. In all - * other cases we will try to use usleep or time_nanosleep instead. - */ - $longSleep = $sleepMillisec > 1000; - $miniSleepSupported = function_exists('usleep') || function_exists('time_nanosleep'); - - if (!$longSleep && $miniSleepSupported) - { - if (function_exists('usleep') && ($sleepMillisec < 1000)) - { - usleep(1000 * $sleepMillisec); - - return; - } - - if (function_exists('time_nanosleep') && ($sleepMillisec < 1000)) - { - time_nanosleep(0, 1000000 * $sleepMillisec); - - return; - } - } - - if (function_exists('sleep')) - { - sleep(ceil($sleepMillisec / 1000)); - - return; - } - - if (function_exists('time_sleep_until')) - { - time_sleep_until(time() + ceil($sleepMillisec / 1000)); - } - } - - /** - * Set the filepath to the ZIP archive which will be extracted. - * - * @param string $value The filepath to the archive. Only LOCAL files are allowed! - * - * @return void - * @since 4.0.4 - */ - public function setFilename(string $value) - { - // Security check: disallow remote filenames - if (!empty($value) && strpos($value, '://') !== false) - { - $this->setError('Invalid archive location'); - - return; - } - - $this->filename = $value; - $this->initializeLog(dirname($this->filename)); - } - - /** - * Sets the path to prefix all extracted files with. Essentially, where the archive will be extracted to. - * - * @param string $addPath The path where the archive will be extracted. - * - * @return void - * @since 4.0.4 - */ - public function setAddPath(string $addPath): void - { - $this->addPath = $addPath; - $this->addPath = str_replace('\\', '/', $this->addPath); - $this->addPath = rtrim($this->addPath, '/'); - - if (!empty($this->addPath)) - { - $this->addPath .= '/'; - } - } - - /** - * Set the list of files to skip when extracting the ZIP file. - * - * @param array $skipFiles A list of files to skip when extracting the ZIP archive - * - * @return void - * @since 4.0.4 - */ - public function setSkipFiles(array $skipFiles): void - { - $this->skipFiles = array_values($skipFiles); - } - - /** - * Set the directories to skip over when extracting the ZIP archive - * - * @param array $ignoreDirectories The list of directories to ignore. - * - * @return void - * @since 4.0.4 - */ - public function setIgnoreDirectories(array $ignoreDirectories): void - { - $this->ignoreDirectories = array_values($ignoreDirectories); - } - - /** - * Prepares for the archive extraction - * - * @return void - * @since 4.0.4 - */ - public function initialize(): void - { - $this->debugMsg(sprintf('Initializing extraction. Filepath: %s', $this->filename)); - $this->totalSize = @filesize($this->filename) ?: 0; - $this->archiveFileIsBeingRead = false; - $this->currentOffset = 0; - $this->runState = self::AK_STATE_NOFILE; - - $this->readArchiveHeader(); - - if (!empty($this->getError())) - { - $this->debugMsg(sprintf('Error: %s', $this->getError()), self::LOG_ERROR); - - return; - } - - $this->archiveFileIsBeingRead = true; - $this->runState = self::AK_STATE_NOFILE; - - $this->debugMsg('Setting state to NOFILE', self::LOG_DEBUG); - } - - /** - * Executes a step of the archive extraction - * - * @return boolean True if we are done extracting or an error occurred - * @since 4.0.4 - */ - public function step(): bool - { - $status = true; - - $this->debugMsg('Starting a new step', self::LOG_INFO); - - while ($status && ($this->getTimeLeft() > 0)) - { - switch ($this->runState) - { - case self::AK_STATE_INITIALIZE: - $this->debugMsg('Current run state: INITIALIZE', self::LOG_DEBUG); - $this->initialize(); - break; - - case self::AK_STATE_NOFILE: - $this->debugMsg('Current run state: NOFILE', self::LOG_DEBUG); - $status = $this->readFileHeader(); - - if ($status) - { - $this->debugMsg('Found file header; updating number of files processed and bytes in/out', self::LOG_DEBUG); - - // Update running tallies when we start extracting a file - $this->filesProcessed++; - $this->compressedTotal += array_key_exists('compressed', get_object_vars($this->fileHeader)) - ? $this->fileHeader->compressed : 0; - $this->uncompressedTotal += $this->fileHeader->uncompressed; - } - - break; - - case self::AK_STATE_HEADER: - case self::AK_STATE_DATA: - $runStateHuman = $this->runState === self::AK_STATE_HEADER ? 'HEADER' : 'DATA'; - $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); - - $status = $this->processFileData(); - break; - - case self::AK_STATE_DATAREAD: - case self::AK_STATE_POSTPROC: - $runStateHuman = $this->runState === self::AK_STATE_DATAREAD ? 'DATAREAD' : 'POSTPROC'; - $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); - - $this->setLastExtractedFileTimestamp($this->fileHeader->timestamp); - $this->processLastExtractedFile(); - - $status = true; - $this->runState = self::AK_STATE_DONE; - break; - - case self::AK_STATE_DONE: - default: - $this->debugMsg('Current run state: DONE', self::LOG_DEBUG); - $this->runState = self::AK_STATE_NOFILE; - - break; - - case self::AK_STATE_FINISHED: - $this->debugMsg('Current run state: FINISHED', self::LOG_DEBUG); - $status = false; - break; - } - - if ($this->getTimeLeft() <= 0) - { - $this->debugMsg('Ran out of time; the step will break.'); - } - elseif (!$status) - { - $this->debugMsg('Last step status is false; the step will break.'); - } - } - - $error = $this->getError() ?? null; - - if (!empty($error)) - { - $this->debugMsg(sprintf('Step failed with error: %s', $error), self::LOG_ERROR); - } - - // Did we just finish or run into an error? - if (!empty($error) || $this->runState === self::AK_STATE_FINISHED) - { - $this->debugMsg('Returning true (must stop running) from step()', self::LOG_DEBUG); - - // Reset internal state, prevents __wakeup from trying to open a non-existent file - $this->archiveFileIsBeingRead = false; - - return true; - } - - $this->debugMsg('Returning false (must continue running) from step()', self::LOG_DEBUG); - - return false; - } - - /** - * Get the most recent error message - * - * @return string|null The message string, null if there's no error - * @since 4.0.4 - */ - public function getError(): ?string - { - return $this->lastErrorMessage; - } - - /** - * Gets the number of seconds left, before we hit the "must break" threshold - * - * @return float - * @since 4.0.4 - */ - private function getTimeLeft(): float - { - return $this->maxExecTime - $this->getRunningTime(); - } - - /** - * Gets the time elapsed since object creation/unserialization, effectively how - * long Akeeba Engine has been processing data - * - * @return float - * @since 4.0.4 - */ - private function getRunningTime(): float - { - return microtime(true) - $this->startTime; - } - - /** - * Process the last extracted file or directory - * - * This invalidates OPcache for .php files. Also applies the correct permissions and timestamp. - * - * @return void - * @since 4.0.4 - */ - private function processLastExtractedFile(): void - { - $this->debugMsg(sprintf('Processing last extracted entity: %s', $this->lastExtractedFilename), self::LOG_DEBUG); - - if (@is_file($this->lastExtractedFilename)) - { - @chmod($this->lastExtractedFilename, 0644); - - clearFileInOPCache($this->lastExtractedFilename); - } - else - { - @chmod($this->lastExtractedFilename, 0755); - } - - if ($this->lastExtractedFileTimestamp > 0) - { - @touch($this->lastExtractedFilename, $this->lastExtractedFileTimestamp); - } - } - - /** - * Set the last extracted filename - * - * @param string|null $lastExtractedFilename The last extracted filename - * - * @return void - * @since 4.0.4 - */ - private function setLastExtractedFilename(?string $lastExtractedFilename): void - { - $this->lastExtractedFilename = $lastExtractedFilename; - } - - /** - * Set the last modification UNIX timestamp for the last extracted file - * - * @param int $lastExtractedFileTimestamp The timestamp - * - * @return void - * @since 4.0.4 - */ - private function setLastExtractedFileTimestamp(int $lastExtractedFileTimestamp): void - { - $this->lastExtractedFileTimestamp = $lastExtractedFileTimestamp; - } - - /** - * Sleep function, called whenever the class is serialized - * - * @return void - * @since 4.0.4 - * @internal - */ - private function shutdown(): void - { - if (is_resource(self::$logFP)) - { - @fclose(self::$logFP); - } - - if (!is_resource($this->fp)) - { - return; - } - - $this->currentOffset = @ftell($this->fp); - - @fclose($this->fp); - } - - /** - * Unicode-safe binary data length - * - * @param string|null $string The binary data to get the length for - * - * @return integer - * @since 4.0.4 - */ - private function binStringLength(?string $string): int - { - if (is_null($string)) - { - return 0; - } - - if (function_exists('mb_strlen')) - { - return mb_strlen($string, '8bit') ?: 0; - } - - return strlen($string) ?: 0; - } - - /** - * Add an error message - * - * @param string $error Error message - * - * @return void - * @since 4.0.4 - */ - private function setError(string $error): void - { - $this->lastErrorMessage = $error; - } - - /** - * Reads data from the archive. - * - * @param resource $fp The file pointer to read data from - * @param int|null $length The volume of data to read, in bytes - * - * @return string The data read from the file - * @since 4.0.4 - */ - private function fread($fp, ?int $length = null): string - { - $readLength = (is_numeric($length) && ($length > 0)) ? $length : PHP_INT_MAX; - $data = fread($fp, $readLength); - - if ($data === false) - { - $this->debugMsg('No more data could be read from the file', self::LOG_WARNING); - - $data = ''; - } - - return $data; - } - - /** - * Read the header of the archive, making sure it's a valid ZIP file. - * - * @return void - * @since 4.0.4 - */ - private function readArchiveHeader(): void - { - $this->debugMsg('Reading the archive header.', self::LOG_DEBUG); - - // Open the first part - $this->openArchiveFile(); - - // Fail for unreadable files - if ($this->fp === false) - { - return; - } - - // Read the header data. - $sigBinary = fread($this->fp, 4); - $headerData = unpack('Vsig', $sigBinary); - - // We only support single part ZIP files - if ($headerData['sig'] != 0x04034b50) - { - $this->setError('The archive file is corrupt: bad header'); - - return; - } - - // Roll back the file pointer - fseek($this->fp, -4, SEEK_CUR); - - $this->currentOffset = @ftell($this->fp); - $this->dataReadLength = 0; - - } - - /** - * Concrete classes must use this method to read the file header - * - * @return boolean True if reading the file was successful, false if an error occurred or we - * reached end of archive. - * @since 4.0.4 - */ - private function readFileHeader(): bool - { - $this->debugMsg('Reading the file entry header.', self::LOG_DEBUG); - - if (!is_resource($this->fp)) - { - $this->setError('The archive is not open for reading.'); - - return false; - } - - // Unexpected end of file - if ($this->isEOF()) - { - $this->debugMsg('EOF when reading file header data', self::LOG_WARNING); - $this->setError('The archive is corrupt or truncated'); - - return false; - } - - $this->currentOffset = ftell($this->fp); - - if ($this->expectDataDescriptor) - { - $this->debugMsg('Expecting data descriptor (bit 3 of general purpose flag was set).', self::LOG_DEBUG); - - /** - * The last file had bit 3 of the general purpose bit flag set. This means that we have a 12 byte data - * descriptor we need to skip. To make things worse, there might also be a 4 byte optional data descriptor - * header (0x08074b50). - */ - $junk = @fread($this->fp, 4); - $junk = unpack('Vsig', $junk); - $readLength = ($junk['sig'] == 0x08074b50) ? 12 : 8; - $junk = @fread($this->fp, $readLength); - - // And check for EOF, too - if ($this->isEOF()) - { - $this->debugMsg('EOF when reading data descriptor', self::LOG_WARNING); - $this->setError('The archive is corrupt or truncated'); - - return false; - } - } - - // Get and decode Local File Header - $headerBinary = fread($this->fp, 30); - $format = 'Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/' - . 'Vuncomp/vfnamelen/veflen'; - $headerData = unpack($format, $headerBinary); - - // Check signature - if (!($headerData['sig'] == 0x04034b50)) - { - // The signature is not the one used for files. Is this a central directory record (i.e. we're done)? - if ($headerData['sig'] == 0x02014b50) - { - $this->debugMsg('Found Central Directory header; the extraction is complete', self::LOG_DEBUG); - - // End of ZIP file detected. We'll just skip to the end of file... - @fseek($this->fp, 0, SEEK_END); - $this->runState = self::AK_STATE_FINISHED; - - return false; - } - - $this->setError('The archive file is corrupt or truncated'); - - return false; - } - - // If bit 3 of the bitflag is set, expectDataDescriptor is true - $this->expectDataDescriptor = ($headerData['bitflag'] & 4) == 4; - $this->fileHeader = new stdClass; - $this->fileHeader->timestamp = 0; - - // Read the last modified date and time - $lastmodtime = $headerData['lastmodtime']; - $lastmoddate = $headerData['lastmoddate']; - - if ($lastmoddate && $lastmodtime) - { - $vHour = ($lastmodtime & 0xF800) >> 11; - $vMInute = ($lastmodtime & 0x07E0) >> 5; - $vSeconds = ($lastmodtime & 0x001F) * 2; - $vYear = (($lastmoddate & 0xFE00) >> 9) + 1980; - $vMonth = ($lastmoddate & 0x01E0) >> 5; - $vDay = $lastmoddate & 0x001F; - - $this->fileHeader->timestamp = @mktime($vHour, $vMInute, $vSeconds, $vMonth, $vDay, $vYear); - } - - $isBannedFile = false; - - $this->fileHeader->compressed = $headerData['compsize']; - $this->fileHeader->uncompressed = $headerData['uncomp']; - $nameFieldLength = $headerData['fnamelen']; - $extraFieldLength = $headerData['eflen']; - - // Read filename field - $this->fileHeader->file = fread($this->fp, $nameFieldLength); - - // Read extra field if present - if ($extraFieldLength > 0) - { - $extrafield = fread($this->fp, $extraFieldLength); - } - - // Decide filetype -- Check for directories - $this->fileHeader->type = 'file'; - - if (strrpos($this->fileHeader->file, '/') == strlen($this->fileHeader->file) - 1) - { - $this->fileHeader->type = 'dir'; - } - - // Decide filetype -- Check for symbolic links - if (($headerData['ver1'] == 10) && ($headerData['ver2'] == 3)) - { - $this->fileHeader->type = 'link'; - } - - switch ($headerData['compmethod']) - { - case 0: - $this->fileHeader->compression = 'none'; - break; - case 8: - $this->fileHeader->compression = 'gzip'; - break; - default: - $messageTemplate = 'This script cannot handle ZIP compression method %d. ' - . 'Only 0 (no compression) and 8 (DEFLATE, gzip) can be handled.'; - $actualMessage = sprintf($messageTemplate, $headerData['compmethod']); - $this->setError($actualMessage); - - return false; - break; - } - - // Find hard-coded banned files - if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) - { - $isBannedFile = true; - } - - // Also try to find banned files passed in class configuration - if ((count($this->skipFiles) > 0) && in_array($this->fileHeader->file, $this->skipFiles)) - { - $isBannedFile = true; - } - - // If we have a banned file, let's skip it - if ($isBannedFile) - { - $debugMessage = sprintf('Current entity (%s) is banned from extraction and will be skipped over.', $this->fileHeader->file); - $this->debugMsg($debugMessage, self::LOG_DEBUG); - - // Advance the file pointer, skipping exactly the size of the compressed data - $seekleft = $this->fileHeader->compressed; - - while ($seekleft > 0) - { - // Ensure that we can seek past archive part boundaries - $curSize = @filesize($this->filename); - $curPos = @ftell($this->fp); - $canSeek = $curSize - $curPos; - $canSeek = ($canSeek > $seekleft) ? $seekleft : $canSeek; - @fseek($this->fp, $canSeek, SEEK_CUR); - $seekleft -= $canSeek; - - if ($seekleft) - { - $this->setError('The archive is corrupt or truncated'); - - return false; - } - } - - $this->currentOffset = @ftell($this->fp); - $this->runState = self::AK_STATE_DONE; - - return true; - } - - // Last chance to prepend a path to the filename - if (!empty($this->addPath)) - { - $this->fileHeader->file = $this->addPath . $this->fileHeader->file; - } - - // Get the translated path name - if ($this->fileHeader->type == 'file') - { - $this->fileHeader->realFile = $this->fileHeader->file; - $this->setLastExtractedFilename($this->fileHeader->file); - } - elseif ($this->fileHeader->type == 'dir') - { - $this->fileHeader->timestamp = 0; - - $dir = $this->fileHeader->file; - - if (!@is_dir($dir)) - { - mkdir($dir, 0755, true); - } - - $this->setLastExtractedFilename(null); - } - else - { - // Symlink; do not post-process - $this->fileHeader->timestamp = 0; - $this->setLastExtractedFilename(null); - } - - $this->createDirectory(); - - // Header is read - $this->runState = self::AK_STATE_HEADER; - - return true; - } - - /** - * Creates the directory this file points to - * - * @return void - * @since 4.0.4 - */ - private function createDirectory(): void - { - // Do we need to create a directory? - if (empty($this->fileHeader->realFile)) - { - $this->fileHeader->realFile = $this->fileHeader->file; - } - - $lastSlash = strrpos($this->fileHeader->realFile, '/'); - $dirName = substr($this->fileHeader->realFile, 0, $lastSlash); - $perms = 0755; - $ignore = $this->isIgnoredDirectory($dirName); - - if (@is_dir($dirName)) - { - return; - } - - if ((@mkdir($dirName, $perms, true) === false) && (!$ignore)) - { - $this->setError(sprintf('Could not create %s folder', $dirName)); - } - - } - - /** - * Concrete classes must use this method to process file data. It must set $runState to self::AK_STATE_DATAREAD when - * it's finished processing the file data. - * - * @return boolean True if processing the file data was successful, false if an error occurred - * @since 4.0.4 - */ - private function processFileData(): bool - { - switch ($this->fileHeader->type) - { - case 'dir': - $this->debugMsg('Extracting entity of type Directory', self::LOG_DEBUG); - - return $this->processTypeDir(); - break; - - case 'link': - $this->debugMsg('Extracting entity of type Symbolic Link', self::LOG_DEBUG); - - return $this->processTypeLink(); - break; - - case 'file': - switch ($this->fileHeader->compression) - { - case 'none': - $this->debugMsg('Extracting entity of type File (Stored)', self::LOG_DEBUG); - - return $this->processTypeFileUncompressed(); - break; - - case 'gzip': - case 'bzip2': - $this->debugMsg('Extracting entity of type File (Compressed)', self::LOG_DEBUG); - - return $this->processTypeFileCompressed(); - break; - - case 'default': - $this->setError(sprintf('Unknown compression type %s.', $this->fileHeader->compression)); - - return false; - break; - } - break; - } - - $this->setError(sprintf('Unknown entry type %s.', $this->fileHeader->type)); - - return false; - } - - /** - * Opens the next part file for reading - * - * @return void - * @since 4.0.4 - */ - private function openArchiveFile(): void - { - $this->debugMsg('Opening archive file for reading', self::LOG_DEBUG); - - if ($this->archiveFileIsBeingRead) - { - return; - } - - if (is_resource($this->fp)) - { - @fclose($this->fp); - } - - $this->fp = @fopen($this->filename, 'rb'); - - if ($this->fp === false) - { - $message = 'Could not open archive for reading. Check that the file exists, is ' - . 'readable by the web server and is not in a directory made out of reach by chroot, ' - . 'open_basedir restrictions or any other restriction put in place by your host.'; - $this->setError($message); - - return; - } - - fseek($this->fp, 0); - $this->currentOffset = 0; - - } - - /** - * Returns true if we have reached the end of file - * - * @return boolean True if we have reached End Of File - * @since 4.0.4 - */ - private function isEOF(): bool - { - /** - * feof() will return false if the file pointer is exactly at the last byte of the file. However, this is a - * condition we want to treat as a proper EOF for the purpose of extracting a ZIP file. Hence the second part - * after the logical OR. - */ - return @feof($this->fp) || (@ftell($this->fp) > @filesize($this->filename)); - } - - /** - * Handles the permissions of the parent directory to a file and the file itself to make it writeable. - * - * @param string $path A path to a file - * - * @return void - * @since 4.0.4 - */ - private function setCorrectPermissions(string $path): void - { - static $rootDir = null; - - if (is_null($rootDir)) - { - $rootDir = rtrim($this->addPath, '/\\'); - } - - $directory = rtrim(dirname($path), '/\\'); - - // Is this an unwritable directory? - if (($directory != $rootDir) && !is_writeable($directory)) - { - @chmod($directory, 0755); - } - - @chmod($path, 0644); - } - - /** - * Is this file or directory contained in a directory we've decided to ignore - * write errors for? This is useful to let the extraction work despite write - * errors in the log, logs and tmp directories which MIGHT be used by the system - * on some low quality hosts and Plesk-powered hosts. - * - * @param string $shortFilename The relative path of the file/directory in the package - * - * @return boolean True if it belongs in an ignored directory - * @since 4.0.4 - */ - private function isIgnoredDirectory(string $shortFilename): bool - { - $check = substr($shortFilename, -1) == '/' ? rtrim($shortFilename, '/') : dirname($shortFilename); - - return in_array($check, $this->ignoreDirectories); - } - - /** - * Process the file data of a directory entry - * - * @return boolean - * @since 4.0.4 - */ - private function processTypeDir(): bool - { - // Directory entries do not have file data, therefore we're done processing the entry. - $this->runState = self::AK_STATE_DATAREAD; - - return true; - } - - /** - * Process the file data of a link entry - * - * @return boolean - * @since 4.0.4 - */ - private function processTypeLink(): bool - { - $toReadBytes = 0; - $leftBytes = $this->fileHeader->compressed; - $data = ''; - - while ($leftBytes > 0) - { - $toReadBytes = min($leftBytes, self::CHUNK_SIZE); - $mydata = $this->fread($this->fp, $toReadBytes); - $reallyReadBytes = $this->binStringLength($mydata); - $data .= $mydata; - $leftBytes -= $reallyReadBytes; - - if ($reallyReadBytes < $toReadBytes) - { - // We read less than requested! - if ($this->isEOF()) - { - $this->debugMsg('EOF when reading symlink data', self::LOG_WARNING); - $this->setError('The archive file is corrupt or truncated'); - - return false; - } - } - } - - $filename = $this->fileHeader->realFile ?? $this->fileHeader->file; - - // Try to remove an existing file or directory by the same name - if (file_exists($filename)) - { - clearFileInOPCache($filename); - @unlink($filename); - @rmdir($filename); - } - - // Remove any trailing slash - if (substr($filename, -1) == '/') - { - $filename = substr($filename, 0, -1); - } - - // Create the symlink - @symlink($data, $filename); - - $this->runState = self::AK_STATE_DATAREAD; - - // No matter if the link was created! - return true; - } - - /** - * Processes an uncompressed (stored) file - * - * @return boolean - * @since 4.0.4 - */ - private function processTypeFileUncompressed(): bool - { - // Uncompressed files are being processed in small chunks, to avoid timeouts - if ($this->dataReadLength == 0) - { - // Before processing file data, ensure permissions are adequate - $this->setCorrectPermissions($this->fileHeader->file); - } - - // Open the output file - $ignore = $this->isIgnoredDirectory($this->fileHeader->file); - - $writeMode = ($this->dataReadLength == 0) ? 'wb' : 'ab'; - $outfp = @fopen($this->fileHeader->realFile, $writeMode); - - // Can we write to the file? - if (($outfp === false) && (!$ignore)) - { - // An error occurred - $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); - - return false; - } - - // Does the file have any data, at all? - if ($this->fileHeader->compressed == 0) - { - // No file data! - if (is_resource($outfp)) - { - @fclose($outfp); - } - - $this->debugMsg('Zero byte Stored file; no data will be read', self::LOG_DEBUG); - - $this->runState = self::AK_STATE_DATAREAD; - - return true; - } - - $leftBytes = $this->fileHeader->compressed - $this->dataReadLength; - - // Loop while there's data to read and enough time to do it - while (($leftBytes > 0) && ($this->getTimeLeft() > 0)) - { - $toReadBytes = min($leftBytes, self::CHUNK_SIZE); - $data = $this->fread($this->fp, $toReadBytes); - $reallyReadBytes = $this->binStringLength($data); - $leftBytes -= $reallyReadBytes; - $this->dataReadLength += $reallyReadBytes; - - if ($reallyReadBytes < $toReadBytes) - { - // We read less than requested! Why? Did we hit local EOF? - if ($this->isEOF()) - { - // Nope. The archive is corrupt - $this->debugMsg('EOF when reading stored file data', self::LOG_WARNING); - $this->setError('The archive file is corrupt or truncated'); - - return false; - } - } - - if (is_resource($outfp)) - { - @fwrite($outfp, $data); - } - - if ($this->getTimeLeft()) - { - $this->debugMsg('Out of time; will resume extraction in the next step', self::LOG_DEBUG); - } - } - - // Close the file pointer - if (is_resource($outfp)) - { - @fclose($outfp); - } - - // Was this a pre-timeout bail out? - if ($leftBytes > 0) - { - $this->debugMsg(sprintf('We have %d bytes left to extract in the next step', $leftBytes), self::LOG_DEBUG); - $this->runState = self::AK_STATE_DATA; - - return true; - } - - // Oh! We just finished! - $this->runState = self::AK_STATE_DATAREAD; - $this->dataReadLength = 0; - - return true; - } - - /** - * Processes a compressed file - * - * @return boolean - * @since 4.0.4 - */ - private function processTypeFileCompressed(): bool - { - // Before processing file data, ensure permissions are adequate - $this->setCorrectPermissions($this->fileHeader->file); - - // Open the output file - $outfp = @fopen($this->fileHeader->realFile, 'wb'); - - // Can we write to the file? - $ignore = $this->isIgnoredDirectory($this->fileHeader->file); - - if (($outfp === false) && (!$ignore)) - { - // An error occurred - $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); - - return false; - } - - // Does the file have any data, at all? - if ($this->fileHeader->compressed == 0) - { - $this->debugMsg('Zero byte Compressed file; no data will be read', self::LOG_DEBUG); - - // No file data! - if (is_resource($outfp)) - { - @fclose($outfp); - } - - $this->runState = self::AK_STATE_DATAREAD; - - return true; - } - - // Simple compressed files are processed as a whole; we can't do chunk processing - $zipData = $this->fread($this->fp, $this->fileHeader->compressed); - - while ($this->binStringLength($zipData) < $this->fileHeader->compressed) - { - // End of local file before reading all data? - if ($this->isEOF()) - { - $this->debugMsg('EOF reading compressed data', self::LOG_WARNING); - $this->setError('The archive file is corrupt or truncated'); - - return false; - } - } - - switch ($this->fileHeader->compression) - { - case 'gzip': - /** @noinspection PhpComposerExtensionStubsInspection */ - $unzipData = gzinflate($zipData); - break; - - case 'bzip2': - /** @noinspection PhpComposerExtensionStubsInspection */ - $unzipData = bzdecompress($zipData); - break; - - default: - $this->setError(sprintf('Unknown compression method %s', $this->fileHeader->compression)); - - return false; - break; - } - - unset($zipData); - - // Write to the file. - if (is_resource($outfp)) - { - @fwrite($outfp, $unzipData, $this->fileHeader->uncompressed); - @fclose($outfp); - } - - unset($unzipData); - - $this->runState = self::AK_STATE_DATAREAD; - - return true; - } - - /** - * Set up the maximum execution time - * - * @return void - * @since 4.0.4 - */ - private function setupMaxExecTime(): void - { - $configMaxTime = self::MAX_EXEC_TIME; - $bias = self::RUNTIME_BIAS / 100; - $this->maxExecTime = min($this->getPhpMaxExecTime(), $configMaxTime) * $bias; - } - - /** - * Get the PHP maximum execution time. - * - * If it's not defined or it's zero (infinite) we use a fake value of 10 seconds. - * - * @return integer - * @since 4.0.4 - */ - private function getPhpMaxExecTime(): int - { - if (!@function_exists('ini_get')) - { - return 10; - } - - $phpMaxTime = @ini_get("maximum_execution_time"); - $phpMaxTime = (!is_numeric($phpMaxTime) ? 10 : @intval($phpMaxTime)) ?: 10; - - return max(1, $phpMaxTime); - } - - /** - * Write a message to the debug error log - * - * @param string $message The message to log - * @param int $priority The message's log priority - * - * @return void - * @since 4.0.4 - */ - private function debugMsg(string $message, int $priority = self::LOG_INFO): void - { - if (!defined('_JOOMLA_UPDATE_DEBUG')) - { - return; - } - - if (!is_resource(self::$logFP) && !is_bool(self::$logFP)) - { - self::$logFP = @fopen(self::$logFilePath, 'at'); - } - - if (!is_resource(self::$logFP)) - { - return; - } - - switch ($priority) - { - case self::LOG_DEBUG: - $priorityString = 'DEBUG'; - break; - - case self::LOG_INFO: - $priorityString = 'INFO'; - break; - - case self::LOG_WARNING: - $priorityString = 'WARNING'; - break; - - case self::LOG_ERROR: - $priorityString = 'ERROR'; - break; - } - - fputs(self::$logFP, sprintf('%s | %7s | %s' . "\r\n", gmdate('Y-m-d H:i:s'), $priorityString, $message)); - } - - /** - * Initialise the debug log file - * - * @param string $logPath The path where the log file will be written to - * - * @return void - * @since 4.0.4 - */ - private function initializeLog(string $logPath): void - { - if (!defined('_JOOMLA_UPDATE_DEBUG')) - { - return; - } - - $logPath = $logPath ?: dirname($this->filename); - $logFile = rtrim($logPath, '/' . DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'joomla_update.txt'; - - self::$logFilePath = $logFile; - } + /** + * How much data to read at once when processing files + * + * @var int + * @since 4.0.4 + */ + private const CHUNK_SIZE = 524288; + + /** + * Maximum execution time (seconds). + * + * Each page load will take at most this much time. Please note that if the ZIP archive contains fairly large, + * compressed files we may overshoot this time since we can't interrupt the decompression. This should not be an + * issue in the context of updating Joomla as the ZIP archive contains fairly small files. + * + * If this is too low it will cause too many requests to hit the server, potentially triggering a DoS protection and + * causing the extraction to fail. If this is too big the extraction will not be as verbose and the user might think + * something is broken. A value between 3 and 7 seconds is, therefore, recommended. + * + * @var int + * @since 4.0.4 + */ + private const MAX_EXEC_TIME = 4; + + /** + * Run-time execution bias (percentage points). + * + * We evaluate the time remaining on the timer before processing each file on the ZIP archive. If we have already + * consumed at least this much percentage of the MAX_EXEC_TIME we will stop processing the archive in this page + * load, return the result to the client and wait for it to call us again so we can resume the extraction. + * + * This becomes important when the MAX_EXEC_TIME is close to the PHP, PHP-FPM or Apache timeout on the server + * (whichever is lowest) and there are fairly large files in the backup archive. If we start extracting a large, + * compressed file close to a hard server timeout it's possible that we will overshoot that hard timeout and see the + * extraction failing. + * + * Since Joomla Update is used to extract a ZIP archive with many small files we can keep at a fairly high 90% + * without much fear that something will break. + * + * Example: if MAX_EXEC_TIME is 10 seconds and RUNTIME_BIAS is 80 each page load will take between 80% and 100% of + * the MAX_EXEC_TIME, i.e. anywhere between 8 and 10 seconds. + * + * Lower values make it less likely to overshoot MAX_EXEC_TIME when extracting large files. + * + * @var int + * @since 4.0.4 + */ + private const RUNTIME_BIAS = 90; + + /** + * Minimum execution time (seconds). + * + * A request cannot take less than this many seconds. If it does, we add “dead time” (sleep) where the script does + * nothing except wait. This is essentially a rate limiting feature to avoid hitting a server-side DoS protection + * which could be triggered if we ended up sending too many requests in a limited amount of time. + * + * This should normally be less than MAX_EXEC * (RUNTIME_BIAS / 100). Values between that and MAX_EXEC_TIME have the + * effect of almost always adding dead time in each request, unless a really large file is being extracted from the + * ZIP archive. Values larger than MAX_EXEC will always add dead time to the request. This can be useful to + * artificially reduce the CPU usage limit. Some servers might kill the request if they see a sustained CPU usage + * spike over a short period of time. + * + * The chosen value of 3 seconds belongs to the first category, essentially making sure that we have a decent rate + * limiting without annoying the user too much but also catering for the most badly configured of shared + * hosting. It's a happy medium which works for the majority (~90%) of commercial servers out there. + * + * @var int + * @since 4.0.4 + */ + private const MIN_EXEC_TIME = 3; + + /** + * Internal state when extracting files: we need to be initialised + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_INITIALIZE = -1; + + /** + * Internal state when extracting files: no file currently being extracted + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_NOFILE = 0; + + /** + * Internal state when extracting files: reading the file header + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_HEADER = 1; + + /** + * Internal state when extracting files: reading file data + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_DATA = 2; + + /** + * Internal state when extracting files: file data has been read thoroughly + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_DATAREAD = 3; + + /** + * Internal state when extracting files: post-processing the file + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_POSTPROC = 4; + + /** + * Internal state when extracting files: done with this file + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_DONE = 5; + + /** + * Internal state when extracting files: finished extracting the ZIP file + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_FINISHED = 999; + + /** + * Internal logging level: debug + * + * @var int + * @since 4.0.4 + */ + private const LOG_DEBUG = 1; + + /** + * Internal logging level: information + * + * @var int + * @since 4.0.4 + */ + private const LOG_INFO = 10; + + /** + * Internal logging level: warning + * + * @var int + * @since 4.0.4 + */ + private const LOG_WARNING = 50; + + /** + * Internal logging level: error + * + * @var int + * @since 4.0.4 + */ + private const LOG_ERROR = 90; + + /** + * Singleton instance + * + * @var null|self + * @since 4.0.4 + */ + private static $instance = null; + + /** + * Debug log file pointer resource + * + * @var null|resource|boolean + * @since 4.0.4 + */ + private static $logFP = null; + + /** + * Debug log filename + * + * @var null|string + * @since 4.0.4 + */ + private static $logFilePath = null; + + /** + * The total size of the ZIP archive + * + * @var integer + * @since 4.0.4 + */ + public $totalSize = 0; + + /** + * Which files to skip + * + * @var array + * @since 4.0.4 + */ + public $skipFiles = []; + + /** + * Current tally of compressed size read + * + * @var integer + * @since 4.0.4 + */ + public $compressedTotal = 0; + + /** + * Current tally of bytes written to disk + * + * @var integer + * @since 4.0.4 + */ + public $uncompressedTotal = 0; + + /** + * Current tally of files extracted + * + * @var integer + * @since 4.0.4 + */ + public $filesProcessed = 0; + + /** + * Maximum execution time allowance per step + * + * @var integer + * @since 4.0.4 + */ + private $maxExecTime = null; + + /** + * Timestamp of execution start + * + * @var integer + * @since 4.0.4 + */ + private $startTime; + + /** + * The last error message + * + * @var string|null + * @since 4.0.4 + */ + private $lastErrorMessage = null; + + /** + * Archive filename + * + * @var string + * @since 4.0.4 + */ + private $filename = null; + + /** + * Current archive part number + * + * @var boolean + * @since 4.0.4 + */ + private $archiveFileIsBeingRead = false; + + /** + * The offset inside the current part + * + * @var integer + * @since 4.0.4 + */ + private $currentOffset = 0; + + /** + * Absolute path to prepend to extracted files + * + * @var string + * @since 4.0.4 + */ + private $addPath = ''; + + /** + * File pointer to the current archive part file + * + * @var resource|null + * @since 4.0.4 + */ + private $fp = null; + + /** + * Run state when processing the current archive file + * + * @var integer + * @since 4.0.4 + */ + private $runState = self::AK_STATE_INITIALIZE; + + /** + * File header data, as read by the readFileHeader() method + * + * @var stdClass + * @since 4.0.4 + */ + private $fileHeader = null; + + /** + * How much of the uncompressed data we've read so far + * + * @var integer + * @since 4.0.4 + */ + private $dataReadLength = 0; + + /** + * Unwritable files in these directories are always ignored and do not cause errors when not + * extracted. + * + * @var array + * @since 4.0.4 + */ + private $ignoreDirectories = []; + + /** + * Internal flag, set when the ZIP file has a data descriptor (which we will be ignoring) + * + * @var boolean + * @since 4.0.4 + */ + private $expectDataDescriptor = false; + + /** + * The UNIX last modification timestamp of the file last extracted + * + * @var integer + * @since 4.0.4 + */ + private $lastExtractedFileTimestamp = 0; + + /** + * The file path of the file last extracted + * + * @var string + * @since 4.0.4 + */ + private $lastExtractedFilename = null; + + /** + * Public constructor. + * + * Sets up the internal timer. + * + * @since 4.0.4 + */ + public function __construct() + { + $this->setupMaxExecTime(); + + // Initialize start time + $this->startTime = microtime(true); + } + + /** + * Singleton implementation. + * + * @return static + * @since 4.0.4 + */ + public static function getInstance(): self + { + if (is_null(self::$instance)) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Returns a serialised copy of the object. + * + * This is different to calling serialise() directly. This operates on a copy of the object which undergoes a + * call to shutdown() first so any open files are closed first. + * + * @return string The serialised data, potentially base64 encoded. + * @since 4.0.4 + */ + public static function getSerialised(): string + { + $clone = clone self::getInstance(); + $clone->shutdown(); + $serialized = serialize($clone); + + return (function_exists('base64_encode') && function_exists('base64_decode')) ? base64_encode($serialized) : $serialized; + } + + /** + * Restores a serialised instance into the singleton implementation and returns it. + * + * If the serialised data is corrupt it will return null. + * + * @param string $serialised The serialised data, potentially base64 encoded, to deserialize. + * + * @return static|null The instance of the object, NULL if it cannot be deserialised. + * @since 4.0.4 + */ + public static function unserialiseInstance(string $serialised): ?self + { + if (function_exists('base64_encode') && function_exists('base64_decode')) { + $serialised = base64_decode($serialised); + } + + $instance = @unserialize($serialised, [ + 'allowed_classes' => [ + self::class, + stdClass::class, + ], + ]); + + if (($instance === false) || !is_object($instance) || !($instance instanceof self)) { + return null; + } + + self::$instance = $instance; + + return self::$instance; + } + + /** + * Wakeup function, called whenever the class is deserialized. + * + * This method does the following: + * - Restart the timer. + * - Reopen the archive file, if one is defined. + * - Seek to the correct offset of the file. + * + * @return void + * @since 4.0.4 + * @internal + */ + public function __wakeup(): void + { + // Reset the timer when deserializing the object. + $this->startTime = microtime(true); + + if (!$this->archiveFileIsBeingRead) { + return; + } + + $this->fp = @fopen($this->filename, 'rb'); + + if ((is_resource($this->fp)) && ($this->currentOffset > 0)) { + @fseek($this->fp, $this->currentOffset); + } + } + + /** + * Enforce the minimum execution time. + * + * @return void + * @since 4.0.4 + */ + public function enforceMinimumExecutionTime() + { + $elapsed = $this->getRunningTime() * 1000; + $minExecTime = 1000.0 * min(1, (min(self::MIN_EXEC_TIME, $this->getPhpMaxExecTime()) - 1)); + + // Only run a sleep delay if we haven't reached the minimum execution time + if (($minExecTime <= $elapsed) || ($elapsed <= 0)) { + return; + } + + $sleepMillisec = intval($minExecTime - $elapsed); + + /** + * If we need to sleep for more than 1 second we should be using sleep() or time_sleep_until() to prevent high + * CPU usage, also because some OS might not support sleeping for over 1 second using these functions. In all + * other cases we will try to use usleep or time_nanosleep instead. + */ + $longSleep = $sleepMillisec > 1000; + $miniSleepSupported = function_exists('usleep') || function_exists('time_nanosleep'); + + if (!$longSleep && $miniSleepSupported) { + if (function_exists('usleep') && ($sleepMillisec < 1000)) { + usleep(1000 * $sleepMillisec); + + return; + } + + if (function_exists('time_nanosleep') && ($sleepMillisec < 1000)) { + time_nanosleep(0, 1000000 * $sleepMillisec); + + return; + } + } + + if (function_exists('sleep')) { + sleep(ceil($sleepMillisec / 1000)); + + return; + } + + if (function_exists('time_sleep_until')) { + time_sleep_until(time() + ceil($sleepMillisec / 1000)); + } + } + + /** + * Set the filepath to the ZIP archive which will be extracted. + * + * @param string $value The filepath to the archive. Only LOCAL files are allowed! + * + * @return void + * @since 4.0.4 + */ + public function setFilename(string $value) + { + // Security check: disallow remote filenames + if (!empty($value) && strpos($value, '://') !== false) { + $this->setError('Invalid archive location'); + + return; + } + + $this->filename = $value; + $this->initializeLog(dirname($this->filename)); + } + + /** + * Sets the path to prefix all extracted files with. Essentially, where the archive will be extracted to. + * + * @param string $addPath The path where the archive will be extracted. + * + * @return void + * @since 4.0.4 + */ + public function setAddPath(string $addPath): void + { + $this->addPath = $addPath; + $this->addPath = str_replace('\\', '/', $this->addPath); + $this->addPath = rtrim($this->addPath, '/'); + + if (!empty($this->addPath)) { + $this->addPath .= '/'; + } + } + + /** + * Set the list of files to skip when extracting the ZIP file. + * + * @param array $skipFiles A list of files to skip when extracting the ZIP archive + * + * @return void + * @since 4.0.4 + */ + public function setSkipFiles(array $skipFiles): void + { + $this->skipFiles = array_values($skipFiles); + } + + /** + * Set the directories to skip over when extracting the ZIP archive + * + * @param array $ignoreDirectories The list of directories to ignore. + * + * @return void + * @since 4.0.4 + */ + public function setIgnoreDirectories(array $ignoreDirectories): void + { + $this->ignoreDirectories = array_values($ignoreDirectories); + } + + /** + * Prepares for the archive extraction + * + * @return void + * @since 4.0.4 + */ + public function initialize(): void + { + $this->debugMsg(sprintf('Initializing extraction. Filepath: %s', $this->filename)); + $this->totalSize = @filesize($this->filename) ?: 0; + $this->archiveFileIsBeingRead = false; + $this->currentOffset = 0; + $this->runState = self::AK_STATE_NOFILE; + + $this->readArchiveHeader(); + + if (!empty($this->getError())) { + $this->debugMsg(sprintf('Error: %s', $this->getError()), self::LOG_ERROR); + + return; + } + + $this->archiveFileIsBeingRead = true; + $this->runState = self::AK_STATE_NOFILE; + + $this->debugMsg('Setting state to NOFILE', self::LOG_DEBUG); + } + + /** + * Executes a step of the archive extraction + * + * @return boolean True if we are done extracting or an error occurred + * @since 4.0.4 + */ + public function step(): bool + { + $status = true; + + $this->debugMsg('Starting a new step', self::LOG_INFO); + + while ($status && ($this->getTimeLeft() > 0)) { + switch ($this->runState) { + case self::AK_STATE_INITIALIZE: + $this->debugMsg('Current run state: INITIALIZE', self::LOG_DEBUG); + $this->initialize(); + break; + + case self::AK_STATE_NOFILE: + $this->debugMsg('Current run state: NOFILE', self::LOG_DEBUG); + $status = $this->readFileHeader(); + + if ($status) { + $this->debugMsg('Found file header; updating number of files processed and bytes in/out', self::LOG_DEBUG); + + // Update running tallies when we start extracting a file + $this->filesProcessed++; + $this->compressedTotal += array_key_exists('compressed', get_object_vars($this->fileHeader)) + ? $this->fileHeader->compressed : 0; + $this->uncompressedTotal += $this->fileHeader->uncompressed; + } + + break; + + case self::AK_STATE_HEADER: + case self::AK_STATE_DATA: + $runStateHuman = $this->runState === self::AK_STATE_HEADER ? 'HEADER' : 'DATA'; + $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); + + $status = $this->processFileData(); + break; + + case self::AK_STATE_DATAREAD: + case self::AK_STATE_POSTPROC: + $runStateHuman = $this->runState === self::AK_STATE_DATAREAD ? 'DATAREAD' : 'POSTPROC'; + $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); + + $this->setLastExtractedFileTimestamp($this->fileHeader->timestamp); + $this->processLastExtractedFile(); + + $status = true; + $this->runState = self::AK_STATE_DONE; + break; + + case self::AK_STATE_DONE: + default: + $this->debugMsg('Current run state: DONE', self::LOG_DEBUG); + $this->runState = self::AK_STATE_NOFILE; + + break; + + case self::AK_STATE_FINISHED: + $this->debugMsg('Current run state: FINISHED', self::LOG_DEBUG); + $status = false; + break; + } + + if ($this->getTimeLeft() <= 0) { + $this->debugMsg('Ran out of time; the step will break.'); + } elseif (!$status) { + $this->debugMsg('Last step status is false; the step will break.'); + } + } + + $error = $this->getError() ?? null; + + if (!empty($error)) { + $this->debugMsg(sprintf('Step failed with error: %s', $error), self::LOG_ERROR); + } + + // Did we just finish or run into an error? + if (!empty($error) || $this->runState === self::AK_STATE_FINISHED) { + $this->debugMsg('Returning true (must stop running) from step()', self::LOG_DEBUG); + + // Reset internal state, prevents __wakeup from trying to open a non-existent file + $this->archiveFileIsBeingRead = false; + + return true; + } + + $this->debugMsg('Returning false (must continue running) from step()', self::LOG_DEBUG); + + return false; + } + + /** + * Get the most recent error message + * + * @return string|null The message string, null if there's no error + * @since 4.0.4 + */ + public function getError(): ?string + { + return $this->lastErrorMessage; + } + + /** + * Gets the number of seconds left, before we hit the "must break" threshold + * + * @return float + * @since 4.0.4 + */ + private function getTimeLeft(): float + { + return $this->maxExecTime - $this->getRunningTime(); + } + + /** + * Gets the time elapsed since object creation/unserialization, effectively how + * long Akeeba Engine has been processing data + * + * @return float + * @since 4.0.4 + */ + private function getRunningTime(): float + { + return microtime(true) - $this->startTime; + } + + /** + * Process the last extracted file or directory + * + * This invalidates OPcache for .php files. Also applies the correct permissions and timestamp. + * + * @return void + * @since 4.0.4 + */ + private function processLastExtractedFile(): void + { + $this->debugMsg(sprintf('Processing last extracted entity: %s', $this->lastExtractedFilename), self::LOG_DEBUG); + + if (@is_file($this->lastExtractedFilename)) { + @chmod($this->lastExtractedFilename, 0644); + + clearFileInOPCache($this->lastExtractedFilename); + } else { + @chmod($this->lastExtractedFilename, 0755); + } + + if ($this->lastExtractedFileTimestamp > 0) { + @touch($this->lastExtractedFilename, $this->lastExtractedFileTimestamp); + } + } + + /** + * Set the last extracted filename + * + * @param string|null $lastExtractedFilename The last extracted filename + * + * @return void + * @since 4.0.4 + */ + private function setLastExtractedFilename(?string $lastExtractedFilename): void + { + $this->lastExtractedFilename = $lastExtractedFilename; + } + + /** + * Set the last modification UNIX timestamp for the last extracted file + * + * @param int $lastExtractedFileTimestamp The timestamp + * + * @return void + * @since 4.0.4 + */ + private function setLastExtractedFileTimestamp(int $lastExtractedFileTimestamp): void + { + $this->lastExtractedFileTimestamp = $lastExtractedFileTimestamp; + } + + /** + * Sleep function, called whenever the class is serialized + * + * @return void + * @since 4.0.4 + * @internal + */ + private function shutdown(): void + { + if (is_resource(self::$logFP)) { + @fclose(self::$logFP); + } + + if (!is_resource($this->fp)) { + return; + } + + $this->currentOffset = @ftell($this->fp); + + @fclose($this->fp); + } + + /** + * Unicode-safe binary data length + * + * @param string|null $string The binary data to get the length for + * + * @return integer + * @since 4.0.4 + */ + private function binStringLength(?string $string): int + { + if (is_null($string)) { + return 0; + } + + if (function_exists('mb_strlen')) { + return mb_strlen($string, '8bit') ?: 0; + } + + return strlen($string) ?: 0; + } + + /** + * Add an error message + * + * @param string $error Error message + * + * @return void + * @since 4.0.4 + */ + private function setError(string $error): void + { + $this->lastErrorMessage = $error; + } + + /** + * Reads data from the archive. + * + * @param resource $fp The file pointer to read data from + * @param int|null $length The volume of data to read, in bytes + * + * @return string The data read from the file + * @since 4.0.4 + */ + private function fread($fp, ?int $length = null): string + { + $readLength = (is_numeric($length) && ($length > 0)) ? $length : PHP_INT_MAX; + $data = fread($fp, $readLength); + + if ($data === false) { + $this->debugMsg('No more data could be read from the file', self::LOG_WARNING); + + $data = ''; + } + + return $data; + } + + /** + * Read the header of the archive, making sure it's a valid ZIP file. + * + * @return void + * @since 4.0.4 + */ + private function readArchiveHeader(): void + { + $this->debugMsg('Reading the archive header.', self::LOG_DEBUG); + + // Open the first part + $this->openArchiveFile(); + + // Fail for unreadable files + if ($this->fp === false) { + return; + } + + // Read the header data. + $sigBinary = fread($this->fp, 4); + $headerData = unpack('Vsig', $sigBinary); + + // We only support single part ZIP files + if ($headerData['sig'] != 0x04034b50) { + $this->setError('The archive file is corrupt: bad header'); + + return; + } + + // Roll back the file pointer + fseek($this->fp, -4, SEEK_CUR); + + $this->currentOffset = @ftell($this->fp); + $this->dataReadLength = 0; + } + + /** + * Concrete classes must use this method to read the file header + * + * @return boolean True if reading the file was successful, false if an error occurred or we + * reached end of archive. + * @since 4.0.4 + */ + private function readFileHeader(): bool + { + $this->debugMsg('Reading the file entry header.', self::LOG_DEBUG); + + if (!is_resource($this->fp)) { + $this->setError('The archive is not open for reading.'); + + return false; + } + + // Unexpected end of file + if ($this->isEOF()) { + $this->debugMsg('EOF when reading file header data', self::LOG_WARNING); + $this->setError('The archive is corrupt or truncated'); + + return false; + } + + $this->currentOffset = ftell($this->fp); + + if ($this->expectDataDescriptor) { + $this->debugMsg('Expecting data descriptor (bit 3 of general purpose flag was set).', self::LOG_DEBUG); + + /** + * The last file had bit 3 of the general purpose bit flag set. This means that we have a 12 byte data + * descriptor we need to skip. To make things worse, there might also be a 4 byte optional data descriptor + * header (0x08074b50). + */ + $junk = @fread($this->fp, 4); + $junk = unpack('Vsig', $junk); + $readLength = ($junk['sig'] == 0x08074b50) ? 12 : 8; + $junk = @fread($this->fp, $readLength); + + // And check for EOF, too + if ($this->isEOF()) { + $this->debugMsg('EOF when reading data descriptor', self::LOG_WARNING); + $this->setError('The archive is corrupt or truncated'); + + return false; + } + } + + // Get and decode Local File Header + $headerBinary = fread($this->fp, 30); + $format = 'Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/' + . 'Vuncomp/vfnamelen/veflen'; + $headerData = unpack($format, $headerBinary); + + // Check signature + if (!($headerData['sig'] == 0x04034b50)) { + // The signature is not the one used for files. Is this a central directory record (i.e. we're done)? + if ($headerData['sig'] == 0x02014b50) { + $this->debugMsg('Found Central Directory header; the extraction is complete', self::LOG_DEBUG); + + // End of ZIP file detected. We'll just skip to the end of file... + @fseek($this->fp, 0, SEEK_END); + $this->runState = self::AK_STATE_FINISHED; + + return false; + } + + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + + // If bit 3 of the bitflag is set, expectDataDescriptor is true + $this->expectDataDescriptor = ($headerData['bitflag'] & 4) == 4; + $this->fileHeader = new stdClass(); + $this->fileHeader->timestamp = 0; + + // Read the last modified date and time + $lastmodtime = $headerData['lastmodtime']; + $lastmoddate = $headerData['lastmoddate']; + + if ($lastmoddate && $lastmodtime) { + $vHour = ($lastmodtime & 0xF800) >> 11; + $vMInute = ($lastmodtime & 0x07E0) >> 5; + $vSeconds = ($lastmodtime & 0x001F) * 2; + $vYear = (($lastmoddate & 0xFE00) >> 9) + 1980; + $vMonth = ($lastmoddate & 0x01E0) >> 5; + $vDay = $lastmoddate & 0x001F; + + $this->fileHeader->timestamp = @mktime($vHour, $vMInute, $vSeconds, $vMonth, $vDay, $vYear); + } + + $isBannedFile = false; + + $this->fileHeader->compressed = $headerData['compsize']; + $this->fileHeader->uncompressed = $headerData['uncomp']; + $nameFieldLength = $headerData['fnamelen']; + $extraFieldLength = $headerData['eflen']; + + // Read filename field + $this->fileHeader->file = fread($this->fp, $nameFieldLength); + + // Read extra field if present + if ($extraFieldLength > 0) { + $extrafield = fread($this->fp, $extraFieldLength); + } + + // Decide filetype -- Check for directories + $this->fileHeader->type = 'file'; + + if (strrpos($this->fileHeader->file, '/') == strlen($this->fileHeader->file) - 1) { + $this->fileHeader->type = 'dir'; + } + + // Decide filetype -- Check for symbolic links + if (($headerData['ver1'] == 10) && ($headerData['ver2'] == 3)) { + $this->fileHeader->type = 'link'; + } + + switch ($headerData['compmethod']) { + case 0: + $this->fileHeader->compression = 'none'; + break; + case 8: + $this->fileHeader->compression = 'gzip'; + break; + default: + $messageTemplate = 'This script cannot handle ZIP compression method %d. ' + . 'Only 0 (no compression) and 8 (DEFLATE, gzip) can be handled.'; + $actualMessage = sprintf($messageTemplate, $headerData['compmethod']); + $this->setError($actualMessage); + + return false; + break; + } + + // Find hard-coded banned files + if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) { + $isBannedFile = true; + } + + // Also try to find banned files passed in class configuration + if ((count($this->skipFiles) > 0) && in_array($this->fileHeader->file, $this->skipFiles)) { + $isBannedFile = true; + } + + // If we have a banned file, let's skip it + if ($isBannedFile) { + $debugMessage = sprintf('Current entity (%s) is banned from extraction and will be skipped over.', $this->fileHeader->file); + $this->debugMsg($debugMessage, self::LOG_DEBUG); + + // Advance the file pointer, skipping exactly the size of the compressed data + $seekleft = $this->fileHeader->compressed; + + while ($seekleft > 0) { + // Ensure that we can seek past archive part boundaries + $curSize = @filesize($this->filename); + $curPos = @ftell($this->fp); + $canSeek = $curSize - $curPos; + $canSeek = ($canSeek > $seekleft) ? $seekleft : $canSeek; + @fseek($this->fp, $canSeek, SEEK_CUR); + $seekleft -= $canSeek; + + if ($seekleft) { + $this->setError('The archive is corrupt or truncated'); + + return false; + } + } + + $this->currentOffset = @ftell($this->fp); + $this->runState = self::AK_STATE_DONE; + + return true; + } + + // Last chance to prepend a path to the filename + if (!empty($this->addPath)) { + $this->fileHeader->file = $this->addPath . $this->fileHeader->file; + } + + // Get the translated path name + if ($this->fileHeader->type == 'file') { + $this->fileHeader->realFile = $this->fileHeader->file; + $this->setLastExtractedFilename($this->fileHeader->file); + } elseif ($this->fileHeader->type == 'dir') { + $this->fileHeader->timestamp = 0; + + $dir = $this->fileHeader->file; + + if (!@is_dir($dir)) { + mkdir($dir, 0755, true); + } + + $this->setLastExtractedFilename(null); + } else { + // Symlink; do not post-process + $this->fileHeader->timestamp = 0; + $this->setLastExtractedFilename(null); + } + + $this->createDirectory(); + + // Header is read + $this->runState = self::AK_STATE_HEADER; + + return true; + } + + /** + * Creates the directory this file points to + * + * @return void + * @since 4.0.4 + */ + private function createDirectory(): void + { + // Do we need to create a directory? + if (empty($this->fileHeader->realFile)) { + $this->fileHeader->realFile = $this->fileHeader->file; + } + + $lastSlash = strrpos($this->fileHeader->realFile, '/'); + $dirName = substr($this->fileHeader->realFile, 0, $lastSlash); + $perms = 0755; + $ignore = $this->isIgnoredDirectory($dirName); + + if (@is_dir($dirName)) { + return; + } + + if ((@mkdir($dirName, $perms, true) === false) && (!$ignore)) { + $this->setError(sprintf('Could not create %s folder', $dirName)); + } + } + + /** + * Concrete classes must use this method to process file data. It must set $runState to self::AK_STATE_DATAREAD when + * it's finished processing the file data. + * + * @return boolean True if processing the file data was successful, false if an error occurred + * @since 4.0.4 + */ + private function processFileData(): bool + { + switch ($this->fileHeader->type) { + case 'dir': + $this->debugMsg('Extracting entity of type Directory', self::LOG_DEBUG); + + return $this->processTypeDir(); + break; + + case 'link': + $this->debugMsg('Extracting entity of type Symbolic Link', self::LOG_DEBUG); + + return $this->processTypeLink(); + break; + + case 'file': + switch ($this->fileHeader->compression) { + case 'none': + $this->debugMsg('Extracting entity of type File (Stored)', self::LOG_DEBUG); + + return $this->processTypeFileUncompressed(); + break; + + case 'gzip': + case 'bzip2': + $this->debugMsg('Extracting entity of type File (Compressed)', self::LOG_DEBUG); + + return $this->processTypeFileCompressed(); + break; + + case 'default': + $this->setError(sprintf('Unknown compression type %s.', $this->fileHeader->compression)); + + return false; + break; + } + break; + } + + $this->setError(sprintf('Unknown entry type %s.', $this->fileHeader->type)); + + return false; + } + + /** + * Opens the next part file for reading + * + * @return void + * @since 4.0.4 + */ + private function openArchiveFile(): void + { + $this->debugMsg('Opening archive file for reading', self::LOG_DEBUG); + + if ($this->archiveFileIsBeingRead) { + return; + } + + if (is_resource($this->fp)) { + @fclose($this->fp); + } + + $this->fp = @fopen($this->filename, 'rb'); + + if ($this->fp === false) { + $message = 'Could not open archive for reading. Check that the file exists, is ' + . 'readable by the web server and is not in a directory made out of reach by chroot, ' + . 'open_basedir restrictions or any other restriction put in place by your host.'; + $this->setError($message); + + return; + } + + fseek($this->fp, 0); + $this->currentOffset = 0; + } + + /** + * Returns true if we have reached the end of file + * + * @return boolean True if we have reached End Of File + * @since 4.0.4 + */ + private function isEOF(): bool + { + /** + * feof() will return false if the file pointer is exactly at the last byte of the file. However, this is a + * condition we want to treat as a proper EOF for the purpose of extracting a ZIP file. Hence the second part + * after the logical OR. + */ + return @feof($this->fp) || (@ftell($this->fp) > @filesize($this->filename)); + } + + /** + * Handles the permissions of the parent directory to a file and the file itself to make it writeable. + * + * @param string $path A path to a file + * + * @return void + * @since 4.0.4 + */ + private function setCorrectPermissions(string $path): void + { + static $rootDir = null; + + if (is_null($rootDir)) { + $rootDir = rtrim($this->addPath, '/\\'); + } + + $directory = rtrim(dirname($path), '/\\'); + + // Is this an unwritable directory? + if (($directory != $rootDir) && !is_writeable($directory)) { + @chmod($directory, 0755); + } + + @chmod($path, 0644); + } + + /** + * Is this file or directory contained in a directory we've decided to ignore + * write errors for? This is useful to let the extraction work despite write + * errors in the log, logs and tmp directories which MIGHT be used by the system + * on some low quality hosts and Plesk-powered hosts. + * + * @param string $shortFilename The relative path of the file/directory in the package + * + * @return boolean True if it belongs in an ignored directory + * @since 4.0.4 + */ + private function isIgnoredDirectory(string $shortFilename): bool + { + $check = substr($shortFilename, -1) == '/' ? rtrim($shortFilename, '/') : dirname($shortFilename); + + return in_array($check, $this->ignoreDirectories); + } + + /** + * Process the file data of a directory entry + * + * @return boolean + * @since 4.0.4 + */ + private function processTypeDir(): bool + { + // Directory entries do not have file data, therefore we're done processing the entry. + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + /** + * Process the file data of a link entry + * + * @return boolean + * @since 4.0.4 + */ + private function processTypeLink(): bool + { + $toReadBytes = 0; + $leftBytes = $this->fileHeader->compressed; + $data = ''; + + while ($leftBytes > 0) { + $toReadBytes = min($leftBytes, self::CHUNK_SIZE); + $mydata = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = $this->binStringLength($mydata); + $data .= $mydata; + $leftBytes -= $reallyReadBytes; + + if ($reallyReadBytes < $toReadBytes) { + // We read less than requested! + if ($this->isEOF()) { + $this->debugMsg('EOF when reading symlink data', self::LOG_WARNING); + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + } + } + + $filename = $this->fileHeader->realFile ?? $this->fileHeader->file; + + // Try to remove an existing file or directory by the same name + if (file_exists($filename)) { + clearFileInOPCache($filename); + @unlink($filename); + @rmdir($filename); + } + + // Remove any trailing slash + if (substr($filename, -1) == '/') { + $filename = substr($filename, 0, -1); + } + + // Create the symlink + @symlink($data, $filename); + + $this->runState = self::AK_STATE_DATAREAD; + + // No matter if the link was created! + return true; + } + + /** + * Processes an uncompressed (stored) file + * + * @return boolean + * @since 4.0.4 + */ + private function processTypeFileUncompressed(): bool + { + // Uncompressed files are being processed in small chunks, to avoid timeouts + if ($this->dataReadLength == 0) { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + } + + // Open the output file + $ignore = $this->isIgnoredDirectory($this->fileHeader->file); + + $writeMode = ($this->dataReadLength == 0) ? 'wb' : 'ab'; + $outfp = @fopen($this->fileHeader->realFile, $writeMode); + + // Can we write to the file? + if (($outfp === false) && (!$ignore)) { + // An error occurred + $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); + + return false; + } + + // Does the file have any data, at all? + if ($this->fileHeader->compressed == 0) { + // No file data! + if (is_resource($outfp)) { + @fclose($outfp); + } + + $this->debugMsg('Zero byte Stored file; no data will be read', self::LOG_DEBUG); + + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + $leftBytes = $this->fileHeader->compressed - $this->dataReadLength; + + // Loop while there's data to read and enough time to do it + while (($leftBytes > 0) && ($this->getTimeLeft() > 0)) { + $toReadBytes = min($leftBytes, self::CHUNK_SIZE); + $data = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = $this->binStringLength($data); + $leftBytes -= $reallyReadBytes; + $this->dataReadLength += $reallyReadBytes; + + if ($reallyReadBytes < $toReadBytes) { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF()) { + // Nope. The archive is corrupt + $this->debugMsg('EOF when reading stored file data', self::LOG_WARNING); + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + } + + if (is_resource($outfp)) { + @fwrite($outfp, $data); + } + + if ($this->getTimeLeft()) { + $this->debugMsg('Out of time; will resume extraction in the next step', self::LOG_DEBUG); + } + } + + // Close the file pointer + if (is_resource($outfp)) { + @fclose($outfp); + } + + // Was this a pre-timeout bail out? + if ($leftBytes > 0) { + $this->debugMsg(sprintf('We have %d bytes left to extract in the next step', $leftBytes), self::LOG_DEBUG); + $this->runState = self::AK_STATE_DATA; + + return true; + } + + // Oh! We just finished! + $this->runState = self::AK_STATE_DATAREAD; + $this->dataReadLength = 0; + + return true; + } + + /** + * Processes a compressed file + * + * @return boolean + * @since 4.0.4 + */ + private function processTypeFileCompressed(): bool + { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + + // Open the output file + $outfp = @fopen($this->fileHeader->realFile, 'wb'); + + // Can we write to the file? + $ignore = $this->isIgnoredDirectory($this->fileHeader->file); + + if (($outfp === false) && (!$ignore)) { + // An error occurred + $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); + + return false; + } + + // Does the file have any data, at all? + if ($this->fileHeader->compressed == 0) { + $this->debugMsg('Zero byte Compressed file; no data will be read', self::LOG_DEBUG); + + // No file data! + if (is_resource($outfp)) { + @fclose($outfp); + } + + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + // Simple compressed files are processed as a whole; we can't do chunk processing + $zipData = $this->fread($this->fp, $this->fileHeader->compressed); + + while ($this->binStringLength($zipData) < $this->fileHeader->compressed) { + // End of local file before reading all data? + if ($this->isEOF()) { + $this->debugMsg('EOF reading compressed data', self::LOG_WARNING); + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + } + + switch ($this->fileHeader->compression) { + case 'gzip': + /** @noinspection PhpComposerExtensionStubsInspection */ + $unzipData = gzinflate($zipData); + break; + + case 'bzip2': + /** @noinspection PhpComposerExtensionStubsInspection */ + $unzipData = bzdecompress($zipData); + break; + + default: + $this->setError(sprintf('Unknown compression method %s', $this->fileHeader->compression)); + + return false; + break; + } + + unset($zipData); + + // Write to the file. + if (is_resource($outfp)) { + @fwrite($outfp, $unzipData, $this->fileHeader->uncompressed); + @fclose($outfp); + } + + unset($unzipData); + + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + /** + * Set up the maximum execution time + * + * @return void + * @since 4.0.4 + */ + private function setupMaxExecTime(): void + { + $configMaxTime = self::MAX_EXEC_TIME; + $bias = self::RUNTIME_BIAS / 100; + $this->maxExecTime = min($this->getPhpMaxExecTime(), $configMaxTime) * $bias; + } + + /** + * Get the PHP maximum execution time. + * + * If it's not defined or it's zero (infinite) we use a fake value of 10 seconds. + * + * @return integer + * @since 4.0.4 + */ + private function getPhpMaxExecTime(): int + { + if (!@function_exists('ini_get')) { + return 10; + } + + $phpMaxTime = @ini_get("maximum_execution_time"); + $phpMaxTime = (!is_numeric($phpMaxTime) ? 10 : @intval($phpMaxTime)) ?: 10; + + return max(1, $phpMaxTime); + } + + /** + * Write a message to the debug error log + * + * @param string $message The message to log + * @param int $priority The message's log priority + * + * @return void + * @since 4.0.4 + */ + private function debugMsg(string $message, int $priority = self::LOG_INFO): void + { + if (!defined('_JOOMLA_UPDATE_DEBUG')) { + return; + } + + if (!is_resource(self::$logFP) && !is_bool(self::$logFP)) { + self::$logFP = @fopen(self::$logFilePath, 'at'); + } + + if (!is_resource(self::$logFP)) { + return; + } + + switch ($priority) { + case self::LOG_DEBUG: + $priorityString = 'DEBUG'; + break; + + case self::LOG_INFO: + $priorityString = 'INFO'; + break; + + case self::LOG_WARNING: + $priorityString = 'WARNING'; + break; + + case self::LOG_ERROR: + $priorityString = 'ERROR'; + break; + } + + fputs(self::$logFP, sprintf('%s | %7s | %s' . "\r\n", gmdate('Y-m-d H:i:s'), $priorityString, $message)); + } + + /** + * Initialise the debug log file + * + * @param string $logPath The path where the log file will be written to + * + * @return void + * @since 4.0.4 + */ + private function initializeLog(string $logPath): void + { + if (!defined('_JOOMLA_UPDATE_DEBUG')) { + return; + } + + $logPath = $logPath ?: dirname($this->filename); + $logFile = rtrim($logPath, '/' . DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'joomla_update.txt'; + + self::$logFilePath = $logFile; + } } // Skip over the mini-controller for testing purposes -if (defined('_JOOMLA_UPDATE_TESTING')) -{ - return; +if (defined('_JOOMLA_UPDATE_TESTING')) { + return; } /** @@ -1782,21 +1685,19 @@ private function initializeLog(string $logPath): void */ function clearFileInOPCache(string $file): bool { - static $hasOpCache = null; + static $hasOpCache = null; - if (is_null($hasOpCache)) - { - $hasOpCache = ini_get('opcache.enable') - && function_exists('opcache_invalidate') - && (!ini_get('opcache.restrict_api') || stripos(realpath($_SERVER['SCRIPT_FILENAME']), ini_get('opcache.restrict_api')) === 0); - } + if (is_null($hasOpCache)) { + $hasOpCache = ini_get('opcache.enable') + && function_exists('opcache_invalidate') + && (!ini_get('opcache.restrict_api') || stripos(realpath($_SERVER['SCRIPT_FILENAME']), ini_get('opcache.restrict_api')) === 0); + } - if ($hasOpCache && (strtolower(substr($file, -4)) === '.php')) - { - return opcache_invalidate($file, true); - } + if ($hasOpCache && (strtolower(substr($file, -4)) === '.php')) { + return opcache_invalidate($file, true); + } - return false; + return false; } /** @@ -1817,28 +1718,25 @@ function clearFileInOPCache(string $file): bool */ function timingSafeEquals($known, $user) { - if (function_exists('hash_equals')) - { - return hash_equals($known, $user); - } + if (function_exists('hash_equals')) { + return hash_equals($known, $user); + } - $safeLen = strlen($known); - $userLen = strlen($user); + $safeLen = strlen($known); + $userLen = strlen($user); - if ($userLen != $safeLen) - { - return false; - } + if ($userLen != $safeLen) { + return false; + } - $result = 0; + $result = 0; - for ($i = 0; $i < $userLen; $i++) - { - $result |= (ord($known[$i]) ^ ord($user[$i])); - } + for ($i = 0; $i < $userLen; $i++) { + $result |= (ord($known[$i]) ^ ord($user[$i])); + } - // They are only identical strings if $result is exactly 0... - return $result === 0; + // They are only identical strings if $result is exactly 0... + return $result === 0; } /** @@ -1850,208 +1748,232 @@ function timingSafeEquals($known, $user) */ function getConfiguration(): ?array { - // Make sure the locale is correct for basename() to work - if (function_exists('setlocale')) - { - @setlocale(LC_ALL, 'en_US.UTF8'); - } - - // Require update.php or fail - $setupFile = __DIR__ . '/update.php'; - - if (!file_exists($setupFile)) - { - return null; - } - - /** - * If the setup file was created more than 1.5 hours ago we can assume that it's stale and someone forgot to - * remove it from the server. - * - * This prevents brute force attacks against the randomly generated password. Even a simple 8 character simple - * alphanum (a-z, 0-9) password yields over 2.8e12 permutation. Assuming a very fast server which can - * serve 100 requests to extract.php per second and an easy to attack password requiring going over just 1% of - * the search space it'd still take over 282 million seconds to brute force it. Our limit is more than 4 orders - * of magnitude lower than this best practical case scenario, giving us adequate protection against all but the - * luckiest attacker (spoiler alert: the mathematics of probabilities say you're not gonna get too lucky). - * - * It is still advisable to remove the update.php file once you are done with the extraction. This check - * here is only meant as a failsafe in case of a server error during the extraction and subsequent lack of user - * action to remove the update.php file from their server. - */ - clearstatcache(true); - $setupFileCreationTime = filectime($setupFile); - - if (abs(time() - $setupFileCreationTime) > 5400) - { - return null; - } - - // Load update.php. It pulls a variable named $restoration_setup into the local scope. - clearFileInOPCache($setupFile); - - require_once $setupFile; - - /** @var array $extractionSetup */ - - // The file exists but no configuration is present? - if (empty($extractionSetup ?? null) || !is_array($extractionSetup)) - { - return null; - } - - /** - * Immediately reject any attempt to run extract.php without a password. - * - * Doing that is a GRAVE SECURITY RISK. It makes it trivial to hack a site. Therefore we are preventing this script - * to run without a password. - */ - $password = $extractionSetup['security.password'] ?? null; - $userPassword = $_REQUEST['password'] ?? ''; - $userPassword = !is_string($userPassword) ? '' : trim($userPassword); - - if (empty($password) || !is_string($password) || (trim($password) == '') || (strlen(trim($password)) < 32)) - { - return null; - } - - // Timing-safe password comparison. See http://blog.ircmaxell.com/2014/11/its-all-about-time.html - if (!timingSafeEquals($password, $userPassword)) - { - return null; - } - - // An "instance" variable will resume the engine from the serialised instance - $serialized = $_REQUEST['instance'] ?? null; - - if (!is_null($serialized) && empty(ZIPExtraction::unserialiseInstance($serialized))) - { - // The serialised instance is corrupt or someone tries to trick us. YOU SHALL NOT PASS! - return null; - } - - return $extractionSetup; + // Make sure the locale is correct for basename() to work + if (function_exists('setlocale')) { + @setlocale(LC_ALL, 'en_US.UTF8'); + } + + // Require update.php or fail + $setupFile = __DIR__ . '/update.php'; + + if (!file_exists($setupFile)) { + return null; + } + + /** + * If the setup file was created more than 1.5 hours ago we can assume that it's stale and someone forgot to + * remove it from the server. + * + * This prevents brute force attacks against the randomly generated password. Even a simple 8 character simple + * alphanum (a-z, 0-9) password yields over 2.8e12 permutation. Assuming a very fast server which can + * serve 100 requests to extract.php per second and an easy to attack password requiring going over just 1% of + * the search space it'd still take over 282 million seconds to brute force it. Our limit is more than 4 orders + * of magnitude lower than this best practical case scenario, giving us adequate protection against all but the + * luckiest attacker (spoiler alert: the mathematics of probabilities say you're not gonna get too lucky). + * + * It is still advisable to remove the update.php file once you are done with the extraction. This check + * here is only meant as a failsafe in case of a server error during the extraction and subsequent lack of user + * action to remove the update.php file from their server. + */ + clearstatcache(true); + $setupFileCreationTime = filectime($setupFile); + + if (abs(time() - $setupFileCreationTime) > 5400) { + return null; + } + + // Load update.php. It pulls a variable named $restoration_setup into the local scope. + clearFileInOPCache($setupFile); + + require_once $setupFile; + + /** @var array $extractionSetup */ + + // The file exists but no configuration is present? + if (empty($extractionSetup ?? null) || !is_array($extractionSetup)) { + return null; + } + + /** + * Immediately reject any attempt to run extract.php without a password. + * + * Doing that is a GRAVE SECURITY RISK. It makes it trivial to hack a site. Therefore we are preventing this script + * to run without a password. + */ + $password = $extractionSetup['security.password'] ?? null; + $userPassword = $_REQUEST['password'] ?? ''; + $userPassword = !is_string($userPassword) ? '' : trim($userPassword); + + if (empty($password) || !is_string($password) || (trim($password) == '') || (strlen(trim($password)) < 32)) { + return null; + } + + // Timing-safe password comparison. See http://blog.ircmaxell.com/2014/11/its-all-about-time.html + if (!timingSafeEquals($password, $userPassword)) { + return null; + } + + // An "instance" variable will resume the engine from the serialised instance + $serialized = $_REQUEST['instance'] ?? null; + + if (!is_null($serialized) && empty(ZIPExtraction::unserialiseInstance($serialized))) { + // The serialised instance is corrupt or someone tries to trick us. YOU SHALL NOT PASS! + return null; + } + + return $extractionSetup; } // Import configuration $retArray = [ - 'status' => true, - 'message' => null, + 'status' => true, + 'message' => null, ]; $configuration = getConfiguration(); $enabled = !empty($configuration); -if ($enabled) +/** + * Sets the PHP timeout to 3600 seconds + * + * @return void + * @since 4.2.0 + */ +function setLongTimeout() { - $sourcePath = $configuration['setup.sourcepath'] ?? ''; - $sourceFile = $configuration['setup.sourcefile'] ?? ''; - $destDir = ($configuration['setup.destdir'] ?? null) ?: __DIR__; - $basePath = rtrim(str_replace('\\', '/', __DIR__), '/'); - $basePath = empty($basePath) ? $basePath : ($basePath . '/'); - $sourceFile = (empty($sourcePath) ? '' : (rtrim($sourcePath, '/\\') . '/')) . $sourceFile; - $engine = ZIPExtraction::getInstance(); - - $engine->setFilename($sourceFile); - $engine->setAddPath($destDir); - $skipFiles = [ - 'administrator/components/com_joomlaupdate/restoration.php', - 'administrator/components/com_joomlaupdate/update.php', - ]; - - if (defined('_JOOMLA_UPDATE_DEBUG')) - { - $skipFiles[] = 'administrator/components/com_joomlaupdate/extract.php'; - } - - $engine->setSkipFiles($skipFiles - ); - $engine->setIgnoreDirectories([ - 'tmp', 'administrator/logs', - ] - ); - - $task = $_REQUEST['task'] ?? null; - - switch ($task) - { - case 'startExtract': - case 'stepExtract': - $done = $engine->step(); - $error = $engine->getError(); - - if ($error != '') - { - $retArray['status'] = false; - $retArray['done'] = true; - $retArray['message'] = $error; - } - elseif ($done) - { - $retArray['files'] = $engine->filesProcessed; - $retArray['bytesIn'] = $engine->compressedTotal; - $retArray['bytesOut'] = $engine->uncompressedTotal; - $retArray['percent'] = 100; - $retArray['status'] = true; - $retArray['done'] = true; - - $retArray['percent'] = min($retArray['percent'], 100); - } - else - { - $retArray['files'] = $engine->filesProcessed; - $retArray['bytesIn'] = $engine->compressedTotal; - $retArray['bytesOut'] = $engine->uncompressedTotal; - $retArray['percent'] = ($engine->totalSize > 0) ? (100 * $engine->compressedTotal / $engine->totalSize) : 0; - $retArray['status'] = true; - $retArray['done'] = false; - $retArray['instance'] = ZIPExtraction::getSerialised(); - } - - $engine->enforceMinimumExecutionTime(); - - break; - - case 'finalizeUpdate': - $root = $configuration['setup.destdir'] ?? ''; - - // Remove update.php - clearFileInOPCache($basePath . 'update.php'); - @unlink($basePath . 'update.php'); - - // Import a custom finalisation file - $filename = dirname(__FILE__) . '/finalisation.php'; - - if (file_exists($filename)) - { - clearFileInOPCache($filename); - - include_once $filename; - } - - // Run a custom finalisation script - if (function_exists('finalizeUpdate')) - { - finalizeUpdate($root, $basePath); - } - - $engine->enforceMinimumExecutionTime(); - - break; - - default: - // Invalid task! - $enabled = false; - break; - } + if (!function_exists('ini_set')) { + return; + } + + ini_set('max_execution_time', 3600); } -// This could happen even if $enabled was true, e.g. if we were asked for an invalid task. -if (!$enabled) +/** + * Sets the memory limit to 1GiB + * + * @return void + * @since 4.2.0 + */ +function setHugeMemoryLimit() { - // Maybe we weren't authorized or the task was invalid? - $retArray['status'] = false; - $retArray['message'] = 'Invalid login'; + if (!function_exists('ini_set')) { + return; + } + + ini_set('memory_limit', 1073741824); +} + +if ($enabled) { + // Try to set a very large memory and timeout limit + setLongTimeout(); + setHugeMemoryLimit(); + + $sourcePath = $configuration['setup.sourcepath'] ?? ''; + $sourceFile = $configuration['setup.sourcefile'] ?? ''; + $destDir = ($configuration['setup.destdir'] ?? null) ?: __DIR__; + $basePath = rtrim(str_replace('\\', '/', __DIR__), '/'); + $basePath = empty($basePath) ? $basePath : ($basePath . '/'); + $sourceFile = (empty($sourcePath) ? '' : (rtrim($sourcePath, '/\\') . '/')) . $sourceFile; + $engine = ZIPExtraction::getInstance(); + + $engine->setFilename($sourceFile); + $engine->setAddPath($destDir); + $skipFiles = [ + 'administrator/components/com_joomlaupdate/restoration.php', + 'administrator/components/com_joomlaupdate/update.php', + ]; + + if (defined('_JOOMLA_UPDATE_DEBUG')) { + $skipFiles[] = 'administrator/components/com_joomlaupdate/extract.php'; + } + + $engine->setSkipFiles($skipFiles); + $engine->setIgnoreDirectories([ + 'tmp', 'administrator/logs', + ]); + + $task = $_REQUEST['task'] ?? null; + + switch ($task) { + case 'startExtract': + case 'stepExtract': + $done = $engine->step(); + $error = $engine->getError(); + + if ($error != '') { + $retArray['status'] = false; + $retArray['done'] = true; + $retArray['message'] = $error; + } elseif ($done) { + $retArray['files'] = $engine->filesProcessed; + $retArray['bytesIn'] = $engine->compressedTotal; + $retArray['bytesOut'] = $engine->uncompressedTotal; + $retArray['percent'] = 100; + $retArray['status'] = true; + $retArray['done'] = true; + + $retArray['percent'] = min($retArray['percent'], 100); + } else { + $retArray['files'] = $engine->filesProcessed; + $retArray['bytesIn'] = $engine->compressedTotal; + $retArray['bytesOut'] = $engine->uncompressedTotal; + $retArray['percent'] = ($engine->totalSize > 0) ? (100 * $engine->compressedTotal / $engine->totalSize) : 0; + $retArray['status'] = true; + $retArray['done'] = false; + $retArray['instance'] = ZIPExtraction::getSerialised(); + } + + $engine->enforceMinimumExecutionTime(); + + break; + + case 'finalizeUpdate': + $root = $configuration['setup.destdir'] ?? ''; + + // Remove the administrator/cache/autoload_psr4.php file + $filename = $root . (empty($root) ? '' : '/') . 'administrator/cache/autoload_psr4.php'; + + if (file_exists($filename)) { + clearFileInOPCache($filename); + clearstatcache(true, $filename); + + @unlink($filename); + } + + // Remove update.php + clearFileInOPCache($basePath . 'update.php'); + @unlink($basePath . 'update.php'); + + // Import a custom finalisation file + $filename = dirname(__FILE__) . '/finalisation.php'; + + if (file_exists($filename)) { + clearFileInOPCache($filename); + + include_once $filename; + } + + // Run a custom finalisation script + if (function_exists('finalizeUpdate')) { + finalizeUpdate($root, $basePath); + } + + $engine->enforceMinimumExecutionTime(); + + break; + + default: + // Invalid task! + $enabled = false; + break; + } +} + +// This could happen even if $enabled was true, e.g. if we were asked for an invalid task. +if (!$enabled) { + // Maybe we weren't authorized or the task was invalid? + $retArray['status'] = false; + $retArray['message'] = 'Invalid login'; } // JSON encode the message diff --git a/code/administrator/components/com_joomlaupdate/finalisation.php b/code/administrator/components/com_joomlaupdate/finalisation.php index d690a54b..697eacff 100644 --- a/code/administrator/components/com_joomlaupdate/finalisation.php +++ b/code/administrator/components/com_joomlaupdate/finalisation.php @@ -1,4 +1,5 @@ deleteUnexistingFiles(); + } - // Remove obsolete files - prevents errors occurring in some system plugins - if (class_exists('JoomlaInstallerScript')) - { - (new JoomlaInstallerScript)->deleteUnexistingFiles(); - } - } - } + /** + * Remove autoload_psr4.php so that namespace map is re-generated on the next request. This is needed + * when there are new classes added to extensions on new Joomla! release. + */ + $namespaceMapFile = JPATH_ROOT . '/administrator/cache/autoload_psr4.php'; + + if (\Joomla\CMS\Filesystem\File::exists($namespaceMapFile)) { + \Joomla\CMS\Filesystem\File::delete($namespaceMapFile); + } + } + } } namespace Joomla\CMS\Filesystem { - // Fake the File class - if (!class_exists('\Joomla\CMS\Filesystem\File')) - { - /** - * File mock class - * - * @since 3.5.1 - */ - abstract class File - { - /** - * Proxies checking a file exists to the native php version - * - * @param string $fileName The path to the file to be checked - * - * @return boolean - * - * @since 3.5.1 - */ - public static function exists(string $fileName): bool - { - return @file_exists($fileName); - } - - /** - * Delete a file and invalidate the PHP OPcache - * - * @param string $fileName The path to the file to be deleted - * - * @return boolean - * - * @since 3.5.1 - */ - public static function delete(string $fileName): bool - { - self::invalidateFileCache($fileName); + // Fake the File class + if (!class_exists('\Joomla\CMS\Filesystem\File')) { + /** + * File mock class + * + * @since 3.5.1 + */ + abstract class File + { + /** + * Proxies checking a file exists to the native php version + * + * @param string $fileName The path to the file to be checked + * + * @return boolean + * + * @since 3.5.1 + */ + public static function exists(string $fileName): bool + { + return @file_exists($fileName); + } - return @unlink($fileName); - } + /** + * Delete a file and invalidate the PHP OPcache + * + * @param string $fileName The path to the file to be deleted + * + * @return boolean + * + * @since 3.5.1 + */ + public static function delete(string $fileName): bool + { + self::invalidateFileCache($fileName); - /** - * Rename a file and invalidate the PHP OPcache - * - * @param string $src The path to the source file - * @param string $dest The path to the destination file - * - * @return boolean True on success - * - * @since 4.0.1 - */ - public static function move(string $src, string $dest): bool - { - self::invalidateFileCache($src); + return @unlink($fileName); + } - $result = @rename($src, $dest); + /** + * Rename a file and invalidate the PHP OPcache + * + * @param string $src The path to the source file + * @param string $dest The path to the destination file + * + * @return boolean True on success + * + * @since 4.0.1 + */ + public static function move(string $src, string $dest): bool + { + self::invalidateFileCache($src); - if ($result) - { - self::invalidateFileCache($dest); - } + $result = @rename($src, $dest); - return $result; - } + if ($result) { + self::invalidateFileCache($dest); + } - /** - * Invalidate opcache for a newly written/deleted file immediately, if opcache* functions exist and if this was a PHP file. - * - * @param string $filepath The path to the file just written to, to flush from opcache - * @param boolean $force If set to true, the script will be invalidated regardless of whether invalidation is necessary - * - * @return boolean TRUE if the opcode cache for script was invalidated/nothing to invalidate, - * or FALSE if the opcode cache is disabled or other conditions returning - * FALSE from opcache_invalidate (like file not found). - * - * @since 4.0.2 - */ - public static function invalidateFileCache($filepath, $force = true) - { - return \clearFileInOPCache($filepath); - } + return $result; + } - } - } + /** + * Invalidate opcache for a newly written/deleted file immediately, if opcache* functions exist and if this was a PHP file. + * + * @param string $filepath The path to the file just written to, to flush from opcache + * @param boolean $force If set to true, the script will be invalidated regardless of whether invalidation is necessary + * + * @return boolean TRUE if the opcode cache for script was invalidated/nothing to invalidate, + * or FALSE if the opcode cache is disabled or other conditions returning + * FALSE from opcache_invalidate (like file not found). + * + * @since 4.0.2 + */ + public static function invalidateFileCache($filepath, $force = true) + { + return \clearFileInOPCache($filepath); + } + } + } - // Fake the Folder class, mapping it to Restore's post-processing class - if (!class_exists('\Joomla\CMS\Filesystem\Folder')) - { - /** - * Folder mock class - * - * @since 3.5.1 - */ - abstract class Folder - { - /** - * Proxies checking a folder exists to the native php version - * - * @param string $folderName The path to the folder to be checked - * - * @return boolean - * - * @since 3.5.1 - */ - public static function exists(string $folderName): bool - { - return @is_dir($folderName); - } + // Fake the Folder class, mapping it to Restore's post-processing class + if (!class_exists('\Joomla\CMS\Filesystem\Folder')) { + /** + * Folder mock class + * + * @since 3.5.1 + */ + abstract class Folder + { + /** + * Proxies checking a folder exists to the native php version + * + * @param string $folderName The path to the folder to be checked + * + * @return boolean + * + * @since 3.5.1 + */ + public static function exists(string $folderName): bool + { + return @is_dir($folderName); + } - /** - * Delete a folder recursively and invalidate the PHP OPcache - * - * @param string $folderName The path to the folder to be deleted - * - * @return boolean - * - * @since 3.5.1 - */ - public static function delete(string $folderName): bool - { - if (substr($folderName, -1) == '/') - { - $folderName = substr($folderName, 0, -1); - } + /** + * Delete a folder recursively and invalidate the PHP OPcache + * + * @param string $folderName The path to the folder to be deleted + * + * @return boolean + * + * @since 3.5.1 + */ + public static function delete(string $folderName): bool + { + if (substr($folderName, -1) == '/') { + $folderName = substr($folderName, 0, -1); + } - if (!@file_exists($folderName) || !@is_dir($folderName) || !is_readable($folderName)) - { - return false; - } + if (!@file_exists($folderName) || !@is_dir($folderName) || !is_readable($folderName)) { + return false; + } - $di = new \DirectoryIterator($folderName); + $di = new \DirectoryIterator($folderName); - /** @var \DirectoryIterator $item */ - foreach ($di as $item) - { - if ($item->isDot()) - { - continue; - } + /** @var \DirectoryIterator $item */ + foreach ($di as $item) { + if ($item->isDot()) { + continue; + } - if ($item->isDir()) - { - $status = self::delete($item->getPathname()); + if ($item->isDir()) { + $status = self::delete($item->getPathname()); - if (!$status) - { - return false; - } + if (!$status) { + return false; + } - continue; - } + continue; + } - \clearFileInOPCache($item->getPathname()); + \clearFileInOPCache($item->getPathname()); - @unlink($item->getPathname()); - } + @unlink($item->getPathname()); + } - return @rmdir($folderName); - } - } - } + return @rmdir($folderName); + } + } + } } namespace Joomla\CMS\Language { - // Fake the Text class - we aren't going to show errors to people anyhow - if (!class_exists('\Joomla\CMS\Language\Text')) - { - /** - * Text mock class - * - * @since 3.5.1 - */ - abstract class Text - { - /** - * No need for translations in a non-interactive script, so always return an empty string here - * - * @param string $text A language constant - * - * @return string - * - * @since 3.5.1 - */ - public static function sprintf(string $text): string - { - return ''; - } - } - } + // Fake the Text class - we aren't going to show errors to people anyhow + if (!class_exists('\Joomla\CMS\Language\Text')) { + /** + * Text mock class + * + * @since 3.5.1 + */ + abstract class Text + { + /** + * No need for translations in a non-interactive script, so always return an empty string here + * + * @param string $text A language constant + * + * @return string + * + * @since 3.5.1 + */ + public static function sprintf(string $text): string + { + return ''; + } + } + } } diff --git a/code/administrator/components/com_joomlaupdate/joomlaupdate.xml b/code/administrator/components/com_joomlaupdate/joomlaupdate.xml index 01a42b77..1d865d07 100644 --- a/code/administrator/components/com_joomlaupdate/joomlaupdate.xml +++ b/code/administrator/components/com_joomlaupdate/joomlaupdate.xml @@ -2,7 +2,7 @@ com_joomlaupdate Joomla! Project - August 2021 + 2021-08 (C) 2012 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_joomlaupdate/services/provider.php b/code/administrator/components/com_joomlaupdate/services/provider.php index 19a9a74e..73e9c148 100644 --- a/code/administrator/components/com_joomlaupdate/services/provider.php +++ b/code/administrator/components/com_joomlaupdate/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Joomlaupdate')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Joomlaupdate')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Joomlaupdate')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Joomlaupdate')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_joomlaupdate/src/Controller/DisplayController.php b/code/administrator/components/com_joomlaupdate/src/Controller/DisplayController.php index 75534fc1..c70e5338 100644 --- a/code/administrator/components/com_joomlaupdate/src/Controller/DisplayController.php +++ b/code/administrator/components/com_joomlaupdate/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'Joomlaupdate'); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - // Only super user can access file upload - if ($view == 'upload' && !$this->app->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) - { - $this->app->redirect(Route::_('index.php?option=com_joomlaupdate', true)); - } - - // Get the model for the view. - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - /** @var ?\Joomla\Component\Installer\Administrator\Model\WarningsModel $warningsModel */ - $warningsModel = $this->app->bootComponent('com_installer') - ->getMVCFactory()->createModel('Warnings', 'Administrator', ['ignore_request' => true]); - - if ($warningsModel !== null) - { - $view->setModel($warningsModel, false); - } - - // Perform update source preference check and refresh update information. - $model->applyUpdateSite(); - $model->refreshUpdates(); - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - $view->display(); - } - - return $this; - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_joomlaupdate')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Update'); - - $model->refreshUpdates(); - - $joomlaUpdate = $model->getUpdateInformation(); - - $hasUpdate = $joomlaUpdate['hasUpdate'] ? $joomlaUpdate['latest'] : ''; - - echo new JsonResponse($hasUpdate); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 2.5.4 + */ + public function display($cachable = false, $urlparams = false) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'Joomlaupdate'); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + // Only super user can access file upload + if ($view == 'upload' && !$this->app->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) { + $this->app->redirect(Route::_('index.php?option=com_joomlaupdate', true)); + } + + // Get the model for the view. + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + /** @var ?\Joomla\Component\Installer\Administrator\Model\WarningsModel $warningsModel */ + $warningsModel = $this->app->bootComponent('com_installer') + ->getMVCFactory()->createModel('Warnings', 'Administrator', ['ignore_request' => true]); + + if ($warningsModel !== null) { + $view->setModel($warningsModel, false); + } + + // Perform update source preference check and refresh update information. + $model->applyUpdateSite(); + $model->refreshUpdates(); + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + $view->display(); + } + + return $this; + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_joomlaupdate')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Update'); + + $model->refreshUpdates(); + + $joomlaUpdate = $model->getUpdateInformation(); + + $hasUpdate = $joomlaUpdate['hasUpdate'] ? $joomlaUpdate['latest'] : ''; + + echo new JsonResponse($hasUpdate); + } } diff --git a/code/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php b/code/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php index 94bba2ca..89fc09d5 100644 --- a/code/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php +++ b/code/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php @@ -1,4 +1,5 @@ checkToken(); - - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - $user = $this->app->getIdentity(); - - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_START', $user->id, $user->name, \JVERSION), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - $result = $model->download(); - $file = $result['basename']; - - $message = null; - $messageType = null; - - // The validation was not successful so abort. - if ($result['check'] === false) - { - $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_CHECKSUM_WRONG'); - $messageType = 'error'; - $url = 'index.php?option=com_joomlaupdate'; - - $this->app->setUserState('com_joomlaupdate.file', null); - $this->setRedirect($url, $message, $messageType); - - try - { - Log::add($message, Log::ERROR, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - return; - } - - if ($file) - { - $this->app->setUserState('com_joomlaupdate.file', $file); - $url = 'index.php?option=com_joomlaupdate&task=update.install&' . $this->app->getSession()->getFormToken() . '=1'; - - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $file), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - } - else - { - $this->app->setUserState('com_joomlaupdate.file', null); - $url = 'index.php?option=com_joomlaupdate'; - $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_DOWNLOADFAILED'); - $messageType = 'error'; - } - - $this->setRedirect($url, $message, $messageType); - } - - /** - * Start the installation of the new Joomla! version - * - * @return void - * - * @since 2.5.4 - */ - public function install() - { - $this->checkToken('get'); - $this->app->setUserState('com_joomlaupdate.oldversion', JVERSION); - - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - - try - { - Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_INSTALL'), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - $file = $this->app->getUserState('com_joomlaupdate.file', null); - $model->createRestorationFile($file); - - $this->display(); - } - - /** - * Finalise the upgrade by running the necessary scripts - * - * @return void - * - * @since 2.5.4 - */ - public function finalise() - { - /* - * Finalize with login page. Used for pre-token check versions - * to allow updates without problems but with a maximum of security. - */ - if (!Session::checkToken('get')) - { - $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); - - return; - } - - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - - try - { - Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_FINALISE'), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - $model->finaliseUpgrade(); - - $url = 'index.php?option=com_joomlaupdate&task=update.cleanup&' . Session::getFormToken() . '=1'; - $this->setRedirect($url); - } - - /** - * Clean up after ourselves - * - * @return void - * - * @since 2.5.4 - */ - public function cleanup() - { - /* - * Cleanup with login page. Used for pre-token check versions to be able to update - * from =< 3.2.7 to allow updates without problems but with a maximum of security. - */ - if (!Session::checkToken('get')) - { - $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); - - return; - } - - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - - try - { - Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_CLEANUP'), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - $model->cleanUp(); - - $url = 'index.php?option=com_joomlaupdate&view=joomlaupdate&layout=complete'; - $this->setRedirect($url); - - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_COMPLETE', \JVERSION), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - } - - /** - * Purges updates. - * - * @return void - * - * @since 3.0 - */ - public function purge() - { - // Check for request forgeries - $this->checkToken('request'); - - // Purge updates - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - $model->purge(); - - $url = 'index.php?option=com_joomlaupdate'; - $this->setRedirect($url, $model->_message); - } - - /** - * Uploads an update package to the temporary directory, under a random name - * - * @return void - * - * @since 3.6.0 - */ - public function upload() - { - // Check for request forgeries - $this->checkToken(); - - // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? - $this->app->getIdentity()->authorise('core.admin') or jexit(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')); - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - try - { - $model->upload(); - } - catch (\RuntimeException $e) - { - $url = 'index.php?option=com_joomlaupdate'; - $this->setRedirect($url, $e->getMessage(), 'error'); - - return; - } - - $token = Session::getFormToken(); - $url = 'index.php?option=com_joomlaupdate&task=update.captive&' . $token . '=1'; - $this->setRedirect($url); - } - - /** - * Checks there is a valid update package and redirects to the captive view for super admin authentication. - * - * @return void - * - * @since 3.6.0 - */ - public function captive() - { - // Check for request forgeries - $this->checkToken('get'); - - // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - // Do I really have an update package? - $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); - - if (empty($tempFile) || !File::exists($tempFile)) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - $this->input->set('view', 'upload'); - $this->input->set('layout', 'captive'); - - $this->display(); - } - - /** - * Checks the admin has super administrator privileges and then proceeds with the update. - * - * @return void - * - * @since 3.6.0 - */ - public function confirm() - { - // Check for request forgeries - $this->checkToken(); - - // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - // Get the captive file before the session resets - $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); - - // Do I really have an update package? - if (!$model->captiveFileExists()) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - // Try to log in - $credentials = array( - 'username' => $this->input->post->get('username', '', 'username'), - 'password' => $this->input->post->get('passwd', '', 'raw'), - 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), - ); - - $result = $model->captiveLogin($credentials); - - if (!$result) - { - $model->removePackageFiles(); - - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - // Set the update source in the session - $this->app->setUserState('com_joomlaupdate.file', basename($tempFile)); - - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $tempFile), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // Redirect to the actual update page - $url = 'index.php?option=com_joomlaupdate&task=update.install&' . Session::getFormToken() . '=1'; - $this->setRedirect($url); - } - - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. - * - * @return static This object to support chaining. - * - * @since 2.5.4 - */ - public function display($cachable = false, $urlparams = array()) - { - // Get the document object. - $document = $this->app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'update'); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - // Get the model for the view. - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - $view->display(); - } - - return $this; - } - - /** - * Checks the admin has super administrator privileges and then proceeds with the final & cleanup steps. - * - * @return void - * - * @since 3.6.3 - */ - public function finaliseconfirm() - { - // Check for request forgeries - $this->checkToken(); - - // Did a non Super User try do this? - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - // Get the model - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - // Try to log in - $credentials = array( - 'username' => $this->input->post->get('username', '', 'username'), - 'password' => $this->input->post->get('passwd', '', 'raw'), - 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), - ); - - $result = $model->captiveLogin($credentials); - - // The login fails? - if (!$result) - { - $this->setMessage(Text::_('JGLOBAL_AUTH_INVALID_PASS'), 'warning'); - $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); - - return; - } - - // Redirect back to the actual finalise page - $this->setRedirect('index.php?option=com_joomlaupdate&task=update.finalise&' . Session::getFormToken() . '=1'); - } - - /** - * Fetch Extension update XML proxy. Used to prevent Access-Control-Allow-Origin errors. - * Prints a JSON string. - * Called from JS. - * - * @since 3.10.0 - * - * @return void - */ - public function fetchExtensionCompatibility() - { - $extensionID = $this->input->get('extension-id', '', 'DEFAULT'); - $joomlaTargetVersion = $this->input->get('joomla-target-version', '', 'DEFAULT'); - $joomlaCurrentVersion = $this->input->get('joomla-current-version', '', JVERSION); - $extensionVersion = $this->input->get('extension-version', '', 'DEFAULT'); - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); - $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); - $upgradeUpdateVersion = false; - $currentUpdateVersion = false; - - $upgradeWarning = 0; - - if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) - { - $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); - } - - if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) - { - $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); - } - - if ($upgradeUpdateVersion !== false) - { - $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; - - if ($currentUpdateVersion !== false) - { - // If there are updates compatible with both CMS versions use these - $bothCompatibleVersions = array_values( - array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) - ); - - if (!empty($bothCompatibleVersions)) - { - $upgradeOldestVersion = $bothCompatibleVersions[0]; - $upgradeUpdateVersion = end($bothCompatibleVersions); - } - } - - if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) - { - // Installed version is empty or older than the oldest compatible update: Update required - $resultGroup = 2; - } - else - { - // Current version is compatible - $resultGroup = 3; - } - - if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) - { - // Special case warning when version compatible with target is lower than current - $upgradeWarning = 2; - } - } - elseif ($currentUpdateVersion !== false) - { - // No compatible version for target version but there is a compatible version for current version - $resultGroup = 1; - } - else - { - // No update server available - $resultGroup = 1; - } - - // Do we need to capture - $combinedCompatibilityStatus = array( - 'upgradeCompatibilityStatus' => (object) array( - 'state' => $upgradeCompatibilityStatus->state, - 'compatibleVersion' => $upgradeUpdateVersion - ), - 'currentCompatibilityStatus' => (object) array( - 'state' => $currentCompatibilityStatus->state, - 'compatibleVersion' => $currentUpdateVersion - ), - 'resultGroup' => $resultGroup, - 'upgradeWarning' => $upgradeWarning, - ); - - $this->app = Factory::getApplication(); - $this->app->mimeType = 'application/json'; - $this->app->charSet = 'utf-8'; - $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); - $this->app->sendHeaders(); - - try - { - echo new JsonResponse($combinedCompatibilityStatus); - } - catch (\Exception $e) - { - echo $e; - } - - $this->app->close(); - } - - /** - * Fetch and report updates in \JSON format, for AJAX requests - * - * @return void - * - * @since 3.10.10 - */ - public function ajax() - { - if (!Session::checkToken('get')) - { - $this->app->setHeader('status', 403, true); - $this->app->sendHeaders(); - echo Text::_('JINVALID_TOKEN_NOTICE'); - $this->app->close(); - } - - /** @var UpdateModel $model */ - $model = $this->getModel('Update'); - $updateInfo = $model->getUpdateInformation(); - - $update = []; - $update[] = ['version' => $updateInfo['latest']]; - - echo json_encode($update); - - $this->app->close(); - } + /** + * Performs the download of the update package + * + * @return void + * + * @since 2.5.4 + */ + public function download() + { + $this->checkToken(); + + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + $user = $this->app->getIdentity(); + + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_START', $user->id, $user->name, \JVERSION), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + $result = $model->download(); + $file = $result['basename']; + + $message = null; + $messageType = null; + + // The validation was not successful so abort. + if ($result['check'] === false) { + $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_CHECKSUM_WRONG'); + $messageType = 'error'; + $url = 'index.php?option=com_joomlaupdate'; + + $this->app->setUserState('com_joomlaupdate.file', null); + $this->setRedirect($url, $message, $messageType); + + try { + Log::add($message, Log::ERROR, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + return; + } + + if ($file) { + $this->app->setUserState('com_joomlaupdate.file', $file); + $url = 'index.php?option=com_joomlaupdate&task=update.install&' . $this->app->getSession()->getFormToken() . '=1'; + + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $file), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + } else { + $this->app->setUserState('com_joomlaupdate.file', null); + $url = 'index.php?option=com_joomlaupdate'; + $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_DOWNLOADFAILED'); + $messageType = 'error'; + } + + $this->setRedirect($url, $message, $messageType); + } + + /** + * Start the installation of the new Joomla! version + * + * @return void + * + * @since 2.5.4 + */ + public function install() + { + $this->checkToken('get'); + $this->app->setUserState('com_joomlaupdate.oldversion', JVERSION); + + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + + try { + Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_INSTALL'), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $file = $this->app->getUserState('com_joomlaupdate.file', null); + $model->createRestorationFile($file); + + $this->display(); + } + + /** + * Finalise the upgrade by running the necessary scripts + * + * @return void + * + * @since 2.5.4 + */ + public function finalise() + { + /* + * Finalize with login page. Used for pre-token check versions + * to allow updates without problems but with a maximum of security. + */ + if (!Session::checkToken('get')) { + $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); + + return; + } + + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + + try { + Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_FINALISE'), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $model->finaliseUpgrade(); + + $url = 'index.php?option=com_joomlaupdate&task=update.cleanup&' . Session::getFormToken() . '=1'; + $this->setRedirect($url); + } + + /** + * Clean up after ourselves + * + * @return void + * + * @since 2.5.4 + */ + public function cleanup() + { + /* + * Cleanup with login page. Used for pre-token check versions to be able to update + * from =< 3.2.7 to allow updates without problems but with a maximum of security. + */ + if (!Session::checkToken('get')) { + $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); + + return; + } + + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + + try { + Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_CLEANUP'), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $model->cleanUp(); + + $url = 'index.php?option=com_joomlaupdate&view=joomlaupdate&layout=complete'; + $this->setRedirect($url); + + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_COMPLETE', \JVERSION), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + } + + /** + * Purges updates. + * + * @return void + * + * @since 3.0 + */ + public function purge() + { + // Check for request forgeries + $this->checkToken('request'); + + // Purge updates + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + $model->purge(); + + $url = 'index.php?option=com_joomlaupdate'; + $this->setRedirect($url, $model->_message); + } + + /** + * Uploads an update package to the temporary directory, under a random name + * + * @return void + * + * @since 3.6.0 + */ + public function upload() + { + // Check for request forgeries + $this->checkToken(); + + // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? + $this->app->getIdentity()->authorise('core.admin') or jexit(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + try { + $model->upload(); + } catch (\RuntimeException $e) { + $url = 'index.php?option=com_joomlaupdate'; + $this->setRedirect($url, $e->getMessage(), 'error'); + + return; + } + + $token = Session::getFormToken(); + $url = 'index.php?option=com_joomlaupdate&task=update.captive&' . $token . '=1'; + $this->setRedirect($url); + } + + /** + * Checks there is a valid update package and redirects to the captive view for super admin authentication. + * + * @return void + * + * @since 3.6.0 + */ + public function captive() + { + // Check for request forgeries + $this->checkToken('get'); + + // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Do I really have an update package? + $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); + + if (empty($tempFile) || !File::exists($tempFile)) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + $this->input->set('view', 'upload'); + $this->input->set('layout', 'captive'); + + $this->display(); + } + + /** + * Checks the admin has super administrator privileges and then proceeds with the update. + * + * @return void + * + * @since 3.6.0 + */ + public function confirm() + { + // Check for request forgeries + $this->checkToken(); + + // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + // Get the captive file before the session resets + $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); + + // Do I really have an update package? + if (!$model->captiveFileExists()) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Try to log in + $credentials = array( + 'username' => $this->input->post->get('username', '', 'username'), + 'password' => $this->input->post->get('passwd', '', 'raw'), + 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), + ); + + $result = $model->captiveLogin($credentials); + + if (!$result) { + $model->removePackageFiles(); + + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Set the update source in the session + $this->app->setUserState('com_joomlaupdate.file', basename($tempFile)); + + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $tempFile), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // Redirect to the actual update page + $url = 'index.php?option=com_joomlaupdate&task=update.install&' . Session::getFormToken() . '=1'; + $this->setRedirect($url); + } + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 2.5.4 + */ + public function display($cachable = false, $urlparams = array()) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'update'); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + // Get the model for the view. + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + $view->display(); + } + + return $this; + } + + /** + * Checks the admin has super administrator privileges and then proceeds with the final & cleanup steps. + * + * @return void + * + * @since 3.6.3 + */ + public function finaliseconfirm() + { + // Check for request forgeries + $this->checkToken(); + + // Did a non Super User try do this? + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Get the model + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + // Try to log in + $credentials = array( + 'username' => $this->input->post->get('username', '', 'username'), + 'password' => $this->input->post->get('passwd', '', 'raw'), + 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), + ); + + $result = $model->captiveLogin($credentials); + + // The login fails? + if (!$result) { + $this->setMessage(Text::_('JGLOBAL_AUTH_INVALID_PASS'), 'warning'); + $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); + + return; + } + + // Redirect back to the actual finalise page + $this->setRedirect('index.php?option=com_joomlaupdate&task=update.finalise&' . Session::getFormToken() . '=1'); + } + + /** + * Fetch Extension update XML proxy. Used to prevent Access-Control-Allow-Origin errors. + * Prints a JSON string. + * Called from JS. + * + * @since 3.10.0 + * @deprecated 5.0 Use batchextensioncompatibility instead. + * + * @return void + */ + public function fetchExtensionCompatibility() + { + $extensionID = $this->input->get('extension-id', '', 'DEFAULT'); + $joomlaTargetVersion = $this->input->get('joomla-target-version', '', 'DEFAULT'); + $joomlaCurrentVersion = $this->input->get('joomla-current-version', '', JVERSION); + $extensionVersion = $this->input->get('extension-version', '', 'DEFAULT'); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); + $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); + $upgradeUpdateVersion = false; + $currentUpdateVersion = false; + + $upgradeWarning = 0; + + if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) { + $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); + } + + if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) { + $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); + } + + if ($upgradeUpdateVersion !== false) { + $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; + + if ($currentUpdateVersion !== false) { + // If there are updates compatible with both CMS versions use these + $bothCompatibleVersions = array_values( + array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) + ); + + if (!empty($bothCompatibleVersions)) { + $upgradeOldestVersion = $bothCompatibleVersions[0]; + $upgradeUpdateVersion = end($bothCompatibleVersions); + } + } + + if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) { + // Installed version is empty or older than the oldest compatible update: Update required + $resultGroup = 2; + } else { + // Current version is compatible + $resultGroup = 3; + } + + if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) { + // Special case warning when version compatible with target is lower than current + $upgradeWarning = 2; + } + } elseif ($currentUpdateVersion !== false) { + // No compatible version for target version but there is a compatible version for current version + $resultGroup = 1; + } else { + // No update server available + $resultGroup = 1; + } + + // Do we need to capture + $combinedCompatibilityStatus = array( + 'upgradeCompatibilityStatus' => (object) array( + 'state' => $upgradeCompatibilityStatus->state, + 'compatibleVersion' => $upgradeUpdateVersion + ), + 'currentCompatibilityStatus' => (object) array( + 'state' => $currentCompatibilityStatus->state, + 'compatibleVersion' => $currentUpdateVersion + ), + 'resultGroup' => $resultGroup, + 'upgradeWarning' => $upgradeWarning, + ); + + $this->app = Factory::getApplication(); + $this->app->mimeType = 'application/json'; + $this->app->charSet = 'utf-8'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + try { + echo new JsonResponse($combinedCompatibilityStatus); + } catch (\Exception $e) { + echo $e; + } + + $this->app->close(); + } + + /** + * Determines the compatibility information for a number of extensions. + * + * Called by the Joomla Update JavaScript (PreUpdateChecker.checkNextChunk). + * + * @return void + * @since 4.2.0 + * + */ + public function batchextensioncompatibility() + { + $joomlaTargetVersion = $this->input->post->get('joomla-target-version', '', 'DEFAULT'); + $joomlaCurrentVersion = $this->input->post->get('joomla-current-version', JVERSION); + $extensionInformation = $this->input->post->get('extensions', []); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $extensionResults = []; + $leftover = []; + $startTime = microtime(true); + + foreach ($extensionInformation as $information) { + // Only process an extension if we have spent less than 5 seconds already + $currentTime = microtime(true); + + if ($currentTime - $startTime > 5.0) { + $leftover[] = $information; + + continue; + } + + // Get the extension information and fetch its compatibility information + $extensionID = $information['eid'] ?: ''; + $extensionVersion = $information['version'] ?: ''; + $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); + $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); + $upgradeUpdateVersion = false; + $currentUpdateVersion = false; + $upgradeWarning = 0; + + if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) { + $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); + } + + if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) { + $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); + } + + if ($upgradeUpdateVersion !== false) { + $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; + + if ($currentUpdateVersion !== false) { + // If there are updates compatible with both CMS versions use these + $bothCompatibleVersions = array_values( + array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) + ); + + if (!empty($bothCompatibleVersions)) { + $upgradeOldestVersion = $bothCompatibleVersions[0]; + $upgradeUpdateVersion = end($bothCompatibleVersions); + } + } + + if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) { + // Installed version is empty or older than the oldest compatible update: Update required + $resultGroup = 2; + } else { + // Current version is compatible + $resultGroup = 3; + } + + if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) { + // Special case warning when version compatible with target is lower than current + $upgradeWarning = 2; + } + } elseif ($currentUpdateVersion !== false) { + // No compatible version for target version but there is a compatible version for current version + $resultGroup = 1; + } else { + // No update server available + $resultGroup = 1; + } + + // Do we need to capture + $extensionResults[] = [ + 'id' => $extensionID, + 'upgradeCompatibilityStatus' => (object) [ + 'state' => $upgradeCompatibilityStatus->state, + 'compatibleVersion' => $upgradeUpdateVersion + ], + 'currentCompatibilityStatus' => (object) [ + 'state' => $currentCompatibilityStatus->state, + 'compatibleVersion' => $currentUpdateVersion + ], + 'resultGroup' => $resultGroup, + 'upgradeWarning' => $upgradeWarning, + ]; + } + + $this->app->mimeType = 'application/json'; + $this->app->charSet = 'utf-8'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + try { + $return = [ + 'compatibility' => $extensionResults, + 'extensions' => $leftover, + ]; + + echo new JsonResponse($return); + } catch (\Exception $e) { + echo $e; + } + + $this->app->close(); + } + + /** + * Fetch and report updates in \JSON format, for AJAX requests + * + * @return void + * + * @since 3.10.10 + */ + public function ajax() + { + if (!Session::checkToken('get')) { + $this->app->setHeader('status', 403, true); + $this->app->sendHeaders(); + echo Text::_('JINVALID_TOKEN_NOTICE'); + $this->app->close(); + } + + /** @var UpdateModel $model */ + $model = $this->getModel('Update'); + $updateInfo = $model->getUpdateInformation(); + + $update = []; + $update[] = ['version' => $updateInfo['latest']]; + + echo json_encode($update); + + $this->app->close(); + } } diff --git a/code/administrator/components/com_joomlaupdate/src/Dispatcher/Dispatcher.php b/code/administrator/components/com_joomlaupdate/src/Dispatcher/Dispatcher.php index 5857cd7c..e96f0522 100644 --- a/code/administrator/components/com_joomlaupdate/src/Dispatcher/Dispatcher.php +++ b/code/administrator/components/com_joomlaupdate/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.admin')) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + /** + * Joomla Update is checked for global core.admin rights - not the usual core.manage for the component + * + * @return void + */ + protected function checkAccess() + { + // Check the user has permission to access this component if in the backend + if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.admin')) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/code/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/code/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index 671aee00..a0899f9d 100644 --- a/code/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/code/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -1,4 +1,5 @@ get('updatesource', 'nochange')) - { - // "Minor & Patch Release for Current version AND Next Major Release". - case 'next': - $updateURL = 'https://update.joomla.org/core/sts/list_sts.xml'; - break; - - // "Testing" - case 'testing': - $updateURL = 'https://update.joomla.org/core/test/list_test.xml'; - break; - - // "Custom" - // @todo: check if the customurl is valid and not just "not empty". - case 'custom': - if (trim($params->get('customurl', '')) != '') - { - $updateURL = trim($params->get('customurl', '')); - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM_ERROR'), 'error'); - - return; - } - break; - - /** - * "Minor & Patch Release for Current version (recommended and default)". - * The commented "case" below are for documenting where 'default' and legacy options falls - * case 'default': - * case 'lts': - * case 'sts': (It's shown as "Default" because that option does not exist any more) - * case 'nochange': - */ - default: - $updateURL = 'https://update.joomla.org/core/list.xml'; - } - - $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('us') . '.*') - ->from($db->quoteName('#__update_sites_extensions', 'map')) - ->join( - 'INNER', - $db->quoteName('#__update_sites', 'us'), - $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('map.update_site_id') - ) - ->where($db->quoteName('map.extension_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $update_site = $db->loadObject(); - - if ($update_site->location != $updateURL) - { - // Modify the database record. - $update_site->last_check_timestamp = 0; - $update_site->location = $updateURL; - $db->updateObject('#__update_sites', $update_site, 'update_site_id'); - - // Remove cached updates. - $query->clear() - ->delete($db->quoteName('#__updates')) - ->where($db->quoteName('extension_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } - } - - /** - * Makes sure that the Joomla! update cache is up-to-date. - * - * @param boolean $force Force reload, ignoring the cache timeout. - * - * @return void - * - * @since 2.5.4 - */ - public function refreshUpdates($force = false) - { - if ($force) - { - $cache_timeout = 0; - } - else - { - $update_params = ComponentHelper::getParams('com_installer'); - $cache_timeout = (int) $update_params->get('cachetimeout', 6); - $cache_timeout = 3600 * $cache_timeout; - } - - $updater = Updater::getInstance(); - $minimumStability = Updater::STABILITY_STABLE; - $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); - - if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) - { - $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); - } - - $reflection = new \ReflectionObject($updater); - $reflectionMethod = $reflection->getMethod('findUpdates'); - $methodParameters = $reflectionMethod->getParameters(); - - if (count($methodParameters) >= 4) - { - // Reinstall support is available in Updater - $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability, true); - } - else - { - $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability); - } - } - - /** - * Makes sure that the Joomla! Update Component Update is in the database and check if there is a new version. - * - * @return boolean True if there is an update else false - * - * @since 4.0.0 - */ - public function getCheckForSelfUpdate() - { - $db = $this->getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('element') . ' = ' . $db->quote('com_joomlaupdate')); - $db->setQuery($query); - - try - { - // Get the component extension ID - $joomlaUpdateComponentId = $db->loadResult(); - } - catch (\RuntimeException $e) - { - // Something is wrong here! - $joomlaUpdateComponentId = 0; - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Try the update only if we have an extension id - if ($joomlaUpdateComponentId != 0) - { - // Always force to check for an update! - $cache_timeout = 0; - - $updater = Updater::getInstance(); - $updater->findUpdates($joomlaUpdateComponentId, $cache_timeout, Updater::STABILITY_STABLE); - - // Fetch the update information from the database. - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__updates')) - ->where($db->quoteName('extension_id') . ' = :id') - ->bind(':id', $joomlaUpdateComponentId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $joomlaUpdateComponentObject = $db->loadObject(); - } - catch (\RuntimeException $e) - { - // Something is wrong here! - $joomlaUpdateComponentObject = null; - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - return !empty($joomlaUpdateComponentObject); - } - - return false; - } - - /** - * Returns an array with the Joomla! update information. - * - * @return array - * - * @since 2.5.4 - */ - public function getUpdateInformation() - { - if ($this->updateInformation) - { - return $this->updateInformation; - } - - // Initialise the return array. - $this->updateInformation = array( - 'installed' => \JVERSION, - 'latest' => null, - 'object' => null, - 'hasUpdate' => false, - 'current' => JVERSION // This is deprecated please use 'installed' or JVERSION directly - ); - - // Fetch the update information from the database. - $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__updates')) - ->where($db->quoteName('extension_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $updateObject = $db->loadObject(); - - if (is_null($updateObject)) - { - // We have not found any update in the database - we seem to be running the latest version. - $this->updateInformation['latest'] = \JVERSION; - - return $this->updateInformation; - } - - // Check whether this is a valid update or not - if (version_compare($updateObject->version, JVERSION, '<')) - { - // This update points to an outdated version. We should not offer to update to this. - $this->updateInformation['latest'] = JVERSION; - - return $this->updateInformation; - } - - $minimumStability = Updater::STABILITY_STABLE; - $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); - - if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) - { - $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); - } - - // Fetch the full update details from the update details URL. - $update = new Update; - $update->loadFromXml($updateObject->detailsurl, $minimumStability); - - // Make sure we use the current information we got from the detailsurl - $this->updateInformation['object'] = $update; - $this->updateInformation['latest'] = $updateObject->version; - - // Check whether this is an update or not. - if (version_compare($this->updateInformation['latest'], JVERSION, '>')) - { - $this->updateInformation['hasUpdate'] = true; - } - - return $this->updateInformation; - } - - /** - * Removes all of the updates from the table and enable all update streams. - * - * @return boolean Result of operation. - * - * @since 3.0 - */ - public function purge() - { - $db = $this->getDbo(); - - // Modify the database record - $update_site = new \stdClass; - $update_site->last_check_timestamp = 0; - $update_site->enabled = 1; - $update_site->update_site_id = 1; - $db->updateObject('#__update_sites', $update_site, 'update_site_id'); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__updates')) - ->where($db->quoteName('update_site_id') . ' = 1'); - $db->setQuery($query); - - if ($db->execute()) - { - $this->_message = Text::_('COM_JOOMLAUPDATE_CHECKED_UPDATES'); - - return true; - } - else - { - $this->_message = Text::_('COM_JOOMLAUPDATE_FAILED_TO_CHECK_UPDATES'); - - return false; - } - } - - /** - * Downloads the update package to the site. - * - * @return array - * - * @since 2.5.4 - */ - public function download() - { - $updateInfo = $this->getUpdateInformation(); - $packageURL = trim($updateInfo['object']->downloadurl->_data); - $sources = $updateInfo['object']->get('downloadSources', array()); - - // We have to manually follow the redirects here so we set the option to false. - $httpOptions = new Registry; - $httpOptions->set('follow_location', false); - - try - { - $head = HttpFactory::getHttp($httpOptions)->head($packageURL); - } - catch (\RuntimeException $e) - { - // Passing false here -> download failed message - $response['basename'] = false; - - return $response; - } - - // Follow the Location headers until the actual download URL is known - while (isset($head->headers['location'])) - { - $packageURL = (string) $head->headers['location'][0]; - - try - { - $head = HttpFactory::getHttp($httpOptions)->head($packageURL); - } - catch (\RuntimeException $e) - { - // Passing false here -> download failed message - $response['basename'] = false; - - return $response; - } - } - - // Remove protocol, path and query string from URL - $basename = basename($packageURL); - - if (strpos($basename, '?') !== false) - { - $basename = substr($basename, 0, strpos($basename, '?')); - } - - // Find the path to the temp directory and the local package. - $tempdir = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(Factory::getApplication()->get('tmp_path'), 'path'); - $target = $tempdir . '/' . $basename; - $response = []; - - // Do we have a cached file? - $exists = File::exists($target); - - if (!$exists) - { - // Not there, let's fetch it. - $mirror = 0; - - while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) - { - $name = $sources[$mirror]; - $packageURL = trim($name->url); - $mirror++; - } - - $response['basename'] = $download; - } - else - { - // Is it a 0-byte file? If so, re-download please. - $filesize = @filesize($target); - - if (empty($filesize)) - { - $mirror = 0; - - while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) - { - $name = $sources[$mirror]; - $packageURL = trim($name->url); - $mirror++; - } - - $response['basename'] = $download; - } - - // Yes, it's there, skip downloading. - $response['basename'] = $basename; - } - - $response['check'] = $this->isChecksumValid($target, $updateInfo['object']); - - return $response; - } - - /** - * Return the result of the checksum of a package with the SHA256/SHA384/SHA512 tags in the update server manifest - * - * @param string $packagefile Location of the package to be installed - * @param Update $updateObject The Update Object - * - * @return boolean False in case the validation did not work; true in any other case. - * - * @note This method has been forked from (JInstallerHelper::isChecksumValid) so it - * does not depend on an up-to-date InstallerHelper at the update time - * - * @since 3.9.0 - */ - private function isChecksumValid($packagefile, $updateObject) - { - $hashes = array('sha256', 'sha384', 'sha512'); - - foreach ($hashes as $hash) - { - if ($updateObject->get($hash, false)) - { - $hashPackage = hash_file($hash, $packagefile); - $hashRemote = $updateObject->$hash->_data; - - if ($hashPackage !== $hashRemote) - { - // Return false in case the hash did not match - return false; - } - } - } - - // Well nothing was provided or all worked - return true; - } - - /** - * Downloads a package file to a specific directory - * - * @param string $url The URL to download from - * @param string $target The directory to store the file - * - * @return boolean True on success - * - * @since 2.5.4 - */ - protected function downloadPackage($url, $target) - { - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_URL', $url), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // Make sure the target does not exist. - File::delete($target); - - // Download the package - try - { - $result = HttpFactory::getHttp([], ['curl', 'stream'])->get($url); - } - catch (\RuntimeException $e) - { - return false; - } - - if (!$result || ($result->code != 200 && $result->code != 310)) - { - return false; - } - - // Write the file to disk - File::write($target, $result->body); - - return basename($target); - } - - /** - * Backwards compatibility. Use createUpdateFile() instead. - * - * @param null $basename The basename of the file to create - * - * @return boolean - * @since 2.5.1 - * @deprecated 5.0 - */ - public function createRestorationFile($basename = null): bool - { - return $this->createUpdateFile($basename); - } - - /** - * Create the update.php file and trigger onJoomlaBeforeUpdate event. - * - * The onJoomlaBeforeUpdate event stores the core files for which overrides have been defined. - * This will be compared in the onJoomlaAfterUpdate event with the current filesystem state, - * thereby determining how many and which overrides need to be checked and possibly updated - * after Joomla installed an update. - * - * @param string $basename Optional base path to the file. - * - * @return boolean True if successful; false otherwise. - * - * @since 2.5.4 - */ - public function createUpdateFile($basename = null): bool - { - // Load overrides plugin. - PluginHelper::importPlugin('installer'); - - // Get a password - $password = UserHelper::genRandomPassword(32); - $app = Factory::getApplication(); - - // Trigger event before joomla update. - $app->triggerEvent('onJoomlaBeforeUpdate'); - - // Get the absolute path to site's root. - $siteroot = JPATH_SITE; - - // If the package name is not specified, get it from the update info. - if (empty($basename)) - { - $updateInfo = $this->getUpdateInformation(); - $packageURL = $updateInfo['object']->downloadurl->_data; - $basename = basename($packageURL); - } - - // Get the package name. - $config = $app->getConfig(); - $tempdir = $config->get('tmp_path'); - $file = $tempdir . '/' . $basename; - - $filesize = @filesize($file); - $app->setUserState('com_joomlaupdate.password', $password); - $app->setUserState('com_joomlaupdate.filesize', $filesize); - - $data = "get('updatesource', 'nochange')) { + // "Minor & Patch Release for Current version AND Next Major Release". + case 'next': + $updateURL = 'https://update.joomla.org/core/sts/list_sts.xml'; + break; + + // "Testing" + case 'testing': + $updateURL = 'https://update.joomla.org/core/test/list_test.xml'; + break; + + // "Custom" + // @todo: check if the customurl is valid and not just "not empty". + case 'custom': + if (trim($params->get('customurl', '')) != '') { + $updateURL = trim($params->get('customurl', '')); + } else { + Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM_ERROR'), 'error'); + + return; + } + break; + + /** + * "Minor & Patch Release for Current version (recommended and default)". + * The commented "case" below are for documenting where 'default' and legacy options falls + * case 'default': + * case 'lts': + * case 'sts': (It's shown as "Default" because that option does not exist any more) + * case 'nochange': + */ + default: + $updateURL = 'https://update.joomla.org/core/list.xml'; + } + + $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('us') . '.*') + ->from($db->quoteName('#__update_sites_extensions', 'map')) + ->join( + 'INNER', + $db->quoteName('#__update_sites', 'us'), + $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('map.update_site_id') + ) + ->where($db->quoteName('map.extension_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $update_site = $db->loadObject(); + + if ($update_site->location != $updateURL) { + // Modify the database record. + $update_site->last_check_timestamp = 0; + $update_site->location = $updateURL; + $db->updateObject('#__update_sites', $update_site, 'update_site_id'); + + // Remove cached updates. + $query->clear() + ->delete($db->quoteName('#__updates')) + ->where($db->quoteName('extension_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + } + } + + /** + * Makes sure that the Joomla! update cache is up-to-date. + * + * @param boolean $force Force reload, ignoring the cache timeout. + * + * @return void + * + * @since 2.5.4 + */ + public function refreshUpdates($force = false) + { + if ($force) { + $cache_timeout = 0; + } else { + $update_params = ComponentHelper::getParams('com_installer'); + $cache_timeout = (int) $update_params->get('cachetimeout', 6); + $cache_timeout = 3600 * $cache_timeout; + } + + $updater = Updater::getInstance(); + $minimumStability = Updater::STABILITY_STABLE; + $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); + + if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) { + $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); + } + + $reflection = new \ReflectionObject($updater); + $reflectionMethod = $reflection->getMethod('findUpdates'); + $methodParameters = $reflectionMethod->getParameters(); + + if (count($methodParameters) >= 4) { + // Reinstall support is available in Updater + $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability, true); + } else { + $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability); + } + } + + /** + * Makes sure that the Joomla! Update Component Update is in the database and check if there is a new version. + * + * @return boolean True if there is an update else false + * + * @since 4.0.0 + */ + public function getCheckForSelfUpdate() + { + $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_joomlaupdate')); + $db->setQuery($query); + + try { + // Get the component extension ID + $joomlaUpdateComponentId = $db->loadResult(); + } catch (\RuntimeException $e) { + // Something is wrong here! + $joomlaUpdateComponentId = 0; + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Try the update only if we have an extension id + if ($joomlaUpdateComponentId != 0) { + // Always force to check for an update! + $cache_timeout = 0; + + $updater = Updater::getInstance(); + $updater->findUpdates($joomlaUpdateComponentId, $cache_timeout, Updater::STABILITY_STABLE); + + // Fetch the update information from the database. + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__updates')) + ->where($db->quoteName('extension_id') . ' = :id') + ->bind(':id', $joomlaUpdateComponentId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $joomlaUpdateComponentObject = $db->loadObject(); + } catch (\RuntimeException $e) { + // Something is wrong here! + $joomlaUpdateComponentObject = null; + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + return !empty($joomlaUpdateComponentObject); + } + + return false; + } + + /** + * Returns an array with the Joomla! update information. + * + * @return array + * + * @since 2.5.4 + */ + public function getUpdateInformation() + { + if ($this->updateInformation) { + return $this->updateInformation; + } + + // Initialise the return array. + $this->updateInformation = array( + 'installed' => \JVERSION, + 'latest' => null, + 'object' => null, + 'hasUpdate' => false, + 'current' => JVERSION // This is deprecated please use 'installed' or JVERSION directly + ); + + // Fetch the update information from the database. + $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__updates')) + ->where($db->quoteName('extension_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $updateObject = $db->loadObject(); + + if (is_null($updateObject)) { + // We have not found any update in the database - we seem to be running the latest version. + $this->updateInformation['latest'] = \JVERSION; + + return $this->updateInformation; + } + + // Check whether this is a valid update or not + if (version_compare($updateObject->version, JVERSION, '<')) { + // This update points to an outdated version. We should not offer to update to this. + $this->updateInformation['latest'] = JVERSION; + + return $this->updateInformation; + } + + $minimumStability = Updater::STABILITY_STABLE; + $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); + + if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) { + $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); + } + + // Fetch the full update details from the update details URL. + $update = new Update(); + $update->loadFromXml($updateObject->detailsurl, $minimumStability); + + // Make sure we use the current information we got from the detailsurl + $this->updateInformation['object'] = $update; + $this->updateInformation['latest'] = $updateObject->version; + + // Check whether this is an update or not. + if (version_compare($this->updateInformation['latest'], JVERSION, '>')) { + $this->updateInformation['hasUpdate'] = true; + } + + return $this->updateInformation; + } + + /** + * Removes all of the updates from the table and enable all update streams. + * + * @return boolean Result of operation. + * + * @since 3.0 + */ + public function purge() + { + $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + + // Modify the database record + $update_site = new \stdClass(); + $update_site->last_check_timestamp = 0; + $update_site->enabled = 1; + $update_site->update_site_id = 1; + $db->updateObject('#__update_sites', $update_site, 'update_site_id'); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__updates')) + ->where($db->quoteName('update_site_id') . ' = 1'); + $db->setQuery($query); + + if ($db->execute()) { + $this->_message = Text::_('COM_JOOMLAUPDATE_CHECKED_UPDATES'); + + return true; + } else { + $this->_message = Text::_('COM_JOOMLAUPDATE_FAILED_TO_CHECK_UPDATES'); + + return false; + } + } + + /** + * Downloads the update package to the site. + * + * @return array + * + * @since 2.5.4 + */ + public function download() + { + $updateInfo = $this->getUpdateInformation(); + $packageURL = trim($updateInfo['object']->downloadurl->_data); + $sources = $updateInfo['object']->get('downloadSources', array()); + + // We have to manually follow the redirects here so we set the option to false. + $httpOptions = new Registry(); + $httpOptions->set('follow_location', false); + + try { + $head = HttpFactory::getHttp($httpOptions)->head($packageURL); + } catch (\RuntimeException $e) { + // Passing false here -> download failed message + $response['basename'] = false; + + return $response; + } + + // Follow the Location headers until the actual download URL is known + while (isset($head->headers['location'])) { + $packageURL = (string) $head->headers['location'][0]; + + try { + $head = HttpFactory::getHttp($httpOptions)->head($packageURL); + } catch (\RuntimeException $e) { + // Passing false here -> download failed message + $response['basename'] = false; + + return $response; + } + } + + // Remove protocol, path and query string from URL + $basename = basename($packageURL); + + if (strpos($basename, '?') !== false) { + $basename = substr($basename, 0, strpos($basename, '?')); + } + + // Find the path to the temp directory and the local package. + $tempdir = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(Factory::getApplication()->get('tmp_path'), 'path'); + $target = $tempdir . '/' . $basename; + $response = []; + + // Do we have a cached file? + $exists = File::exists($target); + + if (!$exists) { + // Not there, let's fetch it. + $mirror = 0; + + while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) { + $name = $sources[$mirror]; + $packageURL = trim($name->url); + $mirror++; + } + + $response['basename'] = $download; + } else { + // Is it a 0-byte file? If so, re-download please. + $filesize = @filesize($target); + + if (empty($filesize)) { + $mirror = 0; + + while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) { + $name = $sources[$mirror]; + $packageURL = trim($name->url); + $mirror++; + } + + $response['basename'] = $download; + } + + // Yes, it's there, skip downloading. + $response['basename'] = $basename; + } + + $response['check'] = $this->isChecksumValid($target, $updateInfo['object']); + + return $response; + } + + /** + * Return the result of the checksum of a package with the SHA256/SHA384/SHA512 tags in the update server manifest + * + * @param string $packagefile Location of the package to be installed + * @param Update $updateObject The Update Object + * + * @return boolean False in case the validation did not work; true in any other case. + * + * @note This method has been forked from (JInstallerHelper::isChecksumValid) so it + * does not depend on an up-to-date InstallerHelper at the update time + * + * @since 3.9.0 + */ + private function isChecksumValid($packagefile, $updateObject) + { + $hashes = array('sha256', 'sha384', 'sha512'); + + foreach ($hashes as $hash) { + if ($updateObject->get($hash, false)) { + $hashPackage = hash_file($hash, $packagefile); + $hashRemote = $updateObject->$hash->_data; + + if ($hashPackage !== $hashRemote) { + // Return false in case the hash did not match + return false; + } + } + } + + // Well nothing was provided or all worked + return true; + } + + /** + * Downloads a package file to a specific directory + * + * @param string $url The URL to download from + * @param string $target The directory to store the file + * + * @return boolean True on success + * + * @since 2.5.4 + */ + protected function downloadPackage($url, $target) + { + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_URL', $url), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // Make sure the target does not exist. + File::delete($target); + + // Download the package + try { + $result = HttpFactory::getHttp([], ['curl', 'stream'])->get($url); + } catch (\RuntimeException $e) { + return false; + } + + if (!$result || ($result->code != 200 && $result->code != 310)) { + return false; + } + + // Write the file to disk + File::write($target, $result->body); + + return basename($target); + } + + /** + * Backwards compatibility. Use createUpdateFile() instead. + * + * @param null $basename The basename of the file to create + * + * @return boolean + * @since 2.5.1 + * @deprecated 5.0 + */ + public function createRestorationFile($basename = null): bool + { + return $this->createUpdateFile($basename); + } + + /** + * Create the update.php file and trigger onJoomlaBeforeUpdate event. + * + * The onJoomlaBeforeUpdate event stores the core files for which overrides have been defined. + * This will be compared in the onJoomlaAfterUpdate event with the current filesystem state, + * thereby determining how many and which overrides need to be checked and possibly updated + * after Joomla installed an update. + * + * @param string $basename Optional base path to the file. + * + * @return boolean True if successful; false otherwise. + * + * @since 2.5.4 + */ + public function createUpdateFile($basename = null): bool + { + // Load overrides plugin. + PluginHelper::importPlugin('installer'); + + // Get a password + $password = UserHelper::genRandomPassword(32); + $app = Factory::getApplication(); + + // Trigger event before joomla update. + $app->triggerEvent('onJoomlaBeforeUpdate'); + + // Get the absolute path to site's root. + $siteroot = JPATH_SITE; + + // If the package name is not specified, get it from the update info. + if (empty($basename)) { + $updateInfo = $this->getUpdateInformation(); + $packageURL = $updateInfo['object']->downloadurl->_data; + $basename = basename($packageURL); + } + + // Get the package name. + $config = $app->getConfig(); + $tempdir = $config->get('tmp_path'); + $file = $tempdir . '/' . $basename; + + $filesize = @filesize($file); + $app->setUserState('com_joomlaupdate.password', $password); + $app->setUserState('com_joomlaupdate.filesize', $filesize); + + $data = " '$password', 'setup.sourcefile' => '$file', 'setup.destdir' => '$siteroot', ENDDATA; - $data .= '];'; - - // Remove the old file, if it's there... - $configpath = JPATH_COMPONENT_ADMINISTRATOR . '/update.php'; - - if (File::exists($configpath)) - { - if (!File::delete($configpath)) - { - File::invalidateFileCache($configpath); - @unlink($configpath); - } - } - - // Write new file. First try with File. - $result = File::write($configpath, $data); - - // In case File used FTP but direct access could help. - if (!$result) - { - if (function_exists('file_put_contents')) - { - $result = @file_put_contents($configpath, $data); - - if ($result !== false) - { - $result = true; - } - } - else - { - $fp = @fopen($configpath, 'wt'); - - if ($fp !== false) - { - $result = @fwrite($fp, $data); - - if ($result !== false) - { - $result = true; - } - - @fclose($fp); - } - } - } - - return $result; - } - - /** - * Finalise the upgrade. - * - * This method will do the following: - * * Run the schema update SQL files. - * * Run the Joomla post-update script. - * * Update the manifest cache and #__extensions entry for Joomla itself. - * - * It performs essentially the same function as InstallerFile::install() without the file copy. - * - * @return boolean True on success. - * - * @since 2.5.4 - */ - public function finaliseUpgrade() - { - $installer = Installer::getInstance(); - - $manifest = $installer->isManifest(JPATH_MANIFESTS . '/files/joomla.xml'); - - if ($manifest === false) - { - $installer->abort(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST')); - - return false; - } - - // Re-create namespace map. It is needed when updating to a Joomla! version has new extension added - (new \JNamespacePsr4Map)->create(); - - $installer->manifest = $manifest; - - $installer->setUpgrade(true); - $installer->setOverwrite(true); - - $installer->extension = new \Joomla\CMS\Table\Extension($this->getDbo()); - $installer->extension->load(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id); - - $installer->setAdapter($installer->extension->type); - - $installer->setPath('manifest', JPATH_MANIFESTS . '/files/joomla.xml'); - $installer->setPath('source', JPATH_MANIFESTS . '/files'); - $installer->setPath('extension_root', JPATH_ROOT); - - // Run the script file. - \JLoader::register('JoomlaInstallerScript', JPATH_ADMINISTRATOR . '/components/com_admin/script.php'); - - $manifestClass = new \JoomlaInstallerScript; - - ob_start(); - ob_implicit_flush(false); - - if ($manifestClass && method_exists($manifestClass, 'preflight')) - { - if ($manifestClass->preflight('update', $installer) === false) - { - $installer->abort( - Text::sprintf( - 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', - Text::_('JLIB_INSTALLER_INSTALL') - ) - ); - - return false; - } - } - - // Create msg object; first use here. - $msg = ob_get_contents(); - ob_end_clean(); - - // Get a database connector object. - $db = $this->getDbo(); - - /* - * Check to see if a file extension by the same name is already installed. - * If it is, then update the table because if the files aren't there - * we can assume that it was (badly) uninstalled. - * If it isn't, add an entry to extensions. - */ - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('file')) - ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - // Install failed, roll back changes. - $installer->abort( - Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $e->getMessage()) - ); - - return false; - } - - $id = $db->loadResult(); - $row = new \Joomla\CMS\Table\Extension($this->getDbo()); - - if ($id) - { - // Load the entry and update the manifest_cache. - $row->load($id); - - // Update name. - $row->set('name', 'files_joomla'); - - // Update manifest. - $row->manifest_cache = $installer->generateManifestCache(); - - if (!$row->store()) - { - // Install failed, roll back changes. - $installer->abort( - Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $row->getError()) - ); - - return false; - } - } - else - { - // Add an entry to the extension table with a whole heap of defaults. - $row->set('name', 'files_joomla'); - $row->set('type', 'file'); - $row->set('element', 'joomla'); - - // There is no folder for files so leave it blank. - $row->set('folder', ''); - $row->set('enabled', 1); - $row->set('protected', 0); - $row->set('access', 0); - $row->set('client_id', 0); - $row->set('params', ''); - $row->set('manifest_cache', $installer->generateManifestCache()); - - if (!$row->store()) - { - // Install failed, roll back changes. - $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_FILE_INSTALL_ROLLBACK', $row->getError())); - - return false; - } - - // Set the insert id. - $row->set('extension_id', $db->insertid()); - - // Since we have created a module item, we add it to the installation step stack - // so that if we have to rollback the changes we can undo it. - $installer->pushStep(array('type' => 'extension', 'extension_id' => $row->extension_id)); - } - - $result = $installer->parseSchemaUpdates($manifest->update->schemas, $row->extension_id); - - if ($result === false) - { - // Install failed, rollback changes (message already logged by the installer). - $installer->abort(); - - return false; - } - - // Reinitialise the installer's extensions table's properties. - $installer->extension->getFields(true); - - // Start Joomla! 1.6. - ob_start(); - ob_implicit_flush(false); - - if ($manifestClass && method_exists($manifestClass, 'update')) - { - if ($manifestClass->update($installer) === false) - { - // Install failed, rollback changes. - $installer->abort( - Text::sprintf( - 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', - Text::_('JLIB_INSTALLER_INSTALL') - ) - ); - - return false; - } - } - - // Append messages. - $msg .= ob_get_contents(); - ob_end_clean(); - - // Clobber any possible pending updates. - $update = new \Joomla\CMS\Table\Update($this->getDbo()); - $uid = $update->find( - array('element' => 'joomla', 'type' => 'file', 'client_id' => '0', 'folder' => '') - ); - - if ($uid) - { - $update->delete($uid); - } - - // And now we run the postflight. - ob_start(); - ob_implicit_flush(false); - - if ($manifestClass && method_exists($manifestClass, 'postflight')) - { - $manifestClass->postflight('update', $installer); - } - - // Append messages. - $msg .= ob_get_contents(); - ob_end_clean(); - - if ($msg != '') - { - $installer->set('extension_message', $msg); - } - - // Refresh versionable assets cache. - Factory::getApplication()->flushAssets(); - - return true; - } - - /** - * Removes the extracted package file and trigger onJoomlaAfterUpdate event. - * - * The onJoomlaAfterUpdate event compares the stored list of files previously overridden with - * the updated core files, finding out which files have changed during the update, thereby - * determining how many and which override files need to be checked and possibly updated after - * the Joomla update. - * - * @return void - * - * @since 2.5.4 - */ - public function cleanUp() - { - // Load overrides plugin. - PluginHelper::importPlugin('installer'); - - $app = Factory::getApplication(); - - // Trigger event after joomla update. - $app->triggerEvent('onJoomlaAfterUpdate'); - - // Remove the update package. - $tempdir = $app->get('tmp_path'); - - $file = $app->getUserState('com_joomlaupdate.file', null); - File::delete($tempdir . '/' . $file); - - // Remove the update.php file used in Joomla 4.0.3 and later. - if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/update.php')) - { - File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/update.php'); - } - - // Remove the legacy restoration.php file (when updating from Joomla 4.0.2 and earlier). - if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php')) - { - File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php'); - } - - // Remove the legacy restore_finalisation.php file used in Joomla 4.0.2 and earlier. - if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php')) - { - File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php'); - } - - // Remove joomla.xml from the site's root. - if (File::exists(JPATH_ROOT . '/joomla.xml')) - { - File::delete(JPATH_ROOT . '/joomla.xml'); - } - - // Unset the update filename from the session. - $app = Factory::getApplication(); - $app->setUserState('com_joomlaupdate.file', null); - $oldVersion = $app->getUserState('com_joomlaupdate.oldversion'); - - // Trigger event after joomla update. - $app->triggerEvent('onJoomlaAfterUpdate', array($oldVersion)); - $app->setUserState('com_joomlaupdate.oldversion', null); - } - - /** - * Uploads what is presumably an update ZIP file under a mangled name in the temporary directory. - * - * @return void - * - * @since 3.6.0 - */ - public function upload() - { - // Get the uploaded file information. - $input = Factory::getApplication()->input; - - // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. - $userfile = $input->files->get('install_package', null, 'raw'); - - // Make sure that file uploads are enabled in php. - if (!(bool) ini_get('file_uploads')) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 500); - } - - // Make sure that zlib is loaded so that the package can be unpacked. - if (!extension_loaded('zlib')) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 500); - } - - // If there is no uploaded file, we have a problem... - if (!is_array($userfile)) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 500); - } - - // Is the PHP tmp directory missing? - if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) - { - throw new \RuntimeException( - Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . - Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), - 500 - ); - } - - // Is the max upload size too small in php.ini? - if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) - { - throw new \RuntimeException( - Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), - 500 - ); - } - - // Check if there was a different problem uploading the file. - if ($userfile['error'] || $userfile['size'] < 1) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); - } - - // Build the appropriate paths. - $tmp_dest = tempnam(Factory::getApplication()->get('tmp_path'), 'ju'); - $tmp_src = $userfile['tmp_name']; - - // Move uploaded file. - $result = File::upload($tmp_src, $tmp_dest, false, true); - - if (!$result) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); - } - - Factory::getApplication()->setUserState('com_joomlaupdate.temp_file', $tmp_dest); - } - - /** - * Checks the super admin credentials are valid for the currently logged in users - * - * @param array $credentials The credentials to authenticate the user with - * - * @return boolean - * - * @since 3.6.0 - */ - public function captiveLogin($credentials) - { - // Make sure the username matches - $username = $credentials['username'] ?? null; - $user = Factory::getUser(); - - if (strtolower($user->username) != strtolower($username)) - { - return false; - } - - // Make sure the user is authorised - if (!$user->authorise('core.admin')) - { - return false; - } - - // Get the global Authentication object. - $authenticate = Authentication::getInstance(); - $response = $authenticate->authenticate($credentials); - - if ($response->status !== Authentication::STATUS_SUCCESS) - { - return false; - } - - return true; - } - - /** - * Does the captive (temporary) file we uploaded before still exist? - * - * @return boolean - * - * @since 3.6.0 - */ - public function captiveFileExists() - { - $file = Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null); - - if (empty($file) || !File::exists($file)) - { - return false; - } - - return true; - } - - /** - * Remove the captive (temporary) file we uploaded before and the . - * - * @return void - * - * @since 3.6.0 - */ - public function removePackageFiles() - { - $files = array( - Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null), - Factory::getApplication()->getUserState('com_joomlaupdate.file', null), - ); - - foreach ($files as $file) - { - if ($file !== null && File::exists($file)) - { - File::delete($file); - } - } - } - - /** - * Gets PHP options. - * @todo: Outsource, build common code base for pre install and pre update check - * - * @return array Array of PHP config options - * - * @since 3.10.0 - */ - public function getPhpOptions() - { - $options = array(); - - /* - * Check the PHP Version. It is already checked in Update. - * A Joomla! Update which is not supported by current PHP - * version is not shown. So this check is actually unnecessary. - */ - $option = new \stdClass; - $option->label = Text::sprintf('INSTL_PHP_VERSION_NEWER', $this->getTargetMinimumPHPVersion()); - $option->state = $this->isPhpVersionSupported(); - $option->notice = null; - $options[] = $option; - - // Check for zlib support. - $option = new \stdClass; - $option->label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); - $option->state = extension_loaded('zlib'); - $option->notice = null; - $options[] = $option; - - // Check for XML support. - $option = new \stdClass; - $option->label = Text::_('INSTL_XML_SUPPORT'); - $option->state = extension_loaded('xml'); - $option->notice = null; - $options[] = $option; - - // Check for mbstring options. - if (extension_loaded('mbstring')) - { - // Check for default MB language. - $option = new \stdClass; - $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); - $option->state = strtolower(ini_get('mbstring.language')) === 'neutral'; - $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBLANGNOTDEFAULT'); - $options[] = $option; - - // Check for MB function overload. - $option = new \stdClass; - $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); - $option->state = ini_get('mbstring.func_overload') == 0; - $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBSTRINGOVERLOAD'); - $options[] = $option; - } - - // Check for a missing native parse_ini_file implementation. - $option = new \stdClass; - $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); - $option->state = $this->getIniParserAvailability(); - $option->notice = null; - $options[] = $option; - - // Check for missing native json_encode / json_decode support. - $option = new \stdClass; - $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); - $option->state = function_exists('json_encode') && function_exists('json_decode'); - $option->notice = null; - $options[] = $option; - $updateInformation = $this->getUpdateInformation(); - - // Check if configured database is compatible with the next major version of Joomla - $nextMajorVersion = Version::MAJOR_VERSION + 1; - - if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) - { - $option = new \stdClass; - $option->label = Text::sprintf('INSTL_DATABASE_SUPPORTED', $this->getConfiguredDatabaseType()); - $option->state = $this->isDatabaseTypeSupported(); - $option->notice = null; - $options[] = $option; - } - - // Check if database structure is up to date - $option = new \stdClass; - $option->label = Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_TITLE'); - $option->state = $this->getDatabaseSchemaCheck(); - $option->notice = $option->state ? null : Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_NOTICE'); - $options[] = $option; - - return $options; - } - - /** - * Gets PHP Settings. - * @todo: Outsource, build common code base for pre install and pre update check - * - * @return array - * - * @since 3.10.0 - */ - public function getPhpSettings() - { - $settings = array(); - - // Check for display errors. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_DISPLAY_ERRORS'); - $setting->state = (bool) ini_get('display_errors'); - $setting->recommended = false; - $settings[] = $setting; - - // Check for file uploads. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_FILE_UPLOADS'); - $setting->state = (bool) ini_get('file_uploads'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for output buffering. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_OUTPUT_BUFFERING'); - $setting->state = (int) ini_get('output_buffering') !== 0; - $setting->recommended = false; - $settings[] = $setting; - - // Check for session auto-start. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_SESSION_AUTO_START'); - $setting->state = (bool) ini_get('session.auto_start'); - $setting->recommended = false; - $settings[] = $setting; - - // Check for native ZIP support. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_ZIP_SUPPORT_AVAILABLE'); - $setting->state = function_exists('zip_open') && function_exists('zip_read'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for GD support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'GD'); - $setting->state = extension_loaded('gd'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for iconv support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'iconv'); - $setting->state = function_exists('iconv'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for intl support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'intl'); - $setting->state = function_exists('transliterator_transliterate'); - $setting->recommended = true; - $settings[] = $setting; - - return $settings; - } - - /** - * Returns the configured database type id (mysqli or sqlsrv or ...) - * - * @return string - * - * @since 3.10.0 - */ - private function getConfiguredDatabaseType() - { - return Factory::getApplication()->get('dbtype'); - } - - /** - * Returns true, if J! version is < 4 or current configured - * database type is compatible with the update. - * - * @return boolean - * - * @since 3.10.0 - */ - public function isDatabaseTypeSupported() - { - $updateInformation = $this->getUpdateInformation(); - $nextMajorVersion = Version::MAJOR_VERSION + 1; - - // Check if configured database is compatible with Joomla 4 - if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) - { - $unsupportedDatabaseTypes = array('sqlsrv', 'sqlazure'); - $currentDatabaseType = $this->getConfiguredDatabaseType(); - - return !in_array($currentDatabaseType, $unsupportedDatabaseTypes); - } - - return true; - } - - - /** - * Returns true, if current installed php version is compatible with the update. - * - * @return boolean - * - * @since 3.10.0 - */ - public function isPhpVersionSupported() - { - return version_compare(PHP_VERSION, $this->getTargetMinimumPHPVersion(), '>='); - } - - /** - * Returns the PHP minimum version for the update. - * Returns JOOMLA_MINIMUM_PHP, if there is no information given. - * - * @return string - * - * @since 3.10.0 - */ - private function getTargetMinimumPHPVersion() - { - $updateInformation = $this->getUpdateInformation(); - - return isset($updateInformation['object']->php_minimum) ? - $updateInformation['object']->php_minimum->_data : - JOOMLA_MINIMUM_PHP; - } - - /** - * Checks the availability of the parse_ini_file and parse_ini_string functions. - * @todo: Outsource, build common code base for pre install and pre update check - * - * @return boolean True if the method exists. - * - * @since 3.10.0 - */ - public function getIniParserAvailability() - { - $disabledFunctions = ini_get('disable_functions'); - - if (!empty($disabledFunctions)) - { - // Attempt to detect them in the PHP INI disable_functions variable. - $disabledFunctions = explode(',', trim($disabledFunctions)); - $numberOfDisabledFunctions = count($disabledFunctions); - - for ($i = 0; $i < $numberOfDisabledFunctions; $i++) - { - $disabledFunctions[$i] = trim($disabledFunctions[$i]); - } - - $result = !in_array('parse_ini_string', $disabledFunctions); - } - else - { - // Attempt to detect their existence; even pure PHP implementations of them will trigger a positive response, though. - $result = function_exists('parse_ini_string'); - } - - return $result; - } - - - /** - * Check if database structure is up to date - * - * @return boolean True if ok, false if not. - * - * @since 3.10.0 - */ - private function getDatabaseSchemaCheck(): bool - { - $mvcFactory = $this->bootComponent('com_installer')->getMVCFactory(); - - /** @var \Joomla\Component\Installer\Administrator\Model\DatabaseModel $model */ - $model = $mvcFactory->createModel('Database', 'Administrator'); - - // Check if no default text filters found - if (!$model->getDefaultTextFilters()) - { - return false; - } - - $coreExtensionInfo = \Joomla\CMS\Extension\ExtensionHelper::getExtensionRecord('joomla', 'file'); - $cache = new \Joomla\Registry\Registry($coreExtensionInfo->manifest_cache); - - $updateVersion = $cache->get('version'); - - // Check if database update version does not match CMS version - if (version_compare($updateVersion, JVERSION) != 0) - { - return false; - } - - // Ensure we only get information for core - $model->setState('filter.extension_id', $coreExtensionInfo->extension_id); - - // We're filtering by a single extension which must always exist - so can safely access this through - // element 0 of the array - $changeInformation = $model->getItems()[0]; - - // Check if schema errors found - if ($changeInformation['errorsCount'] !== 0) - { - return false; - } - - // Check if database schema version does not match CMS version - if ($model->getSchemaVersion($coreExtensionInfo->extension_id) != $changeInformation['schema']) - { - return false; - } - - // No database problems found - return true; - } - - /** - * Gets an array containing all installed extensions, that are not core extensions. - * - * @return array name,version,updateserver - * - * @since 3.10.0 - */ - public function getNonCoreExtensions() - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('ex.name'), - $db->quoteName('ex.extension_id'), - $db->quoteName('ex.manifest_cache'), - $db->quoteName('ex.type'), - $db->quoteName('ex.folder'), - $db->quoteName('ex.element'), - $db->quoteName('ex.client_id'), - ] - ) - ->from($db->quoteName('#__extensions', 'ex')) - ->where($db->quoteName('ex.package_id') . ' = 0') - ->whereNotIn($db->quoteName('ex.extension_id'), ExtensionHelper::getCoreExtensionIds()); - - $db->setQuery($query); - $rows = $db->loadObjectList(); - - foreach ($rows as $extension) - { - $decode = json_decode($extension->manifest_cache); - - // Remove unused fields so they do not cause javascript errors during pre-update check - unset($decode->description); - unset($decode->copyright); - unset($decode->creationDate); - - $this->translateExtensionName($extension); - $extension->version - = isset($decode->version) ? $decode->version : Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); - unset($extension->manifest_cache); - $extension->manifest_cache = $decode; - } - - return $rows; - } - - /** - * Gets an array containing all installed and enabled plugins, that are not core plugins. - * - * @param array $folderFilter Limit the list of plugins to a specific set of folder values - * - * @return array name,version,updateserver - * - * @since 3.10.0 - */ - public function getNonCorePlugins($folderFilter = ['system','user','authentication','actionlog','twofactorauth']) - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query->select( - $db->qn('ex.name') . ', ' . - $db->qn('ex.extension_id') . ', ' . - $db->qn('ex.manifest_cache') . ', ' . - $db->qn('ex.type') . ', ' . - $db->qn('ex.folder') . ', ' . - $db->qn('ex.element') . ', ' . - $db->qn('ex.client_id') . ', ' . - $db->qn('ex.package_id') - )->from( - $db->qn('#__extensions', 'ex') - )->where( - $db->qn('ex.type') . ' = ' . $db->quote('plugin') - )->where( - $db->qn('ex.enabled') . ' = 1' - )->whereNotIn( - $db->quoteName('ex.extension_id'), ExtensionHelper::getCoreExtensionIds() - ); - - if (count($folderFilter) > 0) - { - $folderFilter = array_map(array($db, 'quote'), $folderFilter); - - $query->where($db->qn('folder') . ' IN (' . implode(',', $folderFilter) . ')'); - } - - $db->setQuery($query); - $rows = $db->loadObjectList(); - - foreach ($rows as $plugin) - { - $decode = json_decode($plugin->manifest_cache); - - // Remove unused fields so they do not cause javascript errors during pre-update check - unset($decode->description); - unset($decode->copyright); - unset($decode->creationDate); - - $this->translateExtensionName($plugin); - $plugin->version = $decode->version ?? Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); - unset($plugin->manifest_cache); - $plugin->manifest_cache = $decode; - } - - return $rows; - } - - /** - * Called by controller's fetchExtensionCompatibility, which is called via AJAX. - * - * @param string $extensionID The ID of the checked extension - * @param string $joomlaTargetVersion Target version of Joomla - * - * @return object - * - * @since 3.10.0 - */ - public function fetchCompatibility($extensionID, $joomlaTargetVersion) - { - $updateSites = $this->getUpdateSitesInfo($extensionID); - - if (empty($updateSites)) - { - return (object) array('state' => 2); - } - - foreach ($updateSites as $updateSite) - { - if ($updateSite['type'] === 'collection') - { - $updateFileUrls = $this->getCollectionDetailsUrls($updateSite, $joomlaTargetVersion); - - foreach ($updateFileUrls as $updateFileUrl) - { - $compatibleVersions = $this->checkCompatibility($updateFileUrl, $joomlaTargetVersion); - - // Return the compatible versions - return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); - } - } - else - { - $compatibleVersions = $this->checkCompatibility($updateSite['location'], $joomlaTargetVersion); - - // Return the compatible versions - return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); - } - } - - // In any other case we mark this extension as not compatible - return (object) array('state' => 0); - } - - /** - * Returns records with update sites and extension information for a given extension ID. - * - * @param int $extensionID The extension ID - * - * @return array - * - * @since 3.10.0 - */ - private function getUpdateSitesInfo($extensionID) - { - $id = (int) $extensionID; - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query->select( - $db->qn('us.type') . ', ' . - $db->qn('us.location') . ', ' . - $db->qn('e.element') . ' AS ' . $db->qn('ext_element') . ', ' . - $db->qn('e.type') . ' AS ' . $db->qn('ext_type') . ', ' . - $db->qn('e.folder') . ' AS ' . $db->qn('ext_folder') - ) - ->from($db->quoteName('#__update_sites', 'us')) - ->join( - 'LEFT', - $db->quoteName('#__update_sites_extensions', 'ue'), - $db->quoteName('ue.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') - ) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('ue.extension_id') - ) - ->where($db->quoteName('e.extension_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - $result = $db->loadAssocList(); - - if (!is_array($result)) - { - return array(); - } - - return $result; - } - - /** - * Method to get details URLs from a collection update site for given extension and Joomla target version. - * - * @param array $updateSiteInfo The update site and extension information record to process - * @param string $joomlaTargetVersion The Joomla! version to test against, - * - * @return array An array of URLs. - * - * @since 3.10.0 - */ - private function getCollectionDetailsUrls($updateSiteInfo, $joomlaTargetVersion) - { - $return = array(); - - $http = new Http; - - try - { - $response = $http->get($updateSiteInfo['location']); - } - catch (\RuntimeException $e) - { - $response = null; - } - - if ($response === null || $response->code !== 200) - { - return $return; - } - - $updateSiteXML = simplexml_load_string($response->body); - - foreach ($updateSiteXML->extension as $extension) - { - $attribs = new \stdClass; - - $attribs->element = ''; - $attribs->type = ''; - $attribs->folder = ''; - $attribs->targetplatformversion = ''; - - foreach ($extension->attributes() as $key => $value) - { - $attribs->$key = (string) $value; - } - - if ($attribs->element === $updateSiteInfo['ext_element'] - && $attribs->type === $updateSiteInfo['ext_type'] - && $attribs->folder === $updateSiteInfo['ext_folder'] - && preg_match('/^' . $attribs->targetplatformversion . '/', $joomlaTargetVersion)) - { - $return[] = (string) $extension['detailsurl']; - } - } - - return $return; - } - - /** - * Method to check non core extensions for compatibility. - * - * @param string $updateFileUrl The items update XML url. - * @param string $joomlaTargetVersion The Joomla! version to test against - * - * @return array An array of strings with compatible version numbers - * - * @since 3.10.0 - */ - private function checkCompatibility($updateFileUrl, $joomlaTargetVersion) - { - $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); - - $update = new Update; - $update->set('jversion.full', $joomlaTargetVersion); - $update->loadFromXml($updateFileUrl, $minimumStability); - - $compatibleVersions = $update->get('compatibleVersions'); - - // Check if old version of the updater library - if (!isset($compatibleVersions)) - { - $downloadUrl = $update->get('downloadurl'); - $updateVersion = $update->get('version'); - - return empty($downloadUrl) || empty($downloadUrl->_data) || empty($updateVersion) ? array() : array($updateVersion->_data); - } - - usort($compatibleVersions, 'version_compare'); - - return $compatibleVersions; - } - - /** - * Translates an extension name - * - * @param object &$item The extension of which the name needs to be translated - * - * @return void - * - * @since 3.10.0 - */ - protected function translateExtensionName(&$item) - { - // @todo: Cleanup duplicated code. from com_installer/models/extension.php - $lang = Factory::getLanguage(); - $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; - - $extension = $item->element; - $source = JPATH_SITE; - - switch ($item->type) - { - case 'component': - $extension = $item->element; - $source = $path . '/components/' . $extension; - break; - case 'module': - $extension = $item->element; - $source = $path . '/modules/' . $extension; - break; - case 'file': - $extension = 'files_' . $item->element; - break; - case 'library': - $extension = 'lib_' . $item->element; - break; - case 'plugin': - $extension = 'plg_' . $item->folder . '_' . $item->element; - $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; - break; - case 'template': - $extension = 'tpl_' . $item->element; - $source = $path . '/templates/' . $item->element; - } - - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) - || $lang->load("$extension.sys", $source); - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, $source); - - // Translate the extension name if possible - $item->name = strip_tags(Text::_($item->name)); - } - - /** - * Checks whether a given template is active - * - * @param string $template The template name to be checked - * - * @return boolean - * - * @since 3.10.4 - */ - public function isTemplateActive($template) - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query->select( - $db->qn( - array( - 'id', - 'home' - ) - ) - )->from( - $db->qn('#__template_styles') - )->where( - $db->qn('template') . ' = :template' - )->bind(':template', $template, ParameterType::STRING); - - $templates = $db->setQuery($query)->loadObjectList(); - - $home = array_filter( - $templates, - function ($value) - { - return $value->home > 0; - } - ); - - $ids = ArrayHelper::getColumn($templates, 'id'); - - $menu = false; - - if (count($ids)) - { - $query = $db->getQuery(true); - - $query->select( - 'COUNT(*)' - )->from( - $db->qn('#__menu') - )->whereIn( - $db->qn('template_style_id'), $ids - ); - - $menu = $db->setQuery($query)->loadResult() > 0; - } - - return $home || $menu; - } + $data .= '];'; + + // Remove the old file, if it's there... + $configpath = JPATH_COMPONENT_ADMINISTRATOR . '/update.php'; + + if (File::exists($configpath)) { + if (!File::delete($configpath)) { + File::invalidateFileCache($configpath); + @unlink($configpath); + } + } + + // Write new file. First try with File. + $result = File::write($configpath, $data); + + // In case File used FTP but direct access could help. + if (!$result) { + if (function_exists('file_put_contents')) { + $result = @file_put_contents($configpath, $data); + + if ($result !== false) { + $result = true; + } + } else { + $fp = @fopen($configpath, 'wt'); + + if ($fp !== false) { + $result = @fwrite($fp, $data); + + if ($result !== false) { + $result = true; + } + + @fclose($fp); + } + } + } + + return $result; + } + + /** + * Finalise the upgrade. + * + * This method will do the following: + * * Run the schema update SQL files. + * * Run the Joomla post-update script. + * * Update the manifest cache and #__extensions entry for Joomla itself. + * + * It performs essentially the same function as InstallerFile::install() without the file copy. + * + * @return boolean True on success. + * + * @since 2.5.4 + */ + public function finaliseUpgrade() + { + $installer = Installer::getInstance(); + + $manifest = $installer->isManifest(JPATH_MANIFESTS . '/files/joomla.xml'); + + if ($manifest === false) { + $installer->abort(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST')); + + return false; + } + + $installer->manifest = $manifest; + + $installer->setUpgrade(true); + $installer->setOverwrite(true); + + $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $installer->extension = new \Joomla\CMS\Table\Extension($db); + $installer->extension->load(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id); + + $installer->setAdapter($installer->extension->type); + + $installer->setPath('manifest', JPATH_MANIFESTS . '/files/joomla.xml'); + $installer->setPath('source', JPATH_MANIFESTS . '/files'); + $installer->setPath('extension_root', JPATH_ROOT); + + // Run the script file. + \JLoader::register('JoomlaInstallerScript', JPATH_ADMINISTRATOR . '/components/com_admin/script.php'); + + $manifestClass = new \JoomlaInstallerScript(); + + ob_start(); + ob_implicit_flush(false); + + if ($manifestClass && method_exists($manifestClass, 'preflight')) { + if ($manifestClass->preflight('update', $installer) === false) { + $installer->abort( + Text::sprintf( + 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', + Text::_('JLIB_INSTALLER_INSTALL') + ) + ); + + return false; + } + } + + // Create msg object; first use here. + $msg = ob_get_contents(); + ob_end_clean(); + + // Get a database connector object. + $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + + /* + * Check to see if a file extension by the same name is already installed. + * If it is, then update the table because if the files aren't there + * we can assume that it was (badly) uninstalled. + * If it isn't, add an entry to extensions. + */ + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('file')) + ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + // Install failed, roll back changes. + $installer->abort( + Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $e->getMessage()) + ); + + return false; + } + + $id = $db->loadResult(); + $row = new \Joomla\CMS\Table\Extension($db); + + if ($id) { + // Load the entry and update the manifest_cache. + $row->load($id); + + // Update name. + $row->set('name', 'files_joomla'); + + // Update manifest. + $row->manifest_cache = $installer->generateManifestCache(); + + if (!$row->store()) { + // Install failed, roll back changes. + $installer->abort( + Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $row->getError()) + ); + + return false; + } + } else { + // Add an entry to the extension table with a whole heap of defaults. + $row->set('name', 'files_joomla'); + $row->set('type', 'file'); + $row->set('element', 'joomla'); + + // There is no folder for files so leave it blank. + $row->set('folder', ''); + $row->set('enabled', 1); + $row->set('protected', 0); + $row->set('access', 0); + $row->set('client_id', 0); + $row->set('params', ''); + $row->set('manifest_cache', $installer->generateManifestCache()); + + if (!$row->store()) { + // Install failed, roll back changes. + $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_FILE_INSTALL_ROLLBACK', $row->getError())); + + return false; + } + + // Set the insert id. + $row->set('extension_id', $db->insertid()); + + // Since we have created a module item, we add it to the installation step stack + // so that if we have to rollback the changes we can undo it. + $installer->pushStep(array('type' => 'extension', 'extension_id' => $row->extension_id)); + } + + $result = $installer->parseSchemaUpdates($manifest->update->schemas, $row->extension_id); + + if ($result === false) { + // Install failed, rollback changes (message already logged by the installer). + $installer->abort(); + + return false; + } + + // Reinitialise the installer's extensions table's properties. + $installer->extension->getFields(true); + + // Start Joomla! 1.6. + ob_start(); + ob_implicit_flush(false); + + if ($manifestClass && method_exists($manifestClass, 'update')) { + if ($manifestClass->update($installer) === false) { + // Install failed, rollback changes. + $installer->abort( + Text::sprintf( + 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', + Text::_('JLIB_INSTALLER_INSTALL') + ) + ); + + return false; + } + } + + // Append messages. + $msg .= ob_get_contents(); + ob_end_clean(); + + // Clobber any possible pending updates. + $update = new \Joomla\CMS\Table\Update($db); + $uid = $update->find( + array('element' => 'joomla', 'type' => 'file', 'client_id' => '0', 'folder' => '') + ); + + if ($uid) { + $update->delete($uid); + } + + // And now we run the postflight. + ob_start(); + ob_implicit_flush(false); + + if ($manifestClass && method_exists($manifestClass, 'postflight')) { + $manifestClass->postflight('update', $installer); + } + + // Append messages. + $msg .= ob_get_contents(); + ob_end_clean(); + + if ($msg != '') { + $installer->set('extension_message', $msg); + } + + // Refresh versionable assets cache. + Factory::getApplication()->flushAssets(); + + return true; + } + + /** + * Removes the extracted package file and trigger onJoomlaAfterUpdate event. + * + * The onJoomlaAfterUpdate event compares the stored list of files previously overridden with + * the updated core files, finding out which files have changed during the update, thereby + * determining how many and which override files need to be checked and possibly updated after + * the Joomla update. + * + * @return void + * + * @since 2.5.4 + */ + public function cleanUp() + { + // Load overrides plugin. + PluginHelper::importPlugin('installer'); + + $app = Factory::getApplication(); + + // Trigger event after joomla update. + $app->triggerEvent('onJoomlaAfterUpdate'); + + // Remove the update package. + $tempdir = $app->get('tmp_path'); + + $file = $app->getUserState('com_joomlaupdate.file', null); + File::delete($tempdir . '/' . $file); + + // Remove the update.php file used in Joomla 4.0.3 and later. + if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/update.php')) { + File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/update.php'); + } + + // Remove the legacy restoration.php file (when updating from Joomla 4.0.2 and earlier). + if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php')) { + File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php'); + } + + // Remove the legacy restore_finalisation.php file used in Joomla 4.0.2 and earlier. + if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php')) { + File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php'); + } + + // Remove joomla.xml from the site's root. + if (File::exists(JPATH_ROOT . '/joomla.xml')) { + File::delete(JPATH_ROOT . '/joomla.xml'); + } + + // Unset the update filename from the session. + $app = Factory::getApplication(); + $app->setUserState('com_joomlaupdate.file', null); + $oldVersion = $app->getUserState('com_joomlaupdate.oldversion'); + + // Trigger event after joomla update. + $app->triggerEvent('onJoomlaAfterUpdate', array($oldVersion)); + $app->setUserState('com_joomlaupdate.oldversion', null); + } + + /** + * Uploads what is presumably an update ZIP file under a mangled name in the temporary directory. + * + * @return void + * + * @since 3.6.0 + */ + public function upload() + { + // Get the uploaded file information. + $input = Factory::getApplication()->input; + + // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. + $userfile = $input->files->get('install_package', null, 'raw'); + + // Make sure that file uploads are enabled in php. + if (!(bool) ini_get('file_uploads')) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 500); + } + + // Make sure that zlib is loaded so that the package can be unpacked. + if (!extension_loaded('zlib')) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 500); + } + + // If there is no uploaded file, we have a problem... + if (!is_array($userfile)) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 500); + } + + // Is the PHP tmp directory missing? + if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) { + throw new \RuntimeException( + Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . + Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), + 500 + ); + } + + // Is the max upload size too small in php.ini? + if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) { + throw new \RuntimeException( + Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), + 500 + ); + } + + // Check if there was a different problem uploading the file. + if ($userfile['error'] || $userfile['size'] < 1) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); + } + + // Build the appropriate paths. + $tmp_dest = tempnam(Factory::getApplication()->get('tmp_path'), 'ju'); + $tmp_src = $userfile['tmp_name']; + + // Move uploaded file. + $result = File::upload($tmp_src, $tmp_dest, false, true); + + if (!$result) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); + } + + Factory::getApplication()->setUserState('com_joomlaupdate.temp_file', $tmp_dest); + } + + /** + * Checks the super admin credentials are valid for the currently logged in users + * + * @param array $credentials The credentials to authenticate the user with + * + * @return boolean + * + * @since 3.6.0 + */ + public function captiveLogin($credentials) + { + // Make sure the username matches + $username = $credentials['username'] ?? null; + $user = Factory::getUser(); + + if (strtolower($user->username) != strtolower($username)) { + return false; + } + + // Make sure the user is authorised + if (!$user->authorise('core.admin')) { + return false; + } + + // Get the global Authentication object. + $authenticate = Authentication::getInstance(); + $response = $authenticate->authenticate($credentials); + + if ($response->status !== Authentication::STATUS_SUCCESS) { + return false; + } + + return true; + } + + /** + * Does the captive (temporary) file we uploaded before still exist? + * + * @return boolean + * + * @since 3.6.0 + */ + public function captiveFileExists() + { + $file = Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null); + + if (empty($file) || !File::exists($file)) { + return false; + } + + return true; + } + + /** + * Remove the captive (temporary) file we uploaded before and the . + * + * @return void + * + * @since 3.6.0 + */ + public function removePackageFiles() + { + $files = array( + Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null), + Factory::getApplication()->getUserState('com_joomlaupdate.file', null), + ); + + foreach ($files as $file) { + if ($file !== null && File::exists($file)) { + File::delete($file); + } + } + } + + /** + * Gets PHP options. + * @todo: Outsource, build common code base for pre install and pre update check + * + * @return array Array of PHP config options + * + * @since 3.10.0 + */ + public function getPhpOptions() + { + $options = array(); + + /* + * Check the PHP Version. It is already checked in Update. + * A Joomla! Update which is not supported by current PHP + * version is not shown. So this check is actually unnecessary. + */ + $option = new \stdClass(); + $option->label = Text::sprintf('INSTL_PHP_VERSION_NEWER', $this->getTargetMinimumPHPVersion()); + $option->state = $this->isPhpVersionSupported(); + $option->notice = null; + $options[] = $option; + + // Check for zlib support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); + $option->state = extension_loaded('zlib'); + $option->notice = null; + $options[] = $option; + + // Check for XML support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_XML_SUPPORT'); + $option->state = extension_loaded('xml'); + $option->notice = null; + $options[] = $option; + + // Check for mbstring options. + if (extension_loaded('mbstring')) { + // Check for default MB language. + $option = new \stdClass(); + $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); + $option->state = strtolower(ini_get('mbstring.language')) === 'neutral'; + $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBLANGNOTDEFAULT'); + $options[] = $option; + + // Check for MB function overload. + $option = new \stdClass(); + $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); + $option->state = ini_get('mbstring.func_overload') == 0; + $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBSTRINGOVERLOAD'); + $options[] = $option; + } + + // Check for a missing native parse_ini_file implementation. + $option = new \stdClass(); + $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); + $option->state = $this->getIniParserAvailability(); + $option->notice = null; + $options[] = $option; + + // Check for missing native json_encode / json_decode support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); + $option->state = function_exists('json_encode') && function_exists('json_decode'); + $option->notice = null; + $options[] = $option; + $updateInformation = $this->getUpdateInformation(); + + // Check if configured database is compatible with the next major version of Joomla + $nextMajorVersion = Version::MAJOR_VERSION + 1; + + if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) { + $option = new \stdClass(); + $option->label = Text::sprintf('INSTL_DATABASE_SUPPORTED', $this->getConfiguredDatabaseType()); + $option->state = $this->isDatabaseTypeSupported(); + $option->notice = null; + $options[] = $option; + } + + // Check if database structure is up to date + $option = new \stdClass(); + $option->label = Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_TITLE'); + $option->state = $this->getDatabaseSchemaCheck(); + $option->notice = $option->state ? null : Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_NOTICE'); + $options[] = $option; + + return $options; + } + + /** + * Gets PHP Settings. + * @todo: Outsource, build common code base for pre install and pre update check + * + * @return array + * + * @since 3.10.0 + */ + public function getPhpSettings() + { + $settings = array(); + + // Check for display errors. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_DISPLAY_ERRORS'); + $setting->state = (bool) ini_get('display_errors'); + $setting->recommended = false; + $settings[] = $setting; + + // Check for file uploads. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_FILE_UPLOADS'); + $setting->state = (bool) ini_get('file_uploads'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for output buffering. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_OUTPUT_BUFFERING'); + $setting->state = (int) ini_get('output_buffering') !== 0; + $setting->recommended = false; + $settings[] = $setting; + + // Check for session auto-start. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_SESSION_AUTO_START'); + $setting->state = (bool) ini_get('session.auto_start'); + $setting->recommended = false; + $settings[] = $setting; + + // Check for native ZIP support. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_ZIP_SUPPORT_AVAILABLE'); + $setting->state = function_exists('zip_open') && function_exists('zip_read'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for GD support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'GD'); + $setting->state = extension_loaded('gd'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for iconv support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'iconv'); + $setting->state = function_exists('iconv'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for intl support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'intl'); + $setting->state = function_exists('transliterator_transliterate'); + $setting->recommended = true; + $settings[] = $setting; + + return $settings; + } + + /** + * Returns the configured database type id (mysqli or sqlsrv or ...) + * + * @return string + * + * @since 3.10.0 + */ + private function getConfiguredDatabaseType() + { + return Factory::getApplication()->get('dbtype'); + } + + /** + * Returns true, if J! version is < 4 or current configured + * database type is compatible with the update. + * + * @return boolean + * + * @since 3.10.0 + */ + public function isDatabaseTypeSupported() + { + $updateInformation = $this->getUpdateInformation(); + $nextMajorVersion = Version::MAJOR_VERSION + 1; + + // Check if configured database is compatible with Joomla 4 + if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) { + $unsupportedDatabaseTypes = array('sqlsrv', 'sqlazure'); + $currentDatabaseType = $this->getConfiguredDatabaseType(); + + return !in_array($currentDatabaseType, $unsupportedDatabaseTypes); + } + + return true; + } + + + /** + * Returns true, if current installed php version is compatible with the update. + * + * @return boolean + * + * @since 3.10.0 + */ + public function isPhpVersionSupported() + { + return version_compare(PHP_VERSION, $this->getTargetMinimumPHPVersion(), '>='); + } + + /** + * Returns the PHP minimum version for the update. + * Returns JOOMLA_MINIMUM_PHP, if there is no information given. + * + * @return string + * + * @since 3.10.0 + */ + private function getTargetMinimumPHPVersion() + { + $updateInformation = $this->getUpdateInformation(); + + return isset($updateInformation['object']->php_minimum) ? + $updateInformation['object']->php_minimum->_data : + JOOMLA_MINIMUM_PHP; + } + + /** + * Checks the availability of the parse_ini_file and parse_ini_string functions. + * @todo: Outsource, build common code base for pre install and pre update check + * + * @return boolean True if the method exists. + * + * @since 3.10.0 + */ + public function getIniParserAvailability() + { + $disabledFunctions = ini_get('disable_functions'); + + if (!empty($disabledFunctions)) { + // Attempt to detect them in the PHP INI disable_functions variable. + $disabledFunctions = explode(',', trim($disabledFunctions)); + $numberOfDisabledFunctions = count($disabledFunctions); + + for ($i = 0; $i < $numberOfDisabledFunctions; $i++) { + $disabledFunctions[$i] = trim($disabledFunctions[$i]); + } + + $result = !in_array('parse_ini_string', $disabledFunctions); + } else { + // Attempt to detect their existence; even pure PHP implementations of them will trigger a positive response, though. + $result = function_exists('parse_ini_string'); + } + + return $result; + } + + + /** + * Check if database structure is up to date + * + * @return boolean True if ok, false if not. + * + * @since 3.10.0 + */ + private function getDatabaseSchemaCheck(): bool + { + $mvcFactory = $this->bootComponent('com_installer')->getMVCFactory(); + + /** @var \Joomla\Component\Installer\Administrator\Model\DatabaseModel $model */ + $model = $mvcFactory->createModel('Database', 'Administrator'); + + // Check if no default text filters found + if (!$model->getDefaultTextFilters()) { + return false; + } + + $coreExtensionInfo = \Joomla\CMS\Extension\ExtensionHelper::getExtensionRecord('joomla', 'file'); + $cache = new \Joomla\Registry\Registry($coreExtensionInfo->manifest_cache); + + $updateVersion = $cache->get('version'); + + // Check if database update version does not match CMS version + if (version_compare($updateVersion, JVERSION) != 0) { + return false; + } + + // Ensure we only get information for core + $model->setState('filter.extension_id', $coreExtensionInfo->extension_id); + + // We're filtering by a single extension which must always exist - so can safely access this through + // element 0 of the array + $changeInformation = $model->getItems()[0]; + + // Check if schema errors found + if ($changeInformation['errorsCount'] !== 0) { + return false; + } + + // Check if database schema version does not match CMS version + if ($model->getSchemaVersion($coreExtensionInfo->extension_id) != $changeInformation['schema']) { + return false; + } + + // No database problems found + return true; + } + + /** + * Gets an array containing all installed extensions, that are not core extensions. + * + * @return array name,version,updateserver + * + * @since 3.10.0 + */ + public function getNonCoreExtensions() + { + $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('ex.name'), + $db->quoteName('ex.extension_id'), + $db->quoteName('ex.manifest_cache'), + $db->quoteName('ex.type'), + $db->quoteName('ex.folder'), + $db->quoteName('ex.element'), + $db->quoteName('ex.client_id'), + ] + ) + ->from($db->quoteName('#__extensions', 'ex')) + ->where($db->quoteName('ex.package_id') . ' = 0') + ->whereNotIn($db->quoteName('ex.extension_id'), ExtensionHelper::getCoreExtensionIds()); + + $db->setQuery($query); + $rows = $db->loadObjectList(); + + foreach ($rows as $extension) { + $decode = json_decode($extension->manifest_cache); + + // Remove unused fields so they do not cause javascript errors during pre-update check + unset($decode->description); + unset($decode->copyright); + unset($decode->creationDate); + + $this->translateExtensionName($extension); + $extension->version + = isset($decode->version) ? $decode->version : Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); + unset($extension->manifest_cache); + $extension->manifest_cache = $decode; + } + + return $rows; + } + + /** + * Gets an array containing all installed and enabled plugins, that are not core plugins. + * + * @param array $folderFilter Limit the list of plugins to a specific set of folder values + * + * @return array name,version,updateserver + * + * @since 3.10.0 + */ + public function getNonCorePlugins($folderFilter = ['system','user','authentication','actionlog','multifactorauth']) + { + $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + $db->qn('ex.name') . ', ' . + $db->qn('ex.extension_id') . ', ' . + $db->qn('ex.manifest_cache') . ', ' . + $db->qn('ex.type') . ', ' . + $db->qn('ex.folder') . ', ' . + $db->qn('ex.element') . ', ' . + $db->qn('ex.client_id') . ', ' . + $db->qn('ex.package_id') + )->from( + $db->qn('#__extensions', 'ex') + )->where( + $db->qn('ex.type') . ' = ' . $db->quote('plugin') + )->where( + $db->qn('ex.enabled') . ' = 1' + )->whereNotIn( + $db->quoteName('ex.extension_id'), + ExtensionHelper::getCoreExtensionIds() + ); + + if (count($folderFilter) > 0) { + $folderFilter = array_map(array($db, 'quote'), $folderFilter); + + $query->where($db->qn('folder') . ' IN (' . implode(',', $folderFilter) . ')'); + } + + $db->setQuery($query); + $rows = $db->loadObjectList(); + + foreach ($rows as $plugin) { + $decode = json_decode($plugin->manifest_cache); + + // Remove unused fields so they do not cause javascript errors during pre-update check + unset($decode->description); + unset($decode->copyright); + unset($decode->creationDate); + + $this->translateExtensionName($plugin); + $plugin->version = $decode->version ?? Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); + unset($plugin->manifest_cache); + $plugin->manifest_cache = $decode; + } + + return $rows; + } + + /** + * Called by controller's fetchExtensionCompatibility, which is called via AJAX. + * + * @param string $extensionID The ID of the checked extension + * @param string $joomlaTargetVersion Target version of Joomla + * + * @return object + * + * @since 3.10.0 + */ + public function fetchCompatibility($extensionID, $joomlaTargetVersion) + { + $updateSites = $this->getUpdateSitesInfo($extensionID); + + if (empty($updateSites)) { + return (object) array('state' => 2); + } + + foreach ($updateSites as $updateSite) { + if ($updateSite['type'] === 'collection') { + $updateFileUrls = $this->getCollectionDetailsUrls($updateSite, $joomlaTargetVersion); + + foreach ($updateFileUrls as $updateFileUrl) { + $compatibleVersions = $this->checkCompatibility($updateFileUrl, $joomlaTargetVersion); + + // Return the compatible versions + return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); + } + } else { + $compatibleVersions = $this->checkCompatibility($updateSite['location'], $joomlaTargetVersion); + + // Return the compatible versions + return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); + } + } + + // In any other case we mark this extension as not compatible + return (object) array('state' => 0); + } + + /** + * Returns records with update sites and extension information for a given extension ID. + * + * @param int $extensionID The extension ID + * + * @return array + * + * @since 3.10.0 + */ + private function getUpdateSitesInfo($extensionID) + { + $id = (int) $extensionID; + $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + $db->qn('us.type') . ', ' . + $db->qn('us.location') . ', ' . + $db->qn('e.element') . ' AS ' . $db->qn('ext_element') . ', ' . + $db->qn('e.type') . ' AS ' . $db->qn('ext_type') . ', ' . + $db->qn('e.folder') . ' AS ' . $db->qn('ext_folder') + ) + ->from($db->quoteName('#__update_sites', 'us')) + ->join( + 'LEFT', + $db->quoteName('#__update_sites_extensions', 'ue'), + $db->quoteName('ue.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') + ) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('ue.extension_id') + ) + ->where($db->quoteName('e.extension_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + $result = $db->loadAssocList(); + + if (!is_array($result)) { + return array(); + } + + return $result; + } + + /** + * Method to get details URLs from a collection update site for given extension and Joomla target version. + * + * @param array $updateSiteInfo The update site and extension information record to process + * @param string $joomlaTargetVersion The Joomla! version to test against, + * + * @return array An array of URLs. + * + * @since 3.10.0 + */ + private function getCollectionDetailsUrls($updateSiteInfo, $joomlaTargetVersion) + { + $return = array(); + + $http = new Http(); + + try { + $response = $http->get($updateSiteInfo['location']); + } catch (\RuntimeException $e) { + $response = null; + } + + if ($response === null || $response->code !== 200) { + return $return; + } + + $updateSiteXML = simplexml_load_string($response->body); + + foreach ($updateSiteXML->extension as $extension) { + $attribs = new \stdClass(); + + $attribs->element = ''; + $attribs->type = ''; + $attribs->folder = ''; + $attribs->targetplatformversion = ''; + + foreach ($extension->attributes() as $key => $value) { + $attribs->$key = (string) $value; + } + + if ( + $attribs->element === $updateSiteInfo['ext_element'] + && $attribs->type === $updateSiteInfo['ext_type'] + && $attribs->folder === $updateSiteInfo['ext_folder'] + && preg_match('/^' . $attribs->targetplatformversion . '/', $joomlaTargetVersion) + ) { + $return[] = (string) $extension['detailsurl']; + } + } + + return $return; + } + + /** + * Method to check non core extensions for compatibility. + * + * @param string $updateFileUrl The items update XML url. + * @param string $joomlaTargetVersion The Joomla! version to test against + * + * @return array An array of strings with compatible version numbers + * + * @since 3.10.0 + */ + private function checkCompatibility($updateFileUrl, $joomlaTargetVersion) + { + $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); + + $update = new Update(); + $update->set('jversion.full', $joomlaTargetVersion); + $update->loadFromXml($updateFileUrl, $minimumStability); + + $compatibleVersions = $update->get('compatibleVersions'); + + // Check if old version of the updater library + if (!isset($compatibleVersions)) { + $downloadUrl = $update->get('downloadurl'); + $updateVersion = $update->get('version'); + + return empty($downloadUrl) || empty($downloadUrl->_data) || empty($updateVersion) ? array() : array($updateVersion->_data); + } + + usort($compatibleVersions, 'version_compare'); + + return $compatibleVersions; + } + + /** + * Translates an extension name + * + * @param object &$item The extension of which the name needs to be translated + * + * @return void + * + * @since 3.10.0 + */ + protected function translateExtensionName(&$item) + { + // @todo: Cleanup duplicated code. from com_installer/models/extension.php + $lang = Factory::getLanguage(); + $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; + + $extension = $item->element; + $source = JPATH_SITE; + + switch ($item->type) { + case 'component': + $extension = $item->element; + $source = $path . '/components/' . $extension; + break; + case 'module': + $extension = $item->element; + $source = $path . '/modules/' . $extension; + break; + case 'file': + $extension = 'files_' . $item->element; + break; + case 'library': + $extension = 'lib_' . $item->element; + break; + case 'plugin': + $extension = 'plg_' . $item->folder . '_' . $item->element; + $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; + break; + case 'template': + $extension = 'tpl_' . $item->element; + $source = $path . '/templates/' . $item->element; + } + + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) + || $lang->load("$extension.sys", $source); + $lang->load($extension, JPATH_ADMINISTRATOR) + || $lang->load($extension, $source); + + // Translate the extension name if possible + $item->name = strip_tags(Text::_($item->name)); + } + + /** + * Checks whether a given template is active + * + * @param string $template The template name to be checked + * + * @return boolean + * + * @since 3.10.4 + */ + public function isTemplateActive($template) + { + $db = version_compare(JVERSION, '4.2.0', 'lt') ? $this->getDbo() : $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + $db->qn( + array( + 'id', + 'home' + ) + ) + )->from( + $db->qn('#__template_styles') + )->where( + $db->qn('template') . ' = :template' + )->bind(':template', $template, ParameterType::STRING); + + $templates = $db->setQuery($query)->loadObjectList(); + + $home = array_filter( + $templates, + function ($value) { + return $value->home > 0; + } + ); + + $ids = ArrayHelper::getColumn($templates, 'id'); + + $menu = false; + + if (count($ids)) { + $query = $db->getQuery(true); + + $query->select( + 'COUNT(*)' + )->from( + $db->qn('#__menu') + )->whereIn( + $db->qn('template_style_id'), + $ids + ); + + $menu = $db->setQuery($query)->loadResult() > 0; + } + + return $home || $menu; + } } diff --git a/code/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php b/code/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php index f116aa9e..f4e7f970 100644 --- a/code/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php +++ b/code/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php @@ -1,4 +1,5 @@ updateInfo = $this->get('UpdateInformation'); - $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); - - // Get results of pre update check evaluations - $model = $this->getModel(); - $this->phpOptions = $this->get('PhpOptions'); - $this->phpSettings = $this->get('PhpSettings'); - $this->nonCoreExtensions = $this->get('NonCoreExtensions'); - $this->isDefaultBackendTemplate = (bool) $model->isTemplateActive($this->defaultBackendTemplate); - $nextMajorVersion = Version::MAJOR_VERSION + 1; - - // The critical plugins check is only available for major updates. - if (version_compare($this->updateInfo['latest'], (string) $nextMajorVersion, '>=')) - { - $this->nonCoreCriticalPlugins = $this->get('NonCorePlugins'); - } - - // Set to true if a required PHP option is not ok - $isCritical = false; - - foreach ($this->phpOptions as $option) - { - if (!$option->state) - { - $isCritical = true; - break; - } - } - - $this->state = $this->get('State'); - - $hasUpdate = !empty($this->updateInfo['hasUpdate']); - $hasDownload = isset($this->updateInfo['object']->downloadurl->_data); - - // Fresh update, show it - if ($this->getLayout() == 'complete') - { - // Complete message, nothing to do here - } - // There is an update for the updater itself. So we have to update it first - elseif ($this->selfUpdateAvailable) - { - $this->setLayout('selfupdate'); - } - elseif (!$hasDownload || !$hasUpdate) - { - // Could be that we have a download file but no update, so we offer a re-install - if ($hasDownload) - { - // We can reinstall if we have a URL but no update - $this->setLayout('reinstall'); - } - // No download available - else - { - if ($hasUpdate) - { - $this->messagePrefix = '_NODOWNLOAD'; - } - - $this->setLayout('noupdate'); - } - } - // Here we have now two options: preupdatecheck or update - elseif ($this->getLayout() != 'update' && ($isCritical || $this->shouldDisplayPreUpdateCheck())) - { - $this->setLayout('preupdatecheck'); - } - else - { - $this->setLayout('update'); - } - - if (in_array($this->getLayout(), ['preupdatecheck', 'update', 'upload'])) - { - $language = Factory::getLanguage(); - $language->load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); - $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); - - Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATE_NOTICE'), 'notice'); - } - - $params = ComponentHelper::getParams('com_joomlaupdate'); - - switch ($params->get('updatesource', 'default')) - { - // "Minor & Patch Release for Current version AND Next Major Release". - case 'next': - $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_NEXT'; - $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT'); - break; - - // "Testing" - case 'testing': - $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_TESTING'; - $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_TESTING'); - break; - - // "Custom" - case 'custom': - $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_CUSTOM'; - $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM'); - break; - - /** - * "Minor & Patch Release for Current version (recommended and default)". - * The commented "case" below are for documenting where 'default' and legacy options falls - * case 'default': - * case 'sts': - * case 'lts': - * case 'nochange': - */ - default: - $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_DEFAULT'; - $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_DEFAULT'); - } - - // Remove temporary files - $this->getModel()->removePackageFiles(); - - $this->addToolbar(); - - // Render the view. - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - // Set the toolbar information. - ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'joomla install'); - - if (in_array($this->getLayout(), ['update', 'complete'])) - { - $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - - ToolbarHelper::link('index.php?option=com_joomlaupdate', 'JTOOLBAR_BACK', $arrow); - - ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_TAB_UPLOAD'), 'joomla install'); - } - elseif (!$this->selfUpdateAvailable) - { - ToolbarHelper::custom('update.purge', 'loop', '', 'COM_JOOMLAUPDATE_TOOLBAR_CHECK', false); - } - - // Add toolbar buttons. - if (Factory::getUser()->authorise('core.admin')) - { - ToolbarHelper::preferences('com_joomlaupdate'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Joomla_Update'); - } - - /** - * Returns true, if the pre update check should be displayed. - * - * @return boolean - * - * @since 3.10.0 - */ - public function shouldDisplayPreUpdateCheck() - { - // When the download URL is not found there is no core upgrade path - if (!isset($this->updateInfo['object']->downloadurl->_data)) - { - return false; - } - - $nextMinor = Version::MAJOR_VERSION . '.' . (Version::MINOR_VERSION + 1); - - // Show only when we found a download URL, we have an update and when we update to the next minor or greater. - return $this->updateInfo['hasUpdate'] - && version_compare($this->updateInfo['latest'], $nextMinor, '>='); - } + /** + * An array with the Joomla! update information. + * + * @var array + * + * @since 3.6.0 + */ + protected $updateInfo = null; + + /** + * PHP options. + * + * @var array Array of PHP config options + * + * @since 3.10.0 + */ + protected $phpOptions = null; + + /** + * PHP settings. + * + * @var array Array of PHP settings + * + * @since 3.10.0 + */ + protected $phpSettings = null; + + /** + * Non Core Extensions. + * + * @var array Array of Non-Core-Extensions + * + * @since 3.10.0 + */ + protected $nonCoreExtensions = null; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $state; + + /** + * Flag if the update component itself has to be updated + * + * @var boolean True when update is available otherwise false + * + * @since 4.0.0 + */ + protected $selfUpdateAvailable = false; + + /** + * The default admin template for the major version of Joomla that should be used when + * upgrading to the next major version of Joomla + * + * @var string + * + * @since 4.0.0 + */ + protected $defaultBackendTemplate = 'atum'; + + /** + * Flag if default backend template is being used + * + * @var boolean True when default backend template is being used + * + * @since 4.0.0 + */ + protected $isDefaultBackendTemplate = false; + + /** + * A special prefix used for the emptystate layout variable + * + * @var string The prefix + * + * @since 4.0.0 + */ + protected $messagePrefix = ''; + + /** + * List of non core critical plugins + * + * @var \stdClass[] + * @since 4.0.0 + */ + protected $nonCoreCriticalPlugins = []; + + /** + * Should I disable the confirmation checkbox for pre-update extension version checks? + * + * @var boolean + * @since 4.2.0 + */ + protected $noVersionCheck = false; + + /** + * Should I disable the confirmation checkbox for taking a backup before updating? + * + * @var boolean + * @since 4.2.0 + */ + protected $noBackupCheck = false; + + /** + * Renders the view + * + * @param string $tpl Template name + * + * @return void + * + * @since 2.5.4 + */ + public function display($tpl = null) + { + $this->updateInfo = $this->get('UpdateInformation'); + $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); + + // Get results of pre update check evaluations + $model = $this->getModel(); + $this->phpOptions = $this->get('PhpOptions'); + $this->phpSettings = $this->get('PhpSettings'); + $this->nonCoreExtensions = $this->get('NonCoreExtensions'); + $this->isDefaultBackendTemplate = (bool) $model->isTemplateActive($this->defaultBackendTemplate); + $nextMajorVersion = Version::MAJOR_VERSION + 1; + + // The critical plugins check is only available for major updates. + if (version_compare($this->updateInfo['latest'], (string) $nextMajorVersion, '>=')) { + $this->nonCoreCriticalPlugins = $this->get('NonCorePlugins'); + } + + // Set to true if a required PHP option is not ok + $isCritical = false; + + foreach ($this->phpOptions as $option) { + if (!$option->state) { + $isCritical = true; + break; + } + } + + $this->state = $this->get('State'); + + $hasUpdate = !empty($this->updateInfo['hasUpdate']); + $hasDownload = isset($this->updateInfo['object']->downloadurl->_data); + + // Fresh update, show it + if ($this->getLayout() == 'complete') { + // Complete message, nothing to do here + } elseif ($this->selfUpdateAvailable) { + // There is an update for the updater itself. So we have to update it first + $this->setLayout('selfupdate'); + } elseif (!$hasDownload || !$hasUpdate) { + // Could be that we have a download file but no update, so we offer a re-install + if ($hasDownload) { + // We can reinstall if we have a URL but no update + $this->setLayout('reinstall'); + } else { + // No download available + if ($hasUpdate) { + $this->messagePrefix = '_NODOWNLOAD'; + } + + $this->setLayout('noupdate'); + } + } elseif ($this->getLayout() != 'update' && ($isCritical || $this->shouldDisplayPreUpdateCheck())) { + // Here we have now two options: preupdatecheck or update + $this->setLayout('preupdatecheck'); + } else { + $this->setLayout('update'); + } + + if (in_array($this->getLayout(), ['preupdatecheck', 'update', 'upload'])) { + $language = Factory::getLanguage(); + $language->load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); + $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); + + Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATE_NOTICE'), 'warning'); + } + + $params = ComponentHelper::getParams('com_joomlaupdate'); + + switch ($params->get('updatesource', 'default')) { + // "Minor & Patch Release for Current version AND Next Major Release". + case 'next': + $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_NEXT'; + $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT'); + break; + + // "Testing" + case 'testing': + $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_TESTING'; + $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_TESTING'); + break; + + // "Custom" + case 'custom': + $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_CUSTOM'; + $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM'); + break; + + /** + * "Minor & Patch Release for Current version (recommended and default)". + * The commented "case" below are for documenting where 'default' and legacy options falls + * case 'default': + * case 'sts': + * case 'lts': + * case 'nochange': + */ + default: + $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_DEFAULT'; + $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_DEFAULT'); + } + + $this->noVersionCheck = $params->get('versioncheck', 1) == 0; + $this->noBackupCheck = $params->get('backupcheck', 1) == 0; + + // Remove temporary files + $this->getModel()->removePackageFiles(); + + $this->addToolbar(); + + // Render the view. + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + // Set the toolbar information. + ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'joomla install'); + + if (in_array($this->getLayout(), ['update', 'complete'])) { + $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + + ToolbarHelper::link('index.php?option=com_joomlaupdate', 'JTOOLBAR_BACK', $arrow); + + ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_TAB_UPLOAD'), 'joomla install'); + } elseif (!$this->selfUpdateAvailable) { + ToolbarHelper::custom('update.purge', 'loop', '', 'COM_JOOMLAUPDATE_TOOLBAR_CHECK', false); + } + + // Add toolbar buttons. + $currentUser = version_compare(JVERSION, '4.2.0', 'ge') + ? $this->getCurrentUser() + : Factory::getApplication()->getIdentity(); + + if ($currentUser->authorise('core.admin')) { + ToolbarHelper::preferences('com_joomlaupdate'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Joomla_Update'); + } + + /** + * Returns true, if the pre update check should be displayed. + * + * @return boolean + * + * @since 3.10.0 + */ + public function shouldDisplayPreUpdateCheck() + { + // When the download URL is not found there is no core upgrade path + if (!isset($this->updateInfo['object']->downloadurl->_data)) { + return false; + } + + $nextMinor = Version::MAJOR_VERSION . '.' . (Version::MINOR_VERSION + 1); + + // Show only when we found a download URL, we have an update and when we update to the next minor or greater. + return $this->updateInfo['hasUpdate'] + && version_compare($this->updateInfo['latest'], $nextMinor, '>='); + } } diff --git a/code/administrator/components/com_joomlaupdate/src/View/Update/HtmlView.php b/code/administrator/components/com_joomlaupdate/src/View/Update/HtmlView.php index 1d8471e4..b8571462 100644 --- a/code/administrator/components/com_joomlaupdate/src/View/Update/HtmlView.php +++ b/code/administrator/components/com_joomlaupdate/src/View/Update/HtmlView.php @@ -1,4 +1,5 @@ input->set('hidemainmenu', true); + /** + * Renders the view. + * + * @param string $tpl Template name. + * + * @return void + */ + public function display($tpl = null) + { + Factory::getApplication()->input->set('hidemainmenu', true); - // Set the toolbar information. - ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); + // Set the toolbar information. + ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); - // Render the view. - parent::display($tpl); - } + // Render the view. + parent::display($tpl); + } } diff --git a/code/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php b/code/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php index 9ffbbfdc..d9c54bc6 100644 --- a/code/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php +++ b/code/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php @@ -1,4 +1,5 @@ load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); - $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); - - $this->updateInfo = $this->get('UpdateInformation'); - $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); - - if ($this->getLayout() !== 'captive') - { - $this->warnings = $this->get('Items', 'warnings'); - } - - $this->addToolbar(); - - // Render the view. - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - // Set the toolbar information. - ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); - - $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - ToolbarHelper::link('index.php?option=com_joomlaupdate&' . ($this->getLayout() == 'captive' ? 'view=upload' : ''), 'JTOOLBAR_BACK', $arrow); - ToolbarHelper::divider(); - ToolbarHelper::help('Joomla_Update'); - } + /** + * An array with the Joomla! update information. + * + * @var array + * + * @since 4.0.0 + */ + protected $updateInfo = null; + + /** + * Flag if the update component itself has to be updated + * + * @var boolean True when update is available otherwise false + * + * @since 4.0.0 + */ + protected $selfUpdateAvailable = false; + + /** + * Warnings for the upload update + * + * @var array An array of warnings which could prevent the upload update + * + * @since 4.0.0 + */ + protected $warnings = []; + + /** + * Should I disable the confirmation checkbox for taking a backup before updating? + * + * @var boolean + * @since 4.2.0 + */ + protected $noBackupCheck = false; + + /** + * Renders the view. + * + * @param string $tpl Template name. + * + * @return void + * + * @since 3.6.0 + */ + public function display($tpl = null) + { + // Load com_installer's language + $language = Factory::getLanguage(); + $language->load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); + $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); + + $this->updateInfo = $this->get('UpdateInformation'); + $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); + + if ($this->getLayout() !== 'captive') { + $this->warnings = $this->get('Items', 'warnings'); + } + + $params = ComponentHelper::getParams('com_joomlaupdate'); + $this->noBackupCheck = $params->get('backupcheck', 1) == 0; + + $this->addToolbar(); + + // Render the view. + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + // Set the toolbar information. + ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); + + $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + ToolbarHelper::link('index.php?option=com_joomlaupdate&' . ($this->getLayout() == 'captive' ? 'view=upload' : ''), 'JTOOLBAR_BACK', $arrow); + ToolbarHelper::divider(); + ToolbarHelper::help('Joomla_Update'); + } } diff --git a/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/complete.php b/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/complete.php index 01a8d4eb..e168c269 100644 --- a/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/complete.php +++ b/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/complete.php @@ -1,4 +1,5 @@
-

-
-
- - -
-
+

+
+
+ + +
+
- - + +
diff --git a/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/noupdate.php b/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/noupdate.php index ec72b533..8a16e3ca 100644 --- a/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/noupdate.php +++ b/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/noupdate.php @@ -1,4 +1,5 @@ 'COM_JOOMLAUPDATE' . $this->messagePrefix, - 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), - 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', - 'icon' => 'icon-loop joomlaupdate', - 'createURL' => 'index.php?option=com_joomlaupdate&task=update.purge&' . Session::getFormToken() . '=1' + 'textPrefix' => 'COM_JOOMLAUPDATE' . $this->messagePrefix, + 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), + 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', + 'icon' => 'icon-loop joomlaupdate', + 'createURL' => 'index.php?option=com_joomlaupdate&task=update.purge&' . Session::getFormToken() . '=1' ]; -if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) -{ - $displayData['formAppend'] = ''; +if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) { + $displayData['formAppend'] = ''; } if (isset($this->updateInfo['object']) && isset($this->updateInfo['object']->get('infourl')->_data)) : - $displayData['content'] .= '
' . HTMLHelper::_('link', - $this->updateInfo['object']->get('infourl')->_data, - Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), - [ - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' - ] - ); + $displayData['content'] .= '
' . HTMLHelper::_( + 'link', + $this->updateInfo['object']->get('infourl')->_data, + Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), + [ + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' + ] + ); endif; $content = LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php b/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php index 2961b2d9..58da0e02 100644 --- a/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php +++ b/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.default') - ->useScript('bootstrap.popover') - ->useScript('bootstrap.tab'); + ->useScript('com_joomlaupdate.default') + ->useScript('bootstrap.popover') + ->useScript('bootstrap.tab'); // Text::script doesn't have a sprintf equivalent so work around this $this->document->addScriptOptions('nonCoreCriticalPlugins', $this->nonCoreCriticalPlugins); +// Push Joomla! Update client-side error messages Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_CONFIRM_MESSAGE'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_NO_COMPATIBILITY_INFORMATION'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_WARNING_UNKNOWN'); @@ -41,37 +43,44 @@ Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_CONFIRM_MESSAGE'); Text::script('COM_JOOMLAUPDATE_VIEW_DEFAULT_HELP'); +// Push Joomla! core Joomla.Request error messages +Text::script('JLIB_JS_AJAX_ERROR_CONNECTION_ABORT'); +Text::script('JLIB_JS_AJAX_ERROR_NO_CONTENT'); +Text::script('JLIB_JS_AJAX_ERROR_OTHER'); +Text::script('JLIB_JS_AJAX_ERROR_PARSE'); +Text::script('JLIB_JS_AJAX_ERROR_TIMEOUT'); + $compatibilityTypes = array( - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS' => array( - 'class' => 'info', - 'icon' => 'hourglass fa-spin', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS_NOTES', - 'group' => 0, - ), - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE' => array( - 'class' => 'danger', - 'icon' => 'times', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE_NOTES', - 'group' => 2, - ), - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED' => array( - 'class' => 'warning', - 'icon' => 'exclamation-triangle', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED_NOTES', - 'group' => 4, - ), - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION' => array( - 'class' => 'warning', - 'icon' => 'exclamation-triangle', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION_NOTES', - 'group' => 1, - ), - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE' => array( - 'class' => 'success', - 'icon' => 'check', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE_NOTES', - 'group' => 3, - ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS' => array( + 'class' => 'info', + 'icon' => 'hourglass fa-spin', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS_NOTES', + 'group' => 0, + ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE' => array( + 'class' => 'danger', + 'icon' => 'times', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE_NOTES', + 'group' => 2, + ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED' => array( + 'class' => 'warning', + 'icon' => 'exclamation-triangle', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED_NOTES', + 'group' => 4, + ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION' => array( + 'class' => 'warning', + 'icon' => 'exclamation-triangle', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION_NOTES', + 'group' => 1, + ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE' => array( + 'class' => 'success', + 'icon' => 'check', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE_NOTES', + 'group' => 3, + ), ); $latestJoomlaVersion = $this->updateInfo['latest']; @@ -79,283 +88,284 @@ $updatePossible = true; -if (version_compare($this->updateInfo['latest'], Version::MAJOR_VERSION + 1, '>=') && $this->isDefaultBackendTemplate === false) -{ - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_NON_CORE_BACKEND_TEMPLATE_USED_NOTICE', - ucfirst($this->defaultBackendTemplate) - ), - 'info' - ); +if (version_compare($this->updateInfo['latest'], Version::MAJOR_VERSION + 1, '>=') && $this->isDefaultBackendTemplate === false) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_NON_CORE_BACKEND_TEMPLATE_USED_NOTICE', + ucfirst($this->defaultBackendTemplate) + ), + 'info' + ); } ?>
-

- updateInfo['latest']); ?> -

-

- -

- -
- +

+ updateInfo['latest']); ?> +

+

+ +

-
-
-

- -

-
- - - - - - - - - - phpOptions as $option) : ?> - - - - - - -
- -
- - - -
- label; ?> - notice) : ?> -
- notice; ?> -
- -
- - state ? 'JYES' : 'JNO'); ?> - -
-
-
- -
-

- -

-
-
-

- -

-
-
- -
-
-
-
+
+ - +
+
+

+ +

+
+ + + + + + + + + + phpOptions as $option) : ?> + + + + + + +
+ +
+ + + +
+ label; ?> + notice) : ?> +
+ notice; ?> +
+ +
+ + state ? 'JYES' : 'JNO'); ?> + +
+
+
+ +
+

+ +

+
+
+

+ +

+
+
+ +
+
+
+
- nonCoreExtensions)) : ?> -
- $data) : ?> -
-

- - - 0) : ?> - - -

+ -
- - - - - - - - - - - - - - - nonCoreExtensions as $extension) : ?> - - - - - - - - - - -
- -
- - - -
- name; ?> - - type)); ?> -
-
-
- -
- -
- - -
- -
-
-
+ nonCoreExtensions)) : ?> +
+ $data) : ?> +
+

+ + + 0) : ?> + + +

- +
+ + + + + + + + + + + + + + + nonCoreExtensions as $extension) : ?> + + + + + + + + + + +
+ +
+ + + +
+ name; ?> + + type)); ?> +
+
+
+ +
+ +
+ + +
+ +
+
+
-
+ + -
-
- - -
-
+ noVersionCheck) : ?> +
+
+ + +
+
+ - -
- + + + -
- - -
+
+ + +
- authorise('core.admin')) : ?> -
- - - -
- + authorise('core.admin')) : ?> +
+ + + +
+
diff --git a/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/reinstall.php b/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/reinstall.php index e6e93402..57a5ed36 100644 --- a/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/reinstall.php +++ b/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/reinstall.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.default') - ->useScript('bootstrap.popover'); + ->useScript('com_joomlaupdate.default') + ->useScript('bootstrap.popover'); $uploadLink = 'index.php?option=com_joomlaupdate&view=upload'; $displayData = [ - 'textPrefix' => 'COM_JOOMLAUPDATE_REINSTALL', - 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), - 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', - 'icon' => 'icon-loop joomlaupdate', - 'createURL' => '#' + 'textPrefix' => 'COM_JOOMLAUPDATE_REINSTALL', + 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), + 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', + 'icon' => 'icon-loop joomlaupdate', + 'createURL' => '#' ]; if (isset($this->updateInfo['object']) && isset($this->updateInfo['object']->get('infourl')->_data)) : - $displayData['content'] .= '
' . HTMLHelper::_('link', - $this->updateInfo['object']->get('infourl')->_data, - Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), - [ - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' - ] - ); + $displayData['content'] .= '
' . HTMLHelper::_( + 'link', + $this->updateInfo['object']->get('infourl')->_data, + Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), + [ + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' + ] + ); endif; if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) : - $displayData['formAppend'] = ''; + $displayData['formAppend'] = ''; endif; echo '
'; diff --git a/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/selfupdate.php b/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/selfupdate.php index 0ca24eb8..52044361 100644 --- a/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/selfupdate.php +++ b/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/selfupdate.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Layout\LayoutHelper; $displayData = [ - 'textPrefix' => 'COM_JOOMLAUPDATE_SELF', - 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', - 'icon' => 'icon-loop joomlaupdate', - 'createURL' => 'index.php?option=com_installer&view=update' + 'textPrefix' => 'COM_JOOMLAUPDATE_SELF', + 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', + 'icon' => 'icon-loop joomlaupdate', + 'createURL' => 'index.php?option=com_installer&view=update' ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php b/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php index 4daba322..1ee49824 100644 --- a/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php +++ b/code/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.default') - ->useScript('bootstrap.popover'); + ->useScript('com_joomlaupdate.default') + ->useScript('bootstrap.popover'); $uploadLink = 'index.php?option=com_joomlaupdate&view=upload'; $displayData = [ - 'textPrefix' => 'COM_JOOMLAUPDATE_UPDATE', - 'title' => Text::sprintf('COM_JOOMLAUPDATE_UPDATE_EMPTYSTATE_TITLE', $this->escape($this->updateInfo['latest'])), - 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), - 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', - 'icon' => 'icon-loop joomlaupdate', - 'createURL' => '#' + 'textPrefix' => 'COM_JOOMLAUPDATE_UPDATE', + 'title' => Text::sprintf('COM_JOOMLAUPDATE_UPDATE_EMPTYSTATE_TITLE', $this->escape($this->updateInfo['latest'])), + 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), + 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', + 'icon' => 'icon-loop joomlaupdate', + 'createURL' => '#' ]; if (isset($this->updateInfo['object']) && isset($this->updateInfo['object']->get('infourl')->_data)) : - $displayData['content'] .= '
' . HTMLHelper::_('link', - $this->updateInfo['object']->get('infourl')->_data, - Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), - [ - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' - ] - ); + $displayData['content'] .= '
' . HTMLHelper::_( + 'link', + $this->updateInfo['object']->get('infourl')->_data, + Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), + [ + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' + ] + ); endif; // Confirm backup and check -$displayData['content'] .= '
- +$classVisibility = $this->noBackupCheck ? 'd-none' : ''; +$checked = $this->noBackupCheck ? 'checked' : ''; +$displayData['content'] .= '
+
'; if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) : - $displayData['formAppend'] = ''; + $displayData['formAppend'] = ''; endif; echo '
'; diff --git a/code/administrator/components/com_joomlaupdate/tmpl/update/default.php b/code/administrator/components/com_joomlaupdate/tmpl/update/default.php index 9c2ac08f..0375bd95 100644 --- a/code/administrator/components/com_joomlaupdate/tmpl/update/default.php +++ b/code/administrator/components/com_joomlaupdate/tmpl/update/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.admin-update') - ->useScript('bootstrap.modal'); + ->useScript('com_joomlaupdate.admin-update') + ->useScript('bootstrap.modal'); Text::script('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_FORBIDDEN'); Text::script('COM_JOOMLAUPDATE_ERRORMODAL_BODY_FORBIDDEN'); @@ -44,81 +46,90 @@ $returnUrl = 'index.php?option=com_joomlaupdate&task=update.finalise&' . Factory::getSession()->getFormToken() . '=1'; $this->document->addScriptOptions( - 'joomlaupdate', - [ - 'password' => $password, - 'totalsize' => $filesize, - 'ajax_url' => $ajaxUrl, - 'return_url' => $returnUrl, - ] + 'joomlaupdate', + [ + 'password' => $password, + 'totalsize' => $filesize, + 'ajax_url' => $ajaxUrl, + 'return_url' => $returnUrl, + ] ); $helpUrl = Help::createUrl('JHELP_COMPONENTS_JOOMLA_UPDATE', false); ?> - +
diff --git a/code/administrator/components/com_languages/tmpl/multilangstatus/default.php b/code/administrator/components/com_languages/tmpl/multilangstatus/default.php index 27084582..00ab90ae 100644 --- a/code/administrator/components/com_languages/tmpl/multilangstatus/default.php +++ b/code/administrator/components/com_languages/tmpl/multilangstatus/default.php @@ -1,4 +1,5 @@ homepages, 'language'); ?>
- language_filter && $this->switchers == 0) : ?> - homes == 1) : ?> -
- - - -
- -
- - - -
- - - default_lang, $content_languages)) : ?> -
- - - default_lang); ?> -
- - contentlangs as $contentlang) : ?> - lang_code == $this->default_lang && $contentlang->published != 1) : ?> -
- - - default_lang); ?> -
- - - - defaultHome == true) : ?> -
- - - -
- - statuses as $status) : ?> - - lang_code && $status->published == 1 && $status->home_published != 1) : ?> -
- - - lang_code, $status->lang_code); ?> -
- - - lang_code && $status->published == 0 && $status->home_published != 1) : ?> -
- - - lang_code, $status->lang_code); ?> -
- - - -
- - - -
- - -
- - - -
- - contentlangs as $contentlang) : ?> - lang_code, $this->homepages) && (!array_key_exists($contentlang->lang_code, $this->site_langs) || $contentlang->published != 1)) : ?> -
- - - lang_code); ?> -
- - lang_code, $this->site_langs)) : ?> -
- - - lang_code); ?> -
- - published == -2) : ?> -
- - - lang_code); ?> -
- - sef)) : ?> -
- - - lang_code); ?> -
- - - listUsersError) : ?> -
- - - -
    - listUsersError as $user) : ?> -
  • - name); ?> -
  • - -
-
- - - - -
- - - -
- - - - - - - - - - - - - - - + language_filter && $this->switchers == 0) : ?> + homes == 1) : ?> +
+ + + +
+ +
+ + + +
+ + + default_lang, $content_languages)) : ?> +
+ + + default_lang); ?> +
+ + contentlangs as $contentlang) : ?> + lang_code == $this->default_lang && $contentlang->published != 1) : ?> +
+ + + default_lang); ?> +
+ + + + defaultHome == true) : ?> +
+ + + +
+ + statuses as $status) : ?> + + lang_code && $status->published == 1 && $status->home_published != 1) : ?> +
+ + + lang_code, $status->lang_code); ?> +
+ + + lang_code && $status->published == 0 && $status->home_published != 1) : ?> +
+ + + lang_code, $status->lang_code); ?> +
+ + + +
+ + + +
+ + +
+ + + +
+ + contentlangs as $contentlang) : ?> + lang_code, $this->homepages) && (!array_key_exists($contentlang->lang_code, $this->site_langs) || $contentlang->published != 1)) : ?> +
+ + + lang_code); ?> +
+ + lang_code, $this->site_langs)) : ?> +
+ + + lang_code); ?> +
+ + published == -2) : ?> +
+ + + lang_code); ?> +
+ + sef)) : ?> +
+ + + lang_code); ?> +
+ + + listUsersError) : ?> +
+ + + +
    + listUsersError as $user) : ?> +
  • + name); ?> +
  • + +
+
+ + + + +
+ + + +
+ + +
- - - -
- - - language_filter) : ?> - - - - -
+ + + + + + + + + + + + - - - - - - - - - -
+ + + +
+ + + language_filter) : ?> + + + + +
- - - switchers != 0) : ?> - switchers; ?> - - - -
- homes > 1) : ?> - - - - - - homes > 1) : ?> - homes; ?> - - - -
- - - - - - - - - - - - statuses as $status) : ?> - element) : ?> - - - - - element) : ?> - - - - - - - - - - - contentlangs as $contentlang) : ?> - lang_code, $this->site_langs)) : ?> - - - - - - - - - - - - - - - - - - - - -
- - - - - - - -
- element; ?> - - - - - - - lang_code && $status->published == 1) : ?> - - - lang_code && $status->published == 0) : ?> - - - lang_code && $status->published == -2) : ?> - - - - - - - - home_published == 1) : ?> - - - home_published == 0) : ?> - - - home_published == -2) : ?> - - - - - - -
- lang_code; ?> - - - - - published == 1) : ?> - - - published == 0 && array_key_exists($contentlang->lang_code, $this->homepages)) : ?> - - - published == -2 && array_key_exists($contentlang->lang_code, $this->homepages)) : ?> - - - - - lang_code, $this->homepages)) : ?> - - - - - - -
- - - - - - - - - - -
- + + + + + + switchers != 0) : ?> + switchers; ?> + + + + + + + + homes > 1) : ?> + + + + + + + homes > 1) : ?> + homes; ?> + + + + + + + + + + + + + + + + + + + statuses as $status) : ?> + element) : ?> + + + + + element) : ?> + + + + + + + + + + + contentlangs as $contentlang) : ?> + lang_code, $this->site_langs)) : ?> + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ element; ?> + + + + + + + lang_code && $status->published == 1) : ?> + + + lang_code && $status->published == 0) : ?> + + + lang_code && $status->published == -2) : ?> + + + + + + + + home_published == 1) : ?> + + + home_published == 0) : ?> + + + home_published == -2) : ?> + + + + + + +
+ lang_code; ?> + + + + + published == 1) : ?> + + + published == 0 && array_key_exists($contentlang->lang_code, $this->homepages)) : ?> + + + published == -2 && array_key_exists($contentlang->lang_code, $this->homepages)) : ?> + + + + + lang_code, $this->homepages)) : ?> + + + + + + +
+ + + + + + + + + + +
+
diff --git a/code/administrator/components/com_languages/tmpl/override/edit.php b/code/administrator/components/com_languages/tmpl/override/edit.php index 929c4cb3..81219a07 100644 --- a/code/administrator/components/com_languages/tmpl/override/edit.php +++ b/code/administrator/components/com_languages/tmpl/override/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->usePreset('com_languages.overrider') - ->useScript('com_languages.admin-override-edit-refresh-searchstring'); + ->useScript('form.validate') + ->usePreset('com_languages.overrider') + ->useScript('com_languages.admin-override-edit-refresh-searchstring'); ?>
-
-
-
- item->key) ? Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_NEW_OVERRIDE_LEGEND') : Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_EDIT_OVERRIDE_LEGEND'); ?> -
- form->renderField('language'); ?> - form->renderField('client'); ?> - form->renderField('key'); ?> - form->renderField('override'); ?> - - state->get('filter.client') == 'administrator') : ?> - form->renderField('both'); ?> - - - form->renderField('file'); ?> -
-
-
+
+
+
+ item->key) ? Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_NEW_OVERRIDE_LEGEND') : Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_EDIT_OVERRIDE_LEGEND'); ?> +
+ form->renderField('language'); ?> + form->renderField('client'); ?> + form->renderField('key'); ?> + form->renderField('override'); ?> + form->renderField('both'); ?> + form->renderField('file'); ?> +
+
+
-
- +
+ -
- -
- - - -
+
+ +
+ + + +
- - + + - -
-
+ +
+
diff --git a/code/administrator/components/com_languages/tmpl/overrides/default.php b/code/administrator/components/com_languages/tmpl/overrides/default.php index e50b4500..954a7654 100644 --- a/code/administrator/components/com_languages/tmpl/overrides/default.php +++ b/code/administrator/components/com_languages/tmpl/overrides/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $client = $this->state->get('filter.client') == 'site' ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); $language = $this->state->get('filter.language'); @@ -27,93 +29,92 @@ $oppositeClient = $this->state->get('filter.client') == 'administrator' ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); $oppositeFilename = constant('JPATH_' . strtoupper($this->state->get('filter.client') === 'site' ? 'administrator' : 'site')) - . '/language/overrides/' . $this->state->get('filter.language', 'en-GB') . '.override.ini'; + . '/language/overrides/' . $this->state->get('filter.language', 'en-GB') . '.override.ini'; $oppositeStrings = LanguageHelper::parseIniFile($oppositeFilename); ?>
-
-
-
- $this, 'options' => ['selectorFieldName' => 'language_client'])); ?> -
- items)) : ?> -
- - -
- - - - - - - - - - - - - - authorise('core.edit', 'com_languages'); ?> - - items as $key => $text) : ?> - - - - - - - - - - -
- , - , - -
- - - - - - - - - -
- - - - - escape($key); ?> - - escape($key); ?> - - - escape($text), 200); ?> - - - - - -
+
+
+
+ $this, 'options' => ['selectorFieldName' => 'language_client'])); ?> +
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + authorise('core.edit', 'com_languages'); ?> + + items as $key => $text) : ?> + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ + + + + escape($key); ?> + + escape($key); ?> + + + escape($text), 200); ?> + + + + + +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
-
-
+ + + +
+
+
diff --git a/code/administrator/components/com_login/login.xml b/code/administrator/components/com_login/login.xml index f00d86f5..b56bb41e 100644 --- a/code/administrator/components/com_login/login.xml +++ b/code/administrator/components/com_login/login.xml @@ -2,7 +2,7 @@ com_login Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_login/services/provider.php b/code/administrator/components/com_login/services/provider.php index bf55bc62..eb064b34 100644 --- a/code/administrator/components/com_login/services/provider.php +++ b/code/administrator/components/com_login/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Login')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Login')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Login')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Login')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_login/src/Controller/DisplayController.php b/code/administrator/components/com_login/src/Controller/DisplayController.php index db93e6eb..676c6c75 100644 --- a/code/administrator/components/com_login/src/Controller/DisplayController.php +++ b/code/administrator/components/com_login/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->set('view', 'login'); - $this->input->set('layout', 'default'); - - // For non-html formats we do not have login view, so just display 403 instead - if ($this->input->get('format', 'html') !== 'html') - { - throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - /** - * To prevent clickjacking, only allow the login form to be used inside a frame in the same origin. - * So send a X-Frame-Options HTTP Header with the SAMEORIGIN value. - * - * @link https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet - * @link https://tools.ietf.org/html/rfc7034 - */ - $this->app->setHeader('X-Frame-Options', 'SAMEORIGIN'); - - return parent::display(); - } - - /** - * Method to log in a user. - * - * @return void - */ - public function login() - { - // Check for request forgeries. - $this->checkToken(); - - $app = $this->app; - - $model = $this->getModel('login'); - $credentials = $model->getState('credentials'); - $return = $model->getState('return'); - - $app->login($credentials, array('action' => 'core.login.admin')); - - if (Uri::isInternal($return) && strpos($return, 'tmpl=component') === false) - { - $app->redirect($return); - } - else - { - $app->redirect('index.php'); - } - } - - /** - * Method to log out a user. - * - * @return void - */ - public function logout() - { - $this->checkToken('request'); - - $app = $this->app; - - $userid = $this->input->getInt('uid', null); - - if ($app->get('shared_session', '0')) - { - $clientid = null; - } - else - { - $clientid = $userid ? 0 : 1; - } - - $options = array( - 'clientid' => $clientid, - ); - - $result = $app->logout($userid, $options); - - if (!($result instanceof \Exception)) - { - $model = $this->getModel('login'); - $return = $model->getState('return'); - - // Only redirect to an internal URL. - if (Uri::isInternal($return)) - { - $app->redirect($return); - } - } - - parent::display(); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + * @throws \Exception + */ + public function display($cachable = false, $urlparams = false) + { + /* + * Special treatment is required for this component, as this view may be called + * after a session timeout. We must reset the view and layout prior to display + * otherwise an error will occur. + */ + $this->input->set('view', 'login'); + $this->input->set('layout', 'default'); + + // For non-html formats we do not have login view, so just display 403 instead + if ($this->input->get('format', 'html') !== 'html') { + throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + /** + * To prevent clickjacking, only allow the login form to be used inside a frame in the same origin. + * So send a X-Frame-Options HTTP Header with the SAMEORIGIN value. + * + * @link https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet + * @link https://tools.ietf.org/html/rfc7034 + */ + $this->app->setHeader('X-Frame-Options', 'SAMEORIGIN'); + + return parent::display(); + } + + /** + * Method to log in a user. + * + * @return void + */ + public function login() + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + + $model = $this->getModel('login'); + $credentials = $model->getState('credentials'); + $return = $model->getState('return'); + + $app->login($credentials, array('action' => 'core.login.admin')); + + if (Uri::isInternal($return) && strpos($return, 'tmpl=component') === false) { + $app->redirect($return); + } else { + $app->redirect('index.php'); + } + } + + /** + * Method to log out a user. + * + * @return void + */ + public function logout() + { + $this->checkToken('request'); + + $app = $this->app; + + $userid = $this->input->getInt('uid', null); + + if ($app->get('shared_session', '0')) { + $clientid = null; + } else { + $clientid = $userid ? 0 : 1; + } + + $options = array( + 'clientid' => $clientid, + ); + + $result = $app->logout($userid, $options); + + if (!($result instanceof \Exception)) { + $model = $this->getModel('login'); + $return = $model->getState('return'); + + // Only redirect to an internal URL. + if (Uri::isInternal($return)) { + $app->redirect($return); + } + } + + parent::display(); + } } diff --git a/code/administrator/components/com_login/src/Dispatcher/Dispatcher.php b/code/administrator/components/com_login/src/Dispatcher/Dispatcher.php index f580d9d1..7207b6f9 100644 --- a/code/administrator/components/com_login/src/Dispatcher/Dispatcher.php +++ b/code/administrator/components/com_login/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->get('task'); - - if ($task != 'login' && $task != 'logout') - { - $this->input->set('task', ''); - } - - // Reset controller name - $this->input->set('controller', null); - - parent::dispatch(); - } - - /** - * com_login does not require check permission, so we override checkAccess method and have it empty - * - * @return void - */ - protected function checkAccess() - { - } + /** + * Dispatch a controller task. + * + * @return void + * + * @since 4.0.0 + */ + public function dispatch() + { + // Only accept two values login and logout for `task` + $task = $this->input->get('task'); + + if ($task != 'login' && $task != 'logout') { + $this->input->set('task', ''); + } + + // Reset controller name + $this->input->set('controller', null); + + parent::dispatch(); + } + + /** + * com_login does not require check permission, so we override checkAccess method and have it empty + * + * @return void + */ + protected function checkAccess() + { + } } diff --git a/code/administrator/components/com_login/src/Model/LoginModel.php b/code/administrator/components/com_login/src/Model/LoginModel.php index 59377adb..e250d3c6 100644 --- a/code/administrator/components/com_login/src/Model/LoginModel.php +++ b/code/administrator/components/com_login/src/Model/LoginModel.php @@ -1,4 +1,5 @@ input->getInputForRequestMethod(); - - $credentials = array( - 'username' => $input->get('username', '', 'USERNAME'), - 'password' => $input->get('passwd', '', 'RAW'), - 'secretkey' => $input->get('secretkey', '', 'RAW'), - ); - - $this->setState('credentials', $credentials); - - // Check for return URL from the request first. - if ($return = $input->get('return', '', 'BASE64')) - { - $return = base64_decode($return); - - if (!Uri::isInternal($return)) - { - $return = ''; - } - } - - // Set the return URL if empty. - if (empty($return)) - { - $return = 'index.php'; - } - - $this->setState('return', $return); - } - - /** - * Get the administrator login module by name (real, eg 'login' or folder, eg 'mod_login'). - * - * @param string $name The name of the module. - * @param string $title The title of the module, optional. - * - * @return object The Module object. - * - * @since 1.7.0 - */ - public static function getLoginModule($name = 'mod_login', $title = null) - { - $result = null; - $modules = self::_load($name); - $total = count($modules); - - for ($i = 0; $i < $total; $i++) - { - // Match the title if we're looking for a specific instance of the module. - if (!$title || $modules[$i]->title == $title) - { - $result = $modules[$i]; - break; - } - } - - // If we didn't find it, and the name is mod_something, create a dummy object. - if (is_null($result) && substr($name, 0, 4) == 'mod_') - { - $result = new \stdClass; - $result->id = 0; - $result->title = ''; - $result->module = $name; - $result->position = ''; - $result->content = ''; - $result->showtitle = 0; - $result->control = ''; - $result->params = ''; - $result->user = 0; - } - - return $result; - } - - /** - * Load login modules. - * - * Note that we load regardless of state or access level since access - * for public is the only thing that makes sense since users are not logged in - * and the module lets them log in. - * This is put in as a failsafe to avoid super user lock out caused by an unpublished - * login module or by a module set to have a viewing access level that is not Public. - * - * @param string $module The name of the module. - * - * @return array - * - * @since 1.7.0 - */ - protected static function _load($module) - { - static $clean; - - if (isset($clean)) - { - return $clean; - } - - $app = Factory::getApplication(); - $lang = Factory::getLanguage()->getTag(); - $clientId = (int) $app->getClientId(); - - /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ - $cache = Factory::getCache('com_modules', 'callback'); - - $loader = function () use ($app, $lang, $module) { - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'm.id', - 'm.title', - 'm.module', - 'm.position', - 'm.showtitle', - 'm.params' - ] - ) - ) - ->from($db->quoteName('#__modules', 'm')) - ->where($db->quoteName('m.module') . ' = :module') - ->where($db->quoteName('m.client_id') . ' = 1') - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.element') . ' = ' . $db->quoteName('m.module') . - ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('m.client_id') - ) - ->where($db->quoteName('e.enabled') . ' = 1') - ->bind(':module', $module); - - // Filter by language. - if ($app->isClient('site') && $app->getLanguageFilter()) - { - $query->whereIn($db->quoteName('m.language'), [$lang, '*']); - } - - $query->order('m.position, m.ordering'); - - // Set the query. - $db->setQuery($query); - - return $db->loadObjectList(); - }; - - try - { - return $clean = $cache->get($loader, array(), md5(serialize(array($clientId, $lang)))); - } - catch (CacheExceptionInterface $cacheException) - { - try - { - return $loader(); - } - catch (ExecutionFailureException $databaseException) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $databaseException->getMessage()), - 'error' - ); - - return array(); - } - } - catch (ExecutionFailureException $databaseException) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $databaseException->getMessage()), 'error'); - - return array(); - } - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $input = Factory::getApplication()->input->getInputForRequestMethod(); + + $credentials = array( + 'username' => $input->get('username', '', 'USERNAME'), + 'password' => $input->get('passwd', '', 'RAW'), + 'secretkey' => $input->get('secretkey', '', 'RAW'), + ); + + $this->setState('credentials', $credentials); + + // Check for return URL from the request first. + if ($return = $input->get('return', '', 'BASE64')) { + $return = base64_decode($return); + + if (!Uri::isInternal($return)) { + $return = ''; + } + } + + // Set the return URL if empty. + if (empty($return)) { + $return = 'index.php'; + } + + $this->setState('return', $return); + } + + /** + * Get the administrator login module by name (real, eg 'login' or folder, eg 'mod_login'). + * + * @param string $name The name of the module. + * @param string $title The title of the module, optional. + * + * @return object The Module object. + * + * @since 1.7.0 + */ + public static function getLoginModule($name = 'mod_login', $title = null) + { + $result = null; + $modules = self::_load($name); + $total = count($modules); + + for ($i = 0; $i < $total; $i++) { + // Match the title if we're looking for a specific instance of the module. + if (!$title || $modules[$i]->title == $title) { + $result = $modules[$i]; + break; + } + } + + // If we didn't find it, and the name is mod_something, create a dummy object. + if (is_null($result) && substr($name, 0, 4) == 'mod_') { + $result = new \stdClass(); + $result->id = 0; + $result->title = ''; + $result->module = $name; + $result->position = ''; + $result->content = ''; + $result->showtitle = 0; + $result->control = ''; + $result->params = ''; + $result->user = 0; + } + + return $result; + } + + /** + * Load login modules. + * + * Note that we load regardless of state or access level since access + * for public is the only thing that makes sense since users are not logged in + * and the module lets them log in. + * This is put in as a failsafe to avoid super user lock out caused by an unpublished + * login module or by a module set to have a viewing access level that is not Public. + * + * @param string $module The name of the module. + * + * @return array + * + * @since 1.7.0 + */ + protected static function _load($module) + { + static $clean; + + if (isset($clean)) { + return $clean; + } + + $app = Factory::getApplication(); + $lang = Factory::getLanguage()->getTag(); + $clientId = (int) $app->getClientId(); + + /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ + $cache = Factory::getCache('com_modules', 'callback'); + + $loader = function () use ($app, $lang, $module) { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'm.id', + 'm.title', + 'm.module', + 'm.position', + 'm.showtitle', + 'm.params' + ] + ) + ) + ->from($db->quoteName('#__modules', 'm')) + ->where($db->quoteName('m.module') . ' = :module') + ->where($db->quoteName('m.client_id') . ' = 1') + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.element') . ' = ' . $db->quoteName('m.module') . + ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('m.client_id') + ) + ->where($db->quoteName('e.enabled') . ' = 1') + ->bind(':module', $module); + + // Filter by language. + if ($app->isClient('site') && $app->getLanguageFilter()) { + $query->whereIn($db->quoteName('m.language'), [$lang, '*']); + } + + $query->order('m.position, m.ordering'); + + // Set the query. + $db->setQuery($query); + + return $db->loadObjectList(); + }; + + try { + return $clean = $cache->get($loader, array(), md5(serialize(array($clientId, $lang)))); + } catch (CacheExceptionInterface $cacheException) { + try { + return $loader(); + } catch (ExecutionFailureException $databaseException) { + Factory::getApplication()->enqueueMessage( + Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $databaseException->getMessage()), + 'error' + ); + + return array(); + } + } catch (ExecutionFailureException $databaseException) { + Factory::getApplication()->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $databaseException->getMessage()), 'error'); + + return array(); + } + } } diff --git a/code/administrator/components/com_login/src/View/Login/HtmlView.php b/code/administrator/components/com_login/src/View/Login/HtmlView.php index 40bcab2f..c51ba595 100644 --- a/code/administrator/components/com_login/src/View/Login/HtmlView.php +++ b/code/administrator/components/com_login/src/View/Login/HtmlView.php @@ -1,4 +1,5 @@ module != 'mod_login'){ - echo ModuleHelper::renderModule($module, array('id' => 'section-box')); + if ($module->module != 'mod_login') { + echo ModuleHelper::renderModule($module, array('id' => 'section-box')); + } } diff --git a/code/administrator/components/com_mails/mails.xml b/code/administrator/components/com_mails/mails.xml index d69601bd..9216c74f 100644 --- a/code/administrator/components/com_mails/mails.xml +++ b/code/administrator/components/com_mails/mails.xml @@ -2,7 +2,7 @@ com_mails Joomla! Project - January 2019 + 2019-01 (C) 2019 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_mails/services/provider.php b/code/administrator/components/com_mails/services/provider.php index 77901227..794bdfe5 100644 --- a/code/administrator/components/com_mails/services/provider.php +++ b/code/administrator/components/com_mails/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Mails')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Mails')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Mails')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Mails')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_mails/src/Controller/DisplayController.php b/code/administrator/components/com_mails/src/Controller/DisplayController.php index b6cd1e43..277f1043 100644 --- a/code/administrator/components/com_mails/src/Controller/DisplayController.php +++ b/code/administrator/components/com_mails/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'templates'); - $layout = $this->input->get('layout', ''); - $id = $this->input->getString('template_id'); - - // Check for edit form. - if ($view == 'template' && $layout == 'edit' && !$this->checkEditId('com_mails.edit.template', $id)) - { - // Somehow the person just went to the form - we don't allow that. - $this->setMessage(Text::sprintf('COM_MAILS_ERROR_UNHELD_ID', $id), 'error'); - $this->setRedirect(Route::_('index.php?option=com_mails&view=templates', false)); - - return false; - } - - return parent::display(); - } + /** + * The default view. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'templates'; + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return BaseController|boolean This object to support chaining. + * + * @since 4.0.0 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'templates'); + $layout = $this->input->get('layout', ''); + $id = $this->input->getString('template_id'); + + // Check for edit form. + if ($view == 'template' && $layout == 'edit' && !$this->checkEditId('com_mails.edit.template', $id)) { + // Somehow the person just went to the form - we don't allow that. + $this->setMessage(Text::sprintf('COM_MAILS_ERROR_UNHELD_ID', $id), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mails&view=templates', false)); + + return false; + } + + return parent::display(); + } } diff --git a/code/administrator/components/com_mails/src/Controller/TemplateController.php b/code/administrator/components/com_mails/src/Controller/TemplateController.php index 2b980daa..944031da 100644 --- a/code/administrator/components/com_mails/src/Controller/TemplateController.php +++ b/code/administrator/components/com_mails/src/Controller/TemplateController.php @@ -1,4 +1,5 @@ view_item = 'template'; - $this->view_list = 'templates'; - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = []) - { - return false; - } - - /** - * Method to edit an existing record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key - * (sometimes required to avoid router collisions). - * - * @return boolean True if access level check and checkout passes, false otherwise. - * - * @since 4.0.0 - */ - public function edit($key = null, $urlVar = null) - { - // Do not cache the response to this, its a redirect, and mod_expires and google chrome browser bugs cache it forever! - $this->app->allowCache(false); - - $context = "$this->option.edit.$this->context"; - - // Get the previous record id (if any) and the current record id. - $template_id = $this->input->getCmd('template_id'); - $language = $this->input->getCmd('language'); - - // Access check. - if (!$this->allowEdit(array('template_id' => $template_id, 'language' => $language), $template_id)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false - ) - ); - - return false; - } - - // Check-out succeeded, push the new record id into the session. - $this->holdEditId($context, $template_id . '.' . $language); - $this->app->setUserState($context . '.data', null); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(array($template_id, $language), 'template_id'), false - ) - ); - - return true; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param string[] $recordId The primary key id for the item in the first element and the language of the - * mail template in the second key. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $language = array_pop($recordId); - $return = parent::getRedirectToItemAppend(array_pop($recordId), $urlVar); - $return .= '&language=' . $language; - - return $return; - } - - /** - * Method to save a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 4.0.0 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ - $model = $this->getModel(); - $data = $this->input->post->get('jform', array(), 'array'); - $context = "$this->option.edit.$this->context"; - $task = $this->getTask(); - - $recordId = $this->input->getCmd('template_id'); - $language = $this->input->getCmd('language'); - - // Populate the row id from the session. - $data['template_id'] = $recordId; - $data['language'] = $language; - - // Access check. - if (!$this->allowSave($data, 'template_id')) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false - ) - ); - - return false; - } - - // Validate the posted data. - // Sometimes the form needs some posted data, such as for plugins and modules. - $form = $model->getForm($data, false); - - if (!$form) - { - $this->app->enqueueMessage($model->getError(), 'error'); - - return false; - } - - // Send an object which can be modified through the plugin event - $objData = (object) $data; - $this->app->triggerEvent( - 'onContentNormaliseRequestData', - array($this->option . '.' . $this->context, $objData, $form) - ); - $data = (array) $objData; - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), false - ) - ); - - return false; - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), false - ) - ); - - return false; - } - - $langKey = $this->text_prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS'; - $prefix = Factory::getLanguage()->hasKey($langKey) ? $this->text_prefix : 'COM_MAILS'; - - $this->setMessage(Text::_($prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS')); - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the record data in the session. - $this->holdEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), false - ) - ); - break; - - default: - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - - $url = 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(); - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - break; - } - - // Invoke the postSave method to allow for the child class to access the model. - $this->postSaveHook($model, $validData); - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->view_item = 'template'; + $this->view_list = 'templates'; + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = []) + { + return false; + } + + /** + * Method to edit an existing record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * (sometimes required to avoid router collisions). + * + * @return boolean True if access level check and checkout passes, false otherwise. + * + * @since 4.0.0 + */ + public function edit($key = null, $urlVar = null) + { + // Do not cache the response to this, its a redirect, and mod_expires and google chrome browser bugs cache it forever! + $this->app->allowCache(false); + + $context = "$this->option.edit.$this->context"; + + // Get the previous record id (if any) and the current record id. + $template_id = $this->input->getCmd('template_id'); + $language = $this->input->getCmd('language'); + + // Access check. + if (!$this->allowEdit(array('template_id' => $template_id, 'language' => $language), $template_id)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + + return false; + } + + // Check-out succeeded, push the new record id into the session. + $this->holdEditId($context, $template_id . '.' . $language); + $this->app->setUserState($context . '.data', null); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item + . $this->getRedirectToItemAppend(array($template_id, $language), 'template_id'), + false + ) + ); + + return true; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param string[] $recordId The primary key id for the item in the first element and the language of the + * mail template in the second key. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $language = array_pop($recordId); + $return = parent::getRedirectToItemAppend(array_pop($recordId), $urlVar); + $return .= '&language=' . $language; + + return $return; + } + + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 4.0.0 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + $data = $this->input->post->get('jform', array(), 'array'); + $context = "$this->option.edit.$this->context"; + $task = $this->getTask(); + + $recordId = $this->input->getCmd('template_id'); + $language = $this->input->getCmd('language'); + + // Populate the row id from the session. + $data['template_id'] = $recordId; + $data['language'] = $language; + + // Access check. + if (!$this->allowSave($data, 'template_id')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + + return false; + } + + // Validate the posted data. + // Sometimes the form needs some posted data, such as for plugins and modules. + $form = $model->getForm($data, false); + + if (!$form) { + $this->app->enqueueMessage($model->getError(), 'error'); + + return false; + } + + // Send an object which can be modified through the plugin event + $objData = (object) $data; + $this->app->triggerEvent( + 'onContentNormaliseRequestData', + array($this->option . '.' . $this->context, $objData, $form) + ); + $data = (array) $objData; + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item + . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), + false + ) + ); + + return false; + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item + . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), + false + ) + ); + + return false; + } + + $langKey = $this->text_prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS'; + $prefix = Factory::getLanguage()->hasKey($langKey) ? $this->text_prefix : 'COM_MAILS'; + + $this->setMessage(Text::_($prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS')); + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the record data in the session. + $this->holdEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item + . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), + false + ) + ); + break; + + default: + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + + $url = 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(); + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + break; + } + + // Invoke the postSave method to allow for the child class to access the model. + $this->postSaveHook($model, $validData); + + return true; + } } diff --git a/code/administrator/components/com_mails/src/Helper/MailsHelper.php b/code/administrator/components/com_mails/src/Helper/MailsHelper.php index 87aff21a..944f92f2 100644 --- a/code/administrator/components/com_mails/src/Helper/MailsHelper.php +++ b/code/administrator/components/com_mails/src/Helper/MailsHelper.php @@ -1,4 +1,5 @@ triggerEvent('onMailBeforeTagsRendering', array($mail->template_id, &$mail)); - - if (!isset($mail->params['tags']) || !count($mail->params['tags'])) - { - return ''; - } - - $html = '
    '; - - foreach ($mail->params['tags'] as $tag) - { - $html .= '
  • ' - . '' . $tag . '' - . '
  • '; - } - - $html .= '
'; - - return $html; - } - - /** - * Load the translation files for an extension - * - * @param string $extension Extension name - * - * @return void - * - * @since 4.0.0 - */ - public static function loadTranslationFiles($extension) - { - static $cache = array(); - - $extension = strtolower($extension); - - if (isset($cache[$extension])) - { - return; - } - - $lang = Factory::getLanguage(); - $source = ''; - - switch (substr($extension, 0, 3)) - { - case 'com': - default: - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - break; - - case 'mod': - $source = JPATH_SITE . '/modules/' . $extension; - break; - - case 'plg': - $parts = explode('_', $extension, 3); - - if (count($parts) > 2) - { - $source = JPATH_PLUGINS . '/' . $parts[1] . '/' . $parts[2]; - } - break; - } - - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, $source); - - if (!$lang->hasKey(strtoupper($extension))) - { - $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($extension . '.sys', $source); - } - - $cache[$extension] = true; - } + /** + * Display a clickable list of tags for a mail template + * + * @param object $mail Row of the mail template. + * @param string $fieldname Name of the target field. + * + * @return string List of tags that can be inserted into a field. + * + * @since 4.0.0 + */ + public static function mailtags($mail, $fieldname) + { + Factory::getApplication()->triggerEvent('onMailBeforeTagsRendering', array($mail->template_id, &$mail)); + + if (!isset($mail->params['tags']) || !count($mail->params['tags'])) { + return ''; + } + + $html = '
    '; + + foreach ($mail->params['tags'] as $tag) { + $html .= '
  • ' + . '' . $tag . '' + . '
  • '; + } + + $html .= '
'; + + return $html; + } + + /** + * Load the translation files for an extension + * + * @param string $extension Extension name + * + * @return void + * + * @since 4.0.0 + */ + public static function loadTranslationFiles($extension) + { + static $cache = array(); + + $extension = strtolower($extension); + + if (isset($cache[$extension])) { + return; + } + + $lang = Factory::getLanguage(); + $source = ''; + + switch (substr($extension, 0, 3)) { + case 'com': + default: + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + break; + + case 'mod': + $source = JPATH_SITE . '/modules/' . $extension; + break; + + case 'plg': + $parts = explode('_', $extension, 3); + + if (count($parts) > 2) { + $source = JPATH_PLUGINS . '/' . $parts[1] . '/' . $parts[2]; + } + break; + } + + $lang->load($extension, JPATH_ADMINISTRATOR) + || $lang->load($extension, $source); + + if (!$lang->hasKey(strtoupper($extension))) { + $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($extension . '.sys', $source); + } + + $cache[$extension] = true; + } } diff --git a/code/administrator/components/com_mails/src/Model/TemplateModel.php b/code/administrator/components/com_mails/src/Model/TemplateModel.php index fb5e2146..1777d8ae 100644 --- a/code/administrator/components/com_mails/src/Model/TemplateModel.php +++ b/code/administrator/components/com_mails/src/Model/TemplateModel.php @@ -1,4 +1,5 @@ loadForm('com_mails.template', 'template', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - $params = ComponentHelper::getParams('com_mails'); - - if ($params->get('mail_style', 'plaintext') == 'plaintext') - { - $form->removeField('htmlbody'); - } - - if ($params->get('mail_style', 'plaintext') == 'html') - { - $form->removeField('body'); - } - - if (!$params->get('alternative_mailconfig', '0')) - { - $form->removeField('alternative_mailconfig', 'params'); - $form->removeField('mailfrom', 'params'); - $form->removeField('fromname', 'params'); - $form->removeField('replyto', 'params'); - $form->removeField('replytoname', 'params'); - $form->removeField('mailer', 'params'); - $form->removeField('sendmail', 'params'); - $form->removeField('smtphost', 'params'); - $form->removeField('smtpport', 'params'); - $form->removeField('smtpsecure', 'params'); - $form->removeField('smtpauth', 'params'); - $form->removeField('smtpuser', 'params'); - $form->removeField('smtppass', 'params'); - } - - if (!$params->get('copy_mails')) - { - $form->removeField('copyto', 'params'); - } - - if (!trim($params->get('attachment_folder', ''))) - { - $form->removeField('attachments'); - - return $form; - } - - try - { - $attachmentPath = rtrim(Path::check(JPATH_ROOT . '/' . $params->get('attachment_folder')), \DIRECTORY_SEPARATOR); - } - catch (\Exception $e) - { - $attachmentPath = ''; - } - - if (!$attachmentPath || $attachmentPath === Path::clean(JPATH_ROOT) || !is_dir($attachmentPath)) - { - $form->removeField('attachments'); - - return $form; - } - - $field = $form->getField('attachments'); - $subform = new \SimpleXMLElement($field->formsource); - $files = $subform->xpath('field[@name="file"]'); - $files[0]->addAttribute('directory', $attachmentPath); - $form->load('
' - . str_replace('', '', $subform->asXML()) - . '
' - ); - - return $form; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return CMSObject|boolean Object on success, false on failure. - * - * @since 4.0.0 - */ - public function getItem($pk = null) - { - $templateId = $this->getState($this->getName() . '.template_id'); - $language = $this->getState($this->getName() . '.language'); - $table = $this->getTable('Template', 'Table'); - - if ($templateId != '' && $language != '') - { - // Attempt to load the row. - $return = $table->load(array('template_id' => $templateId, 'language' => $language)); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Convert to the CMSObject before adding other data. - $properties = $table->getProperties(1); - $item = ArrayHelper::toObject($properties, CMSObject::class); - - if (property_exists($item, 'params')) - { - $registry = new Registry($item->params); - $item->params = $registry->toArray(); - } - - if (!$item->template_id) - { - $item->template_id = $templateId; - } - - if (!$item->language) - { - $item->language = $language; - } - - return $item; - } - - /** - * Get the master data for a mail template. - * - * @param integer $pk The id of the primary key. - * - * @return CMSObject|boolean Object on success, false on failure. - * - * @since 4.0.0 - */ - public function getMaster($pk = null) - { - $template_id = $this->getState($this->getName() . '.template_id'); - $table = $this->getTable('Template', 'Table'); - - if ($template_id != '') - { - // Attempt to load the row. - $return = $table->load(array('template_id' => $template_id, 'language' => '')); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Convert to the CMSObject before adding other data. - $properties = $table->getProperties(1); - $item = ArrayHelper::toObject($properties, CMSObject::class); - - if (property_exists($item, 'params')) - { - $registry = new Registry($item->params); - $item->params = $registry->toArray(); - } - - return $item; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 4.0.0 - * @throws \Exception - */ - public function getTable($name = 'Template', $prefix = 'Administrator', $options = array()) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_mails.edit.template.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_mails.template', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @since 4.0.0 - */ - public function validate($form, $data, $group = null) - { - $validLanguages = LanguageHelper::getContentLanguages(array(0, 1)); - - if (!array_key_exists($data['language'], $validLanguages)) - { - $this->setError(Text::_('COM_MAILS_FIELD_LANGUAGE_CODE_INVALID')); - - return false; - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 4.0.0 - */ - public function save($data) - { - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - $key = $table->getKeyName(); - $template_id = (!empty($data['template_id'])) ? $data['template_id'] : $this->getState($this->getName() . '.template_id'); - $language = (!empty($data['language'])) ? $data['language'] : $this->getState($this->getName() . '.language'); - $isNew = true; - - // Include the plugins for the save events. - \Joomla\CMS\Plugin\PluginHelper::importPlugin($this->events_map['save']); - - // Allow an exception to be thrown. - try - { - // Load the row if saving an existing record. - $table->load(array('template_id' => $template_id, 'language' => $language)); - - if ($table->subject) - { - $isNew = false; - } - - // Load the default row - $table->load(array('template_id' => $template_id, 'language' => '')); - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Prepare the row for saving - $this->prepareTable($table); - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, $table, $isNew, $data)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Clean the cache. - $this->cleanCache(); - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, $table, $isNew, $data)); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $this->setState($this->getName() . '.new', $isNew); - - return true; - } - - /** - * Prepare and sanitise the table data prior to saving. - * - * @param Table $table A reference to a Table object. - * - * @return void - * - * @since 4.0.0 - */ - protected function prepareTable($table) - { - - } - - /** - * Stock method to auto-populate the model state. - * - * @return void - * - * @since 4.0.0 - */ - protected function populateState() - { - parent::populateState(); - - $template_id = Factory::getApplication()->input->getCmd('template_id'); - $this->setState($this->getName() . '.template_id', $template_id); - - $language = Factory::getApplication()->input->getCmd('language'); - $this->setState($this->getName() . '.language', $language); - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 4.0.0 + */ + protected $text_prefix = 'COM_MAILS'; + + /** + * The type alias for this content type (for example, 'com_content.article'). + * + * @var string + * @since 4.0.0 + */ + public $typeAlias = 'com_mails.template'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 4.0.0 + */ + protected function canDelete($record) + { + return false; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|bool A JForm object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_mails.template', 'template', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + $params = ComponentHelper::getParams('com_mails'); + + if ($params->get('mail_style', 'plaintext') == 'plaintext') { + $form->removeField('htmlbody'); + } + + if ($params->get('mail_style', 'plaintext') == 'html') { + $form->removeField('body'); + } + + if (!$params->get('alternative_mailconfig', '0')) { + $form->removeField('alternative_mailconfig', 'params'); + $form->removeField('mailfrom', 'params'); + $form->removeField('fromname', 'params'); + $form->removeField('replyto', 'params'); + $form->removeField('replytoname', 'params'); + $form->removeField('mailer', 'params'); + $form->removeField('sendmail', 'params'); + $form->removeField('smtphost', 'params'); + $form->removeField('smtpport', 'params'); + $form->removeField('smtpsecure', 'params'); + $form->removeField('smtpauth', 'params'); + $form->removeField('smtpuser', 'params'); + $form->removeField('smtppass', 'params'); + } + + if (!$params->get('copy_mails')) { + $form->removeField('copyto', 'params'); + } + + if (!trim($params->get('attachment_folder', ''))) { + $form->removeField('attachments'); + + return $form; + } + + try { + $attachmentPath = rtrim(Path::check(JPATH_ROOT . '/' . $params->get('attachment_folder')), \DIRECTORY_SEPARATOR); + } catch (\Exception $e) { + $attachmentPath = ''; + } + + if (!$attachmentPath || $attachmentPath === Path::clean(JPATH_ROOT) || !is_dir($attachmentPath)) { + $form->removeField('attachments'); + + return $form; + } + + $field = $form->getField('attachments'); + $subform = new \SimpleXMLElement($field->formsource); + $files = $subform->xpath('field[@name="file"]'); + $files[0]->addAttribute('directory', $attachmentPath); + $form->load('
' + . str_replace('', '', $subform->asXML()) + . '
'); + + return $form; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return CMSObject|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getItem($pk = null) + { + $templateId = $this->getState($this->getName() . '.template_id'); + $language = $this->getState($this->getName() . '.language'); + $table = $this->getTable('Template', 'Table'); + + if ($templateId != '' && $language != '') { + // Attempt to load the row. + $return = $table->load(array('template_id' => $templateId, 'language' => $language)); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + } + + // Convert to the CMSObject before adding other data. + $properties = $table->getProperties(1); + $item = ArrayHelper::toObject($properties, CMSObject::class); + + if (property_exists($item, 'params')) { + $registry = new Registry($item->params); + $item->params = $registry->toArray(); + } + + if (!$item->template_id) { + $item->template_id = $templateId; + } + + if (!$item->language) { + $item->language = $language; + } + + return $item; + } + + /** + * Get the master data for a mail template. + * + * @param integer $pk The id of the primary key. + * + * @return CMSObject|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getMaster($pk = null) + { + $template_id = $this->getState($this->getName() . '.template_id'); + $table = $this->getTable('Template', 'Table'); + + if ($template_id != '') { + // Attempt to load the row. + $return = $table->load(array('template_id' => $template_id, 'language' => '')); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + } + + // Convert to the CMSObject before adding other data. + $properties = $table->getProperties(1); + $item = ArrayHelper::toObject($properties, CMSObject::class); + + if (property_exists($item, 'params')) { + $registry = new Registry($item->params); + $item->params = $registry->toArray(); + } + + return $item; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 4.0.0 + * @throws \Exception + */ + public function getTable($name = 'Template', $prefix = 'Administrator', $options = array()) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_mails.edit.template.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_mails.template', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @since 4.0.0 + */ + public function validate($form, $data, $group = null) + { + $validLanguages = LanguageHelper::getContentLanguages(array(0, 1)); + + if (!array_key_exists($data['language'], $validLanguages)) { + $this->setError(Text::_('COM_MAILS_FIELD_LANGUAGE_CODE_INVALID')); + + return false; + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 4.0.0 + */ + public function save($data) + { + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + $key = $table->getKeyName(); + $template_id = (!empty($data['template_id'])) ? $data['template_id'] : $this->getState($this->getName() . '.template_id'); + $language = (!empty($data['language'])) ? $data['language'] : $this->getState($this->getName() . '.language'); + $isNew = true; + + // Include the plugins for the save events. + \Joomla\CMS\Plugin\PluginHelper::importPlugin($this->events_map['save']); + + // Allow an exception to be thrown. + try { + // Load the row if saving an existing record. + $table->load(array('template_id' => $template_id, 'language' => $language)); + + if ($table->subject) { + $isNew = false; + } + + // Load the default row + $table->load(array('template_id' => $template_id, 'language' => '')); + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, $table, $isNew, $data)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Clean the cache. + $this->cleanCache(); + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, $table, $isNew, $data)); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->setState($this->getName() . '.new', $isNew); + + return true; + } + + /** + * Prepare and sanitise the table data prior to saving. + * + * @param Table $table A reference to a Table object. + * + * @return void + * + * @since 4.0.0 + */ + protected function prepareTable($table) + { + } + + /** + * Stock method to auto-populate the model state. + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState() + { + parent::populateState(); + + $template_id = Factory::getApplication()->input->getCmd('template_id'); + $this->setState($this->getName() . '.template_id', $template_id); + + $language = Factory::getApplication()->input->getCmd('language'); + $this->setState($this->getName() . '.language', $language); + } } diff --git a/code/administrator/components/com_mails/src/Model/TemplatesModel.php b/code/administrator/components/com_mails/src/Model/TemplatesModel.php index 3ec92653..32caf4f5 100644 --- a/code/administrator/components/com_mails/src/Model/TemplatesModel.php +++ b/code/administrator/components/com_mails/src/Model/TemplatesModel.php @@ -1,4 +1,5 @@ setState('params', $params); - - // List state information. - parent::populateState('a.template_id', 'asc'); - } - - /** - * Get a list of mail templates - * - * @return array - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - $id = ''; - - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('language')) - ->from($db->quoteName('#__mail_templates')) - ->where($db->quoteName('template_id') . ' = :id') - ->where($db->quoteName('language') . ' != ' . $db->quote('')) - ->order($db->quoteName('language') . ' ASC') - ->bind(':id', $id); - - foreach ($items as $item) - { - $id = $item->template_id; - $db->setQuery($query); - $item->languages = $db->loadColumn(); - } - - return $items; - } - - /** - * Build an SQL query to load the list data. - * - * @return QueryInterface - * - * @since 4.0.0 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - $db->quoteName('a') . '.*' - ) - ); - $query->from($db->quoteName('#__mail_templates', 'a')) - ->where($db->quoteName('a.language') . ' = ' . $db->quote('')); - - // Filter by search in title. - if ($search = trim($this->getState('filter.search', ''))) - { - if (stripos($search, 'id:') === 0) - { - $search = substr($search, 3); - $query->where($db->quoteName('a.template_id') . ' = :search') - ->bind(':search', $search); - } - else - { - $search = '%' . str_replace(' ', '%', $search) . '%'; - $query->where( - '(' . $db->quoteName('a.template_id') . ' LIKE :search1' - . ' OR ' . $db->quoteName('a.subject') . ' LIKE :search2' - . ' OR ' . $db->quoteName('a.body') . ' LIKE :search3' - . ' OR ' . $db->quoteName('a.htmlbody') . ' LIKE :search4)' - ) - ->bind([':search1', ':search2', ':search3', ':search4'], $search); - } - } - - // Filter on the extension. - if ($extension = $this->getState('filter.extension')) - { - $query->where($db->quoteName('a.extension') . ' = :extension') - ->bind(':extension', $extension); - } - else - { - // Only show mail template from enabled extensions - $subQuery = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('enabled') . ' = 1'); - - $query->where($db->quoteName('a.extension') . ' IN(' . $subQuery . ')'); - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->join( - 'INNER', - $db->quoteName('#__mail_templates', 'b'), - $db->quoteName('b.template_id') . ' = ' . $db->quoteName('a.template_id') - . ' AND ' . $db->quoteName('b.language') . ' = :language' - ) - ->bind(':language', $language); - } - - // Add the list ordering clause - $listOrdering = $this->state->get('list.ordering', 'a.template_id'); - $orderDirn = $this->state->get('list.direction', 'ASC'); - - $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); - - return $query; - } - - /** - * Get list of extensions which are using mail templates - * - * @return array - * - * @since 4.0.0 - */ - public function getExtensions() - { - $db = $this->getDbo(); - $subQuery = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('enabled') . ' = 1'); - - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('extension')) - ->from($db->quoteName('#__mail_templates')) - ->where($db->quoteName('extension') . ' IN (' . $subQuery . ')'); - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Get a list of the current content languages - * - * @return array - * - * @since 4.0.0 - */ - public function getLanguages() - { - return LanguageHelper::getContentLanguages(array(0,1)); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 4.0.0 + * @throws \Exception + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'template_id', 'a.template_id', + 'language', 'a.language', + 'subject', 'a.subject', + 'body', 'a.body', + 'htmlbody', 'a.htmlbody', + 'extension' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = null, $direction = null) + { + // Load the parameters. + $params = ComponentHelper::getParams('com_mails'); + $this->setState('params', $params); + + // List state information. + parent::populateState('a.template_id', 'asc'); + } + + /** + * Get a list of mail templates + * + * @return array + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + $id = ''; + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('language')) + ->from($db->quoteName('#__mail_templates')) + ->where($db->quoteName('template_id') . ' = :id') + ->where($db->quoteName('language') . ' != ' . $db->quote('')) + ->order($db->quoteName('language') . ' ASC') + ->bind(':id', $id); + + foreach ($items as $item) { + $id = $item->template_id; + $db->setQuery($query); + $item->languages = $db->loadColumn(); + } + + return $items; + } + + /** + * Build an SQL query to load the list data. + * + * @return QueryInterface + * + * @since 4.0.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + $db->quoteName('a') . '.*' + ) + ); + $query->from($db->quoteName('#__mail_templates', 'a')) + ->where($db->quoteName('a.language') . ' = ' . $db->quote('')); + + // Filter by search in title. + if ($search = trim($this->getState('filter.search', ''))) { + if (stripos($search, 'id:') === 0) { + $search = substr($search, 3); + $query->where($db->quoteName('a.template_id') . ' = :search') + ->bind(':search', $search); + } else { + $search = '%' . str_replace(' ', '%', $search) . '%'; + $query->where( + '(' . $db->quoteName('a.template_id') . ' LIKE :search1' + . ' OR ' . $db->quoteName('a.subject') . ' LIKE :search2' + . ' OR ' . $db->quoteName('a.body') . ' LIKE :search3' + . ' OR ' . $db->quoteName('a.htmlbody') . ' LIKE :search4)' + ) + ->bind([':search1', ':search2', ':search3', ':search4'], $search); + } + } + + // Filter on the extension. + if ($extension = $this->getState('filter.extension')) { + $query->where($db->quoteName('a.extension') . ' = :extension') + ->bind(':extension', $extension); + } else { + // Only show mail template from enabled extensions + $subQuery = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('enabled') . ' = 1'); + + $query->where($db->quoteName('a.extension') . ' IN(' . $subQuery . ')'); + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->join( + 'INNER', + $db->quoteName('#__mail_templates', 'b'), + $db->quoteName('b.template_id') . ' = ' . $db->quoteName('a.template_id') + . ' AND ' . $db->quoteName('b.language') . ' = :language' + ) + ->bind(':language', $language); + } + + // Add the list ordering clause + $listOrdering = $this->state->get('list.ordering', 'a.template_id'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + + $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); + + return $query; + } + + /** + * Get list of extensions which are using mail templates + * + * @return array + * + * @since 4.0.0 + */ + public function getExtensions() + { + $db = $this->getDatabase(); + $subQuery = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('enabled') . ' = 1'); + + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('extension')) + ->from($db->quoteName('#__mail_templates')) + ->where($db->quoteName('extension') . ' IN (' . $subQuery . ')'); + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Get a list of the current content languages + * + * @return array + * + * @since 4.0.0 + */ + public function getLanguages() + { + return LanguageHelper::getContentLanguages(array(0,1)); + } } diff --git a/code/administrator/components/com_mails/src/Table/TemplateTable.php b/code/administrator/components/com_mails/src/Table/TemplateTable.php index c4c8f2f5..b26f1b40 100644 --- a/code/administrator/components/com_mails/src/Table/TemplateTable.php +++ b/code/administrator/components/com_mails/src/Table/TemplateTable.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->master = $this->get('Master'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - list($component, $template_id) = explode('.', $this->item->template_id, 2); - $fields = array('subject', 'body', 'htmlbody'); - $this->templateData = array(); - $language = Factory::getLanguage(); - $language->load($component, JPATH_SITE, $this->item->language, true); - $language->load($component, JPATH_SITE . '/components/' . $component, $this->item->language, true); - $language->load($component, JPATH_ADMINISTRATOR, $this->item->language, true); - $language->load($component, JPATH_ADMINISTRATOR . '/components/' . $component, $this->item->language, true); - - $this->master->subject = Text::_($this->master->subject); - $this->master->body = Text::_($this->master->body); - - if ($this->master->htmlbody) - { - $this->master->htmlbody = Text::_($this->master->htmlbody); - } - else - { - $this->master->htmlbody = nl2br($this->master->body, false); - } - - $this->templateData = [ - 'subject' => $this->master->subject, - 'body' => $this->master->body, - 'htmlbody' => $this->master->htmlbody, - ]; - - foreach ($fields as $field) - { - if (is_null($this->item->$field) || $this->item->$field == '') - { - $this->item->$field = $this->master->$field; - $this->form->setValue($field, null, $this->item->$field); - } - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title( - Text::_('COM_MAILS_PAGE_EDIT_MAIL'), - 'pencil-2 article-add' - ); - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) - { - $childBar->apply('template.apply'); - $childBar->save('template.save'); - } - ); - - $toolbar->cancel('template.cancel', 'JTOOLBAR_CLOSE'); - - $toolbar->divider(); - $toolbar->help('Mail_Template:_Edit'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var CMSObject + */ + protected $item; + + /** + * The model state + * + * @var object + */ + protected $state; + + /** + * The template data + * + * @var array + */ + protected $templateData; + + /** + * Master data for the mail template + * + * @var CMSObject + */ + protected $master; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->master = $this->get('Master'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + list($component, $template_id) = explode('.', $this->item->template_id, 2); + $fields = array('subject', 'body', 'htmlbody'); + $this->templateData = array(); + $language = Factory::getLanguage(); + $language->load($component, JPATH_SITE, $this->item->language, true); + $language->load($component, JPATH_SITE . '/components/' . $component, $this->item->language, true); + $language->load($component, JPATH_ADMINISTRATOR, $this->item->language, true); + $language->load($component, JPATH_ADMINISTRATOR . '/components/' . $component, $this->item->language, true); + + $this->master->subject = Text::_($this->master->subject); + $this->master->body = Text::_($this->master->body); + + if ($this->master->htmlbody) { + $this->master->htmlbody = Text::_($this->master->htmlbody); + } else { + $this->master->htmlbody = nl2br($this->master->body, false); + } + + $this->templateData = [ + 'subject' => $this->master->subject, + 'body' => $this->master->body, + 'htmlbody' => $this->master->htmlbody, + ]; + + foreach ($fields as $field) { + if (is_null($this->item->$field) || $this->item->$field == '') { + $this->item->$field = $this->master->$field; + $this->form->setValue($field, null, $this->item->$field); + } + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title( + Text::_('COM_MAILS_PAGE_EDIT_MAIL'), + 'pencil-2 article-add' + ); + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) { + $childBar->apply('template.apply'); + $childBar->save('template.save'); + } + ); + + $toolbar->cancel('template.cancel', 'JTOOLBAR_CLOSE'); + + $toolbar->divider(); + $toolbar->help('Mail_Template:_Edit'); + } } diff --git a/code/administrator/components/com_mails/src/View/Templates/HtmlView.php b/code/administrator/components/com_mails/src/View/Templates/HtmlView.php index 79f938ac..2d067921 100644 --- a/code/administrator/components/com_mails/src/View/Templates/HtmlView.php +++ b/code/administrator/components/com_mails/src/View/Templates/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->languages = $this->get('Languages'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $extensions = $this->get('Extensions'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Find and set site default language - $defaultLanguageTag = ComponentHelper::getParams('com_languages')->get('site'); - - foreach ($this->languages as $tag => $language) - { - if ($tag === $defaultLanguageTag) - { - $this->defaultLanguage = $language; - break; - } - } - - foreach ($extensions as $extension) - { - MailsHelper::loadTranslationFiles($extension); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - $user = Factory::getUser(); - - ToolbarHelper::title(Text::_('COM_MAILS_MAILS_TITLE'), 'envelope'); - - if ($user->authorise('core.admin', 'com_mails') || $user->authorise('core.options', 'com_mails')) - { - $toolbar->preferences('com_mails'); - } - - $toolbar->help('Mail_Templates'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * An array of installed languages + * + * @var array + */ + protected $languages; + + /** + * Site default language + * + * @var \stdClass + */ + protected $defaultLanguage; + + /** + * The pagination object + * + * @var Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->languages = $this->get('Languages'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $extensions = $this->get('Extensions'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Find and set site default language + $defaultLanguageTag = ComponentHelper::getParams('com_languages')->get('site'); + + foreach ($this->languages as $tag => $language) { + if ($tag === $defaultLanguageTag) { + $this->defaultLanguage = $language; + break; + } + } + + foreach ($extensions as $extension) { + MailsHelper::loadTranslationFiles($extension); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + $user = $this->getCurrentUser(); + + ToolbarHelper::title(Text::_('COM_MAILS_MAILS_TITLE'), 'envelope'); + + if ($user->authorise('core.admin', 'com_mails') || $user->authorise('core.options', 'com_mails')) { + $toolbar->preferences('com_mails'); + } + + $toolbar->help('Mail_Templates'); + } } diff --git a/code/administrator/components/com_mails/tmpl/template/edit.php b/code/administrator/components/com_mails/tmpl/template/edit.php index f25a8e15..b0f14e32 100644 --- a/code/administrator/components/com_mails/tmpl/template/edit.php +++ b/code/administrator/components/com_mails/tmpl/template/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_mails.admin-email-template-edit'); + ->useScript('form.validate') + ->useScript('com_mails.admin-email-template-edit'); $this->useCoreUI = true; @@ -36,84 +37,84 @@ ?>
-
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> - - -
-
-

- escape($this->item->language); ?> -

-
- escape($this->master->template_id); ?> -
-

-
-
- -
-
- form->renderField('subject'); ?> -
-
- -
-
- form->getField('body')) : ?> -
-
- form->renderField('body'); ?> -
-
- -
-

- master, 'body'); ?> -
-
-
- - - form->getField('htmlbody')) : ?> -
-
- form->renderField('htmlbody'); ?> -
-
- -
-

- master, 'htmlbody'); ?> -
-
-
- - - form->getField('attachments')) : ?> -
-
- form->renderField('attachments'); ?> -
-
- - - - - form->getFieldset('basic'))) : ?> - - - - -
- form->renderField('template_id'); ?> - form->renderField('language'); ?> - - - +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+
+

- escape($this->item->language); ?> +

+
+ escape($this->master->template_id); ?> +
+

+
+
+ +
+
+ form->renderField('subject'); ?> +
+
+ +
+
+ form->getField('body')) : ?> +
+
+ form->renderField('body'); ?> +
+
+ +
+

+ master, 'body'); ?> +
+
+
+ + + form->getField('htmlbody')) : ?> +
+
+ form->renderField('htmlbody'); ?> +
+
+ +
+

+ master, 'htmlbody'); ?> +
+
+
+ + + form->getField('attachments')) : ?> +
+
+ form->renderField('attachments'); ?> +
+
+ + + + + form->getFieldset('basic'))) : ?> + + + + +
+ form->renderField('template_id'); ?> + form->renderField('language'); ?> + + +
diff --git a/code/administrator/components/com_mails/tmpl/templates/default.php b/code/administrator/components/com_mails/tmpl/templates/default.php index 772904f6..17e770ba 100644 --- a/code/administrator/components/com_mails/tmpl/templates/default.php +++ b/code/administrator/components/com_mails/tmpl/templates/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); +$wa->useScript('table.columns'); + $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
-
-
- $this)); - ?> - items)) : ?> -
- - -
- - - - - - - - languages) > 1) : ?> - - - - - - - - items as $i => $item) : - list($component, $sub_id) = explode('.', $item->template_id, 2); - $sub_id = str_replace('.', '_', $sub_id); - ?> - - - - languages) > 1) : ?> - - - - - - - -
- , - , - -
- - - - - - - - - -
- - - - - - - - - - - template_id; ?> -
+
+
+
+ $this)); + ?> + items)) : ?> +
+ + +
+ + + + + + + + languages) > 1) : ?> + + + + + + + + items as $i => $item) : + list($component, $sub_id) = explode('.', $item->template_id, 2); + $sub_id = str_replace('.', '_', $sub_id); + ?> + + + + languages) > 1) : ?> + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ + + + + + + + + + + template_id; ?> +
- - pagination->getListFooter(); ?> - + + pagination->getListFooter(); ?> + - - - -
-
-
+ + + +
+
+
diff --git a/code/administrator/components/com_media/config.xml b/code/administrator/components/com_media/config.xml index 9f5c25ad..bd43e97d 100644 --- a/code/administrator/components/com_media/config.xml +++ b/code/administrator/components/com_media/config.xml @@ -1,5 +1,6 @@ +
* @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\CMS\Object\CMSObject; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Media helper class. * @@ -18,33 +23,31 @@ */ abstract class MediaHelper { - /** - * Generates the URL to the object in the action logs component - * - * @param string $contentType The content type - * @param integer $id The integer id - * @param CMSObject $mediaObject The media object being uploaded - * - * @return string The link for the action log - * - * @since 3.9.27 - */ - public static function getContentTypeLink($contentType, $id, CMSObject $mediaObject) - { - if ($contentType === 'com_media.file') - { - return ''; - } - - $link = 'index.php?option=com_media'; - $adapter = $mediaObject->get('adapter'); - $uploadedPath = $mediaObject->get('path'); - - if (!empty($adapter) && !empty($uploadedPath)) - { - $link = $link . '&path=' . $adapter . ':' . $uploadedPath; - } - - return $link; - } + /** + * Generates the URL to the object in the action logs component + * + * @param string $contentType The content type + * @param integer $id The integer id + * @param CMSObject $mediaObject The media object being uploaded + * + * @return string The link for the action log + * + * @since 3.9.27 + */ + public static function getContentTypeLink($contentType, $id, CMSObject $mediaObject) + { + if ($contentType === 'com_media.file') { + return ''; + } + + $link = 'index.php?option=com_media'; + $adapter = $mediaObject->get('adapter'); + $uploadedPath = $mediaObject->get('path'); + + if (!empty($adapter) && !empty($uploadedPath)) { + $link = $link . '&path=' . $adapter . ':' . $uploadedPath; + } + + return $link; + } } diff --git a/code/administrator/components/com_media/layouts/toolbar/create-folder.php b/code/administrator/components/com_media/layouts/toolbar/create-folder.php index d5de1dd6..52ddda3b 100644 --- a/code/administrator/components/com_media/layouts/toolbar/create-folder.php +++ b/code/administrator/components/com_media/layouts/toolbar/create-folder.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('webcomponent.toolbar-button'); + ->useScript('webcomponent.toolbar-button'); $title = Text::_('COM_MEDIA_CREATE_NEW_FOLDER'); ?> - + diff --git a/code/administrator/components/com_media/layouts/toolbar/delete.php b/code/administrator/components/com_media/layouts/toolbar/delete.php index 2fc8cccb..392e0e1f 100644 --- a/code/administrator/components/com_media/layouts/toolbar/delete.php +++ b/code/administrator/components/com_media/layouts/toolbar/delete.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('webcomponent.toolbar-button'); + ->useScript('webcomponent.toolbar-button'); $title = Text::_('JTOOLBAR_DELETE'); ?> - + diff --git a/code/administrator/components/com_media/layouts/toolbar/upload.php b/code/administrator/components/com_media/layouts/toolbar/upload.php index 988542e2..11691117 100644 --- a/code/administrator/components/com_media/layouts/toolbar/upload.php +++ b/code/administrator/components/com_media/layouts/toolbar/upload.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('webcomponent.toolbar-button'); + ->useScript('webcomponent.toolbar-button'); $title = Text::_('JTOOLBAR_UPLOAD'); ?> - + diff --git a/code/administrator/components/com_media/media.xml b/code/administrator/components/com_media/media.xml index 203fb76d..edcb2d71 100644 --- a/code/administrator/components/com_media/media.xml +++ b/code/administrator/components/com_media/media.xml @@ -2,7 +2,7 @@ com_media Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_media/services/provider.php b/code/administrator/components/com_media/services/provider.php index 53a08de2..dbab9ac0 100644 --- a/code/administrator/components/com_media/services/provider.php +++ b/code/administrator/components/com_media/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Media')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Media')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Media')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Media')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_media/src/Adapter/AdapterInterface.php b/code/administrator/components/com_media/src/Adapter/AdapterInterface.php index a752e347..0f39fc6f 100644 --- a/code/administrator/components/com_media/src/Adapter/AdapterInterface.php +++ b/code/administrator/components/com_media/src/Adapter/AdapterInterface.php @@ -1,4 +1,5 @@ input->getMethod(); - - $this->task = $task; - $this->method = $method; - - try - { - // Check token for requests which do modify files (all except get requests) - if ($method !== 'GET' && !Session::checkToken('json')) - { - throw new \InvalidArgumentException(Text::_('JINVALID_TOKEN_NOTICE'), 403); - } - - $doTask = strtolower($method) . ucfirst($task); - - // Record the actual task being fired - $this->doTask = $doTask; - - if (!in_array($this->doTask, $this->taskMap)) - { - throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_TASK_NOT_FOUND', $task), 405); - } - - $data = $this->$doTask(); - - // Return the data - $this->sendResponse($data); - } - catch (FileNotFoundException $e) - { - $this->sendResponse($e, 404); - } - catch (FileExistsException $e) - { - $this->sendResponse($e, 409); - } - catch (InvalidPathException $e) - { - $this->sendResponse($e, 400); - } - catch (\Exception $e) - { - $errorCode = 500; - - if ($e->getCode() > 0) - { - $errorCode = $e->getCode(); - } - - $this->sendResponse($e, $errorCode); - } - } - - /** - * Files Get Method - * - * Examples: - * - * - GET a list of folders below the root: - * index.php?option=com_media&task=api.files - * /api/files - * - GET a list of files and subfolders of a given folder: - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia - * /api/files/sampledata/cassiopeia - * - GET a list of files and subfolders of a given folder for a given search term: - * use recursive=1 to search recursively in the working directory - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia&search=nasa5 - * /api/files/sampledata/cassiopeia?search=nasa5 - * To look up in same working directory set flag recursive=0 - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia&search=nasa5&recursive=0 - * /api/files/sampledata/cassiopeia?search=nasa5&recursive=0 - * - GET file information for a specific file: - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - GET a temporary URL to a given file - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg&url=1&temp=1 - * /api/files/sampledata/cassiopeia/test.jpg&url=1&temp=1 - * - GET a temporary URL to a given file - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg&url=1 - * /api/files/sampledata/cassiopeia/test.jpg&url=1 - * - * @return array The data to send with the response - * - * @since 4.0.0 - * @throws \Exception - */ - public function getFiles() - { - // Grab options - $options = []; - $options['url'] = $this->input->getBool('url', false); - $options['search'] = $this->input->getString('search', ''); - $options['recursive'] = $this->input->getBool('recursive', true); - $options['content'] = $this->input->getBool('content', false); - - return $this->getModel()->getFiles($this->getAdapter(), $this->getPath(), $options); - } - - /** - * Files delete Method - * - * Examples: - * - * - DELETE an existing folder in a specific folder: - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test - * /api/files/sampledata/cassiopeia/test - * - DELETE an existing file in a specific folder: - * index.php?option=com_media&task=api.files&path=/sampledata/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - * @return null - * - * @since 4.0.0 - * @throws \Exception - */ - public function deleteFiles() - { - if (!$this->app->getIdentity()->authorise('core.delete', 'com_media')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); - } - - $this->getModel()->delete($this->getAdapter(), $this->getPath()); - - return null; - } - - /** - * Files Post Method - * - * Examples: - * - * - POST a new file or folder into a specific folder, the file or folder information is returned: - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia - * /api/files/sampledata/cassiopeia - * - * New file body: - * { - * "name": "test.jpg", - * "content":"base64 encoded image" - * } - * New folder body: - * { - * "name": "test", - * } - * - * @return array The data to send with the response - * - * @since 4.0.0 - * @throws \Exception - */ - public function postFiles() - { - if (!$this->app->getIdentity()->authorise('core.create', 'com_media')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED'), 403); - } - - $adapter = $this->getAdapter(); - $path = $this->getPath(); - $content = $this->input->json; - $name = $content->getString('name'); - $mediaContent = base64_decode($content->get('content', '', 'raw')); - $override = $content->get('override', false); - - if ($mediaContent) - { - $this->checkContent(); - - // A file needs to be created - $name = $this->getModel()->createFile($adapter, $name, $path, $mediaContent, $override); - } - else - { - // A file needs to be created - $name = $this->getModel()->createFolder($adapter, $name, $path, $override); - } - - $options = []; - $options['url'] = $this->input->getBool('url', false); - - return $this->getModel()->getFile($adapter, $path . '/' . $name, $options); - } - - /** - * Files Put method - * - * Examples: - * - * - PUT a media file, the file or folder information is returned: - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - * Update file body: - * { - * "content":"base64 encoded image" - * } - * - * - PUT move a file, folder to another one - * path : will be taken as the source - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - * JSON body: - * { - * "newPath" : "/path/to/destination", - * "move" : "1" - * } - * - * - PUT copy a file, folder to another one - * path : will be taken as the source - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - * JSON body: - * { - * "newPath" : "/path/to/destination", - * "move" : "0" - * } - * - * @return array The data to send with the response - * - * @since 4.0.0 - * @throws \Exception - */ - public function putFiles() - { - if (!$this->app->getIdentity()->authorise('core.edit', 'com_media')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 403); - } - - $adapter = $this->getAdapter(); - $path = $this->getPath(); - - $content = $this->input->json; - $name = basename($path); - $mediaContent = base64_decode($content->get('content', '', 'raw')); - $newPath = $content->getString('newPath', null); - $move = $content->get('move', true); - - if ($mediaContent != null) - { - $this->checkContent(); - - $this->getModel()->updateFile($adapter, $name, str_replace($name, '', $path), $mediaContent); - } - - if ($newPath != null && $newPath !== $adapter . ':' . $path) - { - list($destinationAdapter, $destinationPath) = explode(':', $newPath, 2); - - if ($move) - { - $destinationPath = $this->getModel()->move($adapter, $path, $destinationPath, false); - } - else - { - $destinationPath = $this->getModel()->copy($adapter, $path, $destinationPath, false); - } - - $path = $destinationPath; - } - - return $this->getModel()->getFile($adapter, $path); - } - - /** - * Send the given data as JSON response in the following format: - * - * {"success":true,"message":"ok","messages":null,"data":[{"type":"dir","name":"banners","path":"//"}]} - * - * @param mixed $data The data to send - * @param integer $responseCode The response code - * - * @return void - * - * @since 4.0.0 - */ - private function sendResponse($data = null, int $responseCode = 200) - { - // Set the correct content type - $this->app->setHeader('Content-Type', 'application/json'); - - // Set the status code for the response - http_response_code($responseCode); - - // Send the data - echo new JsonResponse($data); - - $this->app->close(); - } - - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return BaseModel|boolean Model object on success; otherwise false on failure. - * - * @since 4.0.0 - */ - public function getModel($name = 'Api', $prefix = 'Administrator', $config = []) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Performs various checks if it is allowed to save the content. - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - private function checkContent() - { - $helper = new MediaHelper; - $contentLength = $this->input->server->getInt('CONTENT_LENGTH'); - $params = ComponentHelper::getParams('com_media'); - $paramsUploadMaxsize = $params->get('upload_maxsize', 0) * 1024 * 1024; - $uploadMaxFilesize = $helper->toBytes(ini_get('upload_max_filesize')); - $postMaxSize = $helper->toBytes(ini_get('post_max_size')); - $memoryLimit = $helper->toBytes(ini_get('memory_limit')); - - if (($paramsUploadMaxsize > 0 && $contentLength > $paramsUploadMaxsize) - || ($uploadMaxFilesize > 0 && $contentLength > $uploadMaxFilesize) - || ($postMaxSize > 0 && $contentLength > $postMaxSize) - || ($memoryLimit > -1 && $contentLength > $memoryLimit) - ) - { - throw new \Exception(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 403); - } - } - - /** - * Get the Adapter. - * - * @return string - * - * @since 4.0.0 - */ - private function getAdapter() - { - $parts = explode(':', $this->input->getString('path', ''), 2); - - if (count($parts) < 1) - { - return null; - } - - return $parts[0]; - } - - /** - * Get the Path. - * - * @return string - * - * @since 4.0.0 - */ - private function getPath() - { - $parts = explode(':', $this->input->getString('path', ''), 2); - - if (count($parts) < 2) - { - return null; - } - - return $parts[1]; - } + /** + * Execute a task by triggering a method in the derived class. + * + * @param string $task The task to perform. If no matching task is found, the '__default' task is executed, if defined. + * + * @return mixed The value returned by the called method. + * + * @since 4.0.0 + * @throws \Exception + */ + public function execute($task) + { + $method = $this->input->getMethod(); + + $this->task = $task; + $this->method = $method; + + try { + // Check token for requests which do modify files (all except get requests) + if ($method !== 'GET' && !Session::checkToken('json')) { + throw new \InvalidArgumentException(Text::_('JINVALID_TOKEN_NOTICE'), 403); + } + + $doTask = strtolower($method) . ucfirst($task); + + // Record the actual task being fired + $this->doTask = $doTask; + + if (!in_array($this->doTask, $this->taskMap)) { + throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_TASK_NOT_FOUND', $task), 405); + } + + $data = $this->$doTask(); + + // Return the data + $this->sendResponse($data); + } catch (FileNotFoundException $e) { + $this->sendResponse($e, 404); + } catch (FileExistsException $e) { + $this->sendResponse($e, 409); + } catch (InvalidPathException $e) { + $this->sendResponse($e, 400); + } catch (\Exception $e) { + $errorCode = 500; + + if ($e->getCode() > 0) { + $errorCode = $e->getCode(); + } + + $this->sendResponse($e, $errorCode); + } + } + + /** + * Files Get Method + * + * Examples: + * + * - GET a list of folders below the root: + * index.php?option=com_media&task=api.files + * /api/files + * - GET a list of files and subfolders of a given folder: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia + * /api/files/sampledata/cassiopeia + * - GET a list of files and subfolders of a given folder for a given search term: + * use recursive=1 to search recursively in the working directory + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia&search=nasa5 + * /api/files/sampledata/cassiopeia?search=nasa5 + * To look up in same working directory set flag recursive=0 + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia&search=nasa5&recursive=0 + * /api/files/sampledata/cassiopeia?search=nasa5&recursive=0 + * - GET file information for a specific file: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * - GET a temporary URL to a given file + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg&url=1&temp=1 + * /api/files/sampledata/cassiopeia/test.jpg&url=1&temp=1 + * - GET a temporary URL to a given file + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg&url=1 + * /api/files/sampledata/cassiopeia/test.jpg&url=1 + * + * @return array The data to send with the response + * + * @since 4.0.0 + * @throws \Exception + */ + public function getFiles() + { + // Grab options + $options = []; + $options['url'] = $this->input->getBool('url', false); + $options['search'] = $this->input->getString('search', ''); + $options['recursive'] = $this->input->getBool('recursive', true); + $options['content'] = $this->input->getBool('content', false); + + return $this->getModel()->getFiles($this->getAdapter(), $this->getPath(), $options); + } + + /** + * Files delete Method + * + * Examples: + * + * - DELETE an existing folder in a specific folder: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test + * /api/files/sampledata/cassiopeia/test + * - DELETE an existing file in a specific folder: + * index.php?option=com_media&task=api.files&path=/sampledata/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * + * @return null + * + * @since 4.0.0 + * @throws \Exception + */ + public function deleteFiles() + { + if (!$this->app->getIdentity()->authorise('core.delete', 'com_media')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); + } + + $this->getModel()->delete($this->getAdapter(), $this->getPath()); + + return null; + } + + /** + * Files Post Method + * + * Examples: + * + * - POST a new file or folder into a specific folder, the file or folder information is returned: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia + * /api/files/sampledata/cassiopeia + * + * New file body: + * { + * "name": "test.jpg", + * "content":"base64 encoded image" + * } + * New folder body: + * { + * "name": "test", + * } + * + * @return array The data to send with the response + * + * @since 4.0.0 + * @throws \Exception + */ + public function postFiles() + { + if (!$this->app->getIdentity()->authorise('core.create', 'com_media')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED'), 403); + } + + $adapter = $this->getAdapter(); + $path = $this->getPath(); + $content = $this->input->json; + $name = $content->getString('name'); + $mediaContent = base64_decode($content->get('content', '', 'raw')); + $override = $content->get('override', false); + + if ($mediaContent) { + $this->checkContent(); + + // A file needs to be created + $name = $this->getModel()->createFile($adapter, $name, $path, $mediaContent, $override); + } else { + // A file needs to be created + $name = $this->getModel()->createFolder($adapter, $name, $path, $override); + } + + $options = []; + $options['url'] = $this->input->getBool('url', false); + + return $this->getModel()->getFile($adapter, $path . '/' . $name, $options); + } + + /** + * Files Put method + * + * Examples: + * + * - PUT a media file, the file or folder information is returned: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * + * Update file body: + * { + * "content":"base64 encoded image" + * } + * + * - PUT move a file, folder to another one + * path : will be taken as the source + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * + * JSON body: + * { + * "newPath" : "/path/to/destination", + * "move" : "1" + * } + * + * - PUT copy a file, folder to another one + * path : will be taken as the source + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * + * JSON body: + * { + * "newPath" : "/path/to/destination", + * "move" : "0" + * } + * + * @return array The data to send with the response + * + * @since 4.0.0 + * @throws \Exception + */ + public function putFiles() + { + if (!$this->app->getIdentity()->authorise('core.edit', 'com_media')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 403); + } + + $adapter = $this->getAdapter(); + $path = $this->getPath(); + + $content = $this->input->json; + $name = basename($path); + $mediaContent = base64_decode($content->get('content', '', 'raw')); + $newPath = $content->getString('newPath', null); + $move = $content->get('move', true); + + if ($mediaContent != null) { + $this->checkContent(); + + $this->getModel()->updateFile($adapter, $name, str_replace($name, '', $path), $mediaContent); + } + + if ($newPath != null && $newPath !== $adapter . ':' . $path) { + list($destinationAdapter, $destinationPath) = explode(':', $newPath, 2); + + if ($move) { + $destinationPath = $this->getModel()->move($adapter, $path, $destinationPath, false); + } else { + $destinationPath = $this->getModel()->copy($adapter, $path, $destinationPath, false); + } + + $path = $destinationPath; + } + + return $this->getModel()->getFile($adapter, $path); + } + + /** + * Send the given data as JSON response in the following format: + * + * {"success":true,"message":"ok","messages":null,"data":[{"type":"dir","name":"banners","path":"//"}]} + * + * @param mixed $data The data to send + * @param integer $responseCode The response code + * + * @return void + * + * @since 4.0.0 + */ + private function sendResponse($data = null, int $responseCode = 200) + { + // Set the correct content type + $this->app->setHeader('Content-Type', 'application/json'); + + // Set the status code for the response + http_response_code($responseCode); + + // Send the data + echo new JsonResponse($data); + + $this->app->close(); + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return BaseModel|boolean Model object on success; otherwise false on failure. + * + * @since 4.0.0 + */ + public function getModel($name = 'Api', $prefix = 'Administrator', $config = []) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Performs various checks if it is allowed to save the content. + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + private function checkContent() + { + $helper = new MediaHelper(); + $contentLength = $this->input->server->getInt('CONTENT_LENGTH'); + $params = ComponentHelper::getParams('com_media'); + $paramsUploadMaxsize = $params->get('upload_maxsize', 0) * 1024 * 1024; + $uploadMaxFilesize = $helper->toBytes(ini_get('upload_max_filesize')); + $postMaxSize = $helper->toBytes(ini_get('post_max_size')); + $memoryLimit = $helper->toBytes(ini_get('memory_limit')); + + if ( + ($paramsUploadMaxsize > 0 && $contentLength > $paramsUploadMaxsize) + || ($uploadMaxFilesize > 0 && $contentLength > $uploadMaxFilesize) + || ($postMaxSize > 0 && $contentLength > $postMaxSize) + || ($memoryLimit > -1 && $contentLength > $memoryLimit) + ) { + throw new \Exception(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 403); + } + } + + /** + * Get the Adapter. + * + * @return string + * + * @since 4.0.0 + */ + private function getAdapter() + { + $parts = explode(':', $this->input->getString('path', ''), 2); + + if (count($parts) < 1) { + return null; + } + + return $parts[0]; + } + + /** + * Get the Path. + * + * @return string + * + * @since 4.0.0 + */ + private function getPath() + { + $parts = explode(':', $this->input->getString('path', ''), 2); + + if (count($parts) < 2) { + return null; + } + + return $parts[1]; + } } diff --git a/code/administrator/components/com_media/src/Controller/DisplayController.php b/code/administrator/components/com_media/src/Controller/DisplayController.php index 1afaa99e..55589cf4 100644 --- a/code/administrator/components/com_media/src/Controller/DisplayController.php +++ b/code/administrator/components/com_media/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->getString('plugin', null); - $plugins = PluginHelper::getPlugin('filesystem'); - - // If plugin name was not found in parameters redirect back to control panel - if (!$pluginName || !$this->containsPlugin($plugins, $pluginName)) - { - throw new \Exception('Plugin not found!'); - } - - // Check if the plugin is disabled, if so redirect to control panel - if (!PluginHelper::isEnabled('filesystem', $pluginName)) - { - throw new \Exception('Plugin ' . $pluginName . ' is disabled.'); - } - - // Only import our required plugin, not entire group - PluginHelper::importPlugin('filesystem', $pluginName); - - // Event parameters - $eventParameters = ['context' => $pluginName, 'input' => $this->input]; - $event = new OAuthCallbackEvent('onFileSystemOAuthCallback', $eventParameters); - - // Get results from event - $eventResults = (array) $this->app->triggerEvent('onFileSystemOAuthCallback', $event); - - // If event was not triggered in the selected Plugin, raise a warning and fallback to Control Panel - if (!$eventResults) - { - throw new \Exception( - 'Plugin ' . $pluginName . ' should have implemented onFileSystemOAuthCallback method' - ); - } - - $action = $eventResults['action'] ?? null; - - // If there are any messages display them - if (isset($eventResults['message'])) - { - $message = $eventResults['message']; - $messageType = ($eventResults['message_type'] ?? ''); - - $this->app->enqueueMessage($message, $messageType); - } - - /** - * Execute actions defined by the plugin - * Supported actions - * - close : Closes the current window, use this only for windows opened by javascript - * - redirect : Redirect to a URI defined in 'redirect_uri' parameter, if not fallback to control panel - * - media-manager : Redirect to Media Manager - * - control-panel : Redirect to Control Panel - */ - switch ($action) - { - /** - * Close a window opened by developer - * Use this for close New Windows opened for OAuth Process - */ - case 'close': - $this->setRedirect(Route::_('index.php?option=com_media&view=plugin&action=close', false)); - break; - - // Redirect browser to any page specified by the user - case 'redirect': - if (!isset($eventResults['redirect_uri'])) - { - throw new \Exception("Redirect URI must be set in the plugin"); - } - - $this->setRedirect($eventResults['redirect_uri']); - break; - - // Redirect browser to Control Panel - case 'control-panel': - $this->setRedirect(Route::_('index.php', false)); - break; - - // Redirect browser to Media Manager - case 'media-manager': - default: - $this->setRedirect(Route::_('index.php?option=com_media&view=media', false)); - } - } - catch (\Exception $e) - { - // Display any error - $this->app->enqueueMessage($e->getMessage(), 'error'); - $this->setRedirect(Route::_('index.php', false)); - } - - // Redirect - $this->redirect(); - } - - /** - * Check whether a plugin exists in given plugin array. - * - * @param array $plugins Array of plugin names - * @param string $pluginName Plugin name to look up - * - * @return bool - * - * @since 4.0.0 - */ - private function containsPlugin($plugins, $pluginName) - { - foreach ($plugins as $plugin) - { - if ($plugin->name == $pluginName) - { - return true; - } - } - - return false; - } + /** + * Handles an OAuth Callback request for a specified plugin. + * + * URLs containing [sitename]/administrator/index.php?option=com_media&task=plugin.oauthcallback + * &plugin=[plugin_name] + * + * will be handled by this endpoint. + * It will select the plugin specified by plugin_name and pass all the data received from the provider + * + * @return void + * + * @since 4.0.0 + */ + public function oauthcallback() + { + try { + // Load plugin names + $pluginName = $this->input->getString('plugin', null); + $plugins = PluginHelper::getPlugin('filesystem'); + + // If plugin name was not found in parameters redirect back to control panel + if (!$pluginName || !$this->containsPlugin($plugins, $pluginName)) { + throw new \Exception('Plugin not found!'); + } + + // Check if the plugin is disabled, if so redirect to control panel + if (!PluginHelper::isEnabled('filesystem', $pluginName)) { + throw new \Exception('Plugin ' . $pluginName . ' is disabled.'); + } + + // Only import our required plugin, not entire group + PluginHelper::importPlugin('filesystem', $pluginName); + + // Event parameters + $eventParameters = ['context' => $pluginName, 'input' => $this->input]; + $event = new OAuthCallbackEvent('onFileSystemOAuthCallback', $eventParameters); + + // Get results from event + $eventResults = (array) $this->app->triggerEvent('onFileSystemOAuthCallback', $event); + + // If event was not triggered in the selected Plugin, raise a warning and fallback to Control Panel + if (!$eventResults) { + throw new \Exception( + 'Plugin ' . $pluginName . ' should have implemented onFileSystemOAuthCallback method' + ); + } + + $action = $eventResults['action'] ?? null; + + // If there are any messages display them + if (isset($eventResults['message'])) { + $message = $eventResults['message']; + $messageType = ($eventResults['message_type'] ?? ''); + + $this->app->enqueueMessage($message, $messageType); + } + + /** + * Execute actions defined by the plugin + * Supported actions + * - close : Closes the current window, use this only for windows opened by javascript + * - redirect : Redirect to a URI defined in 'redirect_uri' parameter, if not fallback to control panel + * - media-manager : Redirect to Media Manager + * - control-panel : Redirect to Control Panel + */ + switch ($action) { + /** + * Close a window opened by developer + * Use this for close New Windows opened for OAuth Process + */ + case 'close': + $this->setRedirect(Route::_('index.php?option=com_media&view=plugin&action=close', false)); + break; + + // Redirect browser to any page specified by the user + case 'redirect': + if (!isset($eventResults['redirect_uri'])) { + throw new \Exception("Redirect URI must be set in the plugin"); + } + + $this->setRedirect($eventResults['redirect_uri']); + break; + + // Redirect browser to Control Panel + case 'control-panel': + $this->setRedirect(Route::_('index.php', false)); + break; + + // Redirect browser to Media Manager + case 'media-manager': + default: + $this->setRedirect(Route::_('index.php?option=com_media&view=media', false)); + } + } catch (\Exception $e) { + // Display any error + $this->app->enqueueMessage($e->getMessage(), 'error'); + $this->setRedirect(Route::_('index.php', false)); + } + + // Redirect + $this->redirect(); + } + + /** + * Check whether a plugin exists in given plugin array. + * + * @param array $plugins Array of plugin names + * @param string $pluginName Plugin name to look up + * + * @return bool + * + * @since 4.0.0 + */ + private function containsPlugin($plugins, $pluginName) + { + foreach ($plugins as $plugin) { + if ($plugin->name == $pluginName) { + return true; + } + } + + return false; + } } diff --git a/code/administrator/components/com_media/src/Dispatcher/Dispatcher.php b/code/administrator/components/com_media/src/Dispatcher/Dispatcher.php index f405c09d..58b83adc 100644 --- a/code/administrator/components/com_media/src/Dispatcher/Dispatcher.php +++ b/code/administrator/components/com_media/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getIdentity(); - $asset = $this->input->get('asset'); - $author = $this->input->get('author'); + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + */ + protected function checkAccess() + { + $user = $this->app->getIdentity(); + $asset = $this->input->get('asset'); + $author = $this->input->get('author'); - // Access check - if (!$user->authorise('core.manage', 'com_media') - && (!$asset || (!$user->authorise('core.edit', $asset) - && !$user->authorise('core.create', $asset) - && count($user->getAuthorisedCategories($asset, 'core.create')) == 0) - && !($user->id == $author && $user->authorise('core.edit.own', $asset)))) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + // Access check + if ( + !$user->authorise('core.manage', 'com_media') + && (!$asset || (!$user->authorise('core.edit', $asset) + && !$user->authorise('core.create', $asset) + && count($user->getAuthorisedCategories($asset, 'core.create')) == 0) + && !($user->id == $author && $user->authorise('core.edit.own', $asset))) + ) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/code/administrator/components/com_media/src/Event/AbstractMediaItemValidationEvent.php b/code/administrator/components/com_media/src/Event/AbstractMediaItemValidationEvent.php index 83f29218..a440997f 100644 --- a/code/administrator/components/com_media/src/Event/AbstractMediaItemValidationEvent.php +++ b/code/administrator/components/com_media/src/Event/AbstractMediaItemValidationEvent.php @@ -1,4 +1,5 @@ type) || ($item->type !== 'dir' && $item->type !== 'file')) - { - throw new \BadMethodCallException("Property 'type' of argument 'item' of event {$this->name} has a wrong item. Valid: 'dir' or 'file'"); - } - - // Non empty string - if (empty($item->name) || !is_string($item->name)) - { - throw new \BadMethodCallException("Property 'name' of argument 'item' of event {$this->name} has a wrong item. Valid: non empty string"); - } - - // Non empty string - if (empty($item->path) || !is_string($item->path)) - { - throw new \BadMethodCallException("Property 'path' of argument 'item' of event {$this->name} has a wrong item. Valid: non empty string"); - } - - // A string - if ($item->type === 'file' && (!isset($item->extension) || !is_string($item->extension))) - { - throw new \BadMethodCallException("Property 'extension' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - - // An empty string or an integer - if (!isset($item->size) || - (!is_integer($item->size) && !is_string($item->size)) || - (is_string($item->size) && $item->size !== '') - ) - { - throw new \BadMethodCallException("Property 'size' of argument 'item' of event {$this->name} has a wrong item. Valid: empty string or integer"); - } - - // A string - if (!isset($item->mime_type) || !is_string($item->mime_type)) - { - throw new \BadMethodCallException("Property 'mime_type' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - - // An integer - if (!isset($item->width) || !is_integer($item->width)) - { - throw new \BadMethodCallException("Property 'width' of argument 'item' of event {$this->name} has a wrong item. Valid: integer"); - } - - // An integer - if (!isset($item->height) || !is_integer($item->height)) - { - throw new \BadMethodCallException("Property 'height' of argument 'item' of event {$this->name} has a wrong item. Valid: integer"); - } - - // A string - if (!isset($item->create_date) || !is_string($item->create_date)) - { - throw new \BadMethodCallException("Property 'create_date' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - - // A string - if (!isset($item->create_date_formatted) || !is_string($item->create_date_formatted)) - { - throw new \BadMethodCallException("Property 'create_date_formatted' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - - // A string - if (!isset($item->modified_date) || !is_string($item->modified_date)) - { - throw new \BadMethodCallException("Property 'modified_date' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - - // A string - if (!isset($item->modified_date_formatted) || !is_string($item->modified_date_formatted)) - { - throw new \BadMethodCallException("Property 'modified_date_formatted' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - } + /** + * Validate $item to have all attributes with a valid type. + * + * Properties validated: + * - type: The type can be file or dir + * - name: The name of the item + * - path: The relative path to the root + * - extension: The file extension + * - size: The size of the file + * - create_date: The date created + * - modified_date: The date modified + * - mime_type: The mime type + * - width: The width, when available + * - height: The height, when available + * + * Properties generated: + * - created_date_formatted: DATE_FORMAT_LC5 formatted string based on create_date + * - modified_date_formatted: DATE_FORMAT_LC5 formatted string based on modified_date + * + * @param \stdClass $item The item to set + * + * @return void + * + * @since 4.1.0 + * + * @throws \BadMethodCallException + */ + protected function validate(\stdClass $item): void + { + // Only "dir" or "file" is allowed + if (!isset($item->type) || ($item->type !== 'dir' && $item->type !== 'file')) { + throw new \BadMethodCallException("Property 'type' of argument 'item' of event {$this->name} has a wrong item. Valid: 'dir' or 'file'"); + } + + // Non empty string + if (empty($item->name) || !is_string($item->name)) { + throw new \BadMethodCallException("Property 'name' of argument 'item' of event {$this->name} has a wrong item. Valid: non empty string"); + } + + // Non empty string + if (empty($item->path) || !is_string($item->path)) { + throw new \BadMethodCallException("Property 'path' of argument 'item' of event {$this->name} has a wrong item. Valid: non empty string"); + } + + // A string + if ($item->type === 'file' && (!isset($item->extension) || !is_string($item->extension))) { + throw new \BadMethodCallException("Property 'extension' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + + // An empty string or an integer + if ( + !isset($item->size) || + (!is_integer($item->size) && !is_string($item->size)) || + (is_string($item->size) && $item->size !== '') + ) { + throw new \BadMethodCallException("Property 'size' of argument 'item' of event {$this->name} has a wrong item. Valid: empty string or integer"); + } + + // A string + if (!isset($item->mime_type) || !is_string($item->mime_type)) { + throw new \BadMethodCallException("Property 'mime_type' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + + // An integer + if (!isset($item->width) || !is_integer($item->width)) { + throw new \BadMethodCallException("Property 'width' of argument 'item' of event {$this->name} has a wrong item. Valid: integer"); + } + + // An integer + if (!isset($item->height) || !is_integer($item->height)) { + throw new \BadMethodCallException("Property 'height' of argument 'item' of event {$this->name} has a wrong item. Valid: integer"); + } + + // A string + if (!isset($item->create_date) || !is_string($item->create_date)) { + throw new \BadMethodCallException("Property 'create_date' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + + // A string + if (!isset($item->create_date_formatted) || !is_string($item->create_date_formatted)) { + throw new \BadMethodCallException("Property 'create_date_formatted' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + + // A string + if (!isset($item->modified_date) || !is_string($item->modified_date)) { + throw new \BadMethodCallException("Property 'modified_date' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + + // A string + if (!isset($item->modified_date_formatted) || !is_string($item->modified_date_formatted)) { + throw new \BadMethodCallException("Property 'modified_date_formatted' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + } } diff --git a/code/administrator/components/com_media/src/Event/FetchMediaItemEvent.php b/code/administrator/components/com_media/src/Event/FetchMediaItemEvent.php index 82479be9..f2c589ce 100644 --- a/code/administrator/components/com_media/src/Event/FetchMediaItemEvent.php +++ b/code/administrator/components/com_media/src/Event/FetchMediaItemEvent.php @@ -1,4 +1,5 @@ validate($item); + $this->validate($item); - return $item; - } + return $item; + } } diff --git a/code/administrator/components/com_media/src/Event/FetchMediaItemUrlEvent.php b/code/administrator/components/com_media/src/Event/FetchMediaItemUrlEvent.php index c137e625..fbdd693a 100644 --- a/code/administrator/components/com_media/src/Event/FetchMediaItemUrlEvent.php +++ b/code/administrator/components/com_media/src/Event/FetchMediaItemUrlEvent.php @@ -1,4 +1,5 @@ arguments[$arguments['adapter']] = $arguments['adapter']; - unset($arguments['adapter']); + $this->arguments[$arguments['adapter']] = $arguments['adapter']; + unset($arguments['adapter']); - // Check for required arguments - if (!\array_key_exists('path', $arguments) || !is_string($arguments['path'])) - { - throw new \BadMethodCallException("Argument 'path' of event $name is not of the expected type"); - } + // Check for required arguments + if (!\array_key_exists('path', $arguments) || !is_string($arguments['path'])) { + throw new \BadMethodCallException("Argument 'path' of event $name is not of the expected type"); + } - $this->arguments[$arguments['path']] = $arguments['path']; - unset($arguments['path']); + $this->arguments[$arguments['path']] = $arguments['path']; + unset($arguments['path']); - // Check for required arguments - if (!\array_key_exists('url', $arguments) || !is_string($arguments['url'])) - { - throw new \BadMethodCallException("Argument 'url' of event $name is not of the expected type"); - } + // Check for required arguments + if (!\array_key_exists('url', $arguments) || !is_string($arguments['url'])) { + throw new \BadMethodCallException("Argument 'url' of event $name is not of the expected type"); + } - parent::__construct($name, $arguments); - } + parent::__construct($name, $arguments); + } - /** - * Validate $value to be a string - * - * @param string $value The value to set - * - * @return string - * - * @since 4.1.0 - */ - protected function setUrl(string $value): string - { - return $value; - } + /** + * Validate $value to be a string + * + * @param string $value The value to set + * + * @return string + * + * @since 4.1.0 + */ + protected function setUrl(string $value): string + { + return $value; + } - /** - * Forbid setting $path - * - * @param string $value The value to set - * - * @since 4.1.0 - * - * @throws \BadMethodCallException - */ - protected function setPath(string $value): string - { - throw new \BadMethodCallException('Cannot set the argument "path" of the immutable event ' . $this->name . '.'); - } + /** + * Forbid setting $path + * + * @param string $value The value to set + * + * @since 4.1.0 + * + * @throws \BadMethodCallException + */ + protected function setPath(string $value): string + { + throw new \BadMethodCallException('Cannot set the argument "path" of the immutable event ' . $this->name . '.'); + } - /** - * Forbid setting $path - * - * @param string $value The value to set - * - * @since 4.1.0 - * - * @throws \BadMethodCallException - */ - protected function setAdapter(string $value): string - { - throw new \BadMethodCallException('Cannot set the argument "adapter" of the immutable event ' . $this->name . '.'); - } + /** + * Forbid setting $path + * + * @param string $value The value to set + * + * @since 4.1.0 + * + * @throws \BadMethodCallException + */ + protected function setAdapter(string $value): string + { + throw new \BadMethodCallException('Cannot set the argument "adapter" of the immutable event ' . $this->name . '.'); + } } diff --git a/code/administrator/components/com_media/src/Event/FetchMediaItemsEvent.php b/code/administrator/components/com_media/src/Event/FetchMediaItemsEvent.php index e541e8ac..5e759d0c 100644 --- a/code/administrator/components/com_media/src/Event/FetchMediaItemsEvent.php +++ b/code/administrator/components/com_media/src/Event/FetchMediaItemsEvent.php @@ -1,4 +1,5 @@ validate($clone); + $this->validate($clone); - $result[] = $clone; - } + $result[] = $clone; + } - return $result; - } + return $result; + } - /** - * Returns the items. - * - * @param array $items The value to set - * - * @return array - * - * @since 4.1.0 - */ - protected function getItems(array $items): array - { - $result = []; + /** + * Returns the items. + * + * @param array $items The value to set + * + * @return array + * + * @since 4.1.0 + */ + protected function getItems(array $items): array + { + $result = []; - foreach($items as $item) - { - $result[] = clone $item; - } + foreach ($items as $item) { + $result[] = clone $item; + } - return $result; - } + return $result; + } } diff --git a/code/administrator/components/com_media/src/Event/MediaProviderEvent.php b/code/administrator/components/com_media/src/Event/MediaProviderEvent.php index 8aeb7d60..ac8c543d 100644 --- a/code/administrator/components/com_media/src/Event/MediaProviderEvent.php +++ b/code/administrator/components/com_media/src/Event/MediaProviderEvent.php @@ -1,4 +1,5 @@ providerManager; - } + /** + * Return the ProviderManager + * + * @return ProviderManager + * + * @since 4.0.0 + */ + public function getProviderManager(): ProviderManager + { + return $this->providerManager; + } - /** - * Set the ProviderManager - * - * @param ProviderManager $providerManager The Provider Manager to be set - * - * @return void - * - * @since 4.0.0 - */ - public function setProviderManager(ProviderManager $providerManager) - { - $this->providerManager = $providerManager; - } + /** + * Set the ProviderManager + * + * @param ProviderManager $providerManager The Provider Manager to be set + * + * @return void + * + * @since 4.0.0 + */ + public function setProviderManager(ProviderManager $providerManager) + { + $this->providerManager = $providerManager; + } } diff --git a/code/administrator/components/com_media/src/Event/OAuthCallbackEvent.php b/code/administrator/components/com_media/src/Event/OAuthCallbackEvent.php index d6d612c2..525b5271 100644 --- a/code/administrator/components/com_media/src/Event/OAuthCallbackEvent.php +++ b/code/administrator/components/com_media/src/Event/OAuthCallbackEvent.php @@ -1,4 +1,5 @@ context; - } + /** + * Get the event context. + * + * @return string + * + * @since 4.0.0 + */ + public function getContext() + { + return $this->context; + } - /** - * Set the event context. - * - * @param string $context Event context - * - * @return void - * - * @since 4.0.0 - */ - public function setContext($context) - { - $this->context = $context; - } + /** + * Set the event context. + * + * @param string $context Event context + * + * @return void + * + * @since 4.0.0 + */ + public function setContext($context) + { + $this->context = $context; + } - /** - * Get the event input. - * - * @return Input - * - * @since 4.0.0 - */ - public function getInput() - { - return $this->input; - } + /** + * Get the event input. + * + * @return Input + * + * @since 4.0.0 + */ + public function getInput() + { + return $this->input; + } - /** - * Set the event input. - * - * @param Input $input Event input - * - * @return void - * - * @since 4.0.0 - */ - public function setInput($input) - { - $this->input = $input; - } + /** + * Set the event input. + * + * @param Input $input Event input + * + * @return void + * + * @since 4.0.0 + */ + public function setInput($input) + { + $this->input = $input; + } } diff --git a/code/administrator/components/com_media/src/Exception/FileExistsException.php b/code/administrator/components/com_media/src/Exception/FileExistsException.php index 1e629684..c2c0eaa3 100644 --- a/code/administrator/components/com_media/src/Exception/FileExistsException.php +++ b/code/administrator/components/com_media/src/Exception/FileExistsException.php @@ -1,4 +1,5 @@ getAdapter($adapter)->getFile($path); - - // Check if it is a media file - if ($file->type == 'file' && !$this->isMediaFile($file->path)) - { - throw new InvalidPathException; - } - - if (isset($options['url']) && $options['url'] && $file->type == 'file') - { - $file->url = $this->getUrl($adapter, $file->path); - } - - if (isset($options['content']) && $options['content'] && $file->type == 'file') - { - $resource = $this->getAdapter($adapter)->getResource($file->path); - - if ($resource) - { - $file->content = base64_encode(stream_get_contents($resource)); - } - } - - $file->path = $adapter . ":" . $file->path; - $file->adapter = $adapter; - - $event = new FetchMediaItemEvent('onFetchMediaItem', ['item' => $file]); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - - return $event->getArgument('item'); - } - - /** - * Returns the folders and files for the given path. More information - * can be found in AdapterInterface::getFiles(). - * - * @param string $adapter The adapter - * @param string $path The folder - * @param array $options The options - * - * @return \stdClass[] - * - * @since 4.0.0 - * @throws \Exception - * @see AdapterInterface::getFile() - */ - public function getFiles($adapter, $path = '/', $options = []) - { - // Check whether user searching - if ($options['search'] != null) - { - // Do search - $files = $this->search($adapter, $options['search'], $path, $options['recursive']); - } - else - { - // Grab files for the path - $files = $this->getAdapter($adapter)->getFiles($path); - } - - // Add adapter prefix to all the files to be returned - foreach ($files as $key => $file) - { - // Check if the file is valid - if ($file->type == 'file' && !$this->isMediaFile($file->path)) - { - // Remove the file from the data - unset($files[$key]); - continue; - } - - // Check if we need more information - if (isset($options['url']) && $options['url'] && $file->type == 'file') - { - $file->url = $this->getUrl($adapter, $file->path); - } - - if (isset($options['content']) && $options['content'] && $file->type == 'file') - { - $resource = $this->getAdapter($adapter)->getResource($file->path); - - if ($resource) - { - $file->content = base64_encode(stream_get_contents($resource)); - } - } - - $file->path = $adapter . ":" . $file->path; - $file->adapter = $adapter; - } - - // Make proper indexes - $files = array_values($files); - - $event = new FetchMediaItemsEvent('onFetchMediaItems', ['items' => $files]); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - - return $event->getArgument('items'); - } - - /** - * Creates a folder with the given name in the given path. More information - * can be found in AdapterInterface::createFolder(). - * - * @param string $adapter The adapter - * @param string $name The name - * @param string $path The folder - * @param boolean $override Should the folder being overridden when it exists - * - * @return string - * - * @since 4.0.0 - * @throws \Exception - * @see AdapterInterface::createFolder() - */ - public function createFolder($adapter, $name, $path, $override) - { - try - { - $file = $this->getFile($adapter, $path . '/' . $name); - } - catch (FileNotFoundException $e) - { - // Do nothing - } - - // Check if the file exists - if (isset($file) && !$override) - { - throw new FileExistsException; - } - - $app = Factory::getApplication(); - $object = new CMSObject; - $object->adapter = $adapter; - $object->name = $name; - $object->path = $path; - - PluginHelper::importPlugin('content'); - - $result = $app->triggerEvent('onContentBeforeSave', ['com_media.folder', $object, true, $object]); - - if (in_array(false, $result, true)) - { - throw new \Exception($object->getError()); - } - - $object->name = $this->getAdapter($object->adapter)->createFolder($object->name, $object->path); - - $app->triggerEvent('onContentAfterSave', ['com_media.folder', $object, true, $object]); - - return $object->name; - } - - /** - * Creates a file with the given name in the given path with the data. More information - * can be found in AdapterInterface::createFile(). - * - * @param string $adapter The adapter - * @param string $name The name - * @param string $path The folder - * @param string $data The data - * @param boolean $override Should the file being overridden when it exists - * - * @return string - * - * @since 4.0.0 - * @throws \Exception - * @see AdapterInterface::createFile() - */ - public function createFile($adapter, $name, $path, $data, $override) - { - try - { - $file = $this->getFile($adapter, $path . '/' . $name); - } - catch (FileNotFoundException $e) - { - // Do nothing - } - - // Check if the file exists - if (isset($file) && !$override) - { - throw new FileExistsException; - } - - // Check if it is a media file - if (!$this->isMediaFile($path . '/' . $name)) - { - throw new InvalidPathException; - } - - $app = Factory::getApplication(); - $object = new CMSObject; - $object->adapter = $adapter; - $object->name = $name; - $object->path = $path; - $object->data = $data; - $object->extension = strtolower(File::getExt($name)); - - PluginHelper::importPlugin('content'); - - // Also include the filesystem plugins, perhaps they support batch processing too - PluginHelper::importPlugin('media-action'); - - $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, true, $object]); - - if (in_array(false, $result, true)) - { - throw new \Exception($object->getError()); - } - - $object->name = $this->getAdapter($object->adapter)->createFile($object->name, $object->path, $object->data); - - $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, true, $object]); - - return $object->name; - } - - /** - * Updates the file with the given name in the given path with the data. More information - * can be found in AdapterInterface::updateFile(). - * - * @param string $adapter The adapter - * @param string $name The name - * @param string $path The folder - * @param string $data The data - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - * @see AdapterInterface::updateFile() - */ - public function updateFile($adapter, $name, $path, $data) - { - // Check if it is a media file - if (!$this->isMediaFile($path . '/' . $name)) - { - throw new InvalidPathException; - } - - $app = Factory::getApplication(); - $object = new CMSObject; - $object->adapter = $adapter; - $object->name = $name; - $object->path = $path; - $object->data = $data; - $object->extension = strtolower(File::getExt($name)); - - PluginHelper::importPlugin('content'); - - // Also include the filesystem plugins, perhaps they support batch processing too - PluginHelper::importPlugin('media-action'); - - $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, false, $object]); - - if (in_array(false, $result, true)) - { - throw new \Exception($object->getError()); - } - - $this->getAdapter($object->adapter)->updateFile($object->name, $object->path, $object->data); - - $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, false, $object]); - } - - /** - * Deletes the folder or file of the given path. More information - * can be found in AdapterInterface::delete(). - * - * @param string $adapter The adapter - * @param string $path The path to the file or folder - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - * @see AdapterInterface::delete() - */ - public function delete($adapter, $path) - { - $file = $this->getFile($adapter, $path); - - // Check if it is a media file - if ($file->type == 'file' && !$this->isMediaFile($file->path)) - { - throw new InvalidPathException; - } - - $type = $file->type === 'file' ? 'file' : 'folder'; - $app = Factory::getApplication(); - $object = new CMSObject; - $object->adapter = $adapter; - $object->path = $path; - - PluginHelper::importPlugin('content'); - - // Also include the filesystem plugins, perhaps they support batch processing too - PluginHelper::importPlugin('media-action'); - - $result = $app->triggerEvent('onContentBeforeDelete', ['com_media.' . $type, $object]); - - if (in_array(false, $result, true)) - { - throw new \Exception($object->getError()); - } - - $this->getAdapter($object->adapter)->delete($object->path); - - $app->triggerEvent('onContentAfterDelete', ['com_media.' . $type, $object]); - } - - /** - * Copies file or folder from source path to destination path - * If forced, existing files/folders would be overwritten - * - * @param string $adapter The adapter - * @param string $sourcePath Source path of the file or folder (relative) - * @param string $destinationPath Destination path(relative) - * @param bool $force Force to overwrite - * - * @return string - * - * @since 4.0.0 - * @throws \Exception - */ - public function copy($adapter, $sourcePath, $destinationPath, $force = false) - { - return $this->getAdapter($adapter)->copy($sourcePath, $destinationPath, $force); - } - - /** - * Moves file or folder from source path to destination path - * If forced, existing files/folders would be overwritten - * - * @param string $adapter The adapter - * @param string $sourcePath Source path of the file or folder (relative) - * @param string $destinationPath Destination path(relative) - * @param bool $force Force to overwrite - * - * @return string - * - * @since 4.0.0 - * @throws \Exception - */ - public function move($adapter, $sourcePath, $destinationPath, $force = false) - { - return $this->getAdapter($adapter)->move($sourcePath, $destinationPath, $force); - } - - /** - * Returns a url for serve media files from adapter. - * Url must provide a valid image type to be displayed on Joomla! site. - * - * @param string $adapter The adapter - * @param string $path The relative path for the file - * - * @return string Permalink to the relative file - * - * @since 4.0.0 - * @throws FileNotFoundException - */ - public function getUrl($adapter, $path) - { - // Check if it is a media file - if (!$this->isMediaFile($path)) - { - throw new InvalidPathException; - } - - $url = $this->getAdapter($adapter)->getUrl($path); - - $event = new FetchMediaItemUrlEvent('onFetchMediaFileUrl', ['adapter' => $adapter, 'path' => $path, 'url' => $url]); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - - return $event->getArgument('url'); - } - - /** - * Search for a pattern in a given path - * - * @param string $adapter The adapter to work on - * @param string $needle The search therm - * @param string $path The base path for the search - * @param bool $recursive Do a recursive search - * - * @return \stdClass[] - * - * @since 4.0.0 - * @throws \Exception - */ - public function search($adapter, $needle, $path = '/', $recursive = true) - { - return $this->getAdapter($adapter)->search($path, $needle, $recursive); - } - - /** - * Checks if the given path is an allowed media file. - * - * @param string $path The path to file - * - * @return boolean - * - * @since 4.0.0 - */ - private function isMediaFile($path) - { - // Check if there is an extension available - if (!strrpos($path, '.')) - { - return false; - } - - // Initialize the allowed extensions - if ($this->allowedExtensions === null) - { - // Get options from the input or fallback to images only - $mediaTypes = explode(',', Factory::getApplication()->input->getString('mediatypes', '0')); - $types = []; - $extensions = []; - - // Default to showing all supported formats - if (count($mediaTypes) === 0) { - $mediaTypes = ['0', '1', '2', '3']; - } - - array_map( - function ($mediaType) use (&$types) { - switch ($mediaType) { - case '0': - $types[] = 'images'; - break; - case '1': - $types[] = 'audios'; - break; - case '2': - $types[] = 'videos'; - break; - case '3': - $types[] = 'documents'; - break; - default: - break; - } - }, - $mediaTypes - ); - - $images = array_map( - 'trim', - explode( - ',', - ComponentHelper::getParams('com_media')->get( - 'image_extensions', - 'bmp,gif,jpg,jpeg,png,webp' - ) - ) - ); - $audios = array_map( - 'trim', - explode( - ',', - ComponentHelper::getParams('com_media')->get( - 'audio_extensions', - 'mp3,m4a,mp4a,ogg' - ) - ) - ); - $videos = array_map( - 'trim', - explode( - ',', - ComponentHelper::getParams('com_media')->get( - 'video_extensions', - 'mp4,mp4v,mpeg,mov,webm' - ) - ) - ); - $documents = array_map( - 'trim', - explode( - ',', - ComponentHelper::getParams('com_media')->get( - 'doc_extensions', - 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv' - ) - ) - ); - - foreach ($types as $type) { - if (in_array($type, ['images', 'audios', 'videos', 'documents'])) { - $extensions = array_merge($extensions, ${$type}); - } - } - - // Make them an array - $this->allowedExtensions = $extensions; - } - - // Extract the extension - $extension = strtolower(substr($path, strrpos($path, '.') + 1)); - - // Check if the extension exists in the allowed extensions - return in_array($extension, $this->allowedExtensions); - } + use ProviderManagerHelperTrait; + + /** + * The available extensions. + * + * @var string[] + * @since 4.0.0 + */ + private $allowedExtensions = null; + + /** + * Returns the requested file or folder information. More information + * can be found in AdapterInterface::getFile(). + * + * @param string $adapter The adapter + * @param string $path The path to the file or folder + * @param array $options The options + * + * @return \stdClass + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::getFile() + */ + public function getFile($adapter, $path = '/', $options = []) + { + // Add adapter prefix to the file returned + $file = $this->getAdapter($adapter)->getFile($path); + + // Check if it is a media file + if ($file->type == 'file' && !$this->isMediaFile($file->path)) { + throw new InvalidPathException(); + } + + if (isset($options['url']) && $options['url'] && $file->type == 'file') { + $file->url = $this->getUrl($adapter, $file->path); + } + + if (isset($options['content']) && $options['content'] && $file->type == 'file') { + $resource = $this->getAdapter($adapter)->getResource($file->path); + + if ($resource) { + $file->content = base64_encode(stream_get_contents($resource)); + } + } + + $file->path = $adapter . ":" . $file->path; + $file->adapter = $adapter; + + $event = new FetchMediaItemEvent('onFetchMediaItem', ['item' => $file]); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + return $event->getArgument('item'); + } + + /** + * Returns the folders and files for the given path. More information + * can be found in AdapterInterface::getFiles(). + * + * @param string $adapter The adapter + * @param string $path The folder + * @param array $options The options + * + * @return \stdClass[] + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::getFile() + */ + public function getFiles($adapter, $path = '/', $options = []) + { + // Check whether user searching + if ($options['search'] != null) { + // Do search + $files = $this->search($adapter, $options['search'], $path, $options['recursive']); + } else { + // Grab files for the path + $files = $this->getAdapter($adapter)->getFiles($path); + } + + // Add adapter prefix to all the files to be returned + foreach ($files as $key => $file) { + // Check if the file is valid + if ($file->type == 'file' && !$this->isMediaFile($file->path)) { + // Remove the file from the data + unset($files[$key]); + continue; + } + + // Check if we need more information + if (isset($options['url']) && $options['url'] && $file->type == 'file') { + $file->url = $this->getUrl($adapter, $file->path); + } + + if (isset($options['content']) && $options['content'] && $file->type == 'file') { + $resource = $this->getAdapter($adapter)->getResource($file->path); + + if ($resource) { + $file->content = base64_encode(stream_get_contents($resource)); + } + } + + $file->path = $adapter . ":" . $file->path; + $file->adapter = $adapter; + } + + // Make proper indexes + $files = array_values($files); + + $event = new FetchMediaItemsEvent('onFetchMediaItems', ['items' => $files]); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + return $event->getArgument('items'); + } + + /** + * Creates a folder with the given name in the given path. More information + * can be found in AdapterInterface::createFolder(). + * + * @param string $adapter The adapter + * @param string $name The name + * @param string $path The folder + * @param boolean $override Should the folder being overridden when it exists + * + * @return string + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::createFolder() + */ + public function createFolder($adapter, $name, $path, $override) + { + try { + $file = $this->getFile($adapter, $path . '/' . $name); + } catch (FileNotFoundException $e) { + // Do nothing + } + + // Check if the file exists + if (isset($file) && !$override) { + throw new FileExistsException(); + } + + $app = Factory::getApplication(); + $object = new CMSObject(); + $object->adapter = $adapter; + $object->name = $name; + $object->path = $path; + + PluginHelper::importPlugin('content'); + + $result = $app->triggerEvent('onContentBeforeSave', ['com_media.folder', $object, true, $object]); + + if (in_array(false, $result, true)) { + throw new \Exception($object->getError()); + } + + $object->name = $this->getAdapter($object->adapter)->createFolder($object->name, $object->path); + + $app->triggerEvent('onContentAfterSave', ['com_media.folder', $object, true, $object]); + + return $object->name; + } + + /** + * Creates a file with the given name in the given path with the data. More information + * can be found in AdapterInterface::createFile(). + * + * @param string $adapter The adapter + * @param string $name The name + * @param string $path The folder + * @param string $data The data + * @param boolean $override Should the file being overridden when it exists + * + * @return string + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::createFile() + */ + public function createFile($adapter, $name, $path, $data, $override) + { + try { + $file = $this->getFile($adapter, $path . '/' . $name); + } catch (FileNotFoundException $e) { + // Do nothing + } + + // Check if the file exists + if (isset($file) && !$override) { + throw new FileExistsException(); + } + + // Check if it is a media file + if (!$this->isMediaFile($path . '/' . $name)) { + throw new InvalidPathException(); + } + + $app = Factory::getApplication(); + $object = new CMSObject(); + $object->adapter = $adapter; + $object->name = $name; + $object->path = $path; + $object->data = $data; + $object->extension = strtolower(File::getExt($name)); + + PluginHelper::importPlugin('content'); + + // Also include the filesystem plugins, perhaps they support batch processing too + PluginHelper::importPlugin('media-action'); + + $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, true, $object]); + + if (in_array(false, $result, true)) { + throw new \Exception($object->getError()); + } + + $object->name = $this->getAdapter($object->adapter)->createFile($object->name, $object->path, $object->data); + + $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, true, $object]); + + return $object->name; + } + + /** + * Updates the file with the given name in the given path with the data. More information + * can be found in AdapterInterface::updateFile(). + * + * @param string $adapter The adapter + * @param string $name The name + * @param string $path The folder + * @param string $data The data + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::updateFile() + */ + public function updateFile($adapter, $name, $path, $data) + { + // Check if it is a media file + if (!$this->isMediaFile($path . '/' . $name)) { + throw new InvalidPathException(); + } + + $app = Factory::getApplication(); + $object = new CMSObject(); + $object->adapter = $adapter; + $object->name = $name; + $object->path = $path; + $object->data = $data; + $object->extension = strtolower(File::getExt($name)); + + PluginHelper::importPlugin('content'); + + // Also include the filesystem plugins, perhaps they support batch processing too + PluginHelper::importPlugin('media-action'); + + $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, false, $object]); + + if (in_array(false, $result, true)) { + throw new \Exception($object->getError()); + } + + $this->getAdapter($object->adapter)->updateFile($object->name, $object->path, $object->data); + + $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, false, $object]); + } + + /** + * Deletes the folder or file of the given path. More information + * can be found in AdapterInterface::delete(). + * + * @param string $adapter The adapter + * @param string $path The path to the file or folder + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::delete() + */ + public function delete($adapter, $path) + { + $file = $this->getFile($adapter, $path); + + // Check if it is a media file + if ($file->type == 'file' && !$this->isMediaFile($file->path)) { + throw new InvalidPathException(); + } + + $type = $file->type === 'file' ? 'file' : 'folder'; + $app = Factory::getApplication(); + $object = new CMSObject(); + $object->adapter = $adapter; + $object->path = $path; + + PluginHelper::importPlugin('content'); + + // Also include the filesystem plugins, perhaps they support batch processing too + PluginHelper::importPlugin('media-action'); + + $result = $app->triggerEvent('onContentBeforeDelete', ['com_media.' . $type, $object]); + + if (in_array(false, $result, true)) { + throw new \Exception($object->getError()); + } + + $this->getAdapter($object->adapter)->delete($object->path); + + $app->triggerEvent('onContentAfterDelete', ['com_media.' . $type, $object]); + } + + /** + * Copies file or folder from source path to destination path + * If forced, existing files/folders would be overwritten + * + * @param string $adapter The adapter + * @param string $sourcePath Source path of the file or folder (relative) + * @param string $destinationPath Destination path(relative) + * @param bool $force Force to overwrite + * + * @return string + * + * @since 4.0.0 + * @throws \Exception + */ + public function copy($adapter, $sourcePath, $destinationPath, $force = false) + { + return $this->getAdapter($adapter)->copy($sourcePath, $destinationPath, $force); + } + + /** + * Moves file or folder from source path to destination path + * If forced, existing files/folders would be overwritten + * + * @param string $adapter The adapter + * @param string $sourcePath Source path of the file or folder (relative) + * @param string $destinationPath Destination path(relative) + * @param bool $force Force to overwrite + * + * @return string + * + * @since 4.0.0 + * @throws \Exception + */ + public function move($adapter, $sourcePath, $destinationPath, $force = false) + { + return $this->getAdapter($adapter)->move($sourcePath, $destinationPath, $force); + } + + /** + * Returns a url for serve media files from adapter. + * Url must provide a valid image type to be displayed on Joomla! site. + * + * @param string $adapter The adapter + * @param string $path The relative path for the file + * + * @return string Permalink to the relative file + * + * @since 4.0.0 + * @throws FileNotFoundException + */ + public function getUrl($adapter, $path) + { + // Check if it is a media file + if (!$this->isMediaFile($path)) { + throw new InvalidPathException(); + } + + $url = $this->getAdapter($adapter)->getUrl($path); + + $event = new FetchMediaItemUrlEvent('onFetchMediaFileUrl', ['adapter' => $adapter, 'path' => $path, 'url' => $url]); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + return $event->getArgument('url'); + } + + /** + * Search for a pattern in a given path + * + * @param string $adapter The adapter to work on + * @param string $needle The search therm + * @param string $path The base path for the search + * @param bool $recursive Do a recursive search + * + * @return \stdClass[] + * + * @since 4.0.0 + * @throws \Exception + */ + public function search($adapter, $needle, $path = '/', $recursive = true) + { + return $this->getAdapter($adapter)->search($path, $needle, $recursive); + } + + /** + * Checks if the given path is an allowed media file. + * + * @param string $path The path to file + * + * @return boolean + * + * @since 4.0.0 + */ + private function isMediaFile($path) + { + // Check if there is an extension available + if (!strrpos($path, '.')) { + return false; + } + + // Initialize the allowed extensions + if ($this->allowedExtensions === null) { + // Get options from the input or fallback to images only + $mediaTypes = explode(',', Factory::getApplication()->input->getString('mediatypes', '0')); + $types = []; + $extensions = []; + + // Default to showing all supported formats + if (count($mediaTypes) === 0) { + $mediaTypes = ['0', '1', '2', '3']; + } + + array_map( + function ($mediaType) use (&$types) { + switch ($mediaType) { + case '0': + $types[] = 'images'; + break; + case '1': + $types[] = 'audios'; + break; + case '2': + $types[] = 'videos'; + break; + case '3': + $types[] = 'documents'; + break; + default: + break; + } + }, + $mediaTypes + ); + + $images = array_map( + 'trim', + explode( + ',', + ComponentHelper::getParams('com_media')->get( + 'image_extensions', + 'bmp,gif,jpg,jpeg,png,webp' + ) + ) + ); + $audios = array_map( + 'trim', + explode( + ',', + ComponentHelper::getParams('com_media')->get( + 'audio_extensions', + 'mp3,m4a,mp4a,ogg' + ) + ) + ); + $videos = array_map( + 'trim', + explode( + ',', + ComponentHelper::getParams('com_media')->get( + 'video_extensions', + 'mp4,mp4v,mpeg,mov,webm' + ) + ) + ); + $documents = array_map( + 'trim', + explode( + ',', + ComponentHelper::getParams('com_media')->get( + 'doc_extensions', + 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv' + ) + ) + ); + + foreach ($types as $type) { + if (in_array($type, ['images', 'audios', 'videos', 'documents'])) { + $extensions = array_merge($extensions, ${$type}); + } + } + + // Make them an array + $this->allowedExtensions = $extensions; + } + + // Extract the extension + $extension = strtolower(substr($path, strrpos($path, '.') + 1)); + + // Check if the extension exists in the allowed extensions + return in_array($extension, $this->allowedExtensions); + } } diff --git a/code/administrator/components/com_media/src/Model/FileModel.php b/code/administrator/components/com_media/src/Model/FileModel.php index eb2e5c26..8e651d00 100644 --- a/code/administrator/components/com_media/src/Model/FileModel.php +++ b/code/administrator/components/com_media/src/Model/FileModel.php @@ -1,4 +1,5 @@ loadForm('com_media.file', 'file', ['control' => 'jform', 'load_data' => $loadData]); + // Get the form. + $form = $this->loadForm('com_media.file', 'file', ['control' => 'jform', 'load_data' => $loadData]); - if (empty($form)) - { - return false; - } + if (empty($form)) { + return false; + } - return $form; - } + return $form; + } - /** - * Method to get the file information for the given path. Path must be - * in the format: adapter:path/to/file.extension - * - * @param string $path The path to get the information from. - * - * @return \stdClass An object with file information - * - * @since 4.0.0 - * @see ApiModel::getFile() - */ - public function getFileInformation($path) - { - list($adapter, $path) = explode(':', $path, 2); + /** + * Method to get the file information for the given path. Path must be + * in the format: adapter:path/to/file.extension + * + * @param string $path The path to get the information from. + * + * @return \stdClass An object with file information + * + * @since 4.0.0 + * @see ApiModel::getFile() + */ + public function getFileInformation($path) + { + list($adapter, $path) = explode(':', $path, 2); - return $this->bootComponent('com_media')->getMVCFactory()->createModel('Api', 'Administrator') - ->getFile($adapter, $path, ['url' => true, 'content' => true]); - } + return $this->bootComponent('com_media')->getMVCFactory()->createModel('Api', 'Administrator') + ->getFile($adapter, $path, ['url' => true, 'content' => true]); + } } diff --git a/code/administrator/components/com_media/src/Model/MediaModel.php b/code/administrator/components/com_media/src/Model/MediaModel.php index 257113bc..3030fe54 100644 --- a/code/administrator/components/com_media/src/Model/MediaModel.php +++ b/code/administrator/components/com_media/src/Model/MediaModel.php @@ -1,4 +1,5 @@ getProviderManager()->getProviders() as $provider) - { - $result = new \stdClass; - $result->name = $provider->getID(); - $result->displayName = $provider->getDisplayName(); - $result->adapterNames = []; - - foreach ($provider->getAdapters() as $adapter) - { - $result->adapterNames[] = $adapter->getAdapterName(); - } - - $results[] = $result; - } - - return $results; - } + use ProviderManagerHelperTrait; + + /** + * Obtain list of supported providers + * + * @return array + * + * @since 4.0.0 + */ + public function getProviders() + { + $results = []; + + foreach ($this->getProviderManager()->getProviders() as $provider) { + $result = new \stdClass(); + $result->name = $provider->getID(); + $result->displayName = $provider->getDisplayName(); + $result->adapterNames = []; + + foreach ($provider->getAdapters() as $adapter) { + $result->adapterNames[] = $adapter->getAdapterName(); + } + + $results[] = $result; + } + + return $results; + } } diff --git a/code/administrator/components/com_media/src/Plugin/MediaActionPlugin.php b/code/administrator/components/com_media/src/Plugin/MediaActionPlugin.php index 830aeec6..6a8b19d8 100644 --- a/code/administrator/components/com_media/src/Plugin/MediaActionPlugin.php +++ b/code/administrator/components/com_media/src/Plugin/MediaActionPlugin.php @@ -1,4 +1,5 @@ getName() != 'com_media.file') - { - return; - } + /** + * The form event. Load additional parameters when available into the field form. + * Only when the type of the form is of interest. + * + * @param Form $form The form + * @param \stdClass $data The data + * + * @return void + * + * @since 4.0.0 + */ + public function onContentPrepareForm(Form $form, $data) + { + // Check if it is the right form + if ($form->getName() != 'com_media.file') { + return; + } - $this->loadCss(); - $this->loadJs(); + $this->loadCss(); + $this->loadJs(); - // The file with the params for the edit view - $paramsFile = JPATH_PLUGINS . '/media-action/' . $this->_name . '/form/' . $this->_name . '.xml'; + // The file with the params for the edit view + $paramsFile = JPATH_PLUGINS . '/media-action/' . $this->_name . '/form/' . $this->_name . '.xml'; - // When the file exists, load it into the form - if (file_exists($paramsFile)) - { - $form->loadFile($paramsFile); - } - } + // When the file exists, load it into the form + if (file_exists($paramsFile)) { + $form->loadFile($paramsFile); + } + } - /** - * Load the javascript files of the plugin. - * - * @return void - * - * @since 4.0.0 - */ - protected function loadJs() - { - HTMLHelper::_( - 'script', - 'plg_media-action_' . $this->_name . '/' . $this->_name . '.js', - ['version' => 'auto', 'relative' => true], - ['type' => 'module'] - ); - } + /** + * Load the javascript files of the plugin. + * + * @return void + * + * @since 4.0.0 + */ + protected function loadJs() + { + HTMLHelper::_( + 'script', + 'plg_media-action_' . $this->_name . '/' . $this->_name . '.js', + ['version' => 'auto', 'relative' => true], + ['type' => 'module'] + ); + } - /** - * Load the CSS files of the plugin. - * - * @return void - * - * @since 4.0.0 - */ - protected function loadCss() - { - HTMLHelper::_( - 'stylesheet', - 'plg_media-action_' . $this->_name . '/' . $this->_name . '.css', - ['version' => 'auto', 'relative' => true] - ); - } + /** + * Load the CSS files of the plugin. + * + * @return void + * + * @since 4.0.0 + */ + protected function loadCss() + { + HTMLHelper::_( + 'stylesheet', + 'plg_media-action_' . $this->_name . '/' . $this->_name . '.css', + ['version' => 'auto', 'relative' => true] + ); + } } diff --git a/code/administrator/components/com_media/src/Provider/ProviderInterface.php b/code/administrator/components/com_media/src/Provider/ProviderInterface.php index 13a270c1..76d63b77 100644 --- a/code/administrator/components/com_media/src/Provider/ProviderInterface.php +++ b/code/administrator/components/com_media/src/Provider/ProviderInterface.php @@ -1,4 +1,5 @@ providers; - } - - /** - * Register a provider into the ProviderManager - * - * @param ProviderInterface $provider The provider to be registered - * - * @return void - * - * @since 4.0.0 - */ - public function registerProvider(ProviderInterface $provider) - { - $this->providers[$provider->getID()] = $provider; - } - - /** - * Unregister a provider from the ProviderManager. - * When no provider, or null is passed in, then all providers are cleared. - * - * @param ProviderInterface|null $provider The provider to be unregistered - * - * @return void - * - * @since 4.0.6 - */ - public function unregisterProvider(ProviderInterface $provider = null): void - { - if ($provider === null) - { - $this->providers = []; - return; - } - - if (!array_key_exists($provider->getID(), $this->providers)) - { - return; - } - - unset($this->providers[$provider->getID()]); - } - - /** - * Returns the provider for a particular ID - * - * @param string $id The ID for the provider - * - * @return ProviderInterface - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function getProvider($id) - { - if (!isset($this->providers[$id])) - { - throw new \Exception(Text::_('COM_MEDIA_ERROR_MEDIA_PROVIDER_NOT_FOUND')); - } - - return $this->providers[$id]; - } - - /** - * Returns an adapter for an account - * - * @param string $name The name of an adapter - * - * @return AdapterInterface - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function getAdapter($name) - { - list($provider, $account) = array_pad(explode('-', $name, 2), 2, null); - - if ($account == null) - { - throw new \Exception(Text::_('COM_MEDIA_ERROR_ACCOUNT_NOT_SET')); - } - - $adapters = $this->getProvider($provider)->getAdapters(); - - if (!isset($adapters[$account])) - { - throw new \Exception(Text::_('COM_MEDIA_ERROR_ACCOUNT_NOT_FOUND')); - } - - return $adapters[$account]; - } + /** + * The array of providers + * + * @var ProviderInterface[] + * + * @since 4.0.0 + */ + private $providers = []; + + /** + * Returns an associative array of adapters with provider name as the key + * + * @return ProviderInterface[] + * + * @since 4.0.0 + */ + public function getProviders() + { + return $this->providers; + } + + /** + * Register a provider into the ProviderManager + * + * @param ProviderInterface $provider The provider to be registered + * + * @return void + * + * @since 4.0.0 + */ + public function registerProvider(ProviderInterface $provider) + { + $this->providers[$provider->getID()] = $provider; + } + + /** + * Unregister a provider from the ProviderManager. + * When no provider, or null is passed in, then all providers are cleared. + * + * @param ProviderInterface|null $provider The provider to be unregistered + * + * @return void + * + * @since 4.0.6 + */ + public function unregisterProvider(ProviderInterface $provider = null): void + { + if ($provider === null) { + $this->providers = []; + return; + } + + if (!array_key_exists($provider->getID(), $this->providers)) { + return; + } + + unset($this->providers[$provider->getID()]); + } + + /** + * Returns the provider for a particular ID + * + * @param string $id The ID for the provider + * + * @return ProviderInterface + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getProvider($id) + { + if (!isset($this->providers[$id])) { + throw new \Exception(Text::_('COM_MEDIA_ERROR_MEDIA_PROVIDER_NOT_FOUND')); + } + + return $this->providers[$id]; + } + + /** + * Returns an adapter for an account + * + * @param string $name The name of an adapter + * + * @return AdapterInterface + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getAdapter($name) + { + list($provider, $account) = array_pad(explode('-', $name, 2), 2, null); + + if ($account == null) { + throw new \Exception(Text::_('COM_MEDIA_ERROR_ACCOUNT_NOT_SET')); + } + + $adapters = $this->getProvider($provider)->getAdapters(); + + if (!isset($adapters[$account])) { + throw new \Exception(Text::_('COM_MEDIA_ERROR_ACCOUNT_NOT_FOUND')); + } + + return $adapters[$account]; + } } diff --git a/code/administrator/components/com_media/src/Provider/ProviderManagerHelperTrait.php b/code/administrator/components/com_media/src/Provider/ProviderManagerHelperTrait.php index 7c951656..d535c252 100644 --- a/code/administrator/components/com_media/src/Provider/ProviderManagerHelperTrait.php +++ b/code/administrator/components/com_media/src/Provider/ProviderManagerHelperTrait.php @@ -1,4 +1,5 @@ providerManager) - { - // Fire the event to get the results - $eventParameters = ['context' => 'AdapterManager', 'providerManager' => new ProviderManager]; - $event = new MediaProviderEvent('onSetupProviders', $eventParameters); - PluginHelper::importPlugin('filesystem'); - Factory::getApplication()->triggerEvent('onSetupProviders', $event); - $this->providerManager = $event->getProviderManager(); - } - - return $this->providerManager; - } - - /** - * Returns a provider for the given id. - * - * @return ProviderInterface - * - * @throws \Exception - * - * @since 4.1.0 - */ - public function getProvider(String $id): ProviderInterface - { - return $this->getProviderManager()->getProvider($id); - } - - /** - * Return an adapter for the given name. - * - * @return AdapterInterface - * - * @throws \Exception - * - * @since 4.1.0 - */ - public function getAdapter(String $name): AdapterInterface - { - return $this->getProviderManager()->getAdapter($name); - } - - /** - * Returns an array with the adapter name as key and the path of the file. - * - * @return array - * - * @throws \InvalidArgumentException - * - * @since 4.1.0 - */ - protected function resolveAdapterAndPath(String $path): array - { - $result = []; - $parts = explode(':', $path, 2); - - // If we have 2 parts, we have both an adapter name and a file path - if (\count($parts) === 2) - { - $result['adapter'] = $parts[0]; - $result['path'] = $parts[1]; - - return $result; - } - - if (!$this->getDefaultAdapterName()) - { - throw new \InvalidArgumentException(Text::_('COM_MEDIA_ERROR_NO_ADAPTER_FOUND')); - } - - // If we have less than 2 parts, we return a default adapter name - $result['adapter'] = $this->getDefaultAdapterName(); - - // If we have 1 part, we return it as the path. Otherwise we return a default path - $result['path'] = \count($parts) ? $parts[0] : '/'; - - return $result; - } - - /** - * Returns the default adapter name. - * - * @return string|null - * - * @throws \Exception - * - * @since 4.1.0 - */ - protected function getDefaultAdapterName(): ?string - { - if ($this->defaultAdapterName) - { - return $this->defaultAdapterName; - } - - $defaultAdapter = $this->getAdapter('local-' . ComponentHelper::getParams('com_media')->get('file_path', 'images')); - - if (!$defaultAdapter - && $this->getProviderManager()->getProvider('local') - && $this->getProviderManager()->getProvider('local')->getAdapters()) - { - $defaultAdapter = $this->getProviderManager()->getProvider('local')->getAdapters()[0]; - } - - if (!$defaultAdapter) - { - return null; - } - - $this->defaultAdapterName = 'local-' . $defaultAdapter->getAdapterName(); - - return $this->defaultAdapterName; - } + /** + * Holds the available media file adapters. + * + * @var ProviderManager + * + * @since 4.1.0 + */ + private $providerManager = null; + + /** + * The default adapter name. + * + * @var string + * + * @since 4.1.0 + */ + private $defaultAdapterName = null; + + /** + * Return a provider manager. + * + * @return ProviderManager + * + * @since 4.1.0 + */ + public function getProviderManager(): ProviderManager + { + if (!$this->providerManager) { + // Fire the event to get the results + $eventParameters = ['context' => 'AdapterManager', 'providerManager' => new ProviderManager()]; + $event = new MediaProviderEvent('onSetupProviders', $eventParameters); + PluginHelper::importPlugin('filesystem'); + Factory::getApplication()->triggerEvent('onSetupProviders', $event); + $this->providerManager = $event->getProviderManager(); + } + + return $this->providerManager; + } + + /** + * Returns a provider for the given id. + * + * @return ProviderInterface + * + * @throws \Exception + * + * @since 4.1.0 + */ + public function getProvider(string $id): ProviderInterface + { + return $this->getProviderManager()->getProvider($id); + } + + /** + * Return an adapter for the given name. + * + * @return AdapterInterface + * + * @throws \Exception + * + * @since 4.1.0 + */ + public function getAdapter(string $name): AdapterInterface + { + return $this->getProviderManager()->getAdapter($name); + } + + /** + * Returns an array with the adapter name as key and the path of the file. + * + * @return array + * + * @throws \InvalidArgumentException + * + * @since 4.1.0 + */ + protected function resolveAdapterAndPath(string $path): array + { + $result = []; + $parts = explode(':', $path, 2); + + // If we have 2 parts, we have both an adapter name and a file path + if (\count($parts) === 2) { + $result['adapter'] = $parts[0]; + $result['path'] = $parts[1]; + + return $result; + } + + if (!$this->getDefaultAdapterName()) { + throw new \InvalidArgumentException(Text::_('COM_MEDIA_ERROR_NO_ADAPTER_FOUND')); + } + + // If we have less than 2 parts, we return a default adapter name + $result['adapter'] = $this->getDefaultAdapterName(); + + // If we have 1 part, we return it as the path. Otherwise we return a default path + $result['path'] = \count($parts) ? $parts[0] : '/'; + + return $result; + } + + /** + * Returns the default adapter name. + * + * @return string|null + * + * @throws \Exception + * + * @since 4.1.0 + */ + protected function getDefaultAdapterName(): ?string + { + if ($this->defaultAdapterName) { + return $this->defaultAdapterName; + } + + $defaultAdapter = $this->getAdapter('local-' . ComponentHelper::getParams('com_media')->get('file_path', 'images')); + + if ( + !$defaultAdapter + && $this->getProviderManager()->getProvider('local') + && $this->getProviderManager()->getProvider('local')->getAdapters() + ) { + $defaultAdapter = $this->getProviderManager()->getProvider('local')->getAdapters()[0]; + } + + if (!$defaultAdapter) { + return null; + } + + $this->defaultAdapterName = 'local-' . $defaultAdapter->getAdapterName(); + + return $this->defaultAdapterName; + } } diff --git a/code/administrator/components/com_media/src/View/File/HtmlView.php b/code/administrator/components/com_media/src/View/File/HtmlView.php index 16817584..b98dca8d 100644 --- a/code/administrator/components/com_media/src/View/File/HtmlView.php +++ b/code/administrator/components/com_media/src/View/File/HtmlView.php @@ -1,4 +1,5 @@ input; - /** - * Execute and display a template script. - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $input = Factory::getApplication()->input; - - $this->form = $this->get('Form'); + $this->form = $this->get('Form'); - // The component params - $this->params = ComponentHelper::getParams('com_media'); + // The component params + $this->params = ComponentHelper::getParams('com_media'); - // The requested file - $this->file = $this->getModel()->getFileInformation($input->getString('path', null)); + // The requested file + $this->file = $this->getModel()->getFileInformation($input->getString('path', null)); - if (empty($this->file->content)) - { - // @todo error handling controller redirect files - throw new \Exception(Text::_('COM_MEDIA_ERROR_NO_CONTENT_AVAILABLE')); - } + if (empty($this->file->content)) { + // @todo error handling controller redirect files + throw new \Exception(Text::_('COM_MEDIA_ERROR_NO_CONTENT_AVAILABLE')); + } - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the toolbar buttons - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_MEDIA_EDIT'), 'images mediamanager'); + /** + * Add the toolbar buttons + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_MEDIA_EDIT'), 'images mediamanager'); - ToolbarHelper::apply('apply'); - ToolbarHelper::save('save'); - ToolbarHelper::custom('reset', 'refresh', '', 'COM_MEDIA_RESET', false); + ToolbarHelper::apply('apply'); + ToolbarHelper::save('save'); + ToolbarHelper::custom('reset', 'refresh', '', 'COM_MEDIA_RESET', false); - ToolbarHelper::cancel('cancel', 'JTOOLBAR_CLOSE'); - } + ToolbarHelper::cancel('cancel', 'JTOOLBAR_CLOSE'); + } } diff --git a/code/administrator/components/com_media/src/View/Media/HtmlView.php b/code/administrator/components/com_media/src/View/Media/HtmlView.php index 56e93e73..a8bd7b64 100644 --- a/code/administrator/components/com_media/src/View/Media/HtmlView.php +++ b/code/administrator/components/com_media/src/View/Media/HtmlView.php @@ -88,7 +88,7 @@ protected function prepareToolbar() // Get the toolbar object instance $bar = Toolbar::getInstance('toolbar'); - $user = Factory::getUser(); + $user = $this->getCurrentUser(); // Set the title ToolbarHelper::title(Text::_('COM_MEDIA'), 'images mediamanager'); diff --git a/code/administrator/components/com_media/tmpl/file/default.php b/code/administrator/components/com_media/tmpl/file/default.php index 83a9fd37..2385e580 100644 --- a/code/administrator/components/com_media/tmpl/file/default.php +++ b/code/administrator/components/com_media/tmpl/file/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useStyle('com_media.mediamanager'); + ->useScript('form.validate') + ->useStyle('com_media.mediamanager'); $script = $wa->getAsset('script', 'com_media.edit-images')->getUri(true); @@ -37,25 +38,25 @@ // Load the toolbar when we are in an iframe if ($tmpl == 'component') { - echo '
'; - echo Toolbar::getInstance('toolbar')->render(); - echo '
'; + echo '
'; + echo Toolbar::getInstance('toolbar')->render(); + echo '
'; } $mediaTypes = $input->getString('mediatypes', '0'); // Populate the media config $config = [ - 'apiBaseUrl' => Uri::base() . 'index.php?option=com_media&format=json' . '&mediatypes=' . $mediaTypes, - 'csrfToken' => Session::getFormToken(), - 'uploadPath' => $this->file->path, - 'editViewUrl' => Uri::base() . 'index.php?option=com_media&view=file' . ($tmpl ? '&tmpl=' . $tmpl : '') . '&mediatypes=' . $mediaTypes, - 'imagesExtensions' => explode(',', $params->get('image_extensions', 'bmp,gif,jpg,jpeg,png,webp')), - 'audioExtensions' => explode(',', $params->get('audio_extensions', 'mp3,m4a,mp4a,ogg')), - 'videoExtensions' => explode(',', $params->get('video_extensions', 'mp4,mp4v,mpeg,mov,webm')), - 'documentExtensions' => explode(',', $params->get('doc_extensions', 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv')), - 'maxUploadSizeMb' => $params->get('upload_maxsize', 10), - 'contents' => $this->file->content, + 'apiBaseUrl' => Uri::base() . 'index.php?option=com_media&format=json' . '&mediatypes=' . $mediaTypes, + 'csrfToken' => Session::getFormToken(), + 'uploadPath' => $this->file->path, + 'editViewUrl' => Uri::base() . 'index.php?option=com_media&view=file' . ($tmpl ? '&tmpl=' . $tmpl : '') . '&mediatypes=' . $mediaTypes, + 'imagesExtensions' => array_map('trim', explode(',', $params->get('image_extensions', 'bmp,gif,jpg,jpeg,png,webp'))), + 'audioExtensions' => array_map('trim', explode(',', $params->get('audio_extensions', 'mp3,m4a,mp4a,ogg'))), + 'videoExtensions' => array_map('trim', explode(',', $params->get('video_extensions', 'mp4,mp4v,mpeg,mov,webm'))), + 'documentExtensions' => array_map('trim', explode(',', $params->get('doc_extensions', 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv'))), + 'maxUploadSizeMb' => $params->get('upload_maxsize', 10), + 'contents' => $this->file->content, ]; $this->document->addScriptOptions('com_media', $config); @@ -63,13 +64,13 @@ $this->useCoreUI = true; ?>
- getFieldsets(); ?> - - 'attrib-' . reset($fieldSets)->name, 'breakpoint' => 768]); ?> - -
'; ?> - - - + getFieldsets(); ?> + + 'attrib-' . reset($fieldSets)->name, 'breakpoint' => 768]); ?> + +
'; ?> + + + diff --git a/code/administrator/components/com_media/tmpl/media/default.php b/code/administrator/components/com_media/tmpl/media/default.php index aff6f9b1..5ea50cd9 100644 --- a/code/administrator/components/com_media/tmpl/media/default.php +++ b/code/administrator/components/com_media/tmpl/media/default.php @@ -14,8 +14,10 @@ use Joomla\CMS\Toolbar\Toolbar; use Joomla\CMS\Uri\Uri; +$app = Factory::getApplication(); $params = ComponentHelper::getParams('com_media'); -$input = Factory::getApplication()->input; +$input = $app->input; +$user = $app->getIdentity(); /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); @@ -39,22 +41,25 @@ $mediaTypes = '&mediatypes=' . $input->getString('mediatypes', '0,1,2,3'); // Populate the media config -$config = array( +$config = [ 'apiBaseUrl' => Uri::base() . 'index.php?option=com_media&format=json' . $mediaTypes, 'csrfToken' => Session::getFormToken(), 'filePath' => $params->get('file_path', 'images'), 'fileBaseUrl' => Uri::root() . $params->get('file_path', 'images'), 'fileBaseRelativeUrl' => $params->get('file_path', 'images'), 'editViewUrl' => Uri::base() . 'index.php?option=com_media&view=file' . ($tmpl ? '&tmpl=' . $tmpl : '') . $mediaTypes, - 'imagesExtensions' => explode(',', $params->get('image_extensions', 'bmp,gif,jpg,jpeg,png,webp')), - 'audioExtensions' => explode(',', $params->get('audio_extensions', 'mp3,m4a,mp4a,ogg')), - 'videoExtensions' => explode(',', $params->get('video_extensions', 'mp4,mp4v,mpeg,mov,webm')), - 'documentExtensions' => explode(',', $params->get('doc_extensions', 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv')), + 'imagesExtensions' => array_map('trim', explode(',', $params->get('image_extensions', 'bmp,gif,jpg,jpeg,png,webp'))), + 'audioExtensions' => array_map('trim', explode(',', $params->get('audio_extensions', 'mp3,m4a,mp4a,ogg'))), + 'videoExtensions' => array_map('trim', explode(',', $params->get('video_extensions', 'mp4,mp4v,mpeg,mov,webm'))), + 'documentExtensions' => array_map('trim', explode(',', $params->get('doc_extensions', 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv'))), 'maxUploadSizeMb' => $params->get('upload_maxsize', 10), 'providers' => (array) $this->providers, 'currentPath' => $this->currentPath, 'isModal' => $tmpl === 'component', -); + 'canCreate' => $user->authorise('core.create', 'com_media'), + 'canEdit' => $user->authorise('core.edit', 'com_media'), + 'canDelete' => $user->authorise('core.delete', 'com_media'), +]; $this->document->addScriptOptions('com_media', $config); ?>
diff --git a/code/administrator/components/com_menus/forms/item.xml b/code/administrator/components/com_menus/forms/item.xml index 417f9194..5a05395c 100644 --- a/code/administrator/components/com_menus/forms/item.xml +++ b/code/administrator/components/com_menus/forms/item.xml @@ -95,6 +95,7 @@ name="parent_id" type="MenuParent" label="COM_MENUS_ITEM_FIELD_PARENT_LABEL" + layout="joomla.form.field.list-fancy-select" default="1" filter="int" clientid="0" diff --git a/code/administrator/components/com_menus/helpers/menus.php b/code/administrator/components/com_menus/helpers/menus.php index 570b89ca..44ef8647 100644 --- a/code/administrator/components/com_menus/helpers/menus.php +++ b/code/administrator/components/com_menus/helpers/menus.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Menus component helper. diff --git a/code/administrator/components/com_menus/layouts/joomla/menu/edit_modules.php b/code/administrator/components/com_menus/layouts/joomla/menu/edit_modules.php index 2c4ca108..a8d3e336 100644 --- a/code/administrator/components/com_menus/layouts/joomla/menu/edit_modules.php +++ b/code/administrator/components/com_menus/layouts/joomla/menu/edit_modules.php @@ -1,4 +1,5 @@ input; $component = $input->getCmd('option', 'com_content'); -if ($component == 'com_categories') -{ - $extension = $input->getCmd('extension', 'com_content'); - $parts = explode('.', $extension); - $component = $parts[0]; +if ($component == 'com_categories') { + $extension = $input->getCmd('extension', 'com_content'); + $parts = explode('.', $extension); + $component = $parts[0]; } $saveHistory = ComponentHelper::getParams($component)->get('save_history', 0); $fields = $displayData->get('fields') ?: array( - array('parent', 'parent_id'), - array('published', 'state', 'enabled'), - array('category', 'catid'), - 'featured', - 'sticky', - 'access', - 'language', - 'tags', - 'note', - 'version_note', + array('parent', 'parent_id'), + array('published', 'state', 'enabled'), + array('category', 'catid'), + 'featured', + 'sticky', + 'access', + 'language', + 'tags', + 'note', + 'version_note', ); $hiddenFields = $displayData->get('hidden_fields') ?: array(); -if (!$saveHistory) -{ - $hiddenFields[] = 'version_note'; +if (!$saveHistory) { + $hiddenFields[] = 'version_note'; } $html = array(); $html[] = '
    '; -foreach ($fields as $field) -{ - $field = is_array($field) ? $field : array($field); +foreach ($fields as $field) { + $field = is_array($field) ? $field : array($field); - foreach ($field as $f) - { - if ($form->getField($f)) - { - if (in_array($f, $hiddenFields)) - { - $form->setFieldAttribute($f, 'type', 'hidden'); - } + foreach ($field as $f) { + if ($form->getField($f)) { + if (in_array($f, $hiddenFields)) { + $form->setFieldAttribute($f, 'type', 'hidden'); + } - $html[] = '
  • ' . $form->renderField($f) . '
  • '; - break; - } - } + $html[] = '
  • ' . $form->renderField($f) . '
  • '; + break; + } + } } $html[] = '
'; diff --git a/code/administrator/components/com_menus/layouts/joomla/searchtools/default.php b/code/administrator/components/com_menus/layouts/joomla/searchtools/default.php index 9f075802..09013246 100644 --- a/code/administrator/components/com_menus/layouts/joomla/searchtools/default.php +++ b/code/administrator/components/com_menus/layouts/joomla/searchtools/default.php @@ -1,4 +1,5 @@ filterForm) && !empty($data['view']->filterForm)) -{ - // Checks if a selector (e.g. client_id) exists. - if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) - { - $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; - - // Checks if a selector should be shown in the current layout. - if (isset($data['view']->layout)) - { - $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; - } - - // Unset the selector field from active filters group. - unset($data['view']->activeFilters[$selectorFieldName]); - } - - if ($data['view'] instanceof \Joomla\Component\Menus\Administrator\View\Items\HtmlView) : - unset($data['view']->activeFilters['client_id']); - endif; - - // Checks if the filters button should exist. - $filters = $data['view']->filterForm->getGroup('filter'); - $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; - - // Checks if it should show the be hidden. - $hideActiveFilters = empty($data['view']->activeFilters); - - // Check if the no results message should appear. - if (isset($data['view']->total) && (int) $data['view']->total === 0) - { - $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); - if (!empty($noResults)) - { - $noResultsText = Text::_($noResults); - } - } +if (isset($data['view']->filterForm) && !empty($data['view']->filterForm)) { + // Checks if a selector (e.g. client_id) exists. + if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) { + $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; + + // Checks if a selector should be shown in the current layout. + if (isset($data['view']->layout)) { + $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; + } + + // Unset the selector field from active filters group. + unset($data['view']->activeFilters[$selectorFieldName]); + } + + if ($data['view'] instanceof \Joomla\Component\Menus\Administrator\View\Items\HtmlView) : + unset($data['view']->activeFilters['client_id']); + endif; + + // Checks if the filters button should exist. + $filters = $data['view']->filterForm->getGroup('filter'); + $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; + + // Checks if it should show the be hidden. + $hideActiveFilters = empty($data['view']->activeFilters); + + // Check if the no results message should appear. + if (isset($data['view']->total) && (int) $data['view']->total === 0) { + $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); + if (!empty($noResults)) { + $noResultsText = Text::_($noResults); + } + } } // Set some basic options. $customOptions = array( - 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, - 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, - 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), - 'searchFieldSelector' => '#filter_search', - 'selectorFieldName' => $selectorFieldName, - 'showSelector' => $showSelector, - 'orderFieldSelector' => '#list_fullordering', - 'showNoResults' => !empty($noResultsText), - 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', - 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', + 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, + 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, + 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), + 'searchFieldSelector' => '#filter_search', + 'selectorFieldName' => $selectorFieldName, + 'showSelector' => $showSelector, + 'orderFieldSelector' => '#list_fullordering', + 'showNoResults' => !empty($noResultsText), + 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', + 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', ); // Merge custom options in the options array. @@ -89,39 +85,39 @@ HTMLHelper::_('searchtools.form', $data['options']['formSelector'], $data['options']); ?> - sublayout('noitems', $data); ?> + sublayout('noitems', $data); ?> diff --git a/code/administrator/components/com_menus/menus.xml b/code/administrator/components/com_menus/menus.xml index 4c1bffdd..43df22f9 100644 --- a/code/administrator/components/com_menus/menus.xml +++ b/code/administrator/components/com_menus/menus.xml @@ -2,7 +2,7 @@ com_menus Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_menus/services/provider.php b/code/administrator/components/com_menus/services/provider.php index 3f6056d7..22512347 100644 --- a/code/administrator/components/com_menus/services/provider.php +++ b/code/administrator/components/com_menus/services/provider.php @@ -1,4 +1,5 @@ set(AssociationExtensionInterface::class, new AssociationsHelper); - - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Menus')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Menus')); - - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MenusComponent($container->get(ComponentDispatcherFactoryInterface::class)); - - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); - - return $component; - } - ); - } + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->set(AssociationExtensionInterface::class, new AssociationsHelper()); + + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Menus')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Menus')); + + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MenusComponent($container->get(ComponentDispatcherFactoryInterface::class)); + + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); + + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_menus/src/Controller/AjaxController.php b/code/administrator/components/com_menus/src/Controller/AjaxController.php index 32787bc5..e6f67745 100644 --- a/code/administrator/components/com_menus/src/Controller/AjaxController.php +++ b/code/administrator/components/com_menus/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->getInt('assocId', 0); + /** + * Method to fetch associations of a menu item + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the menu item whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return null + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', (int) $assocId, 'id', '', ''); + $associations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', (int) $assocId, 'id', '', ''); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/tables'); - $menuTable = Table::getInstance('Menu', 'JTable', array()); + // Add the title to each of the associated records + Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/tables'); + $menuTable = Table::getInstance('Menu', 'JTable', array()); - foreach ($associations as $lang => $association) - { - $menuTable->load($association->id); - $associations[$lang]->title = $menuTable->title; - } + foreach ($associations as $lang => $association) { + $menuTable->load($association->id); + $associations[$lang]->title = $menuTable->title; + } - $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/code/administrator/components/com_menus/src/Controller/DisplayController.php b/code/administrator/components/com_menus/src/Controller/DisplayController.php index bb67c8cb..c64149a6 100644 --- a/code/administrator/components/com_menus/src/Controller/DisplayController.php +++ b/code/administrator/components/com_menus/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->post->getCmd('menutype', ''); - - if ($menuType !== '') - { - $uri = Uri::getInstance(); - - if ($uri->getVar('menutype') !== $menuType) - { - $uri->setVar('menutype', $menuType); - - if ($forcedLanguage = $this->input->post->get('forcedLanguage')) - { - $uri->setVar('forcedLanguage', $forcedLanguage); - } - - $this->setRedirect(Route::_('index.php' . $uri->toString(['query']), false)); - - return parent::display(); - } - } - - // Check custom administrator menu modules - if (ModuleHelper::isAdminMultilang()) - { - $languages = LanguageHelper::getInstalledLanguages(1, true); - $langCodes = array(); - - foreach ($languages as $language) - { - if (isset($language->metadata['nativeName'])) - { - $languageName = $language->metadata['nativeName']; - } - else - { - $languageName = $language->metadata['name']; - } - - $langCodes[$language->metadata['tag']] = $languageName; - } - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - $query->select($db->quoteName('m.language')) - ->from($db->quoteName('#__modules', 'm')) - ->where( - [ - $db->quoteName('m.module') . ' = ' . $db->quote('mod_menu'), - $db->quoteName('m.published') . ' = 1', - $db->quoteName('m.client_id') . ' = 1', - ] - ) - ->group($db->quoteName('m.language')); - - $mLanguages = $db->setQuery($query)->loadColumn(); - - // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. - if (!in_array('*', $mLanguages) && count($langMissing = array_diff(array_keys($langCodes), $mLanguages))) - { - $langMissing = array_intersect_key($langCodes, array_flip($langMissing)); - - $this->app->enqueueMessage(Text::sprintf('JMENU_MULTILANG_WARNING_MISSING_MODULES', implode(', ', $langMissing)), 'warning'); - } - } - - return parent::display(); - } + /** + * The default view for the display method. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'menus'; + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array|boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + // Verify menu + $menuType = $this->input->post->getCmd('menutype', ''); + + if ($menuType !== '') { + $uri = Uri::getInstance(); + + if ($uri->getVar('menutype') !== $menuType) { + $uri->setVar('menutype', $menuType); + + if ($forcedLanguage = $this->input->post->get('forcedLanguage')) { + $uri->setVar('forcedLanguage', $forcedLanguage); + } + + $this->setRedirect(Route::_('index.php' . $uri->toString(['query']), false)); + + return parent::display(); + } + } + + // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. + if ($langMissing = $this->getModel('Menus', 'Administrator')->getMissingModuleLanguages()) { + $this->app->enqueueMessage(Text::sprintf('JMENU_MULTILANG_WARNING_MISSING_MODULES', implode(', ', $langMissing)), 'warning'); + } + + return parent::display(); + } } diff --git a/code/administrator/components/com_menus/src/Controller/ItemController.php b/code/administrator/components/com_menus/src/Controller/ItemController.php index cc1b0a56..b9c8899a 100644 --- a/code/administrator/components/com_menus/src/Controller/ItemController.php +++ b/code/administrator/components/com_menus/src/Controller/ItemController.php @@ -1,4 +1,5 @@ app->getIdentity(); - - $menuType = $this->input->getCmd('menutype', $data['menutype'] ?? ''); - - $menutypeID = 0; - - // Load menutype ID - if ($menuType) - { - $menutypeID = (int) $this->getMenuTypeId($menuType); - } - - return $user->authorise('core.create', 'com_menus.menu.' . $menutypeID); - } - - /** - * Method to check if you edit a record. - * - * Extended classes can override this if necessary. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key; default is id. - * - * @return boolean - * - * @since 3.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $user = $this->app->getIdentity(); - - $menutypeID = 0; - - if (isset($data[$key])) - { - $model = $this->getModel(); - $item = $model->getItem($data[$key]); - - if (!empty($item->menutype)) - { - // Protected menutype, do not allow edit - if ($item->menutype == 'main') - { - return false; - } - - $menutypeID = (int) $this->getMenuTypeId($item->menutype); - } - } - - return $user->authorise('core.edit', 'com_menus.menu.' . (int) $menutypeID); - } - - /** - * Loads the menutype ID by a given menutype string - * - * @param string $menutype The given menutype - * - * @return integer - * - * @since 3.6 - */ - protected function getMenuTypeId($menutype) - { - $model = $this->getModel(); - $table = $model->getTable('MenuType'); - - $table->load(array('menutype' => $menutype)); - - return (int) $table->id; - } - - /** - * Method to add a new menu item. - * - * @return mixed True if the record can be added, otherwise false. - * - * @since 1.6 - */ - public function add() - { - $result = parent::add(); - - if ($result) - { - $context = 'com_menus.edit.item'; - - $this->app->setUserState($context . '.type', null); - $this->app->setUserState($context . '.link', null); - } - - return $result; - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 1.6 - */ - public function batch($model = null) - { - $this->checkToken(); - - /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ - $model = $this->getModel('Item', 'Administrator', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_menus&view=items' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } - - /** - * Method to cancel an edit. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 1.6 - */ - public function cancel($key = null) - { - $this->checkToken(); - - $result = parent::cancel(); - - if ($result) - { - // Clear the ancillary data from the session. - $context = 'com_menus.edit.item'; - $this->app->setUserState($context . '.type', null); - $this->app->setUserState($context . '.link', null); - - // Redirect to the list screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend() - . '&menutype=' . $this->app->getUserState('com_menus.items.menutype'), false - ) - ); - } - - return $result; - } - - /** - * Method to edit an existing record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key - * (sometimes required to avoid router collisions). - * - * @return boolean True if access level check and checkout passes, false otherwise. - * - * @since 1.6 - */ - public function edit($key = null, $urlVar = null) - { - $result = parent::edit(); - - if ($result) - { - // Push the new ancillary data into the session. - $this->app->setUserState('com_menus.edit.item.type', null); - $this->app->setUserState('com_menus.edit.item.link', null); - } - - return $result; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 3.0.1 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId, $urlVar); - - if ($recordId) - { - /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ - $model = $this->getModel(); - $item = $model->getItem($recordId); - $clientId = $item->client_id; - $append = '&client_id=' . $clientId . $append; - } - else - { - $clientId = $this->input->get('client_id', '0', 'int'); - $menuType = $this->input->get('menutype', 'mainmenu', 'cmd'); - $append = '&client_id=' . $clientId . ($menuType ? '&menutype=' . $menuType : '') . $append; - } - - return $append; - } - - /** - * Method to save a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 1.6 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ - $model = $this->getModel('Item', 'Administrator', array()); - $table = $model->getTable(); - $data = $this->input->post->get('jform', array(), 'array'); - $task = $this->getTask(); - $context = 'com_menus.edit.item'; - $app = $this->app; - - // Set the menutype should we need it. - if ($data['menutype'] !== '') - { - $this->input->set('menutype', $data['menutype']); - } - - // Determine the name of the primary key for the data. - if (empty($key)) - { - $key = $table->getKeyName(); - } - - // To avoid data collisions the urlVar may be different from the primary key. - if (empty($urlVar)) - { - $urlVar = $key; - } - - $recordId = $this->input->getInt($urlVar); - - // Populate the row id from the session. - $data[$key] = $recordId; - - // The save2copy task needs to be handled slightly differently. - if ($task == 'save2copy') - { - // Check-in the original row. - if ($model->checkin($data['id']) === false) - { - // Check-in failed, go back to the item and display a notice. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'warning'); - - return false; - } - - // Reset the ID and then treat the request as for Apply. - $data['id'] = 0; - $data['associations'] = array(); - $task = 'apply'; - } - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false - ) - ); - - return false; - } - - // Validate the posted data. - // This post is made up of two forms, one for the item and one for params. - $form = $model->getForm($data); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - if ($data['type'] == 'url') - { - $data['link'] = str_replace(array('"', '>', '<'), '', $data['link']); - - if (strstr($data['link'], ':')) - { - $segments = explode(':', $data['link']); - $protocol = strtolower($segments[0]); - $scheme = array( - 'http', 'https', 'ftp', 'ftps', 'gopher', 'mailto', - 'news', 'prospero', 'telnet', 'rlogin', 'tn3270', 'wais', - 'mid', 'cid', 'nntp', 'tel', 'urn', 'ldap', 'file', 'fax', - 'modem', 'git', 'sms', - ); - - if (!in_array($protocol, $scheme)) - { - $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'warning'); - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId), false) - ); - - return false; - } - } - } - - $data = $model->validate($form, $data); - - // Preprocess request fields to ensure that we remove not set or empty request params - $request = $form->getGroup('request', true); - - // Check for the special 'request' entry. - if ($data['type'] == 'component' && !empty($request)) - { - $removeArgs = array(); - - if (!isset($data['request']) || !is_array($data['request'])) - { - $data['request'] = array(); - } - - foreach ($request as $field) - { - $fieldName = $field->getAttribute('name'); - - if (!isset($data['request'][$fieldName]) || $data['request'][$fieldName] == '') - { - $removeArgs[$fieldName] = ''; - } - } - - // Parse the submitted link arguments. - $args = array(); - parse_str(parse_url($data['link'], PHP_URL_QUERY), $args); - - // Merge in the user supplied request arguments. - $args = array_merge($args, $data['request']); - - // Remove the unused request params - if (!empty($args) && !empty($removeArgs)) - { - $args = array_diff_key($args, $removeArgs); - } - - $data['link'] = 'index.php?' . urldecode(http_build_query($args, '', '&')); - } - - // Check for validation errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $app->setUserState('com_menus.edit.item.data', $data); - - // Redirect back to the edit screen. - $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); - $this->setRedirect(Route::_($editUrl, false)); - - return false; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Save the data in the session. - $app->setUserState('com_menus.edit.item.data', $data); - - // Redirect back to the edit screen. - $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect(Route::_($editUrl, false)); - - return false; - } - - // Save succeeded, check-in the row. - if ($model->checkin($data['id']) === false) - { - // Check-in failed, go back to the row and display a notice. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'warning'); - $redirectUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); - $this->setRedirect(Route::_($redirectUrl, false)); - - return false; - } - - $this->setMessage(Text::_('COM_MENUS_SAVE_SUCCESS')); - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the row data in the session. - $recordId = $model->getState($this->context . '.id'); - $this->holdEditId($context, $recordId); - $app->setUserState('com_menus.edit.item.data', null); - $app->setUserState('com_menus.edit.item.type', null); - $app->setUserState('com_menus.edit.item.link', null); - - // Redirect back to the edit screen. - $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); - $this->setRedirect(Route::_($editUrl, false)); - break; - - case 'save2new': - // Clear the row id and data in the session. - $this->releaseEditId($context, $recordId); - $app->setUserState('com_menus.edit.item.data', null); - $app->setUserState('com_menus.edit.item.type', null); - $app->setUserState('com_menus.edit.item.link', null); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(), false)); - break; - - default: - // Clear the row id and data in the session. - $this->releaseEditId($context, $recordId); - $app->setUserState('com_menus.edit.item.data', null); - $app->setUserState('com_menus.edit.item.type', null); - $app->setUserState('com_menus.edit.item.link', null); - - // Redirect to the list screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend() - . '&menutype=' . $app->getUserState('com_menus.items.menutype'), false - ) - ); - break; - } - - return true; - } - - /** - * Sets the type of the menu item currently being edited. - * - * @return void - * - * @since 1.6 - */ - public function setType() - { - $this->checkToken(); - - $app = $this->app; - - // Get the posted values from the request. - $data = $this->input->post->get('jform', array(), 'array'); - - // Get the type. - $type = $data['type']; - - $type = json_decode(base64_decode($type)); - $title = $type->title ?? null; - $recordId = $type->id ?? 0; - - $specialTypes = array('alias', 'separator', 'url', 'heading', 'container'); - - if (!in_array($title, $specialTypes)) - { - $title = 'component'; - } - else - { - // Set correct component id to ensure proper 404 messages with system links - $data['component_id'] = 0; - } - - $app->setUserState('com_menus.edit.item.type', $title); - - if ($title == 'component') - { - if (isset($type->request)) - { - // Clean component name - $type->request->option = InputFilter::getInstance()->clean($type->request->option, 'CMD'); - - $component = ComponentHelper::getComponent($type->request->option); - $data['component_id'] = $component->id; - - $app->setUserState('com_menus.edit.item.link', 'index.php?' . Uri::buildQuery((array) $type->request)); - } - } - // If the type is alias you just need the item id from the menu item referenced. - elseif ($title == 'alias') - { - $app->setUserState('com_menus.edit.item.link', 'index.php?Itemid='); - } - - unset($data['request']); - - $data['type'] = $title; - - if ($this->input->get('fieldtype') == 'type') - { - $data['link'] = $app->getUserState('com_menus.edit.item.link'); - } - - // Save the data in the session. - $app->setUserState('com_menus.edit.item.data', $data); - - $this->type = $type; - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId), false) - ); - } - - /** - * Gets the parent items of the menu location currently. - * - * @return void - * - * @since 3.2 - */ - public function getParentItem() - { - $app = $this->app; - - $results = array(); - $menutype = $this->input->get->get('menutype'); - - if ($menutype) - { - /** @var \Joomla\Component\Menus\Administrator\Model\ItemsModel $model */ - $model = $this->getModel('Items', 'Administrator', array()); - $model->getState(); - $model->setState('filter.menutype', $menutype); - $model->setState('list.select', 'a.id, a.title, a.level'); - $model->setState('list.start', '0'); - $model->setState('list.limit', '0'); - - $results = $model->getItems(); - - // Pad the option text with spaces using depth level as a multiplier. - for ($i = 0, $n = count($results); $i < $n; $i++) - { - $results[$i]->title = str_repeat(' - ', $results[$i]->level) . $results[$i]->title; - } - } - - // Output a \JSON object - echo json_encode($results); - - $app->close(); - } + /** + * Method to check if you can add a new record. + * + * Extended classes can override this if necessary. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 3.6 + */ + protected function allowAdd($data = array()) + { + $user = $this->app->getIdentity(); + + $menuType = $this->input->getCmd('menutype', $data['menutype'] ?? ''); + + $menutypeID = 0; + + // Load menutype ID + if ($menuType) { + $menutypeID = (int) $this->getMenuTypeId($menuType); + } + + return $user->authorise('core.create', 'com_menus.menu.' . $menutypeID); + } + + /** + * Method to check if you edit a record. + * + * Extended classes can override this if necessary. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key; default is id. + * + * @return boolean + * + * @since 3.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $user = $this->app->getIdentity(); + + $menutypeID = 0; + + if (isset($data[$key])) { + $model = $this->getModel(); + $item = $model->getItem($data[$key]); + + if (!empty($item->menutype)) { + // Protected menutype, do not allow edit + if ($item->menutype == 'main') { + return false; + } + + $menutypeID = (int) $this->getMenuTypeId($item->menutype); + } + } + + return $user->authorise('core.edit', 'com_menus.menu.' . (int) $menutypeID); + } + + /** + * Loads the menutype ID by a given menutype string + * + * @param string $menutype The given menutype + * + * @return integer + * + * @since 3.6 + */ + protected function getMenuTypeId($menutype) + { + $model = $this->getModel(); + $table = $model->getTable('MenuType'); + + $table->load(array('menutype' => $menutype)); + + return (int) $table->id; + } + + /** + * Method to add a new menu item. + * + * @return mixed True if the record can be added, otherwise false. + * + * @since 1.6 + */ + public function add() + { + $result = parent::add(); + + if ($result) { + $context = 'com_menus.edit.item'; + + $this->app->setUserState($context . '.type', null); + $this->app->setUserState($context . '.link', null); + } + + return $result; + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 1.6 + */ + public function batch($model = null) + { + $this->checkToken(); + + /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ + $model = $this->getModel('Item', 'Administrator', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_menus&view=items' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } + + /** + * Method to cancel an edit. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 1.6 + */ + public function cancel($key = null) + { + $this->checkToken(); + + $result = parent::cancel(); + + if ($result) { + // Clear the ancillary data from the session. + $context = 'com_menus.edit.item'; + $this->app->setUserState($context . '.type', null); + $this->app->setUserState($context . '.link', null); + + // Redirect to the list screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend() + . '&menutype=' . $this->app->getUserState('com_menus.items.menutype'), + false + ) + ); + } + + return $result; + } + + /** + * Method to edit an existing record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * (sometimes required to avoid router collisions). + * + * @return boolean True if access level check and checkout passes, false otherwise. + * + * @since 1.6 + */ + public function edit($key = null, $urlVar = null) + { + $result = parent::edit(); + + if ($result) { + // Push the new ancillary data into the session. + $this->app->setUserState('com_menus.edit.item.type', null); + $this->app->setUserState('com_menus.edit.item.link', null); + } + + return $result; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 3.0.1 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId, $urlVar); + + if ($recordId) { + /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ + $model = $this->getModel(); + $item = $model->getItem($recordId); + $clientId = $item->client_id; + $append = '&client_id=' . $clientId . $append; + } else { + $clientId = $this->input->get('client_id', '0', 'int'); + $menuType = $this->input->get('menutype', 'mainmenu', 'cmd'); + $append = '&client_id=' . $clientId . ($menuType ? '&menutype=' . $menuType : '') . $append; + } + + return $append; + } + + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 1.6 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ + $model = $this->getModel('Item', 'Administrator', array()); + $table = $model->getTable(); + $data = $this->input->post->get('jform', array(), 'array'); + $task = $this->getTask(); + $context = 'com_menus.edit.item'; + $app = $this->app; + + // Set the menutype should we need it. + if ($data['menutype'] !== '') { + $this->input->set('menutype', $data['menutype']); + } + + // Determine the name of the primary key for the data. + if (empty($key)) { + $key = $table->getKeyName(); + } + + // To avoid data collisions the urlVar may be different from the primary key. + if (empty($urlVar)) { + $urlVar = $key; + } + + $recordId = $this->input->getInt($urlVar); + + // Populate the row id from the session. + $data[$key] = $recordId; + + // The save2copy task needs to be handled slightly differently. + if ($task == 'save2copy') { + // Check-in the original row. + if ($model->checkin($data['id']) === false) { + // Check-in failed, go back to the item and display a notice. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'warning'); + + return false; + } + + // Reset the ID and then treat the request as for Apply. + $data['id'] = 0; + $data['associations'] = array(); + $task = 'apply'; + } + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + + return false; + } + + // Validate the posted data. + // This post is made up of two forms, one for the item and one for params. + $form = $model->getForm($data); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + if ($data['type'] == 'url') { + $data['link'] = str_replace(array('"', '>', '<'), '', $data['link']); + + if (strstr($data['link'], ':')) { + $segments = explode(':', $data['link']); + $protocol = strtolower($segments[0]); + $scheme = array( + 'http', 'https', 'ftp', 'ftps', 'gopher', 'mailto', + 'news', 'prospero', 'telnet', 'rlogin', 'tn3270', 'wais', + 'mid', 'cid', 'nntp', 'tel', 'urn', 'ldap', 'file', 'fax', + 'modem', 'git', 'sms', + ); + + if (!in_array($protocol, $scheme)) { + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'warning'); + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId), false) + ); + + return false; + } + } + } + + $data = $model->validate($form, $data); + + // Preprocess request fields to ensure that we remove not set or empty request params + $request = $form->getGroup('request', true); + + // Check for the special 'request' entry. + if ($data['type'] == 'component' && !empty($request)) { + $removeArgs = array(); + + if (!isset($data['request']) || !is_array($data['request'])) { + $data['request'] = array(); + } + + foreach ($request as $field) { + $fieldName = $field->getAttribute('name'); + + if (!isset($data['request'][$fieldName]) || $data['request'][$fieldName] == '') { + $removeArgs[$fieldName] = ''; + } + } + + // Parse the submitted link arguments. + $args = array(); + parse_str(parse_url($data['link'], PHP_URL_QUERY), $args); + + // Merge in the user supplied request arguments. + $args = array_merge($args, $data['request']); + + // Remove the unused request params + if (!empty($args) && !empty($removeArgs)) { + $args = array_diff_key($args, $removeArgs); + } + + $data['link'] = 'index.php?' . urldecode(http_build_query($args, '', '&')); + } + + // Check for validation errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $app->setUserState('com_menus.edit.item.data', $data); + + // Redirect back to the edit screen. + $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); + $this->setRedirect(Route::_($editUrl, false)); + + return false; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Save the data in the session. + $app->setUserState('com_menus.edit.item.data', $data); + + // Redirect back to the edit screen. + $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect(Route::_($editUrl, false)); + + return false; + } + + // Save succeeded, check-in the row. + if ($model->checkin($data['id']) === false) { + // Check-in failed, go back to the row and display a notice. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'warning'); + $redirectUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); + $this->setRedirect(Route::_($redirectUrl, false)); + + return false; + } + + $this->setMessage(Text::_('COM_MENUS_SAVE_SUCCESS')); + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the row data in the session. + $recordId = $model->getState($this->context . '.id'); + $this->holdEditId($context, $recordId); + $app->setUserState('com_menus.edit.item.data', null); + $app->setUserState('com_menus.edit.item.type', null); + $app->setUserState('com_menus.edit.item.link', null); + + // Redirect back to the edit screen. + $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); + $this->setRedirect(Route::_($editUrl, false)); + break; + + case 'save2new': + // Clear the row id and data in the session. + $this->releaseEditId($context, $recordId); + $app->setUserState('com_menus.edit.item.data', null); + $app->setUserState('com_menus.edit.item.type', null); + $app->setUserState('com_menus.edit.item.link', null); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(), false)); + break; + + default: + // Clear the row id and data in the session. + $this->releaseEditId($context, $recordId); + $app->setUserState('com_menus.edit.item.data', null); + $app->setUserState('com_menus.edit.item.type', null); + $app->setUserState('com_menus.edit.item.link', null); + + // Redirect to the list screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend() + . '&menutype=' . $app->getUserState('com_menus.items.menutype'), + false + ) + ); + break; + } + + return true; + } + + /** + * Sets the type of the menu item currently being edited. + * + * @return void + * + * @since 1.6 + */ + public function setType() + { + $this->checkToken(); + + $app = $this->app; + + // Get the posted values from the request. + $data = $this->input->post->get('jform', array(), 'array'); + + // Get the type. + $type = $data['type']; + + $type = json_decode(base64_decode($type)); + $title = $type->title ?? null; + $recordId = $type->id ?? 0; + + $specialTypes = array('alias', 'separator', 'url', 'heading', 'container'); + + if (!in_array($title, $specialTypes)) { + $title = 'component'; + } else { + // Set correct component id to ensure proper 404 messages with system links + $data['component_id'] = 0; + } + + $app->setUserState('com_menus.edit.item.type', $title); + + if ($title == 'component') { + if (isset($type->request)) { + // Clean component name + $type->request->option = InputFilter::getInstance()->clean($type->request->option, 'CMD'); + + $component = ComponentHelper::getComponent($type->request->option); + $data['component_id'] = $component->id; + + $app->setUserState('com_menus.edit.item.link', 'index.php?' . Uri::buildQuery((array) $type->request)); + } + } elseif ($title == 'alias') { + // If the type is alias you just need the item id from the menu item referenced. + $app->setUserState('com_menus.edit.item.link', 'index.php?Itemid='); + } + + unset($data['request']); + + $data['type'] = $title; + + if ($this->input->get('fieldtype') == 'type') { + $data['link'] = $app->getUserState('com_menus.edit.item.link'); + } + + // Save the data in the session. + $app->setUserState('com_menus.edit.item.data', $data); + + $this->type = $type; + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId), false) + ); + } + + /** + * Gets the parent items of the menu location currently. + * + * @return void + * + * @since 3.2 + */ + public function getParentItem() + { + $app = $this->app; + + $results = array(); + $menutype = $this->input->get->get('menutype'); + + if ($menutype) { + /** @var \Joomla\Component\Menus\Administrator\Model\ItemsModel $model */ + $model = $this->getModel('Items', 'Administrator', array()); + $model->getState(); + $model->setState('filter.menutype', $menutype); + $model->setState('list.select', 'a.id, a.title, a.level'); + $model->setState('list.start', '0'); + $model->setState('list.limit', '0'); + + $results = $model->getItems(); + + // Pad the option text with spaces using depth level as a multiplier. + for ($i = 0, $n = count($results); $i < $n; $i++) { + $results[$i]->title = str_repeat(' - ', $results[$i]->level) . $results[$i]->title; + } + } + + // Output a \JSON object + echo json_encode($results); + + $app->close(); + } } diff --git a/code/administrator/components/com_menus/src/Controller/ItemsController.php b/code/administrator/components/com_menus/src/Controller/ItemsController.php index 484ee17a..5033218b 100644 --- a/code/administrator/components/com_menus/src/Controller/ItemsController.php +++ b/code/administrator/components/com_menus/src/Controller/ItemsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Menus\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Menus\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -20,6 +20,10 @@ use Joomla\Input\Input; use Joomla\Utilities\ArrayHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * The Menu Item Controller * @@ -27,247 +31,219 @@ */ class ItemsController extends AdminController { - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 1.6 - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - $this->registerTask('unsetDefault', 'setDefault'); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.6 - */ - public function getModel($name = 'Item', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to get the number of published frontend menu items for quickicons - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Items'); - - $model->setState('filter.published', 1); - $model->setState('filter.client_id', 0); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_MENUS_ITEMS_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_MENUS_ITEMS_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } - - /** - * Rebuild the nested set tree. - * - * @return boolean False on failure or error, true on success. - * - * @since 1.6 - */ - public function rebuild() - { - $this->checkToken(); - - $this->setRedirect('index.php?option=com_menus&view=items&menutype=' . $this->input->getCmd('menutype')); - - /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ - $model = $this->getModel(); - - if ($model->rebuild()) - { - // Reorder succeeded. - $this->setMessage(Text::_('COM_MENUS_ITEMS_REBUILD_SUCCESS')); - - return true; - } - else - { - // Rebuild failed. - $this->setMessage(Text::sprintf('COM_MENUS_ITEMS_REBUILD_FAILED'), 'error'); - - return false; - } - } - - /** - * Method to set the home property for a list of items - * - * @return void - * - * @since 1.6 - */ - public function setDefault() - { - // Check for request forgeries - $this->checkToken('request'); - - $app = $this->app; - - // Get items to publish from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); - $data = array('setDefault' => 1, 'unsetDefault' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($data, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Publish the items. - if (!$model->setHome($cid, $value)) - { - $this->setMessage($model->getError(), 'warning'); - } - else - { - if ($value == 1) - { - $ntext = 'COM_MENUS_ITEMS_SET_HOME'; - } - else - { - $ntext = 'COM_MENUS_ITEMS_UNSET_HOME'; - } - - $this->setMessage(Text::plural($ntext, count($cid))); - } - } - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&menutype=' . $app->getUserState('com_menus.items.menutype'), false - ) - ); - } - - /** - * Method to publish a list of items - * - * @return void - * - * @since 3.6.0 - */ - public function publish() - { - // Check for request forgeries - $this->checkToken(); - - // Get items to publish from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); - $data = array('publish' => 1, 'unpublish' => 0, 'trash' => -2, 'report' => -3); - $task = $this->getTask(); - $value = ArrayHelper::getValue($data, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - try - { - Log::add(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); - } - } - else - { - // Get the model. - $model = $this->getModel(); - - // Publish the items. - try - { - $model->publish($cid, $value); - $errors = $model->getErrors(); - $messageType = 'message'; - - if ($value == 1) - { - if ($errors) - { - $messageType = 'error'; - $ntext = $this->text_prefix . '_N_ITEMS_FAILED_PUBLISHING'; - } - else - { - $ntext = $this->text_prefix . '_N_ITEMS_PUBLISHED'; - } - } - elseif ($value == 0) - { - $ntext = $this->text_prefix . '_N_ITEMS_UNPUBLISHED'; - } - else - { - $ntext = $this->text_prefix . '_N_ITEMS_TRASHED'; - } - - $this->setMessage(Text::plural($ntext, count($cid)), $messageType); - } - catch (\Exception $e) - { - $this->setMessage($e->getMessage(), 'error'); - } - } - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list . '&menutype=' . - $this->app->getUserState('com_menus.items.menutype'), - false - ) - ); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - return '&menutype=' . $this->app->getUserState('com_menus.items.menutype'); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('unsetDefault', 'setDefault'); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Item', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to get the number of published frontend menu items for quickicons + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Items'); + + $model->setState('filter.published', 1); + $model->setState('filter.client_id', 0); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_MENUS_ITEMS_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_MENUS_ITEMS_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } + + /** + * Rebuild the nested set tree. + * + * @return boolean False on failure or error, true on success. + * + * @since 1.6 + */ + public function rebuild() + { + $this->checkToken(); + + $this->setRedirect('index.php?option=com_menus&view=items&menutype=' . $this->input->getCmd('menutype')); + + /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ + $model = $this->getModel(); + + if ($model->rebuild()) { + // Reorder succeeded. + $this->setMessage(Text::_('COM_MENUS_ITEMS_REBUILD_SUCCESS')); + + return true; + } else { + // Rebuild failed. + $this->setMessage(Text::sprintf('COM_MENUS_ITEMS_REBUILD_FAILED'), 'error'); + + return false; + } + } + + /** + * Method to set the home property for a list of items + * + * @return void + * + * @since 1.6 + */ + public function setDefault() + { + // Check for request forgeries + $this->checkToken('request'); + + $app = $this->app; + + // Get items to publish from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); + $data = array('setDefault' => 1, 'unsetDefault' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel(); + + // Publish the items. + if (!$model->setHome($cid, $value)) { + $this->setMessage($model->getError(), 'warning'); + } else { + if ($value == 1) { + $ntext = 'COM_MENUS_ITEMS_SET_HOME'; + } else { + $ntext = 'COM_MENUS_ITEMS_UNSET_HOME'; + } + + $this->setMessage(Text::plural($ntext, count($cid))); + } + } + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&menutype=' . $app->getUserState('com_menus.items.menutype'), + false + ) + ); + } + + /** + * Method to publish a list of items + * + * @return void + * + * @since 3.6.0 + */ + public function publish() + { + // Check for request forgeries + $this->checkToken(); + + // Get items to publish from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); + $data = array('publish' => 1, 'unpublish' => 0, 'trash' => -2, 'report' => -3); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + try { + Log::add(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); + } + } else { + // Get the model. + $model = $this->getModel(); + + // Publish the items. + try { + $model->publish($cid, $value); + $errors = $model->getErrors(); + $messageType = 'message'; + + if ($value == 1) { + if ($errors) { + $messageType = 'error'; + $ntext = $this->text_prefix . '_N_ITEMS_FAILED_PUBLISHING'; + } else { + $ntext = $this->text_prefix . '_N_ITEMS_PUBLISHED'; + } + } elseif ($value == 0) { + $ntext = $this->text_prefix . '_N_ITEMS_UNPUBLISHED'; + } else { + $ntext = $this->text_prefix . '_N_ITEMS_TRASHED'; + } + + $this->setMessage(Text::plural($ntext, count($cid)), $messageType); + } catch (\Exception $e) { + $this->setMessage($e->getMessage(), 'error'); + } + } + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list . '&menutype=' . + $this->app->getUserState('com_menus.items.menutype'), + false + ) + ); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + return '&menutype=' . $this->app->getUserState('com_menus.items.menutype'); + } } diff --git a/code/administrator/components/com_menus/src/Controller/MenuController.php b/code/administrator/components/com_menus/src/Controller/MenuController.php index c2d2cd53..d0485a96 100644 --- a/code/administrator/components/com_menus/src/Controller/MenuController.php +++ b/code/administrator/components/com_menus/src/Controller/MenuController.php @@ -1,4 +1,5 @@ setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); - } - - /** - * Method to save a menu item. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 1.6 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - $app = $this->app; - $data = $this->input->post->get('jform', array(), 'array'); - $context = 'com_menus.edit.menu'; - $task = $this->getTask(); - $recordId = $this->input->getInt('id'); - - // Prevent using 'main' as menutype as this is reserved for backend menus - if (strtolower($data['menutype']) == 'main') - { - $this->setMessage(Text::_('COM_MENUS_ERROR_MENUTYPE'), 'error'); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); - - return false; - } - - $data['menutype'] = InputFilter::getInstance()->clean($data['menutype'], 'TRIM'); - - // Populate the row id from the session. - $data['id'] = $recordId; - - // Get the model and attempt to validate the posted data. - /** @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ - $model = $this->getModel('Menu', '', ['ignore_request' => false]); - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); - - return false; - } - - if (isset($validData['preset'])) - { - $preset = trim($validData['preset']) ?: null; - - unset($validData['preset']); - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $app->setUserState($context . '.data', $validData); - - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); - - return false; - } - - // Import the preset selected - if (isset($preset) && $data['client_id'] == 1) - { - // Menu Type has not been saved yet. Make sure items get the real menutype. - $menutype = ApplicationHelper::stringURLSafe($data['menutype']); - - try - { - MenusHelper::installPreset($preset, $menutype); - - $this->setMessage(Text::_('COM_MENUS_PRESET_IMPORT_SUCCESS')); - } - catch (\Exception $e) - { - // Save was successful but the preset could not be loaded. Let it through with just a warning - $this->setMessage(Text::sprintf('COM_MENUS_PRESET_IMPORT_FAILED', $e->getMessage())); - } - } - else - { - $this->setMessage(Text::_('COM_MENUS_MENU_SAVE_SUCCESS')); - } - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the record data in the session. - $recordId = $model->getState($this->context . '.id'); - $this->holdEditId($context, $recordId); - $app->setUserState($context . '.data', null); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); - break; - - case 'save2new': - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $app->setUserState($context . '.data', null); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit', false)); - break; - - default: - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $app->setUserState($context . '.data', null); - - // Redirect to the list screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); - break; - } - } - - /** - * Method to display a menu as preset xml. - * - * @return boolean True if successful, false otherwise. - * - * @since 3.8.0 - */ - public function exportXml() - { - // Check for request forgeries. - $this->checkToken(); - - $cid = (array) $this->input->get('cid', array(), 'int'); - - // We know the first element is the one we need because we don't allow multi selection of rows - $id = empty($cid) ? 0 : reset($cid); - - if ($id === 0) - { - $this->setMessage(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), 'warning'); - - $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); - - return false; - } - - $model = $this->getModel('Menu'); - $item = $model->getItem($id); - - if (!$item->menutype) - { - $this->setMessage(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), 'warning'); - - $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); - - return false; - } - - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&menutype=' . $item->menutype . '&format=xml', false)); - - return true; - } + /** + * Dummy method to redirect back to standard controller + * + * @param boolean $cachable If true, the view output will be cached. + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return void + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); + } + + /** + * Method to save a menu item. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 1.6 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + $data = $this->input->post->get('jform', array(), 'array'); + $context = 'com_menus.edit.menu'; + $task = $this->getTask(); + $recordId = $this->input->getInt('id'); + + // Prevent using 'main' as menutype as this is reserved for backend menus + if (strtolower($data['menutype']) == 'main') { + $this->setMessage(Text::_('COM_MENUS_ERROR_MENUTYPE'), 'error'); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); + + return false; + } + + $data['menutype'] = InputFilter::getInstance()->clean($data['menutype'], 'TRIM'); + + // Populate the row id from the session. + $data['id'] = $recordId; + + // Get the model and attempt to validate the posted data. + /** @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ + $model = $this->getModel('Menu', '', ['ignore_request' => false]); + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); + + return false; + } + + if (isset($validData['preset'])) { + $preset = trim($validData['preset']) ?: null; + + unset($validData['preset']); + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $app->setUserState($context . '.data', $validData); + + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); + + return false; + } + + // Import the preset selected + if (isset($preset) && $data['client_id'] == 1) { + // Menu Type has not been saved yet. Make sure items get the real menutype. + $menutype = ApplicationHelper::stringURLSafe($data['menutype']); + + try { + MenusHelper::installPreset($preset, $menutype); + + $this->setMessage(Text::_('COM_MENUS_PRESET_IMPORT_SUCCESS')); + } catch (\Exception $e) { + // Save was successful but the preset could not be loaded. Let it through with just a warning + $this->setMessage(Text::sprintf('COM_MENUS_PRESET_IMPORT_FAILED', $e->getMessage())); + } + } else { + $this->setMessage(Text::_('COM_MENUS_MENU_SAVE_SUCCESS')); + } + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the record data in the session. + $recordId = $model->getState($this->context . '.id'); + $this->holdEditId($context, $recordId); + $app->setUserState($context . '.data', null); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); + break; + + case 'save2new': + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $app->setUserState($context . '.data', null); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit', false)); + break; + + default: + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $app->setUserState($context . '.data', null); + + // Redirect to the list screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); + break; + } + } + + /** + * Method to display a menu as preset xml. + * + * @return boolean True if successful, false otherwise. + * + * @since 3.8.0 + */ + public function exportXml() + { + // Check for request forgeries. + $this->checkToken(); + + $cid = (array) $this->input->get('cid', array(), 'int'); + + // We know the first element is the one we need because we don't allow multi selection of rows + $id = empty($cid) ? 0 : reset($cid); + + if ($id === 0) { + $this->setMessage(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), 'warning'); + + $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); + + return false; + } + + $model = $this->getModel('Menu'); + $item = $model->getItem($id); + + if (!$item->menutype) { + $this->setMessage(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), 'warning'); + + $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); + + return false; + } + + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&menutype=' . $item->menutype . '&format=xml', false)); + + return true; + } } diff --git a/code/administrator/components/com_menus/src/Controller/MenusController.php b/code/administrator/components/com_menus/src/Controller/MenusController.php index 935906a9..c6dad0a7 100644 --- a/code/administrator/components/com_menus/src/Controller/MenusController.php +++ b/code/administrator/components/com_menus/src/Controller/MenusController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Remove an item. - * - * @return void - * - * @since 1.6 - */ - public function delete() - { - // Check for request forgeries - $this->checkToken(); - - $user = $this->app->getIdentity(); - $cids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $cids = array_filter($cids); - - if (empty($cids)) - { - $this->setMessage(Text::_('COM_MENUS_NO_MENUS_SELECTED'), 'warning'); - } - else - { - // Access checks. - foreach ($cids as $i => $id) - { - if (!$user->authorise('core.delete', 'com_menus.menu.' . (int) $id)) - { - // Prune items that you can't change. - unset($cids[$i]); - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'error'); - } - } - - if (count($cids) > 0) - { - // Get the model. - /** @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ - $model = $this->getModel(); - - // Remove the items. - if (!$model->delete($cids)) - { - $this->setMessage($model->getError(), 'error'); - } - else - { - $this->setMessage(Text::plural('COM_MENUS_N_MENUS_DELETED', count($cids))); - } - } - } - - $this->setRedirect('index.php?option=com_menus&view=menus'); - } - - /** - * Temporary method. This should go into the 1.5 to 1.6 upgrade routines. - * - * @return void - * - * @since 1.6 - */ - public function resync() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $parts = null; - - try - { - $query->select( - [ - $db->quoteName('element'), - $db->quoteName('extension_id'), - ] - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component')); - $db->setQuery($query); - - $components = $db->loadAssocList('element', 'extension_id'); - } - catch (\RuntimeException $e) - { - $this->setMessage($e->getMessage(), 'warning'); - - return; - } - - // Load all the component menu links - $query->select( - [ - $db->quoteName('id'), - $db->quoteName('link'), - $db->quoteName('component_id'), - ] - ) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component.item')); - $db->setQuery($query); - - try - { - $items = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setMessage($e->getMessage(), 'warning'); - - return; - } - - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('component_id') . ' = :componentId') - ->where($db->quoteName('id') . ' = :itemId') - ->bind(':componentId', $componentId, ParameterType::INTEGER) - ->bind(':itemId', $itemId, ParameterType::INTEGER); - - foreach ($items as $item) - { - // Parse the link. - parse_str(parse_url($item->link, PHP_URL_QUERY), $parts); - $itemId = $item->id; - - // Tease out the option. - if (isset($parts['option'])) - { - $option = $parts['option']; - - // Lookup the component ID - if (isset($components[$option])) - { - $componentId = $components[$option]; - } - else - { - // Mismatch. Needs human intervention. - $componentId = -1; - } - - // Check for mis-matched component ids in the menu link. - if ($item->component_id != $componentId) - { - // Update the menu table. - $log = "Link $item->id refers to $item->component_id, converting to $componentId ($item->link)"; - echo "
$log"; - - try - { - $db->setQuery($query)->execute(); - } - catch (\RuntimeException $e) - { - $this->setMessage($e->getMessage(), 'warning'); - - return; - } - } - } - } - } + /** + * Display the view + * + * @param boolean $cachable If true, the view output will be cached. + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return void + * + * @since 1.6 + */ + public function display($cachable = false, $urlparams = false) + { + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Menu', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Remove an item. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + // Check for request forgeries + $this->checkToken(); + + $user = $this->app->getIdentity(); + $cids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $cids = array_filter($cids); + + if (empty($cids)) { + $this->setMessage(Text::_('COM_MENUS_NO_MENUS_SELECTED'), 'warning'); + } else { + // Access checks. + foreach ($cids as $i => $id) { + if (!$user->authorise('core.delete', 'com_menus.menu.' . (int) $id)) { + // Prune items that you can't change. + unset($cids[$i]); + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'error'); + } + } + + if (count($cids) > 0) { + // Get the model. + /** @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ + $model = $this->getModel(); + + // Remove the items. + if (!$model->delete($cids)) { + $this->setMessage($model->getError(), 'error'); + } else { + $this->setMessage(Text::plural('COM_MENUS_N_MENUS_DELETED', count($cids))); + } + } + } + + $this->setRedirect('index.php?option=com_menus&view=menus'); + } + + /** + * Temporary method. This should go into the 1.5 to 1.6 upgrade routines. + * + * @return void + * + * @since 1.6 + * + * @deprecated 5.0 Will be removed without replacement as it was only used for the 1.5 to 1.6 upgrade + */ + public function resync() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $parts = null; + + try { + $query->select( + [ + $db->quoteName('element'), + $db->quoteName('extension_id'), + ] + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')); + $db->setQuery($query); + + $components = $db->loadAssocList('element', 'extension_id'); + } catch (\RuntimeException $e) { + $this->setMessage($e->getMessage(), 'warning'); + + return; + } + + // Load all the component menu links + $query->select( + [ + $db->quoteName('id'), + $db->quoteName('link'), + $db->quoteName('component_id'), + ] + ) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component.item')); + $db->setQuery($query); + + try { + $items = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setMessage($e->getMessage(), 'warning'); + + return; + } + + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('component_id') . ' = :componentId') + ->where($db->quoteName('id') . ' = :itemId') + ->bind(':componentId', $componentId, ParameterType::INTEGER) + ->bind(':itemId', $itemId, ParameterType::INTEGER); + + foreach ($items as $item) { + // Parse the link. + parse_str(parse_url($item->link, PHP_URL_QUERY), $parts); + $itemId = $item->id; + + // Tease out the option. + if (isset($parts['option'])) { + $option = $parts['option']; + + // Lookup the component ID + if (isset($components[$option])) { + $componentId = $components[$option]; + } else { + // Mismatch. Needs human intervention. + $componentId = -1; + } + + // Check for mis-matched component ids in the menu link. + if ($item->component_id != $componentId) { + // Update the menu table. + $log = "Link $item->id refers to $item->component_id, converting to $componentId ($item->link)"; + echo "
$log"; + + try { + $db->setQuery($query)->execute(); + } catch (\RuntimeException $e) { + $this->setMessage($e->getMessage(), 'warning'); + + return; + } + } + } + } + } } diff --git a/code/administrator/components/com_menus/src/Extension/MenusComponent.php b/code/administrator/components/com_menus/src/Extension/MenusComponent.php index cc611ed3..d7996715 100644 --- a/code/administrator/components/com_menus/src/Extension/MenusComponent.php +++ b/code/administrator/components/com_menus/src/Extension/MenusComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('menus', new Menus); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('menus', new Menus()); + } } diff --git a/code/administrator/components/com_menus/src/Field/MenuItemByTypeField.php b/code/administrator/components/com_menus/src/Field/MenuItemByTypeField.php index 9808e55a..55b3d29d 100644 --- a/code/administrator/components/com_menus/src/Field/MenuItemByTypeField.php +++ b/code/administrator/components/com_menus/src/Field/MenuItemByTypeField.php @@ -1,4 +1,5 @@ $name; - } - - return parent::__get($name); - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 3.8.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'menuType': - $this->menuType = (string) $value; - break; - - case 'clientId': - $this->clientId = (int) $value; - break; - - case 'language': - case 'published': - case 'disable': - $value = (string) $value; - $this->$name = $value ? explode(',', $value) : array(); - break; - - default: - parent::__set($name, $value); - } - } - - /** - * Method to attach a JForm object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see \Joomla\CMS\Form\FormField::setup() - * @since 3.8.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $result = parent::setup($element, $value, $group); - - if ($result == true) - { - $menuType = (string) $this->element['menu_type']; - - if (!$menuType) - { - $app = Factory::getApplication(); - $currentMenuType = $app->getUserState('com_menus.items.menutype', ''); - $menuType = $app->input->getString('menutype', $currentMenuType); - } - - $this->menuType = $menuType; - $this->clientId = (int) $this->element['client_id']; - $this->published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array(); - $this->disable = $this->element['disable'] ? explode(',', (string) $this->element['disable']) : array(); - $this->language = $this->element['language'] ? explode(',', (string) $this->element['language']) : array(); - } - - return $result; - } - - /** - * Method to get the field option groups. - * - * @return array The field option objects as a nested array in groups. - * - * @since 3.8.0 - */ - protected function getGroups() - { - $groups = array(); - - $menuType = $this->menuType; - - // Get the menu items. - $items = MenusHelper::getMenuLinks($menuType, 0, 0, $this->published, $this->language, $this->clientId); - - // Build group for a specific menu type. - if ($menuType) - { - // If the menutype is empty, group the items by menutype. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - $db->setQuery($query); - - try - { - $menuTitle = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $menuTitle = $menuType; - } - - // Initialize the group. - $groups[$menuTitle] = array(); - - // Build the options array. - foreach ($items as $key => $link) - { - // Unset if item is menu_item_root - if ($link->text === 'Menu_Item_Root') - { - unset($items[$key]); - continue; - } - - $levelPrefix = str_repeat('- ', max(0, $link->level - 1)); - - // Displays language code if not set to All - if ($link->language !== '*') - { - $lang = ' (' . $link->language . ')'; - } - else - { - $lang = ''; - } - - $groups[$menuTitle][] = HTMLHelper::_('select.option', - $link->value, $levelPrefix . $link->text . $lang, - 'value', - 'text', - in_array($link->type, $this->disable) - ); - } - } - // Build groups for all menu types. - else - { - // Build the groups arrays. - foreach ($items as $menu) - { - // Initialize the group. - $groups[$menu->title] = array(); - - // Build the options array. - foreach ($menu->links as $link) - { - $levelPrefix = str_repeat('- ', max(0, $link->level - 1)); - - // Displays language code if not set to All - if ($link->language !== '*') - { - $lang = ' (' . $link->language . ')'; - } - else - { - $lang = ''; - } - - $groups[$menu->title][] = HTMLHelper::_('select.option', - $link->value, - $levelPrefix . $link->text . $lang, - 'value', - 'text', - in_array($link->type, $this->disable) - ); - } - } - } - - // Merge any additional groups in the XML definition. - $groups = array_merge(parent::getGroups(), $groups); - - return $groups; - } + /** + * The form field type. + * + * @var string + * @since 3.8.0 + */ + public $type = 'MenuItemByType'; + + /** + * The menu type. + * + * @var string + * @since 3.8.0 + */ + protected $menuType; + + /** + * The client id. + * + * @var string + * @since 3.8.0 + */ + protected $clientId; + + /** + * The language. + * + * @var array + * @since 3.8.0 + */ + protected $language; + + /** + * The published status. + * + * @var array + * @since 3.8.0 + */ + protected $published; + + /** + * The disabled status. + * + * @var array + * @since 3.8.0 + */ + protected $disable; + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 3.8.0 + */ + public function __get($name) + { + switch ($name) { + case 'menuType': + case 'clientId': + case 'language': + case 'published': + case 'disable': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 3.8.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'menuType': + $this->menuType = (string) $value; + break; + + case 'clientId': + $this->clientId = (int) $value; + break; + + case 'language': + case 'published': + case 'disable': + $value = (string) $value; + $this->$name = $value ? explode(',', $value) : array(); + break; + + default: + parent::__set($name, $value); + } + } + + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see \Joomla\CMS\Form\FormField::setup() + * @since 3.8.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); + + if ($result == true) { + $menuType = (string) $this->element['menu_type']; + + if (!$menuType) { + $app = Factory::getApplication(); + $currentMenuType = $app->getUserState('com_menus.items.menutype', ''); + $menuType = $app->input->getString('menutype', $currentMenuType); + } + + $this->menuType = $menuType; + $this->clientId = (int) $this->element['client_id']; + $this->published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array(); + $this->disable = $this->element['disable'] ? explode(',', (string) $this->element['disable']) : array(); + $this->language = $this->element['language'] ? explode(',', (string) $this->element['language']) : array(); + } + + return $result; + } + + /** + * Method to get the field option groups. + * + * @return array The field option objects as a nested array in groups. + * + * @since 3.8.0 + */ + protected function getGroups() + { + $groups = array(); + + $menuType = $this->menuType; + + // Get the menu items. + $items = MenusHelper::getMenuLinks($menuType, 0, 0, $this->published, $this->language, $this->clientId); + + // Build group for a specific menu type. + if ($menuType) { + // If the menutype is empty, group the items by menutype. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + $db->setQuery($query); + + try { + $menuTitle = $db->loadResult(); + } catch (\RuntimeException $e) { + $menuTitle = $menuType; + } + + // Initialize the group. + $groups[$menuTitle] = array(); + + // Build the options array. + foreach ($items as $key => $link) { + // Unset if item is menu_item_root + if ($link->text === 'Menu_Item_Root') { + unset($items[$key]); + continue; + } + + $levelPrefix = str_repeat('- ', max(0, $link->level - 1)); + + // Displays language code if not set to All + if ($link->language !== '*') { + $lang = ' (' . $link->language . ')'; + } else { + $lang = ''; + } + + $groups[$menuTitle][] = HTMLHelper::_( + 'select.option', + $link->value, + $levelPrefix . $link->text . $lang, + 'value', + 'text', + in_array($link->type, $this->disable) + ); + } + } else { + // Build groups for all menu types. + // Build the groups arrays. + foreach ($items as $menu) { + // Initialize the group. + $groups[$menu->title] = array(); + + // Build the options array. + foreach ($menu->links as $link) { + $levelPrefix = str_repeat('- ', max(0, $link->level - 1)); + + // Displays language code if not set to All + if ($link->language !== '*') { + $lang = ' (' . $link->language . ')'; + } else { + $lang = ''; + } + + $groups[$menu->title][] = HTMLHelper::_( + 'select.option', + $link->value, + $levelPrefix . $link->text . $lang, + 'value', + 'text', + in_array($link->type, $this->disable) + ); + } + } + } + + // Merge any additional groups in the XML definition. + $groups = array_merge(parent::getGroups(), $groups); + + return $groups; + } } diff --git a/code/administrator/components/com_menus/src/Field/MenuOrderingField.php b/code/administrator/components/com_menus/src/Field/MenuOrderingField.php index ea8ff5da..637d6d7d 100644 --- a/code/administrator/components/com_menus/src/Field/MenuOrderingField.php +++ b/code/administrator/components/com_menus/src/Field/MenuOrderingField.php @@ -1,4 +1,5 @@ form->getValue('parent_id', 0); - - if (!$parent_id) - { - return false; - } - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.client_id', 'clientId'), - ] - ) - ->from($db->quoteName('#__menu', 'a')) - - ->where($db->quoteName('a.published') . ' >= 0') - ->where($db->quoteName('a.parent_id') . ' = :parentId') - ->bind(':parentId', $parent_id, ParameterType::INTEGER); - - if ($menuType = $this->form->getValue('menutype')) - { - $query->where($db->quoteName('a.menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - } - else - { - $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('')); - } - - $query->order($db->quoteName('a.lft') . ' ASC'); - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Allow translation of custom admin menus - foreach ($options as &$option) - { - if ($option->clientId != 0) - { - $option->text = Text::_($option->text); - } - } - - $options = array_merge( - array(array('value' => '-1', 'text' => Text::_('COM_MENUS_ITEM_FIELD_ORDERING_VALUE_FIRST'))), - $options, - array(array('value' => '-2', 'text' => Text::_('COM_MENUS_ITEM_FIELD_ORDERING_VALUE_LAST'))) - ); - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); - - return $options; - } - - /** - * Method to get the field input markup. - * - * @return string The field input markup. - * - * @since 1.7 - */ - protected function getInput() - { - if ($this->form->getValue('id', 0) == 0) - { - return '' . Text::_('COM_MENUS_ITEM_FIELD_ORDERING_TEXT') . ''; - } - else - { - return parent::getInput(); - } - } + /** + * The form field type. + * + * @var string + * @since 1.7 + */ + protected $type = 'MenuOrdering'; + + /** + * Method to get the list of siblings in a menu. + * The method requires that parent be set. + * + * @return array|boolean The field option objects or false if the parent field has not been set + * + * @since 1.7 + */ + protected function getOptions() + { + $options = array(); + + // Get the parent + $parent_id = (int) $this->form->getValue('parent_id', 0); + + if (!$parent_id) { + return false; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.client_id', 'clientId'), + ] + ) + ->from($db->quoteName('#__menu', 'a')) + + ->where($db->quoteName('a.published') . ' >= 0') + ->where($db->quoteName('a.parent_id') . ' = :parentId') + ->bind(':parentId', $parent_id, ParameterType::INTEGER); + + if ($menuType = $this->form->getValue('menutype')) { + $query->where($db->quoteName('a.menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + } else { + $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('')); + } + + $query->order($db->quoteName('a.lft') . ' ASC'); + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Allow translation of custom admin menus + foreach ($options as &$option) { + if ($option->clientId != 0) { + $option->text = Text::_($option->text); + } + } + + $options = array_merge( + array(array('value' => '-1', 'text' => Text::_('COM_MENUS_ITEM_FIELD_ORDERING_VALUE_FIRST'))), + $options, + array(array('value' => '-2', 'text' => Text::_('COM_MENUS_ITEM_FIELD_ORDERING_VALUE_LAST'))) + ); + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); + + return $options; + } + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.7 + */ + protected function getInput() + { + if ($this->form->getValue('id', 0) == 0) { + return '' . Text::_('COM_MENUS_ITEM_FIELD_ORDERING_TEXT') . ''; + } else { + return parent::getInput(); + } + } } diff --git a/code/administrator/components/com_menus/src/Field/MenuParentField.php b/code/administrator/components/com_menus/src/Field/MenuParentField.php index b8236034..8c9e7f7c 100644 --- a/code/administrator/components/com_menus/src/Field/MenuParentField.php +++ b/code/administrator/components/com_menus/src/Field/MenuParentField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select( - [ - 'DISTINCT ' . $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.level'), - $db->quoteName('a.lft'), - ] - ) - ->from($db->quoteName('#__menu', 'a')); - - // Filter by menu type. - if ($menuType = $this->form->getValue('menutype')) - { - $query->where($db->quoteName('a.menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - } - else - { - // Skip special menu types - $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('')); - $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('main')); - } - - // Filter by client id. - $clientId = $this->getAttribute('clientid'); - - if (!is_null($clientId)) - { - $clientId = (int) $clientId; - $query->where($db->quoteName('a.client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - // Prevent parenting to children of this item. - if ($id = (int) $this->form->getValue('id')) - { - $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER) - ->where( - 'NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft') - . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')' - ); - } - - $query->where($db->quoteName('a.published') . ' != -2') - ->order($db->quoteName('a.lft') . ' ASC'); - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Pad the option text with spaces using depth level as a multiplier. - for ($i = 0, $n = count($options); $i < $n; $i++) - { - if ($clientId != 0) - { - // Allow translation of custom admin menus - $options[$i]->text = str_repeat('- ', $options[$i]->level) . Text::_($options[$i]->text); - } - else - { - $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->text; - } - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'MenuParent'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 1.6 + */ + protected function getOptions() + { + $options = array(); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + 'DISTINCT ' . $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.level'), + $db->quoteName('a.lft'), + ] + ) + ->from($db->quoteName('#__menu', 'a')); + + // Filter by menu type. + if ($menuType = $this->form->getValue('menutype')) { + $query->where($db->quoteName('a.menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + } else { + // Skip special menu types + $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('')); + $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('main')); + } + + // Filter by client id. + $clientId = $this->getAttribute('clientid'); + + if (!is_null($clientId)) { + $clientId = (int) $clientId; + $query->where($db->quoteName('a.client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + // Prevent parenting to children of this item. + if ($id = (int) $this->form->getValue('id')) { + $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER) + ->where( + 'NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft') + . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')' + ); + } + + $query->where($db->quoteName('a.published') . ' != -2') + ->order($db->quoteName('a.lft') . ' ASC'); + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Pad the option text with spaces using depth level as a multiplier. + for ($i = 0, $n = count($options); $i < $n; $i++) { + if ($clientId != 0) { + // Allow translation of custom admin menus + $options[$i]->text = str_repeat('- ', $options[$i]->level) . Text::_($options[$i]->text); + } else { + $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->text; + } + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); + + return $options; + } } diff --git a/code/administrator/components/com_menus/src/Field/MenuPresetField.php b/code/administrator/components/com_menus/src/Field/MenuPresetField.php index 288da3c3..5aedd1bd 100644 --- a/code/administrator/components/com_menus/src/Field/MenuPresetField.php +++ b/code/administrator/components/com_menus/src/Field/MenuPresetField.php @@ -1,4 +1,5 @@ name, Text::_($preset->title)); - } - - return array_merge(parent::getOptions(), $options); - } + /** + * The form field type. + * + * @var string + * + * @since 3.8.0 + */ + protected $type = 'MenuPreset'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.8.0 + */ + protected function getOptions() + { + $options = array(); + $presets = MenusHelper::getPresets(); + + foreach ($presets as $preset) { + $options[] = HTMLHelper::_('select.option', $preset->name, Text::_($preset->title)); + } + + return array_merge(parent::getOptions(), $options); + } } diff --git a/code/administrator/components/com_menus/src/Field/MenutypeField.php b/code/administrator/components/com_menus/src/Field/MenutypeField.php index c7844663..c579ee04 100644 --- a/code/administrator/components/com_menus/src/Field/MenutypeField.php +++ b/code/administrator/components/com_menus/src/Field/MenutypeField.php @@ -1,4 +1,5 @@ form->getValue('id'); - $size = (string) ($v = $this->element['size']) ? ' size="' . $v . '"' : ''; - $class = (string) ($v = $this->element['class']) ? ' class="form-control ' . $v . '"' : ' class="form-control"'; - $required = (string) $this->element['required'] ? ' required="required"' : ''; - $clientId = (int) $this->element['clientid'] ?: 0; - - // Get a reverse lookup of the base link URL to Title - switch ($this->value) - { - case 'url': - $value = Text::_('COM_MENUS_TYPE_EXTERNAL_URL'); - break; - - case 'alias': - $value = Text::_('COM_MENUS_TYPE_ALIAS'); - break; - - case 'separator': - $value = Text::_('COM_MENUS_TYPE_SEPARATOR'); - break; - - case 'heading': - $value = Text::_('COM_MENUS_TYPE_HEADING'); - break; - - case 'container': - $value = Text::_('COM_MENUS_TYPE_CONTAINER'); - break; - - default: - $link = $this->form->getValue('link'); - $value = ''; - - if ($link !== null) - { - $model = Factory::getApplication()->bootComponent('com_menus') - ->getMVCFactory()->createModel('Menutypes', 'Administrator', array('ignore_request' => true)); - $model->setState('client_id', $clientId); - - $rlu = $model->getReverseLookup(); - - // Clean the link back to the option, view and layout - $value = Text::_(ArrayHelper::getValue($rlu, MenusHelper::getLinkKey($link))); - } - break; - } - - $link = Route::_('index.php?option=com_menus&view=menutypes&tmpl=component&client_id=' . $clientId . '&recordId=' . $recordId); - $html[] = ''; - $html[] = ''; - $html[] = HTMLHelper::_( - 'bootstrap.renderModal', - 'menuTypeModal', - array( - 'url' => $link, - 'title' => Text::_('COM_MENUS_ITEM_FIELD_TYPE_LABEL'), - 'width' => '800px', - 'height' => '300px', - 'modalWidth' => 80, - 'bodyHeight' => 70, - 'footer' => '' - ) - ); - - // This hidden field has an ID so it can be used for showon attributes - $html[] = ''; - - return implode("\n", $html); - } + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'menutype'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $html = array(); + $recordId = (int) $this->form->getValue('id'); + $size = (string) ($v = $this->element['size']) ? ' size="' . $v . '"' : ''; + $class = (string) ($v = $this->element['class']) ? ' class="form-control ' . $v . '"' : ' class="form-control"'; + $required = (string) $this->element['required'] ? ' required="required"' : ''; + $clientId = (int) $this->element['clientid'] ?: 0; + + // Get a reverse lookup of the base link URL to Title + switch ($this->value) { + case 'url': + $value = Text::_('COM_MENUS_TYPE_EXTERNAL_URL'); + break; + + case 'alias': + $value = Text::_('COM_MENUS_TYPE_ALIAS'); + break; + + case 'separator': + $value = Text::_('COM_MENUS_TYPE_SEPARATOR'); + break; + + case 'heading': + $value = Text::_('COM_MENUS_TYPE_HEADING'); + break; + + case 'container': + $value = Text::_('COM_MENUS_TYPE_CONTAINER'); + break; + + default: + $link = $this->form->getValue('link'); + $value = ''; + + if ($link !== null) { + $model = Factory::getApplication()->bootComponent('com_menus') + ->getMVCFactory()->createModel('Menutypes', 'Administrator', array('ignore_request' => true)); + $model->setState('client_id', $clientId); + + $rlu = $model->getReverseLookup(); + + // Clean the link back to the option, view and layout + $value = Text::_(ArrayHelper::getValue($rlu, MenusHelper::getLinkKey($link))); + } + break; + } + + $link = Route::_('index.php?option=com_menus&view=menutypes&tmpl=component&client_id=' . $clientId . '&recordId=' . $recordId); + $html[] = ''; + $html[] = ''; + $html[] = HTMLHelper::_( + 'bootstrap.renderModal', + 'menuTypeModal', + array( + 'url' => $link, + 'title' => Text::_('COM_MENUS_ITEM_FIELD_TYPE_LABEL'), + 'width' => '800px', + 'height' => '300px', + 'modalWidth' => 80, + 'bodyHeight' => 70, + 'footer' => '' + ) + ); + + // This hidden field has an ID so it can be used for showon attributes + $html[] = ''; + + return implode("\n", $html); + } } diff --git a/code/administrator/components/com_menus/src/Field/Modal/MenuField.php b/code/administrator/components/com_menus/src/Field/Modal/MenuField.php index 69b2b21f..79fad3f6 100644 --- a/code/administrator/components/com_menus/src/Field/Modal/MenuField.php +++ b/code/administrator/components/com_menus/src/Field/Modal/MenuField.php @@ -1,4 +1,5 @@ $name; - } - - return parent::__get($name); - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 3.7.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'allowSelect': - case 'allowClear': - case 'allowNew': - case 'allowEdit': - case 'allowPropagate': - $value = (string) $value; - $this->$name = !($value === 'false' || $value === 'off' || $value === '0'); - break; - - default: - parent::__set($name, $value); - } - } - - /** - * Method to attach a JForm object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 3.7.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); - - if ($return) - { - $this->allowSelect = ((string) $this->element['select']) !== 'false'; - $this->allowClear = ((string) $this->element['clear']) !== 'false'; - $this->allowPropagate = ((string) $this->element['propagate']) === 'true'; - - // Creating/editing menu items is not supported in frontend. - $isAdministrator = Factory::getApplication()->isClient('administrator'); - $this->allowNew = $isAdministrator ? ((string) $this->element['new']) === 'true' : false; - $this->allowEdit = $isAdministrator ? ((string) $this->element['edit']) === 'true' : false; - } - - return $return; - } - - /** - * Method to get the field input markup. - * - * @return string The field input markup. - * - * @since 3.7.0 - */ - protected function getInput() - { - $clientId = (int) $this->element['clientid']; - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language - Factory::getLanguage()->load('com_menus', JPATH_ADMINISTRATOR); - - // The active article id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Item_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($this->allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'Modal_Menu'; + + /** + * Determinate, if the select button is shown + * + * @var boolean + * @since 3.7.0 + */ + protected $allowSelect = true; + + /** + * Determinate, if the clear button is shown + * + * @var boolean + * @since 3.7.0 + */ + protected $allowClear = true; + + /** + * Determinate, if the create button is shown + * + * @var boolean + * @since 3.7.0 + */ + protected $allowNew = false; + + /** + * Determinate, if the edit button is shown + * + * @var boolean + * @since 3.7.0 + */ + protected $allowEdit = false; + + /** + * Determinate, if the propagate button is shown + * + * @var boolean + * @since 3.9.0 + */ + protected $allowPropagate = false; + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 3.7.0 + */ + public function __get($name) + { + switch ($name) { + case 'allowSelect': + case 'allowClear': + case 'allowNew': + case 'allowEdit': + case 'allowPropagate': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 3.7.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'allowSelect': + case 'allowClear': + case 'allowNew': + case 'allowEdit': + case 'allowPropagate': + $value = (string) $value; + $this->$name = !($value === 'false' || $value === 'off' || $value === '0'); + break; + + default: + parent::__set($name, $value); + } + } + + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 3.7.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); + + if ($return) { + $this->allowSelect = ((string) $this->element['select']) !== 'false'; + $this->allowClear = ((string) $this->element['clear']) !== 'false'; + $this->allowPropagate = ((string) $this->element['propagate']) === 'true'; + + // Creating/editing menu items is not supported in frontend. + $isAdministrator = Factory::getApplication()->isClient('administrator'); + $this->allowNew = $isAdministrator ? ((string) $this->element['new']) === 'true' : false; + $this->allowEdit = $isAdministrator ? ((string) $this->element['edit']) === 'true' : false; + } + + return $return; + } + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 3.7.0 + */ + protected function getInput() + { + $clientId = (int) $this->element['clientid']; + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language + Factory::getLanguage()->load('com_menus', JPATH_ADMINISTRATOR); + + // The active article id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Item_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($this->allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectMenu_" . $this->id . " = function (id, title, object) { window.processModalSelect('Item', '" . $this->id . "', id, title, '', object); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkSuffix = '&layout=modal&client_id=' . $clientId . '&tmpl=component&' . Session::getFormToken() . '=1'; - $linkItems = 'index.php?option=com_menus&view=items' . $linkSuffix; - $linkItem = 'index.php?option=com_menus&view=item' . $linkSuffix; - $modalTitle = Text::_('COM_MENUS_SELECT_A_MENUITEM'); - - if (isset($this->element['language'])) - { - $linkItems .= '&forcedLanguage=' . $this->element['language']; - $linkItem .= '&forcedLanguage=' . $this->element['language']; - $modalTitle .= ' — ' . $this->element['label']; - } - - $urlSelect = $linkItems . '&function=jSelectMenu_' . $this->id; - $urlEdit = $linkItem . '&task=item.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkItem . '&task=item.add'; - - if ($value) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $value, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - // Placeholder if option is present or not - if (empty($title)) - { - if ($this->element->option && (string) $this->element->option['value'] == '') - { - $title_holder = Text::_($this->element->option); - } - else - { - $title_holder = Text::_('COM_MENUS_SELECT_A_MENUITEM'); - } - } - - $title = empty($title) ? $title_holder : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current menu item display field. - $html = ''; - - if ($this->allowSelect || $this->allowNew || $this->allowEdit || $this->allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select menu item button - if ($this->allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New menu item button - if ($this->allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit menu item button - if ($this->allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear menu item button - if ($this->allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate menu item button - if ($this->allowPropagate && count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectMenu_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($this->allowSelect || $this->allowNew || $this->allowEdit || $this->allowClear) - { - $html .= ''; - } - - // Select menu item modal - if ($this->allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New menu item modal - if ($this->allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_MENUS_NEW_MENUITEM'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit menu item modal - if ($this->allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_MENUS_EDIT_MENUITEM'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Note: class='required' for client side validation. - $class = $this->required ? ' class="required modal-value"' : ''; - - // Placeholder if option is present or not when clearing field - if ($this->element->option && (string) $this->element->option['value'] == '') - { - $title_holder = Text::_($this->element->option); - } - else - { - $title_holder = Text::_('COM_MENUS_SELECT_A_MENUITEM'); - } - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.7.0 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkSuffix = '&layout=modal&client_id=' . $clientId . '&tmpl=component&' . Session::getFormToken() . '=1'; + $linkItems = 'index.php?option=com_menus&view=items' . $linkSuffix; + $linkItem = 'index.php?option=com_menus&view=item' . $linkSuffix; + $modalTitle = Text::_('COM_MENUS_SELECT_A_MENUITEM'); + + if (isset($this->element['language'])) { + $linkItems .= '&forcedLanguage=' . $this->element['language']; + $linkItem .= '&forcedLanguage=' . $this->element['language']; + $modalTitle .= ' — ' . $this->element['label']; + } + + $urlSelect = $linkItems . '&function=jSelectMenu_' . $this->id; + $urlEdit = $linkItem . '&task=item.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkItem . '&task=item.add'; + + if ($value) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $value, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + // Placeholder if option is present or not + if (empty($title)) { + if ($this->element->option && (string) $this->element->option['value'] == '') { + $title_holder = Text::_($this->element->option); + } else { + $title_holder = Text::_('COM_MENUS_SELECT_A_MENUITEM'); + } + } + + $title = empty($title) ? $title_holder : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current menu item display field. + $html = ''; + + if ($this->allowSelect || $this->allowNew || $this->allowEdit || $this->allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select menu item button + if ($this->allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New menu item button + if ($this->allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit menu item button + if ($this->allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear menu item button + if ($this->allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate menu item button + if ($this->allowPropagate && count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectMenu_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($this->allowSelect || $this->allowNew || $this->allowEdit || $this->allowClear) { + $html .= ''; + } + + // Select menu item modal + if ($this->allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New menu item modal + if ($this->allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_MENUS_NEW_MENUITEM'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit menu item modal + if ($this->allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_MENUS_EDIT_MENUITEM'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Note: class='required' for client side validation. + $class = $this->required ? ' class="required modal-value"' : ''; + + // Placeholder if option is present or not when clearing field + if ($this->element->option && (string) $this->element->option['value'] == '') { + $title_holder = Text::_($this->element->option); + } else { + $title_holder = Text::_('COM_MENUS_SELECT_A_MENUITEM'); + } + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.7.0 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/code/administrator/components/com_menus/src/Helper/AssociationsHelper.php b/code/administrator/components/com_menus/src/Helper/AssociationsHelper.php index 14f0c37d..41a58cf9 100644 --- a/code/administrator/components/com_menus/src/Helper/AssociationsHelper.php +++ b/code/administrator/components/com_menus/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getType($typeName); - - $context = $this->extension . '.item'; - - // Get the associations. - $associations = Associations::getAssociations( - $this->extension, - $type['tables']['a'], - $context, - $id, - 'id', - 'alias', - '' - ); - - return $associations; - } - - /** - * Get item information - * - * @param string $typeName The item type - * @param int $id The id of item for which we need the associated items - * - * @return Table|null - * - * @since 3.7.0 - */ - public function getItem($typeName, $id) - { - if (empty($id)) - { - return null; - } - - $table = null; - - switch ($typeName) - { - case 'item': - $table = Table::getInstance('menu'); - break; - } - - if (is_null($table)) - { - return null; - } - - $table->load($id); - - return $table; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - if (in_array($typeName, $this->itemTypes)) - { - switch ($typeName) - { - case 'item': - $fields['ordering'] = 'a.lft'; - $fields['level'] = 'a.level'; - $fields['catid'] = ''; - $fields['state'] = 'a.published'; - $fields['created_user_id'] = ''; - $fields['menutype'] = 'a.menutype'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['level'] = true; - - $tables = array( - 'a' => '#__menu' - ); - - $title = 'menu'; - break; - } - } - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_menus'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array('item'); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = true; + + /** + * Method to get the associations for a given item. + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 4.0.0 + */ + public function getAssociationsForItem($id = 0, $view = null) + { + return []; + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociations($typeName, $id) + { + $type = $this->getType($typeName); + + $context = $this->extension . '.item'; + + // Get the associations. + $associations = Associations::getAssociations( + $this->extension, + $type['tables']['a'], + $context, + $id, + 'id', + 'alias', + '' + ); + + return $associations; + } + + /** + * Get item information + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return Table|null + * + * @since 3.7.0 + */ + public function getItem($typeName, $id) + { + if (empty($id)) { + return null; + } + + $table = null; + + switch ($typeName) { + case 'item': + $table = Table::getInstance('menu'); + break; + } + + if (is_null($table)) { + return null; + } + + $table->load($id); + + return $table; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + if (in_array($typeName, $this->itemTypes)) { + switch ($typeName) { + case 'item': + $fields['ordering'] = 'a.lft'; + $fields['level'] = 'a.level'; + $fields['catid'] = ''; + $fields['state'] = 'a.published'; + $fields['created_user_id'] = ''; + $fields['menutype'] = 'a.menutype'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['level'] = true; + + $tables = array( + 'a' => '#__menu' + ); + + $title = 'menu'; + break; + } + } + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } } diff --git a/code/administrator/components/com_menus/src/Helper/MenusHelper.php b/code/administrator/components/com_menus/src/Helper/MenusHelper.php index 7d515381..18a6c733 100644 --- a/code/administrator/components/com_menus/src/Helper/MenusHelper.php +++ b/code/administrator/components/com_menus/src/Helper/MenusHelper.php @@ -1,4 +1,5 @@ $value) - { - if ((!in_array($name, self::$_filter)) && (!($name == 'task' && !array_key_exists('view', $request)))) - { - // Remove the variables we want to ignore. - unset($request[$name]); - } - } - - ksort($request); - - return 'index.php?' . http_build_query($request, '', '&'); - } - - /** - * Get the menu list for create a menu module - * - * @param int $clientId Optional client id - viz 0 = site, 1 = administrator, can be NULL for all - * - * @return array The menu array list - * - * @since 1.6 - */ - public static function getMenuTypes($clientId = 0) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('a.menutype')) - ->from($db->quoteName('#__menu_types', 'a')); - - if (isset($clientId)) - { - $clientId = (int) $clientId; - $query->where($db->quoteName('a.client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Get a list of menu links for one or all menus. - * - * @param string $menuType An option menu to filter the list on, otherwise all menu with given client id links - * are returned as a grouped array. - * @param integer $parentId An optional parent ID to pivot results around. - * @param integer $mode An optional mode. If parent ID is set and mode=2, the parent and children are excluded from the list. - * @param array $published An optional array of states - * @param array $languages Optional array of specify which languages we want to filter - * @param int $clientId Optional client id - viz 0 = site, 1 = administrator, can be NULL for all (used only if menutype not given) - * - * @return array|boolean - * - * @since 1.6 - */ - public static function getMenuLinks($menuType = null, $parentId = 0, $mode = 0, $published = array(), $languages = array(), $clientId = 0) - { - $hasClientId = $clientId !== null; - $clientId = (int) $clientId; - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - 'DISTINCT ' . $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.alias'), - $db->quoteName('a.level'), - $db->quoteName('a.menutype'), - $db->quoteName('a.client_id'), - $db->quoteName('a.type'), - $db->quoteName('a.published'), - $db->quoteName('a.template_style_id'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.language'), - $db->quoteName('a.lft'), - $db->quoteName('e.name', 'componentname'), - $db->quoteName('e.element'), - ] - ) - ->from($db->quoteName('#__menu', 'a')) - ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')); - - if (Multilanguage::isEnabled()) - { - $query->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('l.sef', 'language_sef'), - ] - ) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); - } - - // Filter by the type if given, this is more specific than client id - if ($menuType) - { - $query->where('(' . $db->quoteName('a.menutype') . ' = :menuType OR ' . $db->quoteName('a.parent_id') . ' = 0)') - ->bind(':menuType', $menuType); - } - elseif ($hasClientId) - { - $query->where($db->quoteName('a.client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - // Prevent the parent and children from showing if requested. - if ($parentId && $mode == 2) - { - $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :parentId') - ->where( - '(' . $db->quoteName('a.lft') . ' <= ' . $db->quoteName('p.lft') - . ' OR ' . $db->quoteName('a.rgt') . ' >= ' . $db->quoteName('p.rgt') . ')' - ) - ->bind(':parentId', $parentId, ParameterType::INTEGER); - } - - if (!empty($languages)) - { - $query->whereIn($db->quoteName('a.language'), (array) $languages, ParameterType::STRING); - } - - if (!empty($published)) - { - $query->whereIn($db->quoteName('a.published'), (array) $published); - } - - $query->where($db->quoteName('a.published') . ' != -2'); - $query->order($db->quoteName('a.lft') . ' ASC'); - - try - { - // Get the options. - $db->setQuery($query); - $links = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - if (empty($menuType)) - { - // If the menutype is empty, group the items by menutype. - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('menutype') . ' <> ' . $db->quote('')) - ->order( - [ - $db->quoteName('title'), - $db->quoteName('menutype'), - ] - ); - - if ($hasClientId) - { - $query->where($db->quoteName('client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - try - { - $db->setQuery($query); - $menuTypes = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Create a reverse lookup and aggregate the links. - $rlu = array(); - - foreach ($menuTypes as &$type) - { - $rlu[$type->menutype] = & $type; - $type->links = array(); - } - - // Loop through the list of menu links. - foreach ($links as &$link) - { - if (isset($rlu[$link->menutype])) - { - $rlu[$link->menutype]->links[] = & $link; - - // Cleanup garbage. - unset($link->menutype); - } - } - - return $menuTypes; - } - else - { - return $links; - } - } - - /** - * Get the associations - * - * @param integer $pk Menu item id - * - * @return array - * - * @since 3.0 - */ - public static function getAssociations($pk) - { - $langAssociations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', $pk, 'id', '', ''); - $associations = array(); - - foreach ($langAssociations as $langAssociation) - { - $associations[$langAssociation->language] = $langAssociation->id; - } - - return $associations; - } - - /** - * Load the menu items from database for the given menutype - * - * @param string $menutype The selected menu type - * @param boolean $enabledOnly Whether to load only enabled/published menu items. - * @param int[] $exclude The menu items to exclude from the list - * - * @return AdministratorMenuItem A root node with the menu items as children - * - * @since 4.0.0 - */ - public static function getMenuItems($menutype, $enabledOnly = false, $exclude = array()) - { - $root = new AdministratorMenuItem; - $db = Factory::getContainer()->get(DatabaseInterface::class); - $query = $db->getQuery(true); - - // Prepare the query. - $query->select($db->quoteName('m') . '.*') - ->from($db->quoteName('#__menu', 'm')) - ->where( - [ - $db->quoteName('m.menutype') . ' = :menutype', - $db->quoteName('m.client_id') . ' = 1', - $db->quoteName('m.id') . ' > 1', - ] - ) - ->bind(':menutype', $menutype); - - if ($enabledOnly) - { - $query->where($db->quoteName('m.published') . ' = 1'); - } - - // Filter on the enabled states. - $query->select($db->quoteName('e.element')) - ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) - ->extendWhere( - 'AND', - [ - $db->quoteName('e.enabled') . ' = 1', - $db->quoteName('e.enabled') . ' IS NULL', - ], - 'OR' - ); - - if (count($exclude)) - { - $exId = array_map('intval', array_filter($exclude, 'is_numeric')); - $exEl = array_filter($exclude, 'is_string'); - - if ($exId) - { - $query->whereNotIn($db->quoteName('m.id'), $exId) - ->whereNotIn($db->quoteName('m.parent_id'), $exId); - } - - if ($exEl) - { - $query->whereNotIn($db->quoteName('e.element'), $exEl, ParameterType::STRING); - } - } - - // Order by lft. - $query->order($db->quoteName('m.lft')); - - try - { - $menuItems = []; - $iterator = $db->setQuery($query)->getIterator(); - - foreach ($iterator as $item) - { - $menuItems[$item->id] = new AdministratorMenuItem((array) $item); - } - - unset($iterator); - - foreach ($menuItems as $menuitem) - { - // Resolve the alias item to get the original item - if ($menuitem->type == 'alias') - { - static::resolveAlias($menuitem); - } - - if ($menuitem->link = in_array($menuitem->type, array('separator', 'heading', 'container')) ? '#' : trim($menuitem->link)) - { - $menuitem->submenu = array(); - $menuitem->class = $menuitem->img ?? ''; - $menuitem->scope = $menuitem->scope ?? null; - $menuitem->target = $menuitem->browserNav ? '_blank' : ''; - } - - $menuitem->ajaxbadge = $menuitem->getParams()->get('ajax-badge'); - $menuitem->dashboard = $menuitem->getParams()->get('dashboard'); - - if ($menuitem->parent_id > 1) - { - if (isset($menuItems[$menuitem->parent_id])) - { - $menuItems[$menuitem->parent_id]->addChild($menuitem); - } - } - else - { - $root->addChild($menuitem); - } - } - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage(Text::_('JERROR_AN_ERROR_HAS_OCCURRED'), 'error'); - } - - return $root; - } - - /** - * Method to install a preset menu into database and link them to the given menutype - * - * @param string $preset The preset name - * @param string $menutype The target menutype - * - * @return void - * - * @throws \Exception - * - * @since 4.0.0 - */ - public static function installPreset($preset, $menutype) - { - $root = static::loadPreset($preset, false); - - if (count($root->getChildren()) == 0) - { - throw new \Exception(Text::_('COM_MENUS_PRESET_LOAD_FAILED')); - } - - static::installPresetItems($root, $menutype); - } - - /** - * Method to install a preset menu item into database and link it to the given menutype - * - * @param AdministratorMenuItem $node The parent node of the items to process - * @param string $menutype The target menutype - * - * @return void - * - * @throws \Exception - * - * @since 4.0.0 - */ - protected static function installPresetItems($node, $menutype) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $items = $node->getChildren(); - - static $components = array(); - - if (!$components) - { - $query->select( - [ - $db->quoteName('extension_id'), - $db->quoteName('element'), - ] - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component')); - $components = $db->setQuery($query)->loadObjectList(); - $components = array_column((array) $components, 'element', 'extension_id'); - } - - Factory::getApplication()->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.import', &$items, null, true)); - - foreach ($items as $item) - { - /** @var \Joomla\CMS\Table\Menu $table */ - $table = Table::getInstance('Menu'); - - $item->alias = $menutype . '-' . $item->title; - - // Temporarily set unicodeslugs if a menu item has an unicode alias - $unicode = Factory::getApplication()->set('unicodeslugs', 1); - $item->alias = ApplicationHelper::stringURLSafe($item->alias); - Factory::getApplication()->set('unicodeslugs', $unicode); - - if ($item->type == 'separator') - { - // Do not reuse a separator - $item->title = $item->title ?: '-'; - $item->alias = microtime(true); - } - elseif ($item->type == 'heading' || $item->type == 'container') - { - // Try to match an existing record to have minimum collision for a heading - $keys = array( - 'menutype' => $menutype, - 'type' => $item->type, - 'title' => $item->title, - 'parent_id' => (int) $item->getParent()->id, - 'client_id' => 1, - ); - $table->load($keys); - } - elseif ($item->type == 'url' || $item->type == 'component') - { - if (substr($item->link, 0, 8) === 'special:') - { - $special = substr($item->link, 8); - - if ($special === 'language-forum') - { - $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; - } - elseif ($special === 'custom-forum') - { - $item->link = ''; - } - } - - // Try to match an existing record to have minimum collision for a link - $keys = array( - 'menutype' => $menutype, - 'type' => $item->type, - 'link' => $item->link, - 'parent_id' => (int) $item->getParent()->id, - 'client_id' => 1, - ); - $table->load($keys); - } - - // Translate "hideitems" param value from "element" into "menu-item-id" - if ($item->type == 'container' && count($hideitems = (array) $item->getParams()->get('hideitems'))) - { - foreach ($hideitems as &$hel) - { - if (!is_numeric($hel)) - { - $hel = array_search($hel, $components); - } - } - - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->whereIn($db->quoteName('component_id'), $hideitems); - $hideitems = $db->setQuery($query)->loadColumn(); - - $item->getParams()->set('hideitems', $hideitems); - } - - $record = array( - 'menutype' => $menutype, - 'title' => $item->title, - 'alias' => $item->alias, - 'type' => $item->type, - 'link' => $item->link, - 'browserNav' => $item->browserNav, - 'img' => $item->class, - 'access' => $item->access, - 'component_id' => array_search($item->element, $components) ?: 0, - 'parent_id' => (int) $item->getParent()->id, - 'client_id' => 1, - 'published' => 1, - 'language' => '*', - 'home' => 0, - 'params' => (string) $item->getParams(), - ); - - if (!$table->bind($record)) - { - throw new \Exception($table->getError()); - } - - $table->setLocation($item->getParent()->id, 'last-child'); - - if (!$table->check()) - { - throw new \Exception($table->getError()); - } - - if (!$table->store()) - { - throw new \Exception($table->getError()); - } - - $item->id = $table->get('id'); - - if ($item->hasChildren()) - { - static::installPresetItems($item, $menutype); - } - } - } - - /** - * Add a custom preset externally via plugin or any other means. - * WARNING: Presets with same name will replace previously added preset *except* Joomla's default preset (joomla) - * - * @param string $name The unique identifier for the preset. - * @param string $title The display label for the preset. - * @param string $path The path to the preset file. - * @param bool $replace Whether to replace the preset with the same name if any (except 'joomla'). - * - * @return void - * - * @since 4.0.0 - */ - public static function addPreset($name, $title, $path, $replace = true) - { - if (static::$presets === null) - { - static::getPresets(); - } - - if ($name == 'joomla') - { - $replace = false; - } - - if (($replace || !array_key_exists($name, static::$presets)) && is_file($path)) - { - $preset = new \stdClass; - - $preset->name = $name; - $preset->title = $title; - $preset->path = $path; - - static::$presets[$name] = $preset; - } - } - - /** - * Get a list of available presets. - * - * @return \stdClass[] - * - * @since 4.0.0 - */ - public static function getPresets() - { - if (static::$presets === null) - { - // Important: 'null' will cause infinite recursion. - static::$presets = array(); - - $components = ComponentHelper::getComponents(); - $lang = Factory::getApplication()->getLanguage(); - - foreach ($components as $component) - { - if (!$component->enabled) - { - continue; - } - - $folder = JPATH_ADMINISTRATOR . '/components/' . $component->option . '/presets/'; - - if (!Folder::exists($folder)) - { - continue; - } - - $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component->option); - - $presets = Folder::files($folder, '.xml'); - - foreach ($presets as $preset) - { - $name = File::stripExt($preset); - $title = strtoupper($component->option . '_MENUS_PRESET_' . $name); - static::addPreset($name, $title, $folder . $preset); - } - } - - // Load from template folder automatically - $app = Factory::getApplication(); - $tpl = JPATH_THEMES . '/' . $app->getTemplate() . '/html/com_menus/presets'; - - if (is_dir($tpl)) - { - $files = Folder::files($tpl, '\.xml$'); - - foreach ($files as $file) - { - $name = substr($file, 0, -4); - $title = str_replace('-', ' ', $name); - - static::addPreset(strtolower($name), ucwords($title), $tpl . '/' . $file); - } - } - } - - return static::$presets; - } - - /** - * Load the menu items from a preset file into a hierarchical list of objects - * - * @param string $name The preset name - * @param bool $fallback Fallback to default (joomla) preset if the specified one could not be loaded? - * @param AdministratorMenuItem $parent Root node of the menu - * - * @return AdministratorMenuItem - * - * @since 4.0.0 - */ - public static function loadPreset($name, $fallback = true, $parent = null) - { - $presets = static::getPresets(); - - if (!$parent) - { - $parent = new AdministratorMenuItem; - } - - if (isset($presets[$name]) && ($xml = simplexml_load_file($presets[$name]->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) - { - static::loadXml($xml, $parent); - } - elseif ($fallback && isset($presets['default'])) - { - if (($xml = simplexml_load_file($presets['default']->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) - { - static::loadXml($xml, $parent); - } - } - - return $parent; - } - - /** - * Method to resolve the menu item alias type menu item - * - * @param AdministratorMenuItem &$item The alias object - * - * @return void - * - * @since 4.0.0 - */ - public static function resolveAlias(&$item) - { - $obj = $item; - - while ($obj->type == 'alias') - { - $aliasTo = (int) $obj->getParams()->get('aliasoptions'); - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('a.id'), - $db->quoteName('a.link'), - $db->quoteName('a.type'), - $db->quoteName('e.element'), - ] - ) - ->from($db->quoteName('#__menu', 'a')) - ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')) - ->where($db->quoteName('a.id') . ' = :aliasTo') - ->bind(':aliasTo', $aliasTo, ParameterType::INTEGER); - - try - { - $obj = new AdministratorMenuItem($db->setQuery($query)->loadAssoc()); - - if (!$obj) - { - $item->link = ''; - - return; - } - } - catch (\Exception $e) - { - $item->link = ''; - - return; - } - } - - $item->id = $obj->id; - $item->link = $obj->link; - $item->type = $obj->type; - $item->element = $obj->element; - } - - /** - * Parse the flat list of menu items and prepare the hierarchy of them using parent-child relationship. - * - * @param AdministratorMenuItem $item Menu item to preprocess - * - * @return void - * - * @since 4.0.0 - */ - public static function preprocess($item) - { - // Resolve the alias item to get the original item - if ($item->type == 'alias') - { - static::resolveAlias($item); - } - - if ($item->link = in_array($item->type, array('separator', 'heading', 'container')) ? '#' : trim($item->link)) - { - $item->class = $item->img ?? ''; - $item->scope = $item->scope ?? null; - $item->target = $item->browserNav ? '_blank' : ''; - } - } - - /** - * Load a menu tree from an XML file - * - * @param \SimpleXMLElement[] $elements The xml menuitem nodes - * @param AdministratorMenuItem $parent The menu hierarchy list to be populated - * @param string[] $replace The substring replacements for iterator type items - * - * @return void - * - * @since 4.0.0 - */ - protected static function loadXml($elements, $parent, $replace = array()) - { - foreach ($elements as $element) - { - if ($element->getName() != 'menuitem') - { - continue; - } - - $select = (string) $element['sql_select']; - $from = (string) $element['sql_from']; - - /** - * Following is a repeatable group based on simple database query. This requires sql_* attributes (sql_select and sql_from are required) - * The values can be used like - "{sql:columnName}" in any attribute of repeated elements. - * The repeated elements are place inside this xml node but they will be populated in the same level in the rendered menu - */ - if ($select && $from) - { - $hidden = $element['hidden'] == 'true'; - $where = (string) $element['sql_where']; - $order = (string) $element['sql_order']; - $group = (string) $element['sql_group']; - $lJoin = (string) $element['sql_leftjoin']; - $iJoin = (string) $element['sql_innerjoin']; - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select($select)->from($from); - - if ($where) - { - $query->where($where); - } - - if ($order) - { - $query->order($order); - } - - if ($group) - { - $query->group($group); - } - - if ($lJoin) - { - $query->join('LEFT', $lJoin); - } - - if ($iJoin) - { - $query->join('INNER', $iJoin); - } - - $results = $db->setQuery($query)->loadObjectList(); - - // Skip the entire group if no items to iterate over. - if ($results) - { - // Show the repeatable group heading node only if not set as hidden. - if (!$hidden) - { - $child = static::parseXmlNode($element, $replace); - $parent->addChild($child); - } - - // Iterate over the matching records, items goes in the same level (not $item->submenu) as this node. - if ('self' == (string) $element['sql_target']) - { - foreach ($results as $result) - { - static::loadXml($element->menuitem, $child, $result); - } - } - else - { - foreach ($results as $result) - { - static::loadXml($element->menuitem, $parent, $result); - } - } - } - } - else - { - $item = static::parseXmlNode($element, $replace); - - // Process the child nodes - static::loadXml($element->menuitem, $item, $replace); - - $parent->addChild($item); - } - } - } - - /** - * Create a menu item node from an xml element - * - * @param \SimpleXMLElement $node A menuitem element from preset xml - * @param string[] $replace The values to substitute in the title, link and element texts - * - * @return \stdClass - * - * @since 4.0.0 - */ - protected static function parseXmlNode($node, $replace = array()) - { - $item = new AdministratorMenuItem; - - $item->id = null; - $item->type = (string) $node['type']; - $item->title = (string) $node['title']; - $item->alias = (string) $node['alias']; - $item->link = (string) $node['link']; - $item->target = (string) $node['target']; - $item->element = (string) $node['element']; - $item->class = (string) $node['class']; - $item->icon = (string) $node['icon']; - $item->access = (int) $node['access']; - $item->scope = (string) $node['scope'] ?: 'default'; - $item->ajaxbadge = (string) $node['ajax-badge']; - $item->dashboard = (string) $node['dashboard']; - - $params = new Registry(trim($node->params)); - $params->set('menu-permission', (string) $node['permission']); - - if ($item->type == 'separator' && trim($item->title, '- ')) - { - $params->set('text_separator', 1); - } - - if ($item->type == 'heading' || $item->type == 'container') - { - $item->link = '#'; - } - - if ((string) $node['quicktask']) - { - $params->set('menu-quicktask', (string) $node['quicktask']); - $params->set('menu-quicktask-title', (string) $node['quicktask-title']); - $params->set('menu-quicktask-icon', (string) $node['quicktask-icon']); - $params->set('menu-quicktask-permission', (string) $node['quicktask-permission']); - } - - // Translate attributes for iterator values - foreach ($replace as $var => $val) - { - $item->title = str_replace("{sql:$var}", $val, $item->title); - $item->element = str_replace("{sql:$var}", $val, $item->element); - $item->link = str_replace("{sql:$var}", $val, $item->link); - $item->class = str_replace("{sql:$var}", $val, $item->class); - $item->icon = str_replace("{sql:$var}", $val, $item->icon); - $params->set('menu-quicktask', str_replace("{sql:$var}", $val, $params->get('menu-quicktask'))); - } - - $item->setParams($params); - - return $item; - } + /** + * Defines the valid request variables for the reverse lookup. + * + * @var array + */ + protected static $_filter = array('option', 'view', 'layout'); + + /** + * List of preset include paths + * + * @var array + * + * @since 4.0.0 + */ + protected static $presets = null; + + /** + * Gets a standard form of a link for lookups. + * + * @param mixed $request A link string or array of request variables. + * + * @return mixed A link in standard option-view-layout form, or false if the supplied response is invalid. + * + * @since 1.6 + */ + public static function getLinkKey($request) + { + if (empty($request)) { + return false; + } + + // Check if the link is in the form of index.php?... + if (is_string($request)) { + $args = array(); + + if (strpos($request, 'index.php') === 0) { + parse_str(parse_url(htmlspecialchars_decode($request), PHP_URL_QUERY), $args); + } else { + parse_str($request, $args); + } + + $request = $args; + } + + // Only take the option, view and layout parts. + foreach ($request as $name => $value) { + if ((!in_array($name, self::$_filter)) && (!($name == 'task' && !array_key_exists('view', $request)))) { + // Remove the variables we want to ignore. + unset($request[$name]); + } + } + + ksort($request); + + return 'index.php?' . http_build_query($request, '', '&'); + } + + /** + * Get the menu list for create a menu module + * + * @param int $clientId Optional client id - viz 0 = site, 1 = administrator, can be NULL for all + * + * @return array The menu array list + * + * @since 1.6 + */ + public static function getMenuTypes($clientId = 0) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('a.menutype')) + ->from($db->quoteName('#__menu_types', 'a')); + + if (isset($clientId)) { + $clientId = (int) $clientId; + $query->where($db->quoteName('a.client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Get a list of menu links for one or all menus. + * + * @param string $menuType An option menu to filter the list on, otherwise all menu with given client id links + * are returned as a grouped array. + * @param integer $parentId An optional parent ID to pivot results around. + * @param integer $mode An optional mode. If parent ID is set and mode=2, the parent and children are excluded from the list. + * @param array $published An optional array of states + * @param array $languages Optional array of specify which languages we want to filter + * @param int $clientId Optional client id - viz 0 = site, 1 = administrator, can be NULL for all (used only if menutype not given) + * + * @return array|boolean + * + * @since 1.6 + */ + public static function getMenuLinks($menuType = null, $parentId = 0, $mode = 0, $published = array(), $languages = array(), $clientId = 0) + { + $hasClientId = $clientId !== null; + $clientId = (int) $clientId; + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + 'DISTINCT ' . $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.alias'), + $db->quoteName('a.level'), + $db->quoteName('a.menutype'), + $db->quoteName('a.client_id'), + $db->quoteName('a.type'), + $db->quoteName('a.published'), + $db->quoteName('a.template_style_id'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.language'), + $db->quoteName('a.lft'), + $db->quoteName('e.name', 'componentname'), + $db->quoteName('e.element'), + ] + ) + ->from($db->quoteName('#__menu', 'a')) + ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')); + + if (Multilanguage::isEnabled()) { + $query->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('l.sef', 'language_sef'), + ] + ) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); + } + + // Filter by the type if given, this is more specific than client id + if ($menuType) { + $query->where('(' . $db->quoteName('a.menutype') . ' = :menuType OR ' . $db->quoteName('a.parent_id') . ' = 0)') + ->bind(':menuType', $menuType); + } elseif ($hasClientId) { + $query->where($db->quoteName('a.client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + // Prevent the parent and children from showing if requested. + if ($parentId && $mode == 2) { + $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :parentId') + ->where( + '(' . $db->quoteName('a.lft') . ' <= ' . $db->quoteName('p.lft') + . ' OR ' . $db->quoteName('a.rgt') . ' >= ' . $db->quoteName('p.rgt') . ')' + ) + ->bind(':parentId', $parentId, ParameterType::INTEGER); + } + + if (!empty($languages)) { + $query->whereIn($db->quoteName('a.language'), (array) $languages, ParameterType::STRING); + } + + if (!empty($published)) { + $query->whereIn($db->quoteName('a.published'), (array) $published); + } + + $query->where($db->quoteName('a.published') . ' != -2'); + $query->order($db->quoteName('a.lft') . ' ASC'); + + try { + // Get the options. + $db->setQuery($query); + $links = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + if (empty($menuType)) { + // If the menutype is empty, group the items by menutype. + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('menutype') . ' <> ' . $db->quote('')) + ->order( + [ + $db->quoteName('title'), + $db->quoteName('menutype'), + ] + ); + + if ($hasClientId) { + $query->where($db->quoteName('client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + try { + $db->setQuery($query); + $menuTypes = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Create a reverse lookup and aggregate the links. + $rlu = array(); + + foreach ($menuTypes as &$type) { + $rlu[$type->menutype] = & $type; + $type->links = array(); + } + + // Loop through the list of menu links. + foreach ($links as &$link) { + if (isset($rlu[$link->menutype])) { + $rlu[$link->menutype]->links[] = & $link; + + // Cleanup garbage. + unset($link->menutype); + } + } + + return $menuTypes; + } else { + return $links; + } + } + + /** + * Get the associations + * + * @param integer $pk Menu item id + * + * @return array + * + * @since 3.0 + */ + public static function getAssociations($pk) + { + $langAssociations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', $pk, 'id', '', ''); + $associations = array(); + + foreach ($langAssociations as $langAssociation) { + $associations[$langAssociation->language] = $langAssociation->id; + } + + return $associations; + } + + /** + * Load the menu items from database for the given menutype + * + * @param string $menutype The selected menu type + * @param boolean $enabledOnly Whether to load only enabled/published menu items. + * @param int[] $exclude The menu items to exclude from the list + * + * @return AdministratorMenuItem A root node with the menu items as children + * + * @since 4.0.0 + */ + public static function getMenuItems($menutype, $enabledOnly = false, $exclude = array()) + { + $root = new AdministratorMenuItem(); + $db = Factory::getContainer()->get(DatabaseInterface::class); + $query = $db->getQuery(true); + + // Prepare the query. + $query->select($db->quoteName('m') . '.*') + ->from($db->quoteName('#__menu', 'm')) + ->where( + [ + $db->quoteName('m.menutype') . ' = :menutype', + $db->quoteName('m.client_id') . ' = 1', + $db->quoteName('m.id') . ' > 1', + ] + ) + ->bind(':menutype', $menutype); + + if ($enabledOnly) { + $query->where($db->quoteName('m.published') . ' = 1'); + } + + // Filter on the enabled states. + $query->select($db->quoteName('e.element')) + ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) + ->extendWhere( + 'AND', + [ + $db->quoteName('e.enabled') . ' = 1', + $db->quoteName('e.enabled') . ' IS NULL', + ], + 'OR' + ); + + if (count($exclude)) { + $exId = array_map('intval', array_filter($exclude, 'is_numeric')); + $exEl = array_filter($exclude, 'is_string'); + + if ($exId) { + $query->whereNotIn($db->quoteName('m.id'), $exId) + ->whereNotIn($db->quoteName('m.parent_id'), $exId); + } + + if ($exEl) { + $query->whereNotIn($db->quoteName('e.element'), $exEl, ParameterType::STRING); + } + } + + // Order by lft. + $query->order($db->quoteName('m.lft')); + + try { + $menuItems = []; + $iterator = $db->setQuery($query)->getIterator(); + + foreach ($iterator as $item) { + $menuItems[$item->id] = new AdministratorMenuItem((array) $item); + } + + unset($iterator); + + foreach ($menuItems as $menuitem) { + // Resolve the alias item to get the original item + if ($menuitem->type == 'alias') { + static::resolveAlias($menuitem); + } + + if ($menuitem->link = in_array($menuitem->type, array('separator', 'heading', 'container')) ? '#' : trim($menuitem->link)) { + $menuitem->submenu = array(); + $menuitem->class = $menuitem->img ?? ''; + $menuitem->scope = $menuitem->scope ?? null; + $menuitem->target = $menuitem->browserNav ? '_blank' : ''; + } + + $menuitem->ajaxbadge = $menuitem->getParams()->get('ajax-badge'); + $menuitem->dashboard = $menuitem->getParams()->get('dashboard'); + + if ($menuitem->parent_id > 1) { + if (isset($menuItems[$menuitem->parent_id])) { + $menuItems[$menuitem->parent_id]->addChild($menuitem); + } + } else { + $root->addChild($menuitem); + } + } + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_AN_ERROR_HAS_OCCURRED'), 'error'); + } + + return $root; + } + + /** + * Method to install a preset menu into database and link them to the given menutype + * + * @param string $preset The preset name + * @param string $menutype The target menutype + * + * @return void + * + * @throws \Exception + * + * @since 4.0.0 + */ + public static function installPreset($preset, $menutype) + { + $root = static::loadPreset($preset, false); + + if (count($root->getChildren()) == 0) { + throw new \Exception(Text::_('COM_MENUS_PRESET_LOAD_FAILED')); + } + + static::installPresetItems($root, $menutype); + } + + /** + * Method to install a preset menu item into database and link it to the given menutype + * + * @param AdministratorMenuItem $node The parent node of the items to process + * @param string $menutype The target menutype + * + * @return void + * + * @throws \Exception + * + * @since 4.0.0 + */ + protected static function installPresetItems($node, $menutype) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $items = $node->getChildren(); + + static $components = array(); + + if (!$components) { + $query->select( + [ + $db->quoteName('extension_id'), + $db->quoteName('element'), + ] + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')); + $components = $db->setQuery($query)->loadObjectList(); + $components = array_column((array) $components, 'element', 'extension_id'); + } + + Factory::getApplication()->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.import', &$items, null, true)); + + foreach ($items as $item) { + /** @var \Joomla\CMS\Table\Menu $table */ + $table = Table::getInstance('Menu'); + + $item->alias = $menutype . '-' . $item->title; + + // Temporarily set unicodeslugs if a menu item has an unicode alias + $unicode = Factory::getApplication()->set('unicodeslugs', 1); + $item->alias = ApplicationHelper::stringURLSafe($item->alias); + Factory::getApplication()->set('unicodeslugs', $unicode); + + if ($item->type == 'separator') { + // Do not reuse a separator + $item->title = $item->title ?: '-'; + $item->alias = microtime(true); + } elseif ($item->type == 'heading' || $item->type == 'container') { + // Try to match an existing record to have minimum collision for a heading + $keys = array( + 'menutype' => $menutype, + 'type' => $item->type, + 'title' => $item->title, + 'parent_id' => (int) $item->getParent()->id, + 'client_id' => 1, + ); + $table->load($keys); + } elseif ($item->type == 'url' || $item->type == 'component') { + if (substr($item->link, 0, 8) === 'special:') { + $special = substr($item->link, 8); + + if ($special === 'language-forum') { + $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; + } elseif ($special === 'custom-forum') { + $item->link = ''; + } + } + + // Try to match an existing record to have minimum collision for a link + $keys = array( + 'menutype' => $menutype, + 'type' => $item->type, + 'link' => $item->link, + 'parent_id' => (int) $item->getParent()->id, + 'client_id' => 1, + ); + $table->load($keys); + } + + // Translate "hideitems" param value from "element" into "menu-item-id" + if ($item->type == 'container' && count($hideitems = (array) $item->getParams()->get('hideitems'))) { + foreach ($hideitems as &$hel) { + if (!is_numeric($hel)) { + $hel = array_search($hel, $components); + } + } + + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->whereIn($db->quoteName('component_id'), $hideitems); + $hideitems = $db->setQuery($query)->loadColumn(); + + $item->getParams()->set('hideitems', $hideitems); + } + + $record = array( + 'menutype' => $menutype, + 'title' => $item->title, + 'alias' => $item->alias, + 'type' => $item->type, + 'link' => $item->link, + 'browserNav' => $item->browserNav, + 'img' => $item->class, + 'access' => $item->access, + 'component_id' => array_search($item->element, $components) ?: 0, + 'parent_id' => (int) $item->getParent()->id, + 'client_id' => 1, + 'published' => 1, + 'language' => '*', + 'home' => 0, + 'params' => (string) $item->getParams(), + ); + + if (!$table->bind($record)) { + throw new \Exception($table->getError()); + } + + $table->setLocation($item->getParent()->id, 'last-child'); + + if (!$table->check()) { + throw new \Exception($table->getError()); + } + + if (!$table->store()) { + throw new \Exception($table->getError()); + } + + $item->id = $table->get('id'); + + if ($item->hasChildren()) { + static::installPresetItems($item, $menutype); + } + } + } + + /** + * Add a custom preset externally via plugin or any other means. + * WARNING: Presets with same name will replace previously added preset *except* Joomla's default preset (joomla) + * + * @param string $name The unique identifier for the preset. + * @param string $title The display label for the preset. + * @param string $path The path to the preset file. + * @param bool $replace Whether to replace the preset with the same name if any (except 'joomla'). + * + * @return void + * + * @since 4.0.0 + */ + public static function addPreset($name, $title, $path, $replace = true) + { + if (static::$presets === null) { + static::getPresets(); + } + + if ($name == 'joomla') { + $replace = false; + } + + if (($replace || !array_key_exists($name, static::$presets)) && is_file($path)) { + $preset = new \stdClass(); + + $preset->name = $name; + $preset->title = $title; + $preset->path = $path; + + static::$presets[$name] = $preset; + } + } + + /** + * Get a list of available presets. + * + * @return \stdClass[] + * + * @since 4.0.0 + */ + public static function getPresets() + { + if (static::$presets === null) { + // Important: 'null' will cause infinite recursion. + static::$presets = array(); + + $components = ComponentHelper::getComponents(); + $lang = Factory::getApplication()->getLanguage(); + + foreach ($components as $component) { + if (!$component->enabled) { + continue; + } + + $folder = JPATH_ADMINISTRATOR . '/components/' . $component->option . '/presets/'; + + if (!Folder::exists($folder)) { + continue; + } + + $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component->option); + + $presets = Folder::files($folder, '.xml'); + + foreach ($presets as $preset) { + $name = File::stripExt($preset); + $title = strtoupper($component->option . '_MENUS_PRESET_' . $name); + static::addPreset($name, $title, $folder . $preset); + } + } + + // Load from template folder automatically + $app = Factory::getApplication(); + $tpl = JPATH_THEMES . '/' . $app->getTemplate() . '/html/com_menus/presets'; + + if (is_dir($tpl)) { + $files = Folder::files($tpl, '\.xml$'); + + foreach ($files as $file) { + $name = substr($file, 0, -4); + $title = str_replace('-', ' ', $name); + + static::addPreset(strtolower($name), ucwords($title), $tpl . '/' . $file); + } + } + } + + return static::$presets; + } + + /** + * Load the menu items from a preset file into a hierarchical list of objects + * + * @param string $name The preset name + * @param bool $fallback Fallback to default (joomla) preset if the specified one could not be loaded? + * @param AdministratorMenuItem $parent Root node of the menu + * + * @return AdministratorMenuItem + * + * @since 4.0.0 + */ + public static function loadPreset($name, $fallback = true, $parent = null) + { + $presets = static::getPresets(); + + if (!$parent) { + $parent = new AdministratorMenuItem(); + } + + if (isset($presets[$name]) && ($xml = simplexml_load_file($presets[$name]->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) { + static::loadXml($xml, $parent); + } elseif ($fallback && isset($presets['default'])) { + if (($xml = simplexml_load_file($presets['default']->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) { + static::loadXml($xml, $parent); + } + } + + return $parent; + } + + /** + * Method to resolve the menu item alias type menu item + * + * @param AdministratorMenuItem &$item The alias object + * + * @return void + * + * @since 4.0.0 + */ + public static function resolveAlias(&$item) + { + $obj = $item; + + while ($obj->type == 'alias') { + $aliasTo = (int) $obj->getParams()->get('aliasoptions'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('a.id'), + $db->quoteName('a.link'), + $db->quoteName('a.type'), + $db->quoteName('e.element'), + ] + ) + ->from($db->quoteName('#__menu', 'a')) + ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')) + ->where($db->quoteName('a.id') . ' = :aliasTo') + ->bind(':aliasTo', $aliasTo, ParameterType::INTEGER); + + try { + $obj = new AdministratorMenuItem($db->setQuery($query)->loadAssoc()); + + if (!$obj) { + $item->link = ''; + + return; + } + } catch (\Exception $e) { + $item->link = ''; + + return; + } + } + + $item->id = $obj->id; + $item->link = $obj->link; + $item->type = $obj->type; + $item->element = $obj->element; + } + + /** + * Parse the flat list of menu items and prepare the hierarchy of them using parent-child relationship. + * + * @param AdministratorMenuItem $item Menu item to preprocess + * + * @return void + * + * @since 4.0.0 + */ + public static function preprocess($item) + { + // Resolve the alias item to get the original item + if ($item->type == 'alias') { + static::resolveAlias($item); + } + + if ($item->link = in_array($item->type, array('separator', 'heading', 'container')) ? '#' : trim($item->link)) { + $item->class = $item->img ?? ''; + $item->scope = $item->scope ?? null; + $item->target = $item->browserNav ? '_blank' : ''; + } + } + + /** + * Load a menu tree from an XML file + * + * @param \SimpleXMLElement[] $elements The xml menuitem nodes + * @param AdministratorMenuItem $parent The menu hierarchy list to be populated + * @param string[] $replace The substring replacements for iterator type items + * + * @return void + * + * @since 4.0.0 + */ + protected static function loadXml($elements, $parent, $replace = array()) + { + foreach ($elements as $element) { + if ($element->getName() != 'menuitem') { + continue; + } + + $select = (string) $element['sql_select']; + $from = (string) $element['sql_from']; + + /** + * Following is a repeatable group based on simple database query. This requires sql_* attributes (sql_select and sql_from are required) + * The values can be used like - "{sql:columnName}" in any attribute of repeated elements. + * The repeated elements are place inside this xml node but they will be populated in the same level in the rendered menu + */ + if ($select && $from) { + $hidden = $element['hidden'] == 'true'; + $where = (string) $element['sql_where']; + $order = (string) $element['sql_order']; + $group = (string) $element['sql_group']; + $lJoin = (string) $element['sql_leftjoin']; + $iJoin = (string) $element['sql_innerjoin']; + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select($select)->from($from); + + if ($where) { + $query->where($where); + } + + if ($order) { + $query->order($order); + } + + if ($group) { + $query->group($group); + } + + if ($lJoin) { + $query->join('LEFT', $lJoin); + } + + if ($iJoin) { + $query->join('INNER', $iJoin); + } + + $results = $db->setQuery($query)->loadObjectList(); + + // Skip the entire group if no items to iterate over. + if ($results) { + // Show the repeatable group heading node only if not set as hidden. + if (!$hidden) { + $child = static::parseXmlNode($element, $replace); + $parent->addChild($child); + } + + // Iterate over the matching records, items goes in the same level (not $item->submenu) as this node. + if ('self' == (string) $element['sql_target']) { + foreach ($results as $result) { + static::loadXml($element->menuitem, $child, $result); + } + } else { + foreach ($results as $result) { + static::loadXml($element->menuitem, $parent, $result); + } + } + } + } else { + $item = static::parseXmlNode($element, $replace); + + // Process the child nodes + static::loadXml($element->menuitem, $item, $replace); + + $parent->addChild($item); + } + } + } + + /** + * Create a menu item node from an xml element + * + * @param \SimpleXMLElement $node A menuitem element from preset xml + * @param string[] $replace The values to substitute in the title, link and element texts + * + * @return \stdClass + * + * @since 4.0.0 + */ + protected static function parseXmlNode($node, $replace = array()) + { + $item = new AdministratorMenuItem(); + + $item->id = null; + $item->type = (string) $node['type']; + $item->title = (string) $node['title']; + $item->alias = (string) $node['alias']; + $item->link = (string) $node['link']; + $item->target = (string) $node['target']; + $item->element = (string) $node['element']; + $item->class = (string) $node['class']; + $item->icon = (string) $node['icon']; + $item->access = (int) $node['access']; + $item->scope = (string) $node['scope'] ?: 'default'; + $item->ajaxbadge = (string) $node['ajax-badge']; + $item->dashboard = (string) $node['dashboard']; + + $params = new Registry(trim($node->params)); + $params->set('menu-permission', (string) $node['permission']); + + if ($item->type == 'separator' && trim($item->title, '- ')) { + $params->set('text_separator', 1); + } + + if ($item->type == 'heading' || $item->type == 'container') { + $item->link = '#'; + } + + if ((string) $node['quicktask']) { + $params->set('menu-quicktask', (string) $node['quicktask']); + $params->set('menu-quicktask-title', (string) $node['quicktask-title']); + $params->set('menu-quicktask-icon', (string) $node['quicktask-icon']); + $params->set('menu-quicktask-permission', (string) $node['quicktask-permission']); + } + + // Translate attributes for iterator values + foreach ($replace as $var => $val) { + $item->title = str_replace("{sql:$var}", $val, $item->title); + $item->element = str_replace("{sql:$var}", $val, $item->element); + $item->link = str_replace("{sql:$var}", $val, $item->link); + $item->class = str_replace("{sql:$var}", $val, $item->class); + $item->icon = str_replace("{sql:$var}", $val, $item->icon); + $params->set('menu-quicktask', str_replace("{sql:$var}", $val, $params->get('menu-quicktask'))); + } + + $item->setParams($params); + + return $item; + } } diff --git a/code/administrator/components/com_menus/src/Model/ItemModel.php b/code/administrator/components/com_menus/src/Model/ItemModel.php index 493e66af..2d0be714 100644 --- a/code/administrator/components/com_menus/src/Model/ItemModel.php +++ b/code/administrator/components/com_menus/src/Model/ItemModel.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage' - ); - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - $menuTypeId = 0; - - if (!empty($record->menutype)) - { - $menuTypeId = $this->getMenuTypeId($record->menutype); - } - - return Factory::getUser()->authorise('core.delete', 'com_menus.menu.' . (int) $menuTypeId); - } - - /** - * Method to test whether the state of a record can be edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. - * - * @since 3.6 - */ - protected function canEditState($record) - { - $menuTypeId = !empty($record->menutype) ? $this->getMenuTypeId($record->menutype) : 0; - $assetKey = $menuTypeId ? 'com_menus.menu.' . (int) $menuTypeId : 'com_menus'; - - return Factory::getUser()->authorise('core.edit.state', $assetKey); - } - - /** - * Batch copy menu items to a new menu or parent. - * - * @param integer $value The new menu or sub-item. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return mixed An array of new IDs on success, boolean false on failure. - * - * @since 1.6 - */ - protected function batchCopy($value, $pks, $contexts) - { - // $value comes as {menutype}.{parent_id} - $parts = explode('.', $value); - $menuType = $parts[0]; - $parentId = ArrayHelper::getValue($parts, 1, 0, 'int'); - - $table = $this->getTable(); - $db = $this->getDbo(); - $query = $db->getQuery(true); - $newIds = array(); - - // Check that the parent exists - if ($parentId) - { - if (!$table->load($parentId)) - { - if ($error = $table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Non-fatal error - $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); - $parentId = 0; - } - } - } - - // If the parent is 0, set it to the ID of the root item in the tree - if (empty($parentId)) - { - if (!$parentId = $table->getRootId()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Check that user has create permission for menus - $user = Factory::getUser(); - - $menuTypeId = (int) $this->getMenuTypeId($menuType); - - if (!$user->authorise('core.create', 'com_menus.menu.' . $menuTypeId)) - { - $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_CREATE')); - - return false; - } - - // We need to log the parent ID - $parents = array(); - - // Calculate the emergency stop count as a precaution against a runaway loop bug - $query->select('COUNT(' . $db->quoteName('id') . ')') - ->from($db->quoteName('#__menu')); - $db->setQuery($query); - - try - { - $count = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Parent exists so let's proceed - while (!empty($pks) && $count > 0) - { - // Pop the first id off the stack - $pk = array_shift($pks); - - $table->reset(); - - // Check that the row actually exists - if (!$table->load($pk)) - { - if ($error = $table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - // Copy is a bit tricky, because we also need to copy the children - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('lft') . ' > :lft', - $db->quoteName('rgt') . ' < :rgt', - ] - ) - ->bind(':lft', $table->lft, ParameterType::INTEGER) - ->bind(':rgt', $table->rgt, ParameterType::INTEGER); - $db->setQuery($query); - $childIds = $db->loadColumn(); - - // Add child IDs to the array only if they aren't already there. - foreach ($childIds as $childId) - { - if (!in_array($childId, $pks)) - { - $pks[] = $childId; - } - } - - // Make a copy of the old ID and Parent ID - $oldId = $table->id; - $oldParentId = $table->parent_id; - - // Reset the id because we are making a copy. - $table->id = 0; - - // If we a copying children, the Old ID will turn up in the parents list - // otherwise it's a new top level item - $table->parent_id = isset($parents[$oldParentId]) ? $parents[$oldParentId] : $parentId; - $table->menutype = $menuType; - - // Set the new location in the tree for the node. - $table->setLocation($table->parent_id, 'last-child'); - - // @todo: Deal with ordering? - // $table->ordering = 1; - $table->level = null; - $table->lft = null; - $table->rgt = null; - $table->home = 0; - - // Alter the title & alias - list($title, $alias) = $this->generateNewTitle($table->parent_id, $table->alias, $table->title); - $table->title = $title; - $table->alias = $alias; - - // Check the row. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Store the row. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Get the new item ID - $newId = $table->get('id'); - - // Add the new ID to the array - $newIds[$pk] = $newId; - - // Now we log the old 'parent' to the new 'parent' - $parents[$oldId] = $table->id; - $count--; - } - - // Rebuild the hierarchy. - if (!$table->rebuild()) - { - $this->setError($table->getError()); - - return false; - } - - // Rebuild the tree path. - if (!$table->rebuildPath($table->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Clean the cache - $this->cleanCache(); - - return $newIds; - } - - /** - * Batch move menu items to a new menu or parent. - * - * @param integer $value The new menu or sub-item. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True on success. - * - * @since 1.6 - */ - protected function batchMove($value, $pks, $contexts) - { - // $value comes as {menutype}.{parent_id} - $parts = explode('.', $value); - $menuType = $parts[0]; - $parentId = ArrayHelper::getValue($parts, 1, 0, 'int'); - - $table = $this->getTable(); - $db = $this->getDbo(); - - // Check that the parent exists. - if ($parentId) - { - if (!$table->load($parentId)) - { - if ($error = $table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Non-fatal error - $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); - $parentId = 0; - } - } - } - - // Check that user has create and edit permission for menus - $user = Factory::getUser(); - - $menuTypeId = (int) $this->getMenuTypeId($menuType); - - if (!$user->authorise('core.create', 'com_menus.menu.' . $menuTypeId)) - { - $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_CREATE')); - - return false; - } - - if (!$user->authorise('core.edit', 'com_menus.menu.' . $menuTypeId)) - { - $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_EDIT')); - - return false; - } - - // We are going to store all the children and just moved the menutype - $children = array(); - - // Parent exists so let's proceed - foreach ($pks as $pk) - { - // Check that the row actually exists - if (!$table->load($pk)) - { - if ($error = $table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - // Set the new location in the tree for the node. - $table->setLocation($parentId, 'last-child'); - - // Set the new Parent Id - $table->parent_id = $parentId; - - // Check if we are moving to a different menu - if ($menuType != $table->menutype) - { - // Add the child node ids to the children array. - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('lft') . ' BETWEEN :lft AND :rgt') - ->bind(':lft', $table->lft, ParameterType::INTEGER) - ->bind(':rgt', $table->rgt, ParameterType::INTEGER); - $db->setQuery($query); - $children = array_merge($children, (array) $db->loadColumn()); - } - - // Check the row. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Store the row. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Rebuild the tree path. - if (!$table->rebuildPath()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Process the child rows - if (!empty($children)) - { - // Remove any duplicates and sanitize ids. - $children = array_unique($children); - $children = ArrayHelper::toInteger($children); - - // Update the menutype field in all nodes where necessary. - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('menutype') . ' = :menuType') - ->whereIn($db->quoteName('id'), $children) - ->bind(':menuType', $menuType); - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to check if you can save a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function canSave($data = array(), $key = 'id') - { - return Factory::getUser()->authorise('core.edit', $this->option); - } - - /** - * Method to get the row form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return mixed A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // The folder and element vars are passed when saving the form. - if (empty($data)) - { - $item = $this->getItem(); - - // The type should already be set. - $this->setState('item.link', $item->link); - } - else - { - $this->setState('item.link', ArrayHelper::getValue($data, 'link')); - $this->setState('item.type', ArrayHelper::getValue($data, 'type')); - } - - $clientId = $this->getState('item.client_id'); - - // Get the form. - if ($clientId == 1) - { - $form = $this->loadForm('com_menus.item.admin', 'itemadmin', array('control' => 'jform', 'load_data' => $loadData), true); - } - else - { - $form = $this->loadForm('com_menus.item', 'item', array('control' => 'jform', 'load_data' => $loadData), true); - } - - if (empty($form)) - { - return false; - } - - if ($loadData) - { - $data = $this->loadFormData(); - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('menuordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is an article you can edit. - $form->setFieldAttribute('menuordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - // Filter available menus - $action = $this->getState('item.id') > 0 ? 'edit' : 'create'; - - $form->setFieldAttribute('menutype', 'accesstype', $action); - $form->setFieldAttribute('type', 'clientid', $clientId); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data, providing it has an ID and it is the same. - $itemData = (array) $this->getItem(); - - // When a new item is requested, unset the access as it will be set later from the filter - if (empty($itemData['id'])) - { - unset($itemData['access']); - } - - $sessionData = (array) Factory::getApplication()->getUserState('com_menus.edit.item.data', array()); - - // Only merge if there is a session and itemId or itemid is null. - if (isset($sessionData['id']) && isset($itemData['id']) && $sessionData['id'] === $itemData['id'] - || is_null($itemData['id'])) - { - $data = array_merge($itemData, $sessionData); - } - else - { - $data = $itemData; - } - - // For a new menu item, pre-select some filters (Status, Language, Access) in edit form if those have been selected in Menu Manager - if (empty($data['id'])) - { - // Get selected fields - $filters = Factory::getApplication()->getUserState('com_menus.items.filter'); - $data['parent_id'] = $data['parent_id'] ?? ($filters['parent_id'] ?? null); - $data['published'] = $data['published'] ?? ($filters['published'] ?? null); - $data['language'] = $data['language'] ?? ($filters['language'] ?? null); - $data['access'] = $data['access'] ?? ($filters['access'] ?? Factory::getApplication()->get('access')); - } - - if (isset($data['menutype']) && !$this->getState('item.menutypeid')) - { - $menuTypeId = (int) $this->getMenuTypeId($data['menutype']); - - $this->setState('item.menutypeid', $menuTypeId); - } - - $data = (object) $data; - - $this->preprocessData('com_menus.item', $data); - - return $data; - } - - /** - * Get the necessary data to load an item help screen. - * - * @return object An object with key, url, and local properties for loading the item help screen. - * - * @since 1.6 - */ - public function getHelp() - { - return (object) array('key' => $this->helpKey, 'url' => $this->helpURL, 'local' => $this->helpLocal); - } - - /** - * Method to get a menu item. - * - * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. - * - * @return mixed Menu item data object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('item.id'); - - // Get a level row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $table->load($pk); - - // Check for a table object error. - if ($error = $table->getError()) - { - $this->setError($error); - - return false; - } - - // Prime required properties. - - if ($type = $this->getState('item.type')) - { - $table->type = $type; - } - - if (empty($table->id)) - { - $table->parent_id = $this->getState('item.parent_id'); - $table->menutype = $this->getState('item.menutype'); - $table->client_id = $this->getState('item.client_id'); - $table->params = '{}'; - } - - // If the link has been set in the state, possibly changing link type. - if ($link = $this->getState('item.link')) - { - // Check if we are changing away from the actual link type. - if (MenusHelper::getLinkKey($table->link) !== MenusHelper::getLinkKey($link) && (int) $table->id === (int) $this->getState('item.id')) - { - $table->link = $link; - } - } - - switch ($table->type) - { - case 'alias': - case 'url': - $table->component_id = 0; - $args = array(); - - if ($table->link) - { - $q = parse_url($table->link, PHP_URL_QUERY); - - if ($q) - { - parse_str($q, $args); - } - } - - break; - - case 'separator': - case 'heading': - case 'container': - $table->link = ''; - $table->component_id = 0; - break; - - case 'component': - default: - // Enforce a valid type. - $table->type = 'component'; - - // Ensure the integrity of the component_id field is maintained, particularly when changing the menu item type. - $args = []; - - if ($table->link) - { - $q = parse_url($table->link, PHP_URL_QUERY); - - if ($q) - { - parse_str($q, $args); - } - } - - if (isset($args['option'])) - { - // Load the language file for the component. - $lang = Factory::getLanguage(); - $lang->load($args['option'], JPATH_ADMINISTRATOR) - || $lang->load($args['option'], JPATH_ADMINISTRATOR . '/components/' . $args['option']); - - // Determine the component id. - $component = ComponentHelper::getComponent($args['option']); - - if (isset($component->id)) - { - $table->component_id = $component->id; - } - } - break; - } - - // We have a valid type, inject it into the state for forms to use. - $this->setState('item.type', $table->type); - - // Convert to the \Joomla\CMS\Object\CMSObject before adding the params. - $properties = $table->getProperties(1); - $result = ArrayHelper::toObject($properties); - - // Convert the params field to an array. - $registry = new Registry($table->params); - $result->params = $registry->toArray(); - - // Merge the request arguments in to the params for a component. - if ($table->type == 'component') - { - // Note that all request arguments become reserved parameter names. - $result->request = $args; - $result->params = array_merge($result->params, $args); - - // Special case for the Login menu item. - // Display the login or logout redirect URL fields if not empty - if ($table->link == 'index.php?option=com_users&view=login') - { - if (!empty($result->params['login_redirect_url'])) - { - $result->params['loginredirectchoice'] = '0'; - } - - if (!empty($result->params['logout_redirect_url'])) - { - $result->params['logoutredirectchoice'] = '0'; - } - } - } - - if ($table->type == 'alias') - { - // Note that all request arguments become reserved parameter names. - $result->params = array_merge($result->params, $args); - } - - if ($table->type == 'url') - { - // Note that all request arguments become reserved parameter names. - $result->params = array_merge($result->params, $args); - } - - // Load associated menu items, only supported for frontend for now - if ($this->getState('item.client_id') == 0 && Associations::isEnabled()) - { - if ($pk != null) - { - $result->associations = MenusHelper::getAssociations($pk); - } - else - { - $result->associations = array(); - } - } - - $result->menuordering = $pk; - - return $result; - } - - /** - * Get the list of modules not in trash. - * - * @return mixed An array of module records (id, title, position), or false on error. - * - * @since 1.6 - */ - public function getModules() - { - $clientId = (int) $this->getState('item.client_id'); - $id = (int) $this->getState('item.id'); - - // Currently any setting that affects target page for a backend menu is not supported, hence load no modules. - if ($clientId == 1) - { - return false; - } - - $db = $this->getDbo(); - $query = $db->getQuery(true); - - /** - * Join on the module-to-menu mapping table. - * We are only interested if the module is displayed on ALL or THIS menu item (or the inverse ID number). - * sqlsrv changes for modulelink to menu manager - */ - $query->select( - [ - $db->quoteName('a.id'), - $db->quoteName('a.title'), - $db->quoteName('a.position'), - $db->quoteName('a.published'), - $db->quoteName('map.menuid'), - ] - ) - ->from($db->quoteName('#__modules', 'a')) - ->join( - 'LEFT', - $db->quoteName('#__modules_menu', 'map'), - $db->quoteName('map.moduleid') . ' = ' . $db->quoteName('a.id') - . ' AND ' . $db->quoteName('map.menuid') . ' IN (' . implode(',', $query->bindArray([0, $id, -$id])) . ')' - ); - - $subQuery = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__modules_menu')) - ->where( - [ - $db->quoteName('moduleid') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('menuid') . ' < 0', - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('except')); - - // Join on the asset groups table. - $query->select($db->quoteName('ag.title', 'access_title')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->where( - [ - $db->quoteName('a.published') . ' >= 0', - $db->quoteName('a.client_id') . ' = :clientId', - ] - ) - ->bind(':clientId', $clientId, ParameterType::INTEGER) - ->order( - [ - $db->quoteName('a.position'), - $db->quoteName('a.ordering'), - ] - ); - - $db->setQuery($query); - - try - { - $result = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $result; - } - - /** - * Get the list of all view levels - * - * @return \stdClass[]|boolean An array of all view levels (id, title). - * - * @since 3.4 - */ - public function getViewLevels() - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Get all the available view levels - $query->select($db->quoteName('id')) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__viewlevels')) - ->order($db->quoteName('id')); - - $db->setQuery($query); - - try - { - $result = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $result; - } - - /** - * Returns a Table object, always creating it - * - * @param string $type The table type to instantiate. - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\Cms\Table\Table|\Joomla\Cms\Table\Nested A database object. - * - * @since 1.6 - */ - public function getTable($type = 'Menu', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * A protected method to get the where clause for the reorder. - * This ensures that the row will be moved relative to a row with the same menutype. - * - * @param \Joomla\CMS\Table\Menu $table - * - * @return array An array of conditions to add to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('menutype') . ' = ' . $this->_db->quote($table->menutype), - ]; - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState('item.id', $pk); - - if (!$app->isClient('api')) - { - $parentId = $app->getUserState('com_menus.edit.item.parent_id'); - $menuType = $app->getUserStateFromRequest('com_menus.items.menutype', 'menutype', '', 'string'); - } - else - { - $parentId = null; - $menuType = $app->input->get('com_menus.items.menutype'); - } - - if (!$parentId) - { - $parentId = $app->input->getInt('parent_id'); - } - - $this->setState('item.parent_id', $parentId); - - // If we have a menutype we take client_id from there, unless forced otherwise - if ($menuType) - { - $menuTypeObj = $this->getMenuType($menuType); - - // An invalid menutype will be handled as clientId = 0 and menuType = '' - $menuType = (string) $menuTypeObj->menutype; - $menuTypeId = (int) $menuTypeObj->client_id; - $clientId = (int) $menuTypeObj->client_id; - } - else - { - $menuTypeId = 0; - $clientId = $app->isClient('api') ? $app->input->get('client_id') : - $app->getUserState('com_menus.items.client_id', 0); - } - - // Forced client id will override/clear menuType if conflicted - $forcedClientId = $app->input->get('client_id', null, 'string'); - - if (!$app->isClient('api')) - { - // Set the menu type and client id on the list view state, so we return to this menu after saving. - $app->setUserState('com_menus.items.menutype', $menuType); - $app->setUserState('com_menus.items.client_id', $clientId); - } - - // Current item if not new, we don't allow changing client id at all - if ($pk) - { - $table = $this->getTable(); - $table->load($pk); - $forcedClientId = $table->get('client_id', $forcedClientId); - } - - if (isset($forcedClientId) && $forcedClientId != $clientId) - { - $clientId = $forcedClientId; - $menuType = ''; - $menuTypeId = 0; - } - - $this->setState('item.menutype', $menuType); - $this->setState('item.client_id', $clientId); - $this->setState('item.menutypeid', $menuTypeId); - - if (!($type = $app->getUserState('com_menus.edit.item.type'))) - { - $type = $app->input->get('type'); - - /** - * Note: a new menu item will have no field type. - * The field is required so the user has to change it. - */ - } - - $this->setState('item.type', $type); - - $link = $app->isClient('api') ? $app->input->get('link') : - $app->getUserState('com_menus.edit.item.link'); - - if ($link) - { - $this->setState('item.link', $link); - } - - // Load the parameters. - $params = ComponentHelper::getParams('com_menus'); - $this->setState('params', $params); - } - - /** - * Loads the menutype object by a given menutype string - * - * @param string $menutype The given menutype - * - * @return \stdClass - * - * @since 3.7.0 - */ - protected function getMenuType($menutype) - { - $table = $this->getTable('MenuType'); - - $table->load(array('menutype' => $menutype)); - - return (object) $table->getProperties(); - } - - /** - * Loads the menutype ID by a given menutype string - * - * @param string $menutype The given menutype - * - * @return integer - * - * @since 3.6 - */ - protected function getMenuTypeId($menutype) - { - $menu = $this->getMenuType($menutype); - - return (int) $menu->id; - } - - /** - * Method to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import. - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $link = $this->getState('item.link'); - $type = $this->getState('item.type'); - $clientId = $this->getState('item.client_id'); - $formFile = false; - - // Load the specific type file - $typeFile = $clientId == 1 ? 'itemadmin_' . $type : 'item_' . $type; - $clientInfo = ApplicationHelper::getClientInfo($clientId); - - // Initialise form with component view params if available. - if ($type == 'component') - { - $link = $link ? htmlspecialchars_decode($link) : ''; - - // Parse the link arguments. - $args = []; - - if ($link) - { - parse_str(parse_url(htmlspecialchars_decode($link), PHP_URL_QUERY), $args); - } - - // Confirm that the option is defined. - $option = ''; - $base = ''; - - if (isset($args['option'])) - { - // The option determines the base path to work with. - $option = $args['option']; - $base = $clientInfo->path . '/components/' . $option; - } - - if (isset($args['view'])) - { - $view = $args['view']; - - // Determine the layout to search for. - if (isset($args['layout'])) - { - $layout = $args['layout']; - } - else - { - $layout = 'default'; - } - - // Check for the layout XML file. Use standard xml file if it exists. - $tplFolders = array( - $base . '/tmpl/' . $view, - $base . '/views/' . $view . '/tmpl', - $base . '/view/' . $view . '/tmpl', - ); - $path = Path::find($tplFolders, $layout . '.xml'); - - if (is_file($path)) - { - $formFile = $path; - } - - // If custom layout, get the xml file from the template folder - // template folder is first part of file name -- template:folder - if (!$formFile && (strpos($layout, ':') > 0)) - { - list($altTmpl, $altLayout) = explode(':', $layout); - - $templatePath = Path::clean($clientInfo->path . '/templates/' . $altTmpl . '/html/' . $option . '/' . $view . '/' . $altLayout . '.xml'); - - if (is_file($templatePath)) - { - $formFile = $templatePath; - } - } - } - - // Now check for a view manifest file - if (!$formFile) - { - if (isset($view)) - { - $metadataFolders = array( - $base . '/view/' . $view, - $base . '/views/' . $view - ); - $metaPath = Path::find($metadataFolders, 'metadata.xml'); - - if (is_file($path = Path::clean($metaPath))) - { - $formFile = $path; - } - } - elseif ($base) - { - // Now check for a component manifest file - $path = Path::clean($base . '/metadata.xml'); - - if (is_file($path)) - { - $formFile = $path; - } - } - } - } - - if ($formFile) - { - // If an XML file was found in the component, load it first. - // We need to qualify the full path to avoid collisions with component file names. - - if ($form->loadFile($formFile, true, '/metadata') == false) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/metadata/layout/help'); - } - else - { - // We don't have a component. Load the form XML to get the help path - $xmlFile = Path::find(JPATH_ADMINISTRATOR . '/components/com_menus/forms', $typeFile . '.xml'); - - if ($xmlFile) - { - if (!$xml = simplexml_load_file($xmlFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/form/help'); - } - } - - if (!empty($help)) - { - $helpKey = trim((string) $help[0]['key']); - $helpURL = trim((string) $help[0]['url']); - $helpLoc = trim((string) $help[0]['local']); - - $this->helpKey = $helpKey ?: $this->helpKey; - $this->helpURL = $helpURL ?: $this->helpURL; - $this->helpLocal = (($helpLoc == 'true') || ($helpLoc == '1') || ($helpLoc == 'local')); - } - - if (!$form->loadFile($typeFile, true, false)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Association menu items, we currently do not support this for admin menu… may be later - if ($clientId == 0 && Associations::isEnabled()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (count($languages) > 1) - { - $addform = new \SimpleXMLElement('
'); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - $fieldset->addAttribute('addfieldprefix', 'Joomla\Component\Menus\Administrator\Field'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_menu'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - $option = $field->addChild('option', 'COM_MENUS_ITEM_FIELD_ASSOCIATION_NO_VALUE'); - $option->addAttribute('value', ''); - } - - $form->load($addform, false); - } - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Method rebuild the entire nested set tree. - * - * @return boolean Boolean true on success, boolean false - * - * @since 1.6 - */ - public function rebuild() - { - // Initialise variables. - $db = $this->getDbo(); - $query = $db->getQuery(true); - $table = $this->getTable(); - - try - { - $rebuildResult = $table->rebuild(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (!$rebuildResult) - { - $this->setError($table->getError()); - - return false; - } - - $query->select( - [ - $db->quoteName('id'), - $db->quoteName('params'), - ] - ) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('params') . ' NOT LIKE ' . $db->quote('{%'), - $db->quoteName('params') . ' <> ' . $db->quote(''), - ] - ); - $db->setQuery($query); - - try - { - $items = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('params') . ' = :params') - ->where($db->quoteName('id') . ' = :id') - ->bind(':params', $params) - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - - foreach ($items as &$item) - { - // Update query parameters. - $id = $item->id; - $params = new Registry($item->params); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $pk = isset($data['id']) ? $data['id'] : (int) $this->getState('item.id'); - $isNew = true; - $db = $this->getDbo(); - $query = $db->getQuery(true); - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - // Include the plugins for the on save events. - PluginHelper::importPlugin($this->events_map['save']); - - // Load the row if saving an existing item. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - if (!$isNew) - { - if ($table->parent_id == $data['parent_id']) - { - // If first is chosen make the item the first child of the selected parent. - if ($data['menuordering'] == -1) - { - $table->setLocation($data['parent_id'], 'first-child'); - } - // If last is chosen make it the last child of the selected parent. - elseif ($data['menuordering'] == -2) - { - $table->setLocation($data['parent_id'], 'last-child'); - } - // Don't try to put an item after itself. All other ones put after the selected item. - // $data['id'] is empty means it's a save as copy - elseif ($data['menuordering'] && $table->id != $data['menuordering'] || empty($data['id'])) - { - $table->setLocation($data['menuordering'], 'after'); - } - // \Just leave it where it is if no change is made. - elseif ($data['menuordering'] && $table->id == $data['menuordering']) - { - unset($data['menuordering']); - } - } - // Set the new parent id if parent id not matched and put in last position - else - { - $table->setLocation($data['parent_id'], 'last-child'); - } - - // Check if we are moving to a different menu - if ($data['menutype'] != $table->menutype) - { - // Add the child node ids to the children array. - $query->clear() - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('lft') . ' BETWEEN ' . (int) $table->lft . ' AND ' . (int) $table->rgt); - $db->setQuery($query); - $children = (array) $db->loadColumn(); - } - } - // We have a new item, so it is not a change. - else - { - $menuType = $this->getMenuType($data['menutype']); - - $data['client_id'] = $menuType->client_id; - - $table->setLocation($data['parent_id'], 'last-child'); - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Alter the title & alias for save2copy when required. Also, unset the home record. - if (Factory::getApplication()->input->get('task') === 'save2copy' && $data['id'] === 0) - { - $origTable = $this->getTable(); - $origTable->load($this->getState('item.id')); - - if ($table->title === $origTable->title) - { - list($title, $alias) = $this->generateNewTitle($table->parent_id, $table->alias, $table->title); - $table->title = $title; - $table->alias = $alias; - } - - if ($table->alias === $origTable->alias) - { - $table->alias = ''; - } - - $table->published = 0; - $table->home = 0; - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew, $data)); - - // Store the data. - if (in_array(false, $result, true)|| !$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); - - // Rebuild the tree path. - if (!$table->rebuildPath($table->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Process the child rows - if (!empty($children)) - { - // Remove any duplicates and sanitize ids. - $children = array_unique($children); - $children = ArrayHelper::toInteger($children); - - // Update the menutype field in all nodes where necessary. - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('menutype') . ' = :menutype') - ->whereIn($db->quoteName('id'), $children) - ->bind(':menutype', $data['menutype']); - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - $this->setState('item.id', $table->id); - $this->setState('item.menutype', $table->menutype); - - // Load associated menu items, for now not supported for admin menu… may be later - if ($table->get('client_id') == 0 && Associations::isEnabled()) - { - // Adding self to the association - $associations = isset($data['associations']) ? $data['associations'] : array(); - - // Unset any invalid associations - $associations = ArrayHelper::toInteger($associations); - - foreach ($associations as $tag => $id) - { - if (!$id) - { - unset($associations[$tag]); - } - } - - // Detecting all item menus - $all_language = $table->language == '*'; - - if ($all_language && !empty($associations)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_ALL_LANGUAGE_ASSOCIATED'), 'notice'); - } - - // Get associationskey for edited item - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('key')) - ->from($db->quoteName('#__associations')) - ->where( - [ - $db->quoteName('context') . ' = :context', - $db->quoteName('id') . ' = :id', - ] - ) - ->bind(':context', $this->associationsContext) - ->bind(':id', $table->id, ParameterType::INTEGER); - $db->setQuery($query); - $oldKey = $db->loadResult(); - - if ($associations || $oldKey !== null) - { - // Deleting old associations for the associated items - $where = []; - $query = $db->getQuery(true) - ->delete($db->quoteName('#__associations')) - ->where($db->quoteName('context') . ' = :context') - ->bind(':context', $this->associationsContext); - - if ($associations) - { - $where[] = $db->quoteName('id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')'; - } - - if ($oldKey !== null) - { - $where[] = $db->quoteName('key') . ' = :oldKey'; - $query->bind(':oldKey', $oldKey); - } - - $query->extendWhere('AND', $where, 'OR'); - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - // Adding self to the association - if (!$all_language) - { - $associations[$table->language] = (int) $table->id; - } - - if (count($associations) > 1) - { - // Adding new association for these items - $key = md5(json_encode($associations)); - $query = $db->getQuery(true) - ->insert($db->quoteName('#__associations')) - ->columns( - [ - $db->quoteName('id'), - $db->quoteName('context'), - $db->quoteName('key'), - ] - ); - - foreach ($associations as $id) - { - $query->values( - implode( - ',', - $query->bindArray( - [$id, $this->associationsContext, $key], - [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] - ) - ) - ); - } - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - } - - // Clean the cache - $this->cleanCache(); - - if (isset($data['link'])) - { - $base = Uri::base(); - $juri = Uri::getInstance($base . $data['link']); - $option = $juri->getVar('option'); - - // Clean the cache - parent::cleanCache($option); - } - - if (Factory::getApplication()->input->get('task') === 'editAssociations') - { - return $this->redirectToAssociations($data); - } - - return true; - } - - /** - * Method to save the reordered nested set tree. - * First we save the new order values in the lft values of the changed ids. - * Then we invoke the table rebuild to implement the new ordering. - * - * @param array $idArray Rows identifiers to be reordered - * @param array $lftArray lft values of rows to be reordered - * - * @return boolean false on failure or error, true otherwise. - * - * @since 1.6 - */ - public function saveorder($idArray = null, $lftArray = null) - { - // Get an instance of the table object. - $table = $this->getTable(); - - if (!$table->saveorder($idArray, $lftArray)) - { - $this->setError($table->getError()); - - return false; - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the home state of one or more items. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the home state. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function setHome(&$pks, $value = 1) - { - $table = $this->getTable(); - $pks = (array) $pks; - - $languages = array(); - $onehome = false; - - // Remember that we can set a home page for different languages, - // so we need to loop through the primary key array. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if (!array_key_exists($table->language, $languages)) - { - $languages[$table->language] = true; - - if ($table->home == $value) - { - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_ALREADY_HOME'), 'notice'); - } - elseif ($table->menutype == 'main') - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_MENUTYPE_HOME'), 'error'); - } - else - { - $table->home = $value; - - if ($table->language == '*') - { - $table->published = 1; - } - - if (!$this->canSave($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - } - elseif (!$table->check()) - { - // Prune the items that failed pre-save checks. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage($table->getError(), 'error'); - } - elseif (!$table->store()) - { - // Prune the items that could not be stored. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage($table->getError(), 'error'); - } - } - } - else - { - unset($pks[$i]); - - if (!$onehome) - { - $onehome = true; - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_MENUS_ERROR_ONE_HOME'), 'notice'); - } - } - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the published state of one or more records. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function publish(&$pks, $value = 1) - { - $table = $this->getTable(); - $pks = (array) $pks; - - // Default menu item existence checks. - if ($value != 1) - { - foreach ($pks as $i => $pk) - { - if ($table->load($pk) && $table->home && $table->language == '*') - { - // Prune items that you can't change. - Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_MENU_UNPUBLISH_DEFAULT_HOME'), 'error'); - unset($pks[$i]); - break; - } - } - } - - // Clean the cache - $this->cleanCache(); - - // Ensure that previous checks doesn't empty the array - if (empty($pks)) - { - return true; - } - - return parent::publish($pks, $value); - } - - /** - * Method to change the title & alias. - * - * @param integer $parentId The id of the parent. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 1.6 - */ - protected function generateNewTitle($parentId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) - { - if ($title == $table->title) - { - $title = StringHelper::increment($title); - } - - $alias = StringHelper::increment($alias, 'dash'); - } - - return array($title, $alias); - } - - /** - * Custom clean the cache - * - * @param string $group Cache group name. - * @param integer $clientId @deprecated 5.0 No Longer Used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_menus'); - parent::cleanCache('com_modules'); - parent::cleanCache('mod_menu'); - } + /** + * The type alias for this content type. + * + * @var string + * @since 3.4 + */ + public $typeAlias = 'com_menus.item'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_menus.item'; + + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_MENUS_ITEM'; + + /** + * @var string The help screen key for the menu item. + * @since 1.6 + */ + protected $helpKey = 'Menu_Item:_New_Item'; + + /** + * @var string The help screen base URL for the menu item. + * @since 1.6 + */ + protected $helpURL; + + /** + * @var boolean True to use local lookup for the help screen. + * @since 1.6 + */ + protected $helpLocal = false; + + /** + * Batch copy/move command. If set to false, + * the batch copy/move command is not supported + * + * @var string + */ + protected $batch_copymove = 'menu_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage' + ); + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + $menuTypeId = 0; + + if (!empty($record->menutype)) { + $menuTypeId = $this->getMenuTypeId($record->menutype); + } + + return Factory::getUser()->authorise('core.delete', 'com_menus.menu.' . (int) $menuTypeId); + } + + /** + * Method to test whether the state of a record can be edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. + * + * @since 3.6 + */ + protected function canEditState($record) + { + $menuTypeId = !empty($record->menutype) ? $this->getMenuTypeId($record->menutype) : 0; + $assetKey = $menuTypeId ? 'com_menus.menu.' . (int) $menuTypeId : 'com_menus'; + + return Factory::getUser()->authorise('core.edit.state', $assetKey); + } + + /** + * Batch copy menu items to a new menu or parent. + * + * @param integer $value The new menu or sub-item. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return mixed An array of new IDs on success, boolean false on failure. + * + * @since 1.6 + */ + protected function batchCopy($value, $pks, $contexts) + { + // $value comes as {menutype}.{parent_id} + $parts = explode('.', $value); + $menuType = $parts[0]; + $parentId = ArrayHelper::getValue($parts, 1, 0, 'int'); + + $table = $this->getTable(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $newIds = array(); + + // Check that the parent exists + if ($parentId) { + if (!$table->load($parentId)) { + if ($error = $table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Non-fatal error + $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); + $parentId = 0; + } + } + } + + // If the parent is 0, set it to the ID of the root item in the tree + if (empty($parentId)) { + if (!$parentId = $table->getRootId()) { + $this->setError($table->getError()); + + return false; + } + } + + // Check that user has create permission for menus + $user = Factory::getUser(); + + $menuTypeId = (int) $this->getMenuTypeId($menuType); + + if (!$user->authorise('core.create', 'com_menus.menu.' . $menuTypeId)) { + $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_CREATE')); + + return false; + } + + // We need to log the parent ID + $parents = array(); + + // Calculate the emergency stop count as a precaution against a runaway loop bug + $query->select('COUNT(' . $db->quoteName('id') . ')') + ->from($db->quoteName('#__menu')); + $db->setQuery($query); + + try { + $count = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Parent exists so let's proceed + while (!empty($pks) && $count > 0) { + // Pop the first id off the stack + $pk = array_shift($pks); + + $table->reset(); + + // Check that the row actually exists + if (!$table->load($pk)) { + if ($error = $table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + // Copy is a bit tricky, because we also need to copy the children + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('lft') . ' > :lft', + $db->quoteName('rgt') . ' < :rgt', + ] + ) + ->bind(':lft', $table->lft, ParameterType::INTEGER) + ->bind(':rgt', $table->rgt, ParameterType::INTEGER); + $db->setQuery($query); + $childIds = $db->loadColumn(); + + // Add child IDs to the array only if they aren't already there. + foreach ($childIds as $childId) { + if (!in_array($childId, $pks)) { + $pks[] = $childId; + } + } + + // Make a copy of the old ID and Parent ID + $oldId = $table->id; + $oldParentId = $table->parent_id; + + // Reset the id because we are making a copy. + $table->id = 0; + + // If we a copying children, the Old ID will turn up in the parents list + // otherwise it's a new top level item + $table->parent_id = isset($parents[$oldParentId]) ? $parents[$oldParentId] : $parentId; + $table->menutype = $menuType; + + // Set the new location in the tree for the node. + $table->setLocation($table->parent_id, 'last-child'); + + // @todo: Deal with ordering? + // $table->ordering = 1; + $table->level = null; + $table->lft = null; + $table->rgt = null; + $table->home = 0; + + // Alter the title & alias + list($title, $alias) = $this->generateNewTitle($table->parent_id, $table->alias, $table->title); + $table->title = $title; + $table->alias = $alias; + + // Check the row. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Store the row. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Get the new item ID + $newId = $table->get('id'); + + // Add the new ID to the array + $newIds[$pk] = $newId; + + // Now we log the old 'parent' to the new 'parent' + $parents[$oldId] = $table->id; + $count--; + } + + // Rebuild the hierarchy. + if (!$table->rebuild()) { + $this->setError($table->getError()); + + return false; + } + + // Rebuild the tree path. + if (!$table->rebuildPath($table->id)) { + $this->setError($table->getError()); + + return false; + } + + // Clean the cache + $this->cleanCache(); + + return $newIds; + } + + /** + * Batch move menu items to a new menu or parent. + * + * @param integer $value The new menu or sub-item. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True on success. + * + * @since 1.6 + */ + protected function batchMove($value, $pks, $contexts) + { + // $value comes as {menutype}.{parent_id} + $parts = explode('.', $value); + $menuType = $parts[0]; + $parentId = ArrayHelper::getValue($parts, 1, 0, 'int'); + + $table = $this->getTable(); + $db = $this->getDatabase(); + + // Check that the parent exists. + if ($parentId) { + if (!$table->load($parentId)) { + if ($error = $table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Non-fatal error + $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); + $parentId = 0; + } + } + } + + // Check that user has create and edit permission for menus + $user = Factory::getUser(); + + $menuTypeId = (int) $this->getMenuTypeId($menuType); + + if (!$user->authorise('core.create', 'com_menus.menu.' . $menuTypeId)) { + $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_CREATE')); + + return false; + } + + if (!$user->authorise('core.edit', 'com_menus.menu.' . $menuTypeId)) { + $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_EDIT')); + + return false; + } + + // We are going to store all the children and just moved the menutype + $children = array(); + + // Parent exists so let's proceed + foreach ($pks as $pk) { + // Check that the row actually exists + if (!$table->load($pk)) { + if ($error = $table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + // Set the new location in the tree for the node. + $table->setLocation($parentId, 'last-child'); + + // Set the new Parent Id + $table->parent_id = $parentId; + + // Check if we are moving to a different menu + if ($menuType != $table->menutype) { + // Add the child node ids to the children array. + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('lft') . ' BETWEEN :lft AND :rgt') + ->bind(':lft', $table->lft, ParameterType::INTEGER) + ->bind(':rgt', $table->rgt, ParameterType::INTEGER); + $db->setQuery($query); + $children = array_merge($children, (array) $db->loadColumn()); + } + + // Check the row. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Store the row. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Rebuild the tree path. + if (!$table->rebuildPath()) { + $this->setError($table->getError()); + + return false; + } + } + + // Process the child rows + if (!empty($children)) { + // Remove any duplicates and sanitize ids. + $children = array_unique($children); + $children = ArrayHelper::toInteger($children); + + // Update the menutype field in all nodes where necessary. + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('menutype') . ' = :menuType') + ->whereIn($db->quoteName('id'), $children) + ->bind(':menuType', $menuType); + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to check if you can save a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function canSave($data = array(), $key = 'id') + { + return Factory::getUser()->authorise('core.edit', $this->option); + } + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return mixed A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // The folder and element vars are passed when saving the form. + if (empty($data)) { + $item = $this->getItem(); + + // The type should already be set. + $this->setState('item.link', $item->link); + } else { + $this->setState('item.link', ArrayHelper::getValue($data, 'link')); + $this->setState('item.type', ArrayHelper::getValue($data, 'type')); + } + + $clientId = $this->getState('item.client_id'); + + // Get the form. + if ($clientId == 1) { + $form = $this->loadForm('com_menus.item.admin', 'itemadmin', array('control' => 'jform', 'load_data' => $loadData), true); + } else { + $form = $this->loadForm('com_menus.item', 'item', array('control' => 'jform', 'load_data' => $loadData), true); + } + + if (empty($form)) { + return false; + } + + if ($loadData) { + $data = $this->loadFormData(); + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('menuordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is an article you can edit. + $form->setFieldAttribute('menuordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + // Filter available menus + $action = $this->getState('item.id') > 0 ? 'edit' : 'create'; + + $form->setFieldAttribute('menutype', 'accesstype', $action); + $form->setFieldAttribute('type', 'clientid', $clientId); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data, providing it has an ID and it is the same. + $itemData = (array) $this->getItem(); + + // When a new item is requested, unset the access as it will be set later from the filter + if (empty($itemData['id'])) { + unset($itemData['access']); + } + + $sessionData = (array) Factory::getApplication()->getUserState('com_menus.edit.item.data', array()); + + // Only merge if there is a session and itemId or itemid is null. + if ( + isset($sessionData['id']) && isset($itemData['id']) && $sessionData['id'] === $itemData['id'] + || is_null($itemData['id']) + ) { + $data = array_merge($itemData, $sessionData); + } else { + $data = $itemData; + } + + // For a new menu item, pre-select some filters (Status, Language, Access) in edit form if those have been selected in Menu Manager + if (empty($data['id'])) { + // Get selected fields + $filters = Factory::getApplication()->getUserState('com_menus.items.filter'); + $data['parent_id'] = $data['parent_id'] ?? ($filters['parent_id'] ?? null); + $data['published'] = $data['published'] ?? ($filters['published'] ?? null); + $data['language'] = $data['language'] ?? ($filters['language'] ?? null); + $data['access'] = $data['access'] ?? ($filters['access'] ?? Factory::getApplication()->get('access')); + } + + if (isset($data['menutype']) && !$this->getState('item.menutypeid')) { + $menuTypeId = (int) $this->getMenuTypeId($data['menutype']); + + $this->setState('item.menutypeid', $menuTypeId); + } + + $data = (object) $data; + + $this->preprocessData('com_menus.item', $data); + + return $data; + } + + /** + * Get the necessary data to load an item help screen. + * + * @return object An object with key, url, and local properties for loading the item help screen. + * + * @since 1.6 + */ + public function getHelp() + { + return (object) array('key' => $this->helpKey, 'url' => $this->helpURL, 'local' => $this->helpLocal); + } + + /** + * Method to get a menu item. + * + * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. + * + * @return mixed Menu item data object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('item.id'); + + // Get a level row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $table->load($pk); + + // Check for a table object error. + if ($error = $table->getError()) { + $this->setError($error); + + return false; + } + + // Prime required properties. + + if ($type = $this->getState('item.type')) { + $table->type = $type; + } + + if (empty($table->id)) { + $table->parent_id = $this->getState('item.parent_id'); + $table->menutype = $this->getState('item.menutype'); + $table->client_id = $this->getState('item.client_id'); + $table->params = '{}'; + } + + // If the link has been set in the state, possibly changing link type. + if ($link = $this->getState('item.link')) { + // Check if we are changing away from the actual link type. + if (MenusHelper::getLinkKey($table->link) !== MenusHelper::getLinkKey($link) && (int) $table->id === (int) $this->getState('item.id')) { + $table->link = $link; + } + } + + switch ($table->type) { + case 'alias': + case 'url': + $table->component_id = 0; + $args = array(); + + if ($table->link) { + $q = parse_url($table->link, PHP_URL_QUERY); + + if ($q) { + parse_str($q, $args); + } + } + + break; + + case 'separator': + case 'heading': + case 'container': + $table->link = ''; + $table->component_id = 0; + break; + + case 'component': + default: + // Enforce a valid type. + $table->type = 'component'; + + // Ensure the integrity of the component_id field is maintained, particularly when changing the menu item type. + $args = []; + + if ($table->link) { + $q = parse_url($table->link, PHP_URL_QUERY); + + if ($q) { + parse_str($q, $args); + } + } + + if (isset($args['option'])) { + // Load the language file for the component. + $lang = Factory::getLanguage(); + $lang->load($args['option'], JPATH_ADMINISTRATOR) + || $lang->load($args['option'], JPATH_ADMINISTRATOR . '/components/' . $args['option']); + + // Determine the component id. + $component = ComponentHelper::getComponent($args['option']); + + if (isset($component->id)) { + $table->component_id = $component->id; + } + } + break; + } + + // We have a valid type, inject it into the state for forms to use. + $this->setState('item.type', $table->type); + + // Convert to the \Joomla\CMS\Object\CMSObject before adding the params. + $properties = $table->getProperties(1); + $result = ArrayHelper::toObject($properties); + + // Convert the params field to an array. + $registry = new Registry($table->params); + $result->params = $registry->toArray(); + + // Merge the request arguments in to the params for a component. + if ($table->type == 'component') { + // Note that all request arguments become reserved parameter names. + $result->request = $args; + $result->params = array_merge($result->params, $args); + + // Special case for the Login menu item. + // Display the login or logout redirect URL fields if not empty + if ($table->link == 'index.php?option=com_users&view=login') { + if (!empty($result->params['login_redirect_url'])) { + $result->params['loginredirectchoice'] = '0'; + } + + if (!empty($result->params['logout_redirect_url'])) { + $result->params['logoutredirectchoice'] = '0'; + } + } + } + + if ($table->type == 'alias') { + // Note that all request arguments become reserved parameter names. + $result->params = array_merge($result->params, $args); + } + + if ($table->type == 'url') { + // Note that all request arguments become reserved parameter names. + $result->params = array_merge($result->params, $args); + } + + // Load associated menu items, only supported for frontend for now + if ($this->getState('item.client_id') == 0 && Associations::isEnabled()) { + if ($pk != null) { + $result->associations = MenusHelper::getAssociations($pk); + } else { + $result->associations = array(); + } + } + + $result->menuordering = $pk; + + return $result; + } + + /** + * Get the list of modules not in trash. + * + * @return mixed An array of module records (id, title, position), or false on error. + * + * @since 1.6 + */ + public function getModules() + { + $clientId = (int) $this->getState('item.client_id'); + $id = (int) $this->getState('item.id'); + + // Currently any setting that affects target page for a backend menu is not supported, hence load no modules. + if ($clientId == 1) { + return false; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + /** + * Join on the module-to-menu mapping table. + * We are only interested if the module is displayed on ALL or THIS menu item (or the inverse ID number). + * sqlsrv changes for modulelink to menu manager + */ + $query->select( + [ + $db->quoteName('a.id'), + $db->quoteName('a.title'), + $db->quoteName('a.position'), + $db->quoteName('a.published'), + $db->quoteName('map.menuid'), + ] + ) + ->from($db->quoteName('#__modules', 'a')) + ->join( + 'LEFT', + $db->quoteName('#__modules_menu', 'map'), + $db->quoteName('map.moduleid') . ' = ' . $db->quoteName('a.id') + . ' AND ' . $db->quoteName('map.menuid') . ' IN (' . implode(',', $query->bindArray([0, $id, -$id])) . ')' + ); + + $subQuery = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__modules_menu')) + ->where( + [ + $db->quoteName('moduleid') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('menuid') . ' < 0', + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('except')); + + // Join on the asset groups table. + $query->select($db->quoteName('ag.title', 'access_title')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->where( + [ + $db->quoteName('a.published') . ' >= 0', + $db->quoteName('a.client_id') . ' = :clientId', + ] + ) + ->bind(':clientId', $clientId, ParameterType::INTEGER) + ->order( + [ + $db->quoteName('a.position'), + $db->quoteName('a.ordering'), + ] + ); + + $db->setQuery($query); + + try { + $result = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $result; + } + + /** + * Get the list of all view levels + * + * @return \stdClass[]|boolean An array of all view levels (id, title). + * + * @since 3.4 + */ + public function getViewLevels() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Get all the available view levels + $query->select($db->quoteName('id')) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__viewlevels')) + ->order($db->quoteName('id')); + + $db->setQuery($query); + + try { + $result = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $result; + } + + /** + * Returns a Table object, always creating it + * + * @param string $type The table type to instantiate. + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\Cms\Table\Table|\Joomla\Cms\Table\Nested A database object. + * + * @since 1.6 + */ + public function getTable($type = 'Menu', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * A protected method to get the where clause for the reorder. + * This ensures that the row will be moved relative to a row with the same menutype. + * + * @param \Joomla\CMS\Table\Menu $table + * + * @return array An array of conditions to add to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('menutype') . ' = ' . $db->quote($table->menutype), + ]; + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState('item.id', $pk); + + if (!$app->isClient('api')) { + $parentId = $app->getUserState('com_menus.edit.item.parent_id'); + $menuType = $app->getUserStateFromRequest('com_menus.items.menutype', 'menutype', '', 'string'); + } else { + $parentId = null; + $menuType = $app->input->get('com_menus.items.menutype'); + } + + if (!$parentId) { + $parentId = $app->input->getInt('parent_id'); + } + + $this->setState('item.parent_id', $parentId); + + // If we have a menutype we take client_id from there, unless forced otherwise + if ($menuType) { + $menuTypeObj = $this->getMenuType($menuType); + + // An invalid menutype will be handled as clientId = 0 and menuType = '' + $menuType = (string) $menuTypeObj->menutype; + $menuTypeId = (int) $menuTypeObj->client_id; + $clientId = (int) $menuTypeObj->client_id; + } else { + $menuTypeId = 0; + $clientId = $app->isClient('api') ? $app->input->get('client_id') : + $app->getUserState('com_menus.items.client_id', 0); + } + + // Forced client id will override/clear menuType if conflicted + $forcedClientId = $app->input->get('client_id', null, 'string'); + + if (!$app->isClient('api')) { + // Set the menu type and client id on the list view state, so we return to this menu after saving. + $app->setUserState('com_menus.items.menutype', $menuType); + $app->setUserState('com_menus.items.client_id', $clientId); + } + + // Current item if not new, we don't allow changing client id at all + if ($pk) { + $table = $this->getTable(); + $table->load($pk); + $forcedClientId = $table->get('client_id', $forcedClientId); + } + + if (isset($forcedClientId) && $forcedClientId != $clientId) { + $clientId = $forcedClientId; + $menuType = ''; + $menuTypeId = 0; + } + + $this->setState('item.menutype', $menuType); + $this->setState('item.client_id', $clientId); + $this->setState('item.menutypeid', $menuTypeId); + + if (!($type = $app->getUserState('com_menus.edit.item.type'))) { + $type = $app->input->get('type'); + + /** + * Note: a new menu item will have no field type. + * The field is required so the user has to change it. + */ + } + + $this->setState('item.type', $type); + + $link = $app->isClient('api') ? $app->input->get('link') : + $app->getUserState('com_menus.edit.item.link'); + + if ($link) { + $this->setState('item.link', $link); + } + + // Load the parameters. + $params = ComponentHelper::getParams('com_menus'); + $this->setState('params', $params); + } + + /** + * Loads the menutype object by a given menutype string + * + * @param string $menutype The given menutype + * + * @return \stdClass + * + * @since 3.7.0 + */ + protected function getMenuType($menutype) + { + $table = $this->getTable('MenuType'); + + $table->load(array('menutype' => $menutype)); + + return (object) $table->getProperties(); + } + + /** + * Loads the menutype ID by a given menutype string + * + * @param string $menutype The given menutype + * + * @return integer + * + * @since 3.6 + */ + protected function getMenuTypeId($menutype) + { + $menu = $this->getMenuType($menutype); + + return (int) $menu->id; + } + + /** + * Method to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import. + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $link = $this->getState('item.link'); + $type = $this->getState('item.type'); + $clientId = $this->getState('item.client_id'); + $formFile = false; + + // Load the specific type file + $typeFile = $clientId == 1 ? 'itemadmin_' . $type : 'item_' . $type; + $clientInfo = ApplicationHelper::getClientInfo($clientId); + + // Initialise form with component view params if available. + if ($type == 'component') { + $link = $link ? htmlspecialchars_decode($link) : ''; + + // Parse the link arguments. + $args = []; + + if ($link) { + parse_str(parse_url(htmlspecialchars_decode($link), PHP_URL_QUERY), $args); + } + + // Confirm that the option is defined. + $option = ''; + $base = ''; + + if (isset($args['option'])) { + // The option determines the base path to work with. + $option = $args['option']; + $base = $clientInfo->path . '/components/' . $option; + } + + if (isset($args['view'])) { + $view = $args['view']; + + // Determine the layout to search for. + if (isset($args['layout'])) { + $layout = $args['layout']; + } else { + $layout = 'default'; + } + + // Check for the layout XML file. Use standard xml file if it exists. + $tplFolders = array( + $base . '/tmpl/' . $view, + $base . '/views/' . $view . '/tmpl', + $base . '/view/' . $view . '/tmpl', + ); + $path = Path::find($tplFolders, $layout . '.xml'); + + if (is_file($path)) { + $formFile = $path; + } + + // If custom layout, get the xml file from the template folder + // template folder is first part of file name -- template:folder + if (!$formFile && (strpos($layout, ':') > 0)) { + list($altTmpl, $altLayout) = explode(':', $layout); + + $templatePath = Path::clean($clientInfo->path . '/templates/' . $altTmpl . '/html/' . $option . '/' . $view . '/' . $altLayout . '.xml'); + + if (is_file($templatePath)) { + $formFile = $templatePath; + } + } + } + + // Now check for a view manifest file + if (!$formFile) { + if (isset($view)) { + $metadataFolders = array( + $base . '/view/' . $view, + $base . '/views/' . $view + ); + $metaPath = Path::find($metadataFolders, 'metadata.xml'); + + if (is_file($path = Path::clean($metaPath))) { + $formFile = $path; + } + } elseif ($base) { + // Now check for a component manifest file + $path = Path::clean($base . '/metadata.xml'); + + if (is_file($path)) { + $formFile = $path; + } + } + } + } + + if ($formFile) { + // If an XML file was found in the component, load it first. + // We need to qualify the full path to avoid collisions with component file names. + + if ($form->loadFile($formFile, true, '/metadata') == false) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/metadata/layout/help'); + } else { + // We don't have a component. Load the form XML to get the help path + $xmlFile = Path::find(JPATH_ADMINISTRATOR . '/components/com_menus/forms', $typeFile . '.xml'); + + if ($xmlFile) { + if (!$xml = simplexml_load_file($xmlFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/form/help'); + } + } + + if (!empty($help)) { + $helpKey = trim((string) $help[0]['key']); + $helpURL = trim((string) $help[0]['url']); + $helpLoc = trim((string) $help[0]['local']); + + $this->helpKey = $helpKey ?: $this->helpKey; + $this->helpURL = $helpURL ?: $this->helpURL; + $this->helpLocal = (($helpLoc == 'true') || ($helpLoc == '1') || ($helpLoc == 'local')); + } + + if (!$form->loadFile($typeFile, true, false)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Association menu items, we currently do not support this for admin menu… may be later + if ($clientId == 0 && Associations::isEnabled()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + $fieldset->addAttribute('addfieldprefix', 'Joomla\Component\Menus\Administrator\Field'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_menu'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + $option = $field->addChild('option', 'COM_MENUS_ITEM_FIELD_ASSOCIATION_NO_VALUE'); + $option->addAttribute('value', ''); + } + + $form->load($addform, false); + } + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Method rebuild the entire nested set tree. + * + * @return boolean Boolean true on success, boolean false + * + * @since 1.6 + */ + public function rebuild() + { + // Initialise variables. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $table = $this->getTable(); + + try { + $rebuildResult = $table->rebuild(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (!$rebuildResult) { + $this->setError($table->getError()); + + return false; + } + + $query->select( + [ + $db->quoteName('id'), + $db->quoteName('params'), + ] + ) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('params') . ' NOT LIKE ' . $db->quote('{%'), + $db->quoteName('params') . ' <> ' . $db->quote(''), + ] + ); + $db->setQuery($query); + + try { + $items = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('params') . ' = :params') + ->where($db->quoteName('id') . ' = :id') + ->bind(':params', $params) + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + + foreach ($items as &$item) { + // Update query parameters. + $id = $item->id; + $params = new Registry($item->params); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $pk = isset($data['id']) ? $data['id'] : (int) $this->getState('item.id'); + $isNew = true; + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + // Include the plugins for the on save events. + PluginHelper::importPlugin($this->events_map['save']); + + // Load the row if saving an existing item. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + if (!$isNew) { + if ($table->parent_id == $data['parent_id']) { + // If first is chosen make the item the first child of the selected parent. + if ($data['menuordering'] == -1) { + $table->setLocation($data['parent_id'], 'first-child'); + } elseif ($data['menuordering'] == -2) { + // If last is chosen make it the last child of the selected parent. + $table->setLocation($data['parent_id'], 'last-child'); + } elseif ($data['menuordering'] && $table->id != $data['menuordering'] || empty($data['id'])) { + // Don't try to put an item after itself. All other ones put after the selected item. + // $data['id'] is empty means it's a save as copy + $table->setLocation($data['menuordering'], 'after'); + } elseif ($data['menuordering'] && $table->id == $data['menuordering']) { + // \Just leave it where it is if no change is made. + unset($data['menuordering']); + } + } else { + // Set the new parent id if parent id not matched and put in last position + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Check if we are moving to a different menu + if ($data['menutype'] != $table->menutype) { + // Add the child node ids to the children array. + $query->clear() + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('lft') . ' BETWEEN ' . (int) $table->lft . ' AND ' . (int) $table->rgt); + $db->setQuery($query); + $children = (array) $db->loadColumn(); + } + } else { + // We have a new item, so it is not a change. + $menuType = $this->getMenuType($data['menutype']); + + $data['client_id'] = $menuType->client_id; + + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Alter the title & alias for save2copy when required. Also, unset the home record. + if (Factory::getApplication()->input->get('task') === 'save2copy' && $data['id'] === 0) { + $origTable = $this->getTable(); + $origTable->load($this->getState('item.id')); + + if ($table->title === $origTable->title) { + list($title, $alias) = $this->generateNewTitle($table->parent_id, $table->alias, $table->title); + $table->title = $title; + $table->alias = $alias; + } + + if ($table->alias === $origTable->alias) { + $table->alias = ''; + } + + $table->published = 0; + $table->home = 0; + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew, $data)); + + // Store the data. + if (in_array(false, $result, true) || !$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); + + // Rebuild the tree path. + if (!$table->rebuildPath($table->id)) { + $this->setError($table->getError()); + + return false; + } + + // Process the child rows + if (!empty($children)) { + // Remove any duplicates and sanitize ids. + $children = array_unique($children); + $children = ArrayHelper::toInteger($children); + + // Update the menutype field in all nodes where necessary. + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('menutype') . ' = :menutype') + ->whereIn($db->quoteName('id'), $children) + ->bind(':menutype', $data['menutype']); + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + $this->setState('item.id', $table->id); + $this->setState('item.menutype', $table->menutype); + + // Load associated menu items, for now not supported for admin menu… may be later + if ($table->get('client_id') == 0 && Associations::isEnabled()) { + // Adding self to the association + $associations = isset($data['associations']) ? $data['associations'] : array(); + + // Unset any invalid associations + $associations = ArrayHelper::toInteger($associations); + + foreach ($associations as $tag => $id) { + if (!$id) { + unset($associations[$tag]); + } + } + + // Detecting all item menus + $all_language = $table->language == '*'; + + if ($all_language && !empty($associations)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_ALL_LANGUAGE_ASSOCIATED'), 'notice'); + } + + // Get associationskey for edited item + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('key')) + ->from($db->quoteName('#__associations')) + ->where( + [ + $db->quoteName('context') . ' = :context', + $db->quoteName('id') . ' = :id', + ] + ) + ->bind(':context', $this->associationsContext) + ->bind(':id', $table->id, ParameterType::INTEGER); + $db->setQuery($query); + $oldKey = $db->loadResult(); + + if ($associations || $oldKey !== null) { + // Deleting old associations for the associated items + $where = []; + $query = $db->getQuery(true) + ->delete($db->quoteName('#__associations')) + ->where($db->quoteName('context') . ' = :context') + ->bind(':context', $this->associationsContext); + + if ($associations) { + $where[] = $db->quoteName('id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')'; + } + + if ($oldKey !== null) { + $where[] = $db->quoteName('key') . ' = :oldKey'; + $query->bind(':oldKey', $oldKey); + } + + $query->extendWhere('AND', $where, 'OR'); + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + // Adding self to the association + if (!$all_language) { + $associations[$table->language] = (int) $table->id; + } + + if (count($associations) > 1) { + // Adding new association for these items + $key = md5(json_encode($associations)); + $query = $db->getQuery(true) + ->insert($db->quoteName('#__associations')) + ->columns( + [ + $db->quoteName('id'), + $db->quoteName('context'), + $db->quoteName('key'), + ] + ); + + foreach ($associations as $id) { + $query->values( + implode( + ',', + $query->bindArray( + [$id, $this->associationsContext, $key], + [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] + ) + ) + ); + } + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + } + + // Clean the cache + $this->cleanCache(); + + if (isset($data['link'])) { + $base = Uri::base(); + $juri = Uri::getInstance($base . $data['link']); + $option = $juri->getVar('option'); + + // Clean the cache + parent::cleanCache($option); + } + + if (Factory::getApplication()->input->get('task') === 'editAssociations') { + return $this->redirectToAssociations($data); + } + + return true; + } + + /** + * Method to save the reordered nested set tree. + * First we save the new order values in the lft values of the changed ids. + * Then we invoke the table rebuild to implement the new ordering. + * + * @param array $idArray Rows identifiers to be reordered + * @param array $lftArray lft values of rows to be reordered + * + * @return boolean false on failure or error, true otherwise. + * + * @since 1.6 + */ + public function saveorder($idArray = null, $lftArray = null) + { + // Get an instance of the table object. + $table = $this->getTable(); + + if (!$table->saveorder($idArray, $lftArray)) { + $this->setError($table->getError()); + + return false; + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the home state of one or more items. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the home state. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function setHome(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + + $languages = array(); + $onehome = false; + + // Remember that we can set a home page for different languages, + // so we need to loop through the primary key array. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if (!array_key_exists($table->language, $languages)) { + $languages[$table->language] = true; + + if ($table->home == $value) { + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_ALREADY_HOME'), 'notice'); + } elseif ($table->menutype == 'main') { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_MENUTYPE_HOME'), 'error'); + } else { + $table->home = $value; + + if ($table->language == '*') { + $table->published = 1; + } + + if (!$this->canSave($table)) { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + } elseif (!$table->check()) { + // Prune the items that failed pre-save checks. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage($table->getError(), 'error'); + } elseif (!$table->store()) { + // Prune the items that could not be stored. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage($table->getError(), 'error'); + } + } + } else { + unset($pks[$i]); + + if (!$onehome) { + $onehome = true; + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_MENUS_ERROR_ONE_HOME'), 'notice'); + } + } + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the published state of one or more records. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function publish(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + + // Default menu item existence checks. + if ($value != 1) { + foreach ($pks as $i => $pk) { + if ($table->load($pk) && $table->home && $table->language == '*') { + // Prune items that you can't change. + Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_MENU_UNPUBLISH_DEFAULT_HOME'), 'error'); + unset($pks[$i]); + break; + } + } + } + + // Clean the cache + $this->cleanCache(); + + // Ensure that previous checks doesn't empty the array + if (empty($pks)) { + return true; + } + + return parent::publish($pks, $value); + } + + /** + * Method to change the title & alias. + * + * @param integer $parentId The id of the parent. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 1.6 + */ + protected function generateNewTitle($parentId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) { + if ($title == $table->title) { + $title = StringHelper::increment($title); + } + + $alias = StringHelper::increment($alias, 'dash'); + } + + return array($title, $alias); + } + + /** + * Custom clean the cache + * + * @param string $group Cache group name. + * @param integer $clientId @deprecated 5.0 No Longer Used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_menus'); + parent::cleanCache('com_modules'); + parent::cleanCache('mod_menu'); + } } diff --git a/code/administrator/components/com_menus/src/Model/ItemsModel.php b/code/administrator/components/com_menus/src/Model/ItemsModel.php index b66dd4c0..65500ffa 100644 --- a/code/administrator/components/com_menus/src/Model/ItemsModel.php +++ b/code/administrator/components/com_menus/src/Model/ItemsModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - $search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search'); - $this->setState('filter.search', $search); - - $published = $this->getUserStateFromRequest($this->context . '.published', 'filter_published', ''); - $this->setState('filter.published', $published); - - $access = $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); - $this->setState('filter.access', $access); - - $parentId = $this->getUserStateFromRequest($this->context . '.filter.parent_id', 'filter_parent_id'); - $this->setState('filter.parent_id', $parentId); - - $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); - $this->setState('filter.level', $level); - - // Watch changes in client_id and menutype and keep sync whenever needed. - $currentClientId = $app->getUserState($this->context . '.client_id', 0); - $clientId = $app->input->getInt('client_id', $currentClientId); - - // Load mod_menu.ini file when client is administrator - if ($clientId == 1) - { - Factory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR); - } - - $currentMenuType = $app->getUserState($this->context . '.menutype', ''); - $menuType = $app->input->getString('menutype', $currentMenuType); - - // If client_id changed clear menutype and reset pagination - if ($clientId != $currentClientId) - { - $menuType = ''; - - $app->input->set('limitstart', 0); - $app->input->set('menutype', ''); - } - - // If menutype changed reset pagination. - if ($menuType != $currentMenuType) - { - $app->input->set('limitstart', 0); - } - - if (!$menuType) - { - $app->setUserState($this->context . '.menutype', ''); - $this->setState('menutypetitle', ''); - $this->setState('menutypeid', ''); - } - // Special menu types, if selected explicitly, will be allowed as a filter - elseif ($menuType == 'main') - { - // Adjust client_id to match the menutype. This is safe as client_id was not changed in this request. - $app->input->set('client_id', 1); - - $app->setUserState($this->context . '.menutype', $menuType); - $this->setState('menutypetitle', ucfirst($menuType)); - $this->setState('menutypeid', -1); - } - // Get the menutype object with appropriate checks. - elseif ($cMenu = $this->getMenu($menuType, true)) - { - // Adjust client_id to match the menutype. This is safe as client_id was not changed in this request. - $app->input->set('client_id', $cMenu->client_id); - - $app->setUserState($this->context . '.menutype', $menuType); - $this->setState('menutypetitle', $cMenu->title); - $this->setState('menutypeid', $cMenu->id); - } - // This menutype does not exist, leave client id unchanged but reset menutype and pagination - else - { - $menuType = ''; - - $app->input->set('limitstart', 0); - $app->input->set('menutype', $menuType); - - $app->setUserState($this->context . '.menutype', $menuType); - $this->setState('menutypetitle', ''); - $this->setState('menutypeid', ''); - } - - // Client id filter - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $this->setState('filter.client_id', $clientId); - - // Use a different filter file when client is administrator - if ($clientId == 1) - { - $this->filterFormName = 'filter_itemsadmin'; - } - - $this->setState('filter.menutype', $menuType); - - $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); - $this->setState('filter.language', $language); - - // Component parameters. - $params = ComponentHelper::getParams('com_menus'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.parent_id'); - $id .= ':' . $this->getState('filter.menutype'); - $id .= ':' . $this->getState('filter.client_id'); - - return parent::getStoreId($id); - } - - /** - * Builds an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery A query object. - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - $clientId = (int) $this->getState('filter.client_id'); - - // Select all fields from the table. - $query->select( - // We can't quote state values because they could contain expressions. - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.menutype'), - $db->quoteName('a.title'), - $db->quoteName('a.alias'), - $db->quoteName('a.note'), - $db->quoteName('a.path'), - $db->quoteName('a.link'), - $db->quoteName('a.type'), - $db->quoteName('a.parent_id'), - $db->quoteName('a.level'), - $db->quoteName('a.component_id'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.browserNav'), - $db->quoteName('a.access'), - $db->quoteName('a.img'), - $db->quoteName('a.template_style_id'), - $db->quoteName('a.params'), - $db->quoteName('a.lft'), - $db->quoteName('a.rgt'), - $db->quoteName('a.home'), - $db->quoteName('a.language'), - $db->quoteName('a.client_id'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - ] - ) - ) - ->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('l.sef', 'language_sef'), - $db->quoteName('u.name', 'editor'), - $db->quoteName('c.element', 'componentname'), - $db->quoteName('ag.title', 'access_level'), - $db->quoteName('mt.id', 'menutype_id'), - $db->quoteName('mt.title', 'menutype_title'), - $db->quoteName('e.enabled'), - $db->quoteName('e.name'), - 'CASE WHEN ' . $db->quoteName('a.type') . ' = ' . $db->quote('component') - . ' THEN ' . $db->quoteName('a.published') . ' +2 * (' . $db->quoteName('e.enabled') . ' -1)' - . ' ELSE ' . $db->quoteName('a.published') . ' END AS ' . $db->quoteName('published'), - ] - ) - ->from($db->quoteName('#__menu', 'a')); - - // Join over the language - $query->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); - - // Join over the users. - $query->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Join over components - $query->join('LEFT', $db->quoteName('#__extensions', 'c'), $db->quoteName('c.extension_id') . ' = ' . $db->quoteName('a.component_id')); - - // Join over the asset groups. - $query->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')); - - // Join over the menu types. - $query->join('LEFT', $db->quoteName('#__menu_types', 'mt'), $db->quoteName('mt.menutype') . ' = ' . $db->quoteName('a.menutype')); - - // Join over the extensions - $query->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')); - - // Join over the associations. - if (Associations::isEnabled()) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') - ->from($db->quoteName('#__associations', 'asso1')) - ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) - ->where( - [ - $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('asso1.context') . ' = ' . $db->quote('com_menus.item'), - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); - } - - // Exclude the root category. - $query->where( - [ - $db->quoteName('a.id') . ' > 1', - $db->quoteName('a.client_id') . ' = :clientId', - ] - ) - ->bind(':clientId', $clientId, ParameterType::INTEGER); - - // Filter on the published state. - $published = $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.published') . ' IN (0, 1)'); - } - - // Filter by search in title, alias or id - if ($search = trim($this->getState('filter.search', ''))) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - elseif (stripos($search, 'link:') === 0) - { - if ($search = str_replace(' ', '%', trim(substr($search, 5)))) - { - $query->where($db->quoteName('a.link') . ' LIKE :search') - ->bind(':search', $search); - } - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.title') . ' LIKE :search1', - $db->quoteName('a.alias') . ' LIKE :search2', - $db->quoteName('a.note') . ' LIKE :search3', - ], - 'OR' - ) - ->bind([':search1', ':search2', ':search3'], $search); - } - } - - // Filter the items over the parent id if set. - $parentId = (int) $this->getState('filter.parent_id'); - $level = (int) $this->getState('filter.level'); - - if ($parentId) - { - // Create a subquery for the sub-items list - $subQuery = $db->getQuery(true) - ->select($db->quoteName('sub.id')) - ->from($db->quoteName('#__menu', 'sub')) - ->join( - 'INNER', - $db->quoteName('#__menu', 'this'), - $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') - . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') - ) - ->where($db->quoteName('this.id') . ' = :parentId1'); - - if ($level) - { - $subQuery->where($db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :level - 1'); - $query->bind(':level', $level, ParameterType::INTEGER); - } - - // Add the subquery to the main query - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.parent_id') . ' = :parentId2', - $db->quoteName('a.parent_id') . ' IN (' . (string) $subQuery . ')', - ], - 'OR' - ) - ->bind([':parentId1', ':parentId2'], $parentId, ParameterType::INTEGER); - } - - // Filter on the level. - elseif ($level) - { - $query->where($db->quoteName('a.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter the items over the menu id if set. - $menuType = $this->getState('filter.menutype'); - - // A value "" means all - if ($menuType == '') - { - // Load all menu types we have manage access - $query2 = $db->getQuery(true) - ->select( - [ - $db->quoteName('id'), - $db->quoteName('menutype'), - ] - ) - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER) - ->order($db->quoteName('title')); - - // Show protected items on explicit filter only - $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('main')); - - $menuTypes = $db->setQuery($query2)->loadObjectList(); - - if ($menuTypes) - { - $types = array(); - - foreach ($menuTypes as $type) - { - if ($user->authorise('core.manage', 'com_menus.menu.' . (int) $type->id)) - { - $types[] = $type->menutype; - } - } - - if ($types) - { - $query->whereIn($db->quoteName('a.menutype'), $types); - } - else - { - $query->where(0); - } - } - } - // Default behavior => load all items from a specific menu - elseif (strlen($menuType)) - { - $query->where($db->quoteName('a.menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - } - // Empty menu type => error - else - { - $query->where('1 != 1'); - } - - // Filter on the access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - if ($groups = $user->getAuthorisedViewLevels()) - { - $query->whereIn($db->quoteName('a.access'), $groups); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 3.2 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $name = $form->getName(); - - if ($name == 'com_menus.items.filter') - { - $clientId = $this->getState('filter.client_id'); - $form->setFieldAttribute('menutype', 'clientid', $clientId); - } - elseif (false !== strpos($name, 'com_menus.items.modal.')) - { - $form->removeField('client_id'); - - $clientId = $this->getState('filter.client_id'); - $form->setFieldAttribute('menutype', 'clientid', $clientId); - } - } - - /** - * Get the client id for a menu - * - * @param string $menuType The menutype identifier for the menu - * @param boolean $check Flag whether to perform check against ACL as well as existence - * - * @return integer - * - * @since 3.7.0 - */ - protected function getMenu($menuType, $check = false) - { - $query = $this->_db->getQuery(true); - - $query->select($this->_db->quoteName('a') . '.*') - ->from($this->_db->quoteName('#__menu_types', 'a')) - ->where($this->_db->quoteName('menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - - $cMenu = $this->_db->setQuery($query)->loadObject(); - - if ($check) - { - // Check if menu type exists. - if (!$cMenu) - { - Log::add(Text::_('COM_MENUS_ERROR_MENUTYPE_NOT_FOUND'), Log::ERROR, 'jerror'); - - return false; - } - // Check if menu type is valid against ACL. - elseif (!Factory::getUser()->authorise('core.manage', 'com_menus.menu.' . $cMenu->id)) - { - Log::add(Text::_('JERROR_ALERTNOAUTHOR'), Log::ERROR, 'jerror'); - - return false; - } - } - - return $cMenu; - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.0.1 - */ - public function getItems() - { - $store = $this->getStoreId(); - - if (!isset($this->cache[$store])) - { - $items = parent::getItems(); - $lang = Factory::getLanguage(); - $client = $this->state->get('filter.client_id'); - - if ($items) - { - foreach ($items as $item) - { - if ($extension = $item->componentname) - { - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) - || $lang->load("$extension.sys", JPATH_ADMINISTRATOR . '/components/' . $extension); - } - - // Translate component name - if ($client === 1) - { - $item->title = Text::_($item->title); - } - } - } - - $this->cache[$store] = $items; - } - - return $this->cache[$store]; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'menutype', 'a.menutype', 'menutype_title', + 'title', 'a.title', + 'alias', 'a.alias', + 'published', 'a.published', + 'access', 'a.access', 'access_level', + 'language', 'a.language', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'lft', 'a.lft', + 'rgt', 'a.rgt', + 'level', 'a.level', + 'path', 'a.path', + 'client_id', 'a.client_id', + 'home', 'a.home', + 'parent_id', 'a.parent_id', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'a.ordering' + ); + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + $search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search'); + $this->setState('filter.search', $search); + + $published = $this->getUserStateFromRequest($this->context . '.published', 'filter_published', ''); + $this->setState('filter.published', $published); + + $access = $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); + $this->setState('filter.access', $access); + + $parentId = $this->getUserStateFromRequest($this->context . '.filter.parent_id', 'filter_parent_id'); + $this->setState('filter.parent_id', $parentId); + + $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); + $this->setState('filter.level', $level); + + // Watch changes in client_id and menutype and keep sync whenever needed. + $currentClientId = $app->getUserState($this->context . '.client_id', 0); + $clientId = $app->input->getInt('client_id', $currentClientId); + + // Load mod_menu.ini file when client is administrator + if ($clientId == 1) { + Factory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR); + } + + $currentMenuType = $app->getUserState($this->context . '.menutype', ''); + $menuType = $app->input->getString('menutype', $currentMenuType); + + // If client_id changed clear menutype and reset pagination + if ($clientId != $currentClientId) { + $menuType = ''; + + $app->input->set('limitstart', 0); + $app->input->set('menutype', ''); + } + + // If menutype changed reset pagination. + if ($menuType != $currentMenuType) { + $app->input->set('limitstart', 0); + } + + if (!$menuType) { + $app->setUserState($this->context . '.menutype', ''); + $this->setState('menutypetitle', ''); + $this->setState('menutypeid', ''); + } elseif ($menuType == 'main') { + // Special menu types, if selected explicitly, will be allowed as a filter + // Adjust client_id to match the menutype. This is safe as client_id was not changed in this request. + $app->input->set('client_id', 1); + + $app->setUserState($this->context . '.menutype', $menuType); + $this->setState('menutypetitle', ucfirst($menuType)); + $this->setState('menutypeid', -1); + } elseif ($cMenu = $this->getMenu($menuType, true)) { + // Get the menutype object with appropriate checks. + // Adjust client_id to match the menutype. This is safe as client_id was not changed in this request. + $app->input->set('client_id', $cMenu->client_id); + + $app->setUserState($this->context . '.menutype', $menuType); + $this->setState('menutypetitle', $cMenu->title); + $this->setState('menutypeid', $cMenu->id); + } else { + // This menutype does not exist, leave client id unchanged but reset menutype and pagination + $menuType = ''; + + $app->input->set('limitstart', 0); + $app->input->set('menutype', $menuType); + + $app->setUserState($this->context . '.menutype', $menuType); + $this->setState('menutypetitle', ''); + $this->setState('menutypeid', ''); + } + + // Client id filter + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $this->setState('filter.client_id', $clientId); + + // Use a different filter file when client is administrator + if ($clientId == 1) { + $this->filterFormName = 'filter_itemsadmin'; + } + + $this->setState('filter.menutype', $menuType); + + $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); + $this->setState('filter.language', $language); + + // Component parameters. + $params = ComponentHelper::getParams('com_menus'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.parent_id'); + $id .= ':' . $this->getState('filter.menutype'); + $id .= ':' . $this->getState('filter.client_id'); + + return parent::getStoreId($id); + } + + /** + * Builds an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery A query object. + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + $clientId = (int) $this->getState('filter.client_id'); + + // Select all fields from the table. + $query->select( + // We can't quote state values because they could contain expressions. + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.menutype'), + $db->quoteName('a.title'), + $db->quoteName('a.alias'), + $db->quoteName('a.note'), + $db->quoteName('a.path'), + $db->quoteName('a.link'), + $db->quoteName('a.type'), + $db->quoteName('a.parent_id'), + $db->quoteName('a.level'), + $db->quoteName('a.component_id'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.browserNav'), + $db->quoteName('a.access'), + $db->quoteName('a.img'), + $db->quoteName('a.template_style_id'), + $db->quoteName('a.params'), + $db->quoteName('a.lft'), + $db->quoteName('a.rgt'), + $db->quoteName('a.home'), + $db->quoteName('a.language'), + $db->quoteName('a.client_id'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + ] + ) + ) + ->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('l.sef', 'language_sef'), + $db->quoteName('u.name', 'editor'), + $db->quoteName('c.element', 'componentname'), + $db->quoteName('ag.title', 'access_level'), + $db->quoteName('mt.id', 'menutype_id'), + $db->quoteName('mt.title', 'menutype_title'), + $db->quoteName('e.enabled'), + $db->quoteName('e.name'), + 'CASE WHEN ' . $db->quoteName('a.type') . ' = ' . $db->quote('component') + . ' THEN ' . $db->quoteName('a.published') . ' +2 * (' . $db->quoteName('e.enabled') . ' -1)' + . ' ELSE ' . $db->quoteName('a.published') . ' END AS ' . $db->quoteName('published'), + ] + ) + ->from($db->quoteName('#__menu', 'a')); + + // Join over the language + $query->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); + + // Join over the users. + $query->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Join over components + $query->join('LEFT', $db->quoteName('#__extensions', 'c'), $db->quoteName('c.extension_id') . ' = ' . $db->quoteName('a.component_id')); + + // Join over the asset groups. + $query->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')); + + // Join over the menu types. + $query->join('LEFT', $db->quoteName('#__menu_types', 'mt'), $db->quoteName('mt.menutype') . ' = ' . $db->quoteName('a.menutype')); + + // Join over the extensions + $query->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')); + + // Join over the associations. + if (Associations::isEnabled()) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') + ->from($db->quoteName('#__associations', 'asso1')) + ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) + ->where( + [ + $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('asso1.context') . ' = ' . $db->quote('com_menus.item'), + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); + } + + // Exclude the root category. + $query->where( + [ + $db->quoteName('a.id') . ' > 1', + $db->quoteName('a.client_id') . ' = :clientId', + ] + ) + ->bind(':clientId', $clientId, ParameterType::INTEGER); + + // Filter on the published state. + $published = $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.published') . ' IN (0, 1)'); + } + + // Filter by search in title, alias or id + if ($search = trim($this->getState('filter.search', ''))) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } elseif (stripos($search, 'link:') === 0) { + if ($search = str_replace(' ', '%', trim(substr($search, 5)))) { + $query->where($db->quoteName('a.link') . ' LIKE :search') + ->bind(':search', $search); + } + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.title') . ' LIKE :search1', + $db->quoteName('a.alias') . ' LIKE :search2', + $db->quoteName('a.note') . ' LIKE :search3', + ], + 'OR' + ) + ->bind([':search1', ':search2', ':search3'], $search); + } + } + + // Filter the items over the parent id if set. + $parentId = (int) $this->getState('filter.parent_id'); + $level = (int) $this->getState('filter.level'); + + if ($parentId) { + // Create a subquery for the sub-items list + $subQuery = $db->getQuery(true) + ->select($db->quoteName('sub.id')) + ->from($db->quoteName('#__menu', 'sub')) + ->join( + 'INNER', + $db->quoteName('#__menu', 'this'), + $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') + . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') + ) + ->where($db->quoteName('this.id') . ' = :parentId1'); + + if ($level) { + $subQuery->where($db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :level - 1'); + $query->bind(':level', $level, ParameterType::INTEGER); + } + + // Add the subquery to the main query + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.parent_id') . ' = :parentId2', + $db->quoteName('a.parent_id') . ' IN (' . (string) $subQuery . ')', + ], + 'OR' + ) + ->bind([':parentId1', ':parentId2'], $parentId, ParameterType::INTEGER); + } elseif ($level) { + // Filter on the level. + $query->where($db->quoteName('a.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter the items over the menu id if set. + $menuType = $this->getState('filter.menutype'); + + // A value "" means all + if ($menuType == '') { + // Load all menu types we have manage access + $query2 = $db->getQuery(true) + ->select( + [ + $db->quoteName('id'), + $db->quoteName('menutype'), + ] + ) + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER) + ->order($db->quoteName('title')); + + // Show protected items on explicit filter only + $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('main')); + + $menuTypes = $db->setQuery($query2)->loadObjectList(); + + if ($menuTypes) { + $types = array(); + + foreach ($menuTypes as $type) { + if ($user->authorise('core.manage', 'com_menus.menu.' . (int) $type->id)) { + $types[] = $type->menutype; + } + } + + if ($types) { + $query->whereIn($db->quoteName('a.menutype'), $types); + } else { + $query->where(0); + } + } + } elseif (strlen($menuType)) { + // Default behavior => load all items from a specific menu + $query->where($db->quoteName('a.menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + } else { + // Empty menu type => error + $query->where('1 != 1'); + } + + // Filter on the access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + if ($groups = $user->getAuthorisedViewLevels()) { + $query->whereIn($db->quoteName('a.access'), $groups); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 3.2 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $name = $form->getName(); + + if ($name == 'com_menus.items.filter') { + $clientId = $this->getState('filter.client_id'); + $form->setFieldAttribute('menutype', 'clientid', $clientId); + } elseif (false !== strpos($name, 'com_menus.items.modal.')) { + $form->removeField('client_id'); + + $clientId = $this->getState('filter.client_id'); + $form->setFieldAttribute('menutype', 'clientid', $clientId); + } + } + + /** + * Get the client id for a menu + * + * @param string $menuType The menutype identifier for the menu + * @param boolean $check Flag whether to perform check against ACL as well as existence + * + * @return integer + * + * @since 3.7.0 + */ + protected function getMenu($menuType, $check = false) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('a') . '.*') + ->from($db->quoteName('#__menu_types', 'a')) + ->where($db->quoteName('menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + + $cMenu = $db->setQuery($query)->loadObject(); + + if ($check) { + // Check if menu type exists. + if (!$cMenu) { + Log::add(Text::_('COM_MENUS_ERROR_MENUTYPE_NOT_FOUND'), Log::ERROR, 'jerror'); + + return false; + } elseif (!Factory::getUser()->authorise('core.manage', 'com_menus.menu.' . $cMenu->id)) { + // Check if menu type is valid against ACL. + Log::add(Text::_('JERROR_ALERTNOAUTHOR'), Log::ERROR, 'jerror'); + + return false; + } + } + + return $cMenu; + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.0.1 + */ + public function getItems() + { + $store = $this->getStoreId(); + + if (!isset($this->cache[$store])) { + $items = parent::getItems(); + $lang = Factory::getLanguage(); + $client = $this->state->get('filter.client_id'); + + if ($items) { + foreach ($items as $item) { + if ($extension = $item->componentname) { + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) + || $lang->load("$extension.sys", JPATH_ADMINISTRATOR . '/components/' . $extension); + } + + // Translate component name + if ($client === 1) { + $item->title = Text::_($item->title); + } + } + } + + $this->cache[$store] = $items; + } + + return $this->cache[$store]; + } } diff --git a/code/administrator/components/com_menus/src/Model/MenuModel.php b/code/administrator/components/com_menus/src/Model/MenuModel.php index b6ab3340..6f246987 100644 --- a/code/administrator/components/com_menus/src/Model/MenuModel.php +++ b/code/administrator/components/com_menus/src/Model/MenuModel.php @@ -1,4 +1,5 @@ authorise('core.delete', 'com_menus.menu.' . (int) $record->id); - } - - /** - * Method to test whether the state of a record can be edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - return Factory::getUser()->authorise('core.edit.state', 'com_menus.menu.' . (int) $record->id); - } - - /** - * Returns a Table object, always creating it - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'MenuType', $prefix = '\JTable', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $id = $app->input->getInt('id'); - $this->setState('menu.id', $id); - - // Load the parameters. - $params = ComponentHelper::getParams('com_menus'); - $this->setState('params', $params); - - // Load the clientId. - $clientId = $app->getUserStateFromRequest('com_menus.menus.client_id', 'client_id', 0, 'int'); - $this->setState('client_id', $clientId); - } - - /** - * Method to get a menu item. - * - * @param integer $itemId The id of the menu item to get. - * - * @return mixed Menu item data object on success, false on failure. - * - * @since 1.6 - */ - public function &getItem($itemId = null) - { - $itemId = (!empty($itemId)) ? $itemId : (int) $this->getState('menu.id'); - - // Get a menu item row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($itemId); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - - $properties = $table->getProperties(1); - $value = ArrayHelper::toObject($properties, CMSObject::class); - - return $value; - } - - /** - * Method to get the menu item form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_menus.menu', 'menu', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - if (!$this->getState('client_id', 0)) - { - $form->removeField('preset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_menus.edit.menu.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - if (empty($data->id)) - { - $data->client_id = $this->state->get('client_id', 0); - } - } - else - { - unset($data['preset']); - } - - $this->preprocessData('com_menus.menu', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see JFormRule - * @see JFilterInput - * @since 3.9.23 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_menus')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $id = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('menu.id'); - $isNew = true; - - // Get a row instance. - $table = $this->getTable(); - - // Include the plugins for the save events. - PluginHelper::importPlugin('content'); - - // Load the row if saving an existing item. - if ($id > 0) - { - $isNew = false; - $table->load($id); - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before event. - $result = Factory::getApplication()->triggerEvent('onContentBeforeSave', array($this->_context, &$table, $isNew, $data)); - - // Store the data. - if (in_array(false, $result, true) || !$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent('onContentAfterSave', array($this->_context, &$table, $isNew)); - - $this->setState('menu.id', $table->id); - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to delete groups. - * - * @param array $itemIds An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - */ - public function delete($itemIds) - { - // Sanitize the ids. - $itemIds = ArrayHelper::toInteger((array) $itemIds); - - // Get a group row instance. - $table = $this->getTable(); - - // Include the plugins for the delete events. - PluginHelper::importPlugin('content'); - - // Iterate the items to delete each one. - foreach ($itemIds as $itemId) - { - if ($table->load($itemId)) - { - // Trigger the before delete event. - $result = Factory::getApplication()->triggerEvent('onContentBeforeDelete', array($this->_context, $table)); - - if (in_array(false, $result, true) || !$table->delete($itemId)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after delete event. - Factory::getApplication()->triggerEvent('onContentAfterDelete', array($this->_context, $table)); - - // @todo: Delete the menu associations - Menu items and Modules - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Gets a list of all mod_mainmenu modules and collates them by menutype - * - * @return array - * - * @since 1.6 - */ - public function &getModules() - { - $db = $this->getDbo(); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('a.id'), - $db->quoteName('a.title'), - $db->quoteName('a.params'), - $db->quoteName('a.position'), - $db->quoteName('ag.title', 'access_title'), - ] - ) - ->from($db->quoteName('#__modules', 'a')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->where($db->quoteName('a.module') . ' = ' . $db->quote('mod_menu')); - $db->setQuery($query); - - $modules = $db->loadObjectList(); - - $result = array(); - - foreach ($modules as &$module) - { - $params = new Registry($module->params); - - $menuType = $params->get('menutype'); - - if (!isset($result[$menuType])) - { - $result[$menuType] = array(); - } - - $result[$menuType][] = & $module; - } - - return $result; - } - - /** - * Custom clean the cache - * - * @param string $group Cache group name. - * @param integer $clientId @deprecated 5.0 No Longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_menus'); - parent::cleanCache('com_modules'); - parent::cleanCache('mod_menu'); - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_MENUS_MENU'; + + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_menus.menu'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + return Factory::getUser()->authorise('core.delete', 'com_menus.menu.' . (int) $record->id); + } + + /** + * Method to test whether the state of a record can be edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + return Factory::getUser()->authorise('core.edit.state', 'com_menus.menu.' . (int) $record->id); + } + + /** + * Returns a Table object, always creating it + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'MenuType', $prefix = '\JTable', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $id = $app->input->getInt('id'); + $this->setState('menu.id', $id); + + // Load the parameters. + $params = ComponentHelper::getParams('com_menus'); + $this->setState('params', $params); + + // Load the clientId. + $clientId = $app->getUserStateFromRequest('com_menus.menus.client_id', 'client_id', 0, 'int'); + $this->setState('client_id', $clientId); + } + + /** + * Method to get a menu item. + * + * @param integer $itemId The id of the menu item to get. + * + * @return mixed Menu item data object on success, false on failure. + * + * @since 1.6 + */ + public function &getItem($itemId = null) + { + $itemId = (!empty($itemId)) ? $itemId : (int) $this->getState('menu.id'); + + // Get a menu item row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($itemId); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + + $properties = $table->getProperties(1); + $value = ArrayHelper::toObject($properties, CMSObject::class); + + return $value; + } + + /** + * Method to get the menu item form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_menus.menu', 'menu', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + if (!$this->getState('client_id', 0)) { + $form->removeField('preset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_menus.edit.menu.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + if (empty($data->id)) { + $data->client_id = $this->state->get('client_id', 0); + } + } else { + unset($data['preset']); + } + + $this->preprocessData('com_menus.menu', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see JFormRule + * @see JFilterInput + * @since 3.9.23 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_menus')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $id = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('menu.id'); + $isNew = true; + + // Get a row instance. + $table = $this->getTable(); + + // Include the plugins for the save events. + PluginHelper::importPlugin('content'); + + // Load the row if saving an existing item. + if ($id > 0) { + $isNew = false; + $table->load($id); + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before event. + $result = Factory::getApplication()->triggerEvent('onContentBeforeSave', array($this->_context, &$table, $isNew, $data)); + + // Store the data. + if (in_array(false, $result, true) || !$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent('onContentAfterSave', array($this->_context, &$table, $isNew)); + + $this->setState('menu.id', $table->id); + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to delete groups. + * + * @param array $itemIds An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + */ + public function delete($itemIds) + { + // Sanitize the ids. + $itemIds = ArrayHelper::toInteger((array) $itemIds); + + // Get a group row instance. + $table = $this->getTable(); + + // Include the plugins for the delete events. + PluginHelper::importPlugin('content'); + + // Iterate the items to delete each one. + foreach ($itemIds as $itemId) { + if ($table->load($itemId)) { + // Trigger the before delete event. + $result = Factory::getApplication()->triggerEvent('onContentBeforeDelete', array($this->_context, $table)); + + if (in_array(false, $result, true) || !$table->delete($itemId)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after delete event. + Factory::getApplication()->triggerEvent('onContentAfterDelete', array($this->_context, $table)); + + // @todo: Delete the menu associations - Menu items and Modules + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Gets a list of all mod_mainmenu modules and collates them by menutype + * + * @return array + * + * @since 1.6 + */ + public function &getModules() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('a.id'), + $db->quoteName('a.title'), + $db->quoteName('a.params'), + $db->quoteName('a.position'), + $db->quoteName('ag.title', 'access_title'), + ] + ) + ->from($db->quoteName('#__modules', 'a')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->where($db->quoteName('a.module') . ' = ' . $db->quote('mod_menu')); + $db->setQuery($query); + + $modules = $db->loadObjectList(); + + $result = array(); + + foreach ($modules as &$module) { + $params = new Registry($module->params); + + $menuType = $params->get('menutype'); + + if (!isset($result[$menuType])) { + $result[$menuType] = array(); + } + + $result[$menuType][] = & $module; + } + + return $result; + } + + /** + * Returns the extension elements for the given items + * + * @param array $itemIds The item ids + * + * @return array + * + * @since 4.2.0 + */ + public function getExtensionElementsForMenuItems(array $itemIds): array + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName('e.element')) + ->from($db->quoteName('#__extensions', 'e')) + ->join('INNER', $db->quoteName('#__menu', 'm'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) + ->whereIn($db->quoteName('m.id'), ArrayHelper::toInteger($itemIds)); + + return $db->setQuery($query)->loadColumn(); + } + + /** + * Custom clean the cache + * + * @param string $group Cache group name. + * @param integer $clientId @deprecated 5.0 No Longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_menus'); + parent::cleanCache('com_modules'); + parent::cleanCache('mod_menu'); + } } diff --git a/code/administrator/components/com_menus/src/Model/MenusModel.php b/code/administrator/components/com_menus/src/Model/MenusModel.php index 732a2b5f..f8f9c4a0 100644 --- a/code/administrator/components/com_menus/src/Model/MenusModel.php +++ b/code/administrator/components/com_menus/src/Model/MenusModel.php @@ -1,4 +1,5 @@ getStoreId('getItems'); - - // Try to load the data from internal storage. - if (!empty($this->cache[$store])) - { - return $this->cache[$store]; - } - - // Load the list items. - $items = parent::getItems(); - - // If empty or an error, just return. - if (empty($items)) - { - return array(); - } - - // Getting the following metric by joins is WAY TOO SLOW. - // Faster to do three queries for very large menu trees. - - // Get the menu types of menus in the list. - $db = $this->getDbo(); - $menuTypes = array_column((array) $items, 'menutype'); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('m.menutype'), - 'COUNT(DISTINCT ' . $db->quoteName('m.id') . ') AS ' . $db->quoteName('count_published'), - ] - ) - ->from($db->quoteName('#__menu', 'm')) - ->where($db->quoteName('m.published') . ' = :published') - ->whereIn($db->quoteName('m.menutype'), $menuTypes, ParameterType::STRING) - ->group($db->quoteName('m.menutype')) - ->bind(':published', $published, ParameterType::INTEGER); - - $db->setQuery($query); - - // Get the published menu counts. - try - { - $published = 1; - $countPublished = $db->loadAssocList('menutype', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the unpublished menu counts. - try - { - $published = 0; - $countUnpublished = $db->loadAssocList('menutype', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the trashed menu counts. - try - { - $published = -2; - $countTrashed = $db->loadAssocList('menutype', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Inject the values back into the array. - foreach ($items as $item) - { - $item->count_published = $countPublished[$item->menutype] ?? 0; - $item->count_unpublished = $countUnpublished[$item->menutype] ?? 0; - $item->count_trashed = $countTrashed[$item->menutype] ?? 0; - } - - // Add the items to the internal cache. - $this->cache[$store] = $items; - - return $this->cache[$store]; - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - $clientId = (int) $this->getState('client_id'); - - // Select all fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.menutype'), - $db->quoteName('a.title'), - $db->quoteName('a.description'), - $db->quoteName('a.client_id'), - ] - ) - ) - ->from($db->quoteName('#__menu_types', 'a')) - ->where( - [ - $db->quoteName('a.id') . ' > 0', - $db->quoteName('a.client_id') . ' = :clientId', - ] - ) - ->bind(':clientId', $clientId, ParameterType::INTEGER); - - // Filter by search in title or menutype - if ($search = trim($this->getState('filter.search', ''))) - { - $search = '%' . str_replace(' ', '%', $search) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.title') . ' LIKE :search1' , - $db->quoteName('a.menutype') . ' LIKE :search2', - ], - 'OR' - ) - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.id')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.title', $direction = 'asc') - { - $search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search'); - $this->setState('filter.search', $search); - - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $this->setState('client_id', $clientId); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Gets the extension id of the core mod_menu module. - * - * @return integer - * - * @since 2.5 - */ - public function getModMenuId() - { - $clientId = (int) $this->getState('client_id'); - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('e.extension_id')) - ->from($db->quoteName('#__extensions', 'e')) - ->where( - [ - $db->quoteName('e.type') . ' = ' . $db->quote('module'), - $db->quoteName('e.element') . ' = ' . $db->quote('mod_menu'), - $db->quoteName('e.client_id') . ' = :clientId', - ] - ) - ->bind(':clientId', $clientId, ParameterType::INTEGER); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Gets a list of all mod_mainmenu modules and collates them by menutype - * - * @return array - * - * @since 1.6 - */ - public function &getModules() - { - $model = $this->bootComponent('com_menus') - ->getMVCFactory()->createModel('Menu', 'Administrator', ['ignore_request' => true]); - $result = $model->getModules(); - - return $result; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'menutype', 'a.menutype', + 'client_id', 'a.client_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Overrides the getItems method to attach additional metrics to the list. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6.1 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId('getItems'); + + // Try to load the data from internal storage. + if (!empty($this->cache[$store])) { + return $this->cache[$store]; + } + + // Load the list items. + $items = parent::getItems(); + + // If empty or an error, just return. + if (empty($items)) { + return array(); + } + + // Getting the following metric by joins is WAY TOO SLOW. + // Faster to do three queries for very large menu trees. + + // Get the menu types of menus in the list. + $db = $this->getDatabase(); + $menuTypes = array_column((array) $items, 'menutype'); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('m.menutype'), + 'COUNT(DISTINCT ' . $db->quoteName('m.id') . ') AS ' . $db->quoteName('count_published'), + ] + ) + ->from($db->quoteName('#__menu', 'm')) + ->where($db->quoteName('m.published') . ' = :published') + ->whereIn($db->quoteName('m.menutype'), $menuTypes, ParameterType::STRING) + ->group($db->quoteName('m.menutype')) + ->bind(':published', $published, ParameterType::INTEGER); + + $db->setQuery($query); + + // Get the published menu counts. + try { + $published = 1; + $countPublished = $db->loadAssocList('menutype', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the unpublished menu counts. + try { + $published = 0; + $countUnpublished = $db->loadAssocList('menutype', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the trashed menu counts. + try { + $published = -2; + $countTrashed = $db->loadAssocList('menutype', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Inject the values back into the array. + foreach ($items as $item) { + $item->count_published = $countPublished[$item->menutype] ?? 0; + $item->count_unpublished = $countUnpublished[$item->menutype] ?? 0; + $item->count_trashed = $countTrashed[$item->menutype] ?? 0; + } + + // Add the items to the internal cache. + $this->cache[$store] = $items; + + return $this->cache[$store]; + } + + /** + * Method to build an SQL query to load the list data. + * + * @return string An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $clientId = (int) $this->getState('client_id'); + + // Select all fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.menutype'), + $db->quoteName('a.title'), + $db->quoteName('a.description'), + $db->quoteName('a.client_id'), + ] + ) + ) + ->from($db->quoteName('#__menu_types', 'a')) + ->where( + [ + $db->quoteName('a.id') . ' > 0', + $db->quoteName('a.client_id') . ' = :clientId', + ] + ) + ->bind(':clientId', $clientId, ParameterType::INTEGER); + + // Filter by search in title or menutype + if ($search = trim($this->getState('filter.search', ''))) { + $search = '%' . str_replace(' ', '%', $search) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.title') . ' LIKE :search1' , + $db->quoteName('a.menutype') . ' LIKE :search2', + ], + 'OR' + ) + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.id')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.title', $direction = 'asc') + { + $search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search'); + $this->setState('filter.search', $search); + + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $this->setState('client_id', $clientId); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Gets the extension id of the core mod_menu module. + * + * @return integer + * + * @since 2.5 + */ + public function getModMenuId() + { + $clientId = (int) $this->getState('client_id'); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('e.extension_id')) + ->from($db->quoteName('#__extensions', 'e')) + ->where( + [ + $db->quoteName('e.type') . ' = ' . $db->quote('module'), + $db->quoteName('e.element') . ' = ' . $db->quote('mod_menu'), + $db->quoteName('e.client_id') . ' = :clientId', + ] + ) + ->bind(':clientId', $clientId, ParameterType::INTEGER); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Gets a list of all mod_mainmenu modules and collates them by menutype + * + * @return array + * + * @since 1.6 + */ + public function &getModules() + { + $model = $this->bootComponent('com_menus') + ->getMVCFactory()->createModel('Menu', 'Administrator', ['ignore_request' => true]); + $result = $model->getModules(); + + return $result; + } + + /** + * Returns the missing module languages. + * + * @return array + * + * @since _DEPLOY_VERSION__ + */ + public function getMissingModuleLanguages(): array + { + // Check custom administrator menu modules + if (!ModuleHelper::isAdminMultilang()) { + return []; + } + + $languages = LanguageHelper::getInstalledLanguages(1, true); + $langCodes = []; + + foreach ($languages as $language) { + if (isset($language->metadata['nativeName'])) { + $languageName = $language->metadata['nativeName']; + } else { + $languageName = $language->metadata['name']; + } + + $langCodes[$language->metadata['tag']] = $languageName; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('m.language')) + ->from($db->quoteName('#__modules', 'm')) + ->where( + [ + $db->quoteName('m.module') . ' = ' . $db->quote('mod_menu'), + $db->quoteName('m.published') . ' = 1', + $db->quoteName('m.client_id') . ' = 1', + ] + ) + ->group($db->quoteName('m.language')); + + $mLanguages = $db->setQuery($query)->loadColumn(); + + // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. + if (!in_array('*', $mLanguages) && count($langMissing = array_diff(array_keys($langCodes), $mLanguages))) { + return array_intersect_key($langCodes, array_flip($langMissing)); + } + + return []; + } } diff --git a/code/administrator/components/com_menus/src/Model/MenutypesModel.php b/code/administrator/components/com_menus/src/Model/MenutypesModel.php index 6df02675..8402492b 100644 --- a/code/administrator/components/com_menus/src/Model/MenutypesModel.php +++ b/code/administrator/components/com_menus/src/Model/MenutypesModel.php @@ -1,4 +1,5 @@ input->get('client_id', 0); - - $this->state->set('client_id', $clientId); - } - - /** - * Method to get the reverse lookup of the base link URL to Title - * - * @return array Array of reverse lookup of the base link URL to Title - * - * @since 1.6 - */ - public function getReverseLookup() - { - if (empty($this->rlu)) - { - $this->getTypeOptions(); - } - - return $this->rlu; - } - - /** - * Method to get the available menu item type options. - * - * @return array Array of groups with menu item types. - * - * @since 1.6 - */ - public function getTypeOptions() - { - $lang = Factory::getLanguage(); - $list = array(); - - // Get the list of components. - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('name'), - $db->quoteName('element', 'option'), - ] - ) - ->from($db->quoteName('#__extensions')) - ->where( - [ - $db->quoteName('type') . ' = ' . $db->quote('component'), - $db->quoteName('enabled') . ' = 1', - ] - ) - ->order($db->quoteName('name') . ' ASC'); - $db->setQuery($query); - $components = $db->loadObjectList(); - - foreach ($components as $component) - { - $options = $this->getTypeOptionsByComponent($component->option); - - if ($options) - { - $list[$component->name] = $options; - - // Create the reverse lookup for link-to-name. - foreach ($options as $option) - { - if (isset($option->request)) - { - $this->addReverseLookupUrl($option); - - if (isset($option->request['option'])) - { - $componentLanguageFolder = JPATH_ADMINISTRATOR . '/components/' . $option->request['option']; - $lang->load($option->request['option'] . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($option->request['option'] . '.sys', $componentLanguageFolder); - } - } - } - } - } - - // Allow a system plugin to insert dynamic menu types to the list shown in menus: - Factory::getApplication()->triggerEvent('onAfterGetMenuTypeOptions', array(&$list, $this)); - - return $list; - } - - /** - * Method to create the reverse lookup for link-to-name. - * (can be used from onAfterGetMenuTypeOptions handlers) - * - * @param CMSObject $option Object with request array or string and title public variables - * - * @return void - * - * @since 3.1 - */ - public function addReverseLookupUrl($option) - { - $this->rlu[MenusHelper::getLinkKey($option->request)] = $option->get('title'); - } - - /** - * Get menu types by component. - * - * @param string $component Component URL option. - * - * @return array - * - * @since 1.6 - */ - protected function getTypeOptionsByComponent($component) - { - $options = array(); - $client = ApplicationHelper::getClientInfo($this->getState('client_id')); - $mainXML = $client->path . '/components/' . $component . '/metadata.xml'; - - if (is_file($mainXML)) - { - $options = $this->getTypeOptionsFromXml($mainXML, $component); - } - - if (empty($options)) - { - $options = $this->getTypeOptionsFromMvc($component); - } - - if ($client->id == 1 && empty($options)) - { - $options = $this->getTypeOptionsFromManifest($component); - } - - return $options; - } - - /** - * Get the menu types from an XML file - * - * @param string $file File path - * @param string $component Component option as in URL - * - * @return array|boolean - * - * @since 1.6 - */ - protected function getTypeOptionsFromXml($file, $component) - { - $options = array(); - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($file)) - { - return false; - } - - // Look for the first menu node off of the root node. - if (!$menu = $xml->xpath('menu[1]')) - { - return false; - } - else - { - $menu = $menu[0]; - } - - // If we have no options to parse, just add the base component to the list of options. - if (!empty($menu['options']) && $menu['options'] == 'none') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $menu['name']; - $o->description = (string) $menu['msg']; - $o->request = array('option' => $component); - - $options[] = $o; - - return $options; - } - - // Look for the first options node off of the menu node. - if (!$optionsNode = $menu->xpath('options[1]')) - { - return false; - } - else - { - $optionsNode = $optionsNode[0]; - } - - // Make sure the options node has children. - if (!$children = $optionsNode->children()) - { - return false; - } - - // Process each child as an option. - foreach ($children as $child) - { - if ($child->getName() == 'option') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $child['name']; - $o->description = (string) $child['msg']; - $o->request = array('option' => $component, (string) $optionsNode['var'] => (string) $child['value']); - - $options[] = $o; - } - elseif ($child->getName() == 'default') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $child['name']; - $o->description = (string) $child['msg']; - $o->request = array('option' => $component); - - $options[] = $o; - } - } - - return $options; - } - - /** - * Get menu types from MVC - * - * @param string $component Component option like in URLs - * - * @return array|boolean - * - * @since 1.6 - */ - protected function getTypeOptionsFromMvc($component) - { - $options = array(); - $views = array(); - - foreach ($this->getFolders($component) as $path) - { - if (!is_dir($path)) - { - continue; - } - - $views = array_merge($views, Folder::folders($path, '.', false, true)); - } - - foreach ($views as $viewPath) - { - $view = basename($viewPath); - - // Ignore private views. - if (strpos($view, '_') !== 0) - { - // Determine if a metadata file exists for the view. - $file = $viewPath . '/metadata.xml'; - - if (is_file($file)) - { - // Attempt to load the xml file. - if ($xml = simplexml_load_file($file)) - { - // Look for the first view node off of the root node. - if ($menu = $xml->xpath('view[1]')) - { - $menu = $menu[0]; - - // If the view is hidden from the menu, discard it and move on to the next view. - if (!empty($menu['hidden']) && $menu['hidden'] == 'true') - { - unset($xml); - continue; - } - - // Do we have an options node or should we process layouts? - // Look for the first options node off of the menu node. - if ($optionsNode = $menu->xpath('options[1]')) - { - $optionsNode = $optionsNode[0]; - - // Make sure the options node has children. - if ($children = $optionsNode->children()) - { - // Process each child as an option. - foreach ($children as $child) - { - if ($child->getName() == 'option') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $child['name']; - $o->description = (string) $child['msg']; - $o->request = array('option' => $component, 'view' => $view, (string) $optionsNode['var'] => (string) $child['value']); - - $options[] = $o; - } - elseif ($child->getName() == 'default') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $child['name']; - $o->description = (string) $child['msg']; - $o->request = array('option' => $component, 'view' => $view); - - $options[] = $o; - } - } - } - } - else - { - $options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view)); - } - } - - unset($xml); - } - } - else - { - $options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view)); - } - } - } - - return $options; - } - - /** - * Get menu types from Component manifest - * - * @param string $component Component option like in URLs - * - * @return array|boolean - * - * @since 3.7.0 - */ - protected function getTypeOptionsFromManifest($component) - { - // Load the component manifest - $fileName = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'; - - if (!is_file($fileName)) - { - return false; - } - - if (!($manifest = simplexml_load_file($fileName))) - { - return false; - } - - // Check for a valid XML root tag. - if ($manifest->getName() != 'extension') - { - return false; - } - - $options = array(); - - // Start with the component root menu. - $rootMenu = $manifest->administration->menu; - - // If the menu item doesn't exist or is hidden do nothing. - if (!$rootMenu || in_array((string) $rootMenu['hidden'], array('true', 'hidden'))) - { - return $options; - } - - // Create the root menu option. - $ro = new \stdClass; - $ro->title = (string) trim($rootMenu); - $ro->description = ''; - $ro->request = array('option' => $component); - - // Process submenu options. - $submenu = $manifest->administration->submenu; - - if (!$submenu) - { - return $options; - } - - foreach ($submenu->menu as $child) - { - $attributes = $child->attributes(); - - $o = new \stdClass; - $o->title = (string) trim($child); - $o->description = ''; - - if ((string) $attributes->link) - { - parse_str((string) $attributes->link, $request); - } - else - { - $request = array(); - - $request['option'] = $component; - $request['act'] = (string) $attributes->act; - $request['task'] = (string) $attributes->task; - $request['controller'] = (string) $attributes->controller; - $request['view'] = (string) $attributes->view; - $request['layout'] = (string) $attributes->layout; - $request['sub'] = (string) $attributes->sub; - } - - $o->request = array_filter($request, 'strlen'); - $options[] = new CMSObject($o); - - // Do not repeat the default view link (index.php?option=com_abc). - if (count($o->request) == 1) - { - $ro = null; - } - } - - if ($ro) - { - $options[] = new CMSObject($ro); - } - - return $options; - } - - /** - * Get the menu types from component layouts - * - * @param string $component Component option as in URLs - * @param string $view Name of the view - * - * @return array - * - * @since 1.6 - */ - protected function getTypeOptionsFromLayouts($component, $view) - { - $options = array(); - $layouts = array(); - $layoutNames = array(); - $lang = Factory::getLanguage(); - $client = ApplicationHelper::getClientInfo($this->getState('client_id')); - - // Get the views for this component. - foreach ($this->getFolders($component) as $folder) - { - $path = $folder . '/' . $view . '/tmpl'; - - if (!is_dir($path)) - { - $path = $folder . '/' . $view; - } - - if (!is_dir($path)) - { - continue; - } - - $layouts = array_merge($layouts, Folder::files($path, '.xml$', false, true)); - } - - // Build list of standard layout names - foreach ($layouts as $layout) - { - // Ignore private layouts. - if (strpos(basename($layout), '_') === false) - { - // Get the layout name. - $layoutNames[] = basename($layout, '.xml'); - } - } - - // Get the template layouts - // @todo: This should only search one template -- the current template for this item (default of specified) - $folders = Folder::folders($client->path . '/templates', '', false, true); - - // Array to hold association between template file names and templates - $templateName = array(); - - foreach ($folders as $folder) - { - if (is_dir($folder . '/html/' . $component . '/' . $view)) - { - $template = basename($folder); - $lang->load('tpl_' . $template . '.sys', $client->path) - || $lang->load('tpl_' . $template . '.sys', $client->path . '/templates/' . $template); - - $templateLayouts = Folder::files($folder . '/html/' . $component . '/' . $view, '.xml$', false, true); - - foreach ($templateLayouts as $layout) - { - // Get the layout name. - $templateLayoutName = basename($layout, '.xml'); - - // Add to the list only if it is not a standard layout - if (array_search($templateLayoutName, $layoutNames) === false) - { - $layouts[] = $layout; - - // Set template name array so we can get the right template for the layout - $templateName[$layout] = basename($folder); - } - } - } - } - - // Process the found layouts. - foreach ($layouts as $layout) - { - // Ignore private layouts. - if (strpos(basename($layout), '_') === false) - { - $file = $layout; - - // Get the layout name. - $layout = basename($layout, '.xml'); - - // Create the menu option for the layout. - $o = new CMSObject; - $o->title = ucfirst($layout); - $o->description = ''; - $o->request = array('option' => $component, 'view' => $view); - - // Only add the layout request argument if not the default layout. - if ($layout != 'default') - { - // If the template is set, add in format template:layout so we save the template name - $o->request['layout'] = isset($templateName[$file]) ? $templateName[$file] . ':' . $layout : $layout; - } - - // Load layout metadata if it exists. - if (is_file($file)) - { - // Attempt to load the xml file. - if ($xml = simplexml_load_file($file)) - { - // Look for the first view node off of the root node. - if ($menu = $xml->xpath('layout[1]')) - { - $menu = $menu[0]; - - // If the view is hidden from the menu, discard it and move on to the next view. - if (!empty($menu['hidden']) && $menu['hidden'] == 'true') - { - unset($xml); - unset($o); - continue; - } - - // Populate the title and description if they exist. - if (!empty($menu['title'])) - { - $o->title = trim((string) $menu['title']); - } - - if (!empty($menu->message[0])) - { - $o->description = trim((string) $menu->message[0]); - } - } - } - } - - // Add the layout to the options array. - $options[] = $o; - } - } - - return $options; - } - - /** - * Get the folders with template files for the given component. - * - * @param string $component Component option as in URLs - * - * @return array - * - * @since 4.0.0 - */ - private function getFolders($component) - { - $client = ApplicationHelper::getClientInfo($this->getState('client_id')); - - if (!is_dir($client->path . '/components/' . $component)) - { - return array(); - } - - $folders = Folder::folders($client->path . '/components/' . $component, '^view[s]?$', false, true); - $folders = array_merge($folders, Folder::folders($client->path . '/components/' . $component, '^tmpl?$', false, true)); - - if (!$folders) - { - return array(); - } - - return $folders; - } + /** + * A reverse lookup of the base link URL to Title + * + * @var array + */ + protected $rlu = array(); + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * @return void + * + * @note Calling getState in this method will result in recursion. + * @since 3.0.1 + */ + protected function populateState() + { + parent::populateState(); + + $clientId = Factory::getApplication()->input->get('client_id', 0); + + $this->state->set('client_id', $clientId); + } + + /** + * Method to get the reverse lookup of the base link URL to Title + * + * @return array Array of reverse lookup of the base link URL to Title + * + * @since 1.6 + */ + public function getReverseLookup() + { + if (empty($this->rlu)) { + $this->getTypeOptions(); + } + + return $this->rlu; + } + + /** + * Method to get the available menu item type options. + * + * @return array Array of groups with menu item types. + * + * @since 1.6 + */ + public function getTypeOptions() + { + $lang = Factory::getLanguage(); + $list = array(); + + // Get the list of components. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('name'), + $db->quoteName('element', 'option'), + ] + ) + ->from($db->quoteName('#__extensions')) + ->where( + [ + $db->quoteName('type') . ' = ' . $db->quote('component'), + $db->quoteName('enabled') . ' = 1', + ] + ) + ->order($db->quoteName('name') . ' ASC'); + $db->setQuery($query); + $components = $db->loadObjectList(); + + foreach ($components as $component) { + $options = $this->getTypeOptionsByComponent($component->option); + + if ($options) { + $list[$component->name] = $options; + + // Create the reverse lookup for link-to-name. + foreach ($options as $option) { + if (isset($option->request)) { + $this->addReverseLookupUrl($option); + + if (isset($option->request['option'])) { + $componentLanguageFolder = JPATH_ADMINISTRATOR . '/components/' . $option->request['option']; + $lang->load($option->request['option'] . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($option->request['option'] . '.sys', $componentLanguageFolder); + } + } + } + } + } + + // Allow a system plugin to insert dynamic menu types to the list shown in menus: + Factory::getApplication()->triggerEvent('onAfterGetMenuTypeOptions', array(&$list, $this)); + + return $list; + } + + /** + * Method to create the reverse lookup for link-to-name. + * (can be used from onAfterGetMenuTypeOptions handlers) + * + * @param CMSObject $option Object with request array or string and title public variables + * + * @return void + * + * @since 3.1 + */ + public function addReverseLookupUrl($option) + { + $this->rlu[MenusHelper::getLinkKey($option->request)] = $option->get('title'); + } + + /** + * Get menu types by component. + * + * @param string $component Component URL option. + * + * @return array + * + * @since 1.6 + */ + protected function getTypeOptionsByComponent($component) + { + $options = array(); + $client = ApplicationHelper::getClientInfo($this->getState('client_id')); + $mainXML = $client->path . '/components/' . $component . '/metadata.xml'; + + if (is_file($mainXML)) { + $options = $this->getTypeOptionsFromXml($mainXML, $component); + } + + if (empty($options)) { + $options = $this->getTypeOptionsFromMvc($component); + } + + if ($client->id == 1 && empty($options)) { + $options = $this->getTypeOptionsFromManifest($component); + } + + return $options; + } + + /** + * Get the menu types from an XML file + * + * @param string $file File path + * @param string $component Component option as in URL + * + * @return array|boolean + * + * @since 1.6 + */ + protected function getTypeOptionsFromXml($file, $component) + { + $options = array(); + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($file)) { + return false; + } + + // Look for the first menu node off of the root node. + if (!$menu = $xml->xpath('menu[1]')) { + return false; + } else { + $menu = $menu[0]; + } + + // If we have no options to parse, just add the base component to the list of options. + if (!empty($menu['options']) && $menu['options'] == 'none') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $menu['name']; + $o->description = (string) $menu['msg']; + $o->request = array('option' => $component); + + $options[] = $o; + + return $options; + } + + // Look for the first options node off of the menu node. + if (!$optionsNode = $menu->xpath('options[1]')) { + return false; + } else { + $optionsNode = $optionsNode[0]; + } + + // Make sure the options node has children. + if (!$children = $optionsNode->children()) { + return false; + } + + // Process each child as an option. + foreach ($children as $child) { + if ($child->getName() == 'option') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $child['name']; + $o->description = (string) $child['msg']; + $o->request = array('option' => $component, (string) $optionsNode['var'] => (string) $child['value']); + + $options[] = $o; + } elseif ($child->getName() == 'default') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $child['name']; + $o->description = (string) $child['msg']; + $o->request = array('option' => $component); + + $options[] = $o; + } + } + + return $options; + } + + /** + * Get menu types from MVC + * + * @param string $component Component option like in URLs + * + * @return array|boolean + * + * @since 1.6 + */ + protected function getTypeOptionsFromMvc($component) + { + $options = array(); + $views = array(); + + foreach ($this->getFolders($component) as $path) { + if (!is_dir($path)) { + continue; + } + + $views = array_merge($views, Folder::folders($path, '.', false, true)); + } + + foreach ($views as $viewPath) { + $view = basename($viewPath); + + // Ignore private views. + if (strpos($view, '_') !== 0) { + // Determine if a metadata file exists for the view. + $file = $viewPath . '/metadata.xml'; + + if (is_file($file)) { + // Attempt to load the xml file. + if ($xml = simplexml_load_file($file)) { + // Look for the first view node off of the root node. + if ($menu = $xml->xpath('view[1]')) { + $menu = $menu[0]; + + // If the view is hidden from the menu, discard it and move on to the next view. + if (!empty($menu['hidden']) && $menu['hidden'] == 'true') { + unset($xml); + continue; + } + + // Do we have an options node or should we process layouts? + // Look for the first options node off of the menu node. + if ($optionsNode = $menu->xpath('options[1]')) { + $optionsNode = $optionsNode[0]; + + // Make sure the options node has children. + if ($children = $optionsNode->children()) { + // Process each child as an option. + foreach ($children as $child) { + if ($child->getName() == 'option') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $child['name']; + $o->description = (string) $child['msg']; + $o->request = array('option' => $component, 'view' => $view, (string) $optionsNode['var'] => (string) $child['value']); + + $options[] = $o; + } elseif ($child->getName() == 'default') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $child['name']; + $o->description = (string) $child['msg']; + $o->request = array('option' => $component, 'view' => $view); + + $options[] = $o; + } + } + } + } else { + $options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view)); + } + } + + unset($xml); + } + } else { + $options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view)); + } + } + } + + return $options; + } + + /** + * Get menu types from Component manifest + * + * @param string $component Component option like in URLs + * + * @return array|boolean + * + * @since 3.7.0 + */ + protected function getTypeOptionsFromManifest($component) + { + // Load the component manifest + $fileName = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'; + + if (!is_file($fileName)) { + return false; + } + + if (!($manifest = simplexml_load_file($fileName))) { + return false; + } + + // Check for a valid XML root tag. + if ($manifest->getName() != 'extension') { + return false; + } + + $options = array(); + + // Start with the component root menu. + $rootMenu = $manifest->administration->menu; + + // If the menu item doesn't exist or is hidden do nothing. + if (!$rootMenu || in_array((string) $rootMenu['hidden'], array('true', 'hidden'))) { + return $options; + } + + // Create the root menu option. + $ro = new \stdClass(); + $ro->title = (string) trim($rootMenu); + $ro->description = ''; + $ro->request = array('option' => $component); + + // Process submenu options. + $submenu = $manifest->administration->submenu; + + if (!$submenu) { + return $options; + } + + foreach ($submenu->menu as $child) { + $attributes = $child->attributes(); + + $o = new \stdClass(); + $o->title = (string) trim($child); + $o->description = ''; + + if ((string) $attributes->link) { + parse_str((string) $attributes->link, $request); + } else { + $request = array(); + + $request['option'] = $component; + $request['act'] = (string) $attributes->act; + $request['task'] = (string) $attributes->task; + $request['controller'] = (string) $attributes->controller; + $request['view'] = (string) $attributes->view; + $request['layout'] = (string) $attributes->layout; + $request['sub'] = (string) $attributes->sub; + } + + $o->request = array_filter($request, 'strlen'); + $options[] = new CMSObject($o); + + // Do not repeat the default view link (index.php?option=com_abc). + if (count($o->request) == 1) { + $ro = null; + } + } + + if ($ro) { + $options[] = new CMSObject($ro); + } + + return $options; + } + + /** + * Get the menu types from component layouts + * + * @param string $component Component option as in URLs + * @param string $view Name of the view + * + * @return array + * + * @since 1.6 + */ + protected function getTypeOptionsFromLayouts($component, $view) + { + $options = array(); + $layouts = array(); + $layoutNames = array(); + $lang = Factory::getLanguage(); + $client = ApplicationHelper::getClientInfo($this->getState('client_id')); + + // Get the views for this component. + foreach ($this->getFolders($component) as $folder) { + $path = $folder . '/' . $view . '/tmpl'; + + if (!is_dir($path)) { + $path = $folder . '/' . $view; + } + + if (!is_dir($path)) { + continue; + } + + $layouts = array_merge($layouts, Folder::files($path, '.xml$', false, true)); + } + + // Build list of standard layout names + foreach ($layouts as $layout) { + // Ignore private layouts. + if (strpos(basename($layout), '_') === false) { + // Get the layout name. + $layoutNames[] = basename($layout, '.xml'); + } + } + + // Get the template layouts + // @todo: This should only search one template -- the current template for this item (default of specified) + $folders = Folder::folders($client->path . '/templates', '', false, true); + + // Array to hold association between template file names and templates + $templateName = array(); + + foreach ($folders as $folder) { + if (is_dir($folder . '/html/' . $component . '/' . $view)) { + $template = basename($folder); + $lang->load('tpl_' . $template . '.sys', $client->path) + || $lang->load('tpl_' . $template . '.sys', $client->path . '/templates/' . $template); + + $templateLayouts = Folder::files($folder . '/html/' . $component . '/' . $view, '.xml$', false, true); + + foreach ($templateLayouts as $layout) { + // Get the layout name. + $templateLayoutName = basename($layout, '.xml'); + + // Add to the list only if it is not a standard layout + if (array_search($templateLayoutName, $layoutNames) === false) { + $layouts[] = $layout; + + // Set template name array so we can get the right template for the layout + $templateName[$layout] = basename($folder); + } + } + } + } + + // Process the found layouts. + foreach ($layouts as $layout) { + // Ignore private layouts. + if (strpos(basename($layout), '_') === false) { + $file = $layout; + + // Get the layout name. + $layout = basename($layout, '.xml'); + + // Create the menu option for the layout. + $o = new CMSObject(); + $o->title = ucfirst($layout); + $o->description = ''; + $o->request = array('option' => $component, 'view' => $view); + + // Only add the layout request argument if not the default layout. + if ($layout != 'default') { + // If the template is set, add in format template:layout so we save the template name + $o->request['layout'] = isset($templateName[$file]) ? $templateName[$file] . ':' . $layout : $layout; + } + + // Load layout metadata if it exists. + if (is_file($file)) { + // Attempt to load the xml file. + if ($xml = simplexml_load_file($file)) { + // Look for the first view node off of the root node. + if ($menu = $xml->xpath('layout[1]')) { + $menu = $menu[0]; + + // If the view is hidden from the menu, discard it and move on to the next view. + if (!empty($menu['hidden']) && $menu['hidden'] == 'true') { + unset($xml); + unset($o); + continue; + } + + // Populate the title and description if they exist. + if (!empty($menu['title'])) { + $o->title = trim((string) $menu['title']); + } + + if (!empty($menu->message[0])) { + $o->description = trim((string) $menu->message[0]); + } + } + } + } + + // Add the layout to the options array. + $options[] = $o; + } + } + + return $options; + } + + /** + * Get the folders with template files for the given component. + * + * @param string $component Component option as in URLs + * + * @return array + * + * @since 4.0.0 + */ + private function getFolders($component) + { + $client = ApplicationHelper::getClientInfo($this->getState('client_id')); + + if (!is_dir($client->path . '/components/' . $component)) { + return array(); + } + + $folders = Folder::folders($client->path . '/components/' . $component, '^view[s]?$', false, true); + $folders = array_merge($folders, Folder::folders($client->path . '/components/' . $component, '^tmpl?$', false, true)); + + if (!$folders) { + return array(); + } + + return $folders; + } } diff --git a/code/administrator/components/com_menus/src/Service/HTML/Menus.php b/code/administrator/components/com_menus/src/Service/HTML/Menus.php index 4fac4b10..5e602e59 100644 --- a/code/administrator/components/com_menus/src/Service/HTML/Menus.php +++ b/code/administrator/components/com_menus/src/Service/HTML/Menus.php @@ -1,4 +1,5 @@ getQuery(true) - ->select( - [ - $db->quoteName('m.id'), - $db->quoteName('m.title'), - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('l.lang_code'), - $db->quoteName('mt.title', 'menu_title'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__menu', 'm')) - ->join('LEFT', $db->quoteName('#__menu_types', 'mt'), $db->quoteName('mt.menutype') . ' = ' . $db->quoteName('m.menutype')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('m.language') . ' = ' . $db->quoteName('l.lang_code')) - ->whereIn($db->quoteName('m.id'), array_values($associations)) - ->where($db->quoteName('m.id') . ' != :itemid') - ->bind(':itemid', $itemid, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - // Construct html - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); - - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_menus&task=item.edit&id=' . (int) $item->id); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('COM_MENUS_MENU_SPRINTF', $item->menu_title); - $classes = 'badge bg-secondary'; - - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } - - $html = LayoutHelper::render('joomla.content.associations', $items); - } - - return $html; - } - - /** - * Returns a visibility state on a grid - * - * @param integer $params Params of item. - * - * @return string The Html code - * - * @since 3.7.0 - */ - public function visibility($params) - { - $registry = new Registry; - - try - { - $registry->loadString($params); - } - catch (\Exception $e) - { - // Invalid JSON - } - - $show_menu = $registry->get('menu_show'); - - return ($show_menu === 0) ? '' . Text::_('COM_MENUS_LABEL_HIDDEN') . '' : ''; - } + /** + * Generate the markup to display the item associations + * + * @param int $itemid The menu item id + * + * @return string + * + * @since 3.0 + * + * @throws \Exception If there is an error on the query + */ + public function association($itemid) + { + // Defaults + $html = ''; + + // Get the associations + if ($associations = MenusHelper::getAssociations($itemid)) { + // Get the associated menu items + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('m.id'), + $db->quoteName('m.title'), + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('l.lang_code'), + $db->quoteName('mt.title', 'menu_title'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__menu', 'm')) + ->join('LEFT', $db->quoteName('#__menu_types', 'mt'), $db->quoteName('mt.menutype') . ' = ' . $db->quoteName('m.menutype')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('m.language') . ' = ' . $db->quoteName('l.lang_code')) + ->whereIn($db->quoteName('m.id'), array_values($associations)) + ->where($db->quoteName('m.id') . ' != :itemid') + ->bind(':itemid', $itemid, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } + + // Construct html + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); + + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_menus&task=item.edit&id=' . (int) $item->id); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('COM_MENUS_MENU_SPRINTF', $item->menu_title); + $classes = 'badge bg-secondary'; + + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } + + $html = LayoutHelper::render('joomla.content.associations', $items); + } + + return $html; + } + + /** + * Returns a visibility state on a grid + * + * @param integer $params Params of item. + * + * @return string The Html code + * + * @since 3.7.0 + */ + public function visibility($params) + { + $registry = new Registry(); + + try { + $registry->loadString($params); + } catch (\Exception $e) { + // Invalid JSON + } + + $show_menu = $registry->get('menu_show'); + + return ($show_menu === 0) ? '' . Text::_('COM_MENUS_LABEL_HIDDEN') . '' : ''; + } } diff --git a/code/administrator/components/com_menus/src/Table/MenuTable.php b/code/administrator/components/com_menus/src/Table/MenuTable.php index c0203d14..8836a3aa 100644 --- a/code/administrator/components/com_menus/src/Table/MenuTable.php +++ b/code/administrator/components/com_menus/src/Table/MenuTable.php @@ -1,4 +1,5 @@ getQuery(true) - ->delete($db->quoteName('#__modules_menu')) - ->where($db->quoteName('menuid') . ' = :pk') - ->bind(':pk', $pk, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } + if ($return) { + // Delete key from the #__modules_menu table + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__modules_menu')) + ->where($db->quoteName('menuid') . ' = :pk') + ->bind(':pk', $pk, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + } - return $return; - } + return $return; + } - /** - * Overloaded check function - * - * @return boolean True on success, false on failure - * - * @see JTable::check - * @since 4.0.0 - */ - public function check() - { - $return = parent::check(); + /** + * Overloaded check function + * + * @return boolean True on success, false on failure + * + * @see JTable::check + * @since 4.0.0 + */ + public function check() + { + $return = parent::check(); - if ($return) - { - // Set publish_up to null date if not set - if (!$this->publish_up) - { - $this->publish_up = null; - } + if ($return) { + // Set publish_up to null date if not set + if (!$this->publish_up) { + $this->publish_up = null; + } - // Set publish_down to null date if not set - if (!$this->publish_down) - { - $this->publish_down = null; - } + // Set publish_down to null date if not set + if (!$this->publish_down) { + $this->publish_down = null; + } - // Check the publish down date is not earlier than publish up. - if (!is_null($this->publish_down) && !is_null($this->publish_up) && $this->publish_down < $this->publish_up) - { - $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); + // Check the publish down date is not earlier than publish up. + if (!is_null($this->publish_down) && !is_null($this->publish_up) && $this->publish_down < $this->publish_up) { + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); - return false; - } + return false; + } - if ((int) $this->home) - { - // Set the publish down/up always for home. - $this->publish_up = null; - $this->publish_down = null; - } - } + if ((int) $this->home) { + // Set the publish down/up always for home. + $this->publish_up = null; + $this->publish_down = null; + } + } - return $return; - } + return $return; + } } diff --git a/code/administrator/components/com_menus/src/Table/MenuTypeTable.php b/code/administrator/components/com_menus/src/Table/MenuTypeTable.php index 10865629..0b63481e 100644 --- a/code/administrator/components/com_menus/src/Table/MenuTypeTable.php +++ b/code/administrator/components/com_menus/src/Table/MenuTypeTable.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->modules = $this->get('Modules'); - $this->levels = $this->get('ViewLevels'); - $this->canDo = ContentHelper::getActions('com_menus', 'menu', (int) $this->state->get('item.menutypeid')); - - // Check if we're allowed to edit this item - // No need to check for create, because then the moduletype select is empty - if (!empty($this->item->id) && !$this->canDo->get('core.edit')) - { - throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('parent_id', 'language', '*,' . $forcedLanguage); - } - - parent::display($tpl); - $this->addToolbar(); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $input = Factory::getApplication()->input; - $input->set('hidemainmenu', true); - - $user = Factory::getUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); - $canDo = $this->canDo; - $clientId = $this->state->get('item.client_id', 0); - - ToolbarHelper::title(Text::_($isNew ? 'COM_MENUS_VIEW_NEW_ITEM_TITLE' : 'COM_MENUS_VIEW_EDIT_ITEM_TITLE'), 'list menu-add'); - - $toolbarButtons = []; - - // If a new item, can save the item. Allow users with edit permissions to apply changes to prevent returning to grid. - if ($isNew && $canDo->get('core.create')) - { - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('item.apply'); - } - - $toolbarButtons[] = ['save', 'item.save']; - } - - // If not checked out, can save the item. - if (!$isNew && !$checkedOut && $canDo->get('core.edit')) - { - ToolbarHelper::apply('item.apply'); - - $toolbarButtons[] = ['save', 'item.save']; - } - - // If the user can create new items, allow them to see Save & New - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'item.save2new']; - } - - // If an existing item, can save to a copy only if we have create rights. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'item.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (!$isNew && Associations::isEnabled() && ComponentHelper::isEnabled('com_associations') && $clientId != 1) - { - ToolbarHelper::custom('item.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); - } - - if ($isNew) - { - ToolbarHelper::cancel('item.cancel'); - } - else - { - ToolbarHelper::cancel('item.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - - // Get the help information for the menu item. - $lang = Factory::getLanguage(); - - $help = $this->get('Help'); - - if ($lang->hasKey($help->url)) - { - $debug = $lang->setDebug(false); - $url = Text::_($help->url); - $lang->setDebug($debug); - } - else - { - $url = $help->url; - } - - ToolbarHelper::help($help->key, $help->local, $url); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var CMSObject + */ + protected $item; + + /** + * @var mixed + */ + protected $modules; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * @since 3.7.0 + */ + protected $canDo; + + /** + * A list of view levels containing the id and title of the view level + * + * @var \stdClass[] + * @since 4.0.0 + */ + protected $levels; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->modules = $this->get('Modules'); + $this->levels = $this->get('ViewLevels'); + $this->canDo = ContentHelper::getActions('com_menus', 'menu', (int) $this->state->get('item.menutypeid')); + + // Check if we're allowed to edit this item + // No need to check for create, because then the moduletype select is empty + if (!empty($this->item->id) && !$this->canDo->get('core.edit')) { + throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('parent_id', 'language', '*,' . $forcedLanguage); + } + + parent::display($tpl); + $this->addToolbar(); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $input = Factory::getApplication()->input; + $input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); + $canDo = $this->canDo; + $clientId = $this->state->get('item.client_id', 0); + + ToolbarHelper::title(Text::_($isNew ? 'COM_MENUS_VIEW_NEW_ITEM_TITLE' : 'COM_MENUS_VIEW_EDIT_ITEM_TITLE'), 'list menu-add'); + + $toolbarButtons = []; + + // If a new item, can save the item. Allow users with edit permissions to apply changes to prevent returning to grid. + if ($isNew && $canDo->get('core.create')) { + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('item.apply'); + } + + $toolbarButtons[] = ['save', 'item.save']; + } + + // If not checked out, can save the item. + if (!$isNew && !$checkedOut && $canDo->get('core.edit')) { + ToolbarHelper::apply('item.apply'); + + $toolbarButtons[] = ['save', 'item.save']; + } + + // If the user can create new items, allow them to see Save & New + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'item.save2new']; + } + + // If an existing item, can save to a copy only if we have create rights. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'item.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (!$isNew && Associations::isEnabled() && ComponentHelper::isEnabled('com_associations') && $clientId != 1) { + ToolbarHelper::custom('item.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); + } + + if ($isNew) { + ToolbarHelper::cancel('item.cancel'); + } else { + ToolbarHelper::cancel('item.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + + // Get the help information for the menu item. + $lang = Factory::getLanguage(); + + $help = $this->get('Help'); + + if ($lang->hasKey($help->url)) { + $debug = $lang->setDebug(false); + $url = Text::_($help->url); + $lang->setDebug($debug); + } else { + $url = $help->url; + } + + ToolbarHelper::help($help->key, $help->local, $url); + } } diff --git a/code/administrator/components/com_menus/src/View/Items/HtmlView.php b/code/administrator/components/com_menus/src/View/Items/HtmlView.php index f2b6f872..350b62c3 100644 --- a/code/administrator/components/com_menus/src/View/Items/HtmlView.php +++ b/code/administrator/components/com_menus/src/View/Items/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->total = $this->get('Total'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->ordering = array(); - - // Preprocess the list of items to find ordering divisions. - foreach ($this->items as $item) - { - $this->ordering[$item->parent_id][] = $item->id; - - // Item type text - switch ($item->type) - { - case 'url': - $value = Text::_('COM_MENUS_TYPE_EXTERNAL_URL'); - break; - - case 'alias': - $value = Text::_('COM_MENUS_TYPE_ALIAS'); - break; - - case 'separator': - $value = Text::_('COM_MENUS_TYPE_SEPARATOR'); - break; - - case 'heading': - $value = Text::_('COM_MENUS_TYPE_HEADING'); - break; - - case 'container': - $value = Text::_('COM_MENUS_TYPE_CONTAINER'); - break; - - case 'component': - default: - // Load language - $lang->load($item->componentname . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($item->componentname . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->componentname); - - if (!empty($item->componentname)) - { - $titleParts = array(); - $titleParts[] = Text::_($item->componentname); - $vars = null; - - parse_str($item->link, $vars); - - if (isset($vars['view'])) - { - // Attempt to load the view xml file. - $file = JPATH_SITE . '/components/' . $item->componentname . '/views/' . $vars['view'] . '/metadata.xml'; - - if (!is_file($file)) - { - $file = JPATH_SITE . '/components/' . $item->componentname . '/view/' . $vars['view'] . '/metadata.xml'; - } - - if (is_file($file) && $xml = simplexml_load_file($file)) - { - // Look for the first view node off of the root node. - if ($view = $xml->xpath('view[1]')) - { - // Add view title if present. - if (!empty($view[0]['title'])) - { - $viewTitle = trim((string) $view[0]['title']); - - // Check if the key is valid. Needed due to B/C so we don't show untranslated keys. This check should be removed with Joomla 4. - if ($lang->hasKey($viewTitle)) - { - $titleParts[] = Text::_($viewTitle); - } - } - } - } - - $vars['layout'] = $vars['layout'] ?? 'default'; - - // Attempt to load the layout xml file. - // If Alternative Menu Item, get template folder for layout file - if (strpos($vars['layout'], ':') > 0) - { - // Use template folder for layout file - $temp = explode(':', $vars['layout']); - $file = JPATH_SITE . '/templates/' . $temp[0] . '/html/' . $item->componentname . '/' . $vars['view'] . '/' . $temp[1] . '.xml'; - - // Load template language file - $lang->load('tpl_' . $temp[0] . '.sys', JPATH_SITE) - || $lang->load('tpl_' . $temp[0] . '.sys', JPATH_SITE . '/templates/' . $temp[0]); - } - else - { - $base = $this->state->get('filter.client_id') == 0 ? JPATH_SITE : JPATH_ADMINISTRATOR; - - // Get XML file from component folder for standard layouts - $file = $base . '/components/' . $item->componentname . '/tmpl/' . $vars['view'] - . '/' . $vars['layout'] . '.xml'; - - if (!file_exists($file)) - { - $file = $base . '/components/' . $item->componentname . '/views/' - . $vars['view'] . '/tmpl/' . $vars['layout'] . '.xml'; - - if (!file_exists($file)) - { - $file = $base . '/components/' . $item->componentname . '/view/' - . $vars['view'] . '/tmpl/' . $vars['layout'] . '.xml'; - } - } - } - - if (is_file($file) && $xml = simplexml_load_file($file)) - { - // Look for the first view node off of the root node. - if ($layout = $xml->xpath('layout[1]')) - { - if (!empty($layout[0]['title'])) - { - $titleParts[] = Text::_(trim((string) $layout[0]['title'])); - } - } - - if (!empty($layout[0]->message[0])) - { - $item->item_type_desc = Text::_(trim((string) $layout[0]->message[0])); - } - } - - unset($xml); - - // Special case if neither a view nor layout title is found - if (count($titleParts) == 1) - { - $titleParts[] = $vars['view']; - } - } - - $value = implode(' » ', $titleParts); - } - else - { - if (preg_match("/^index.php\?option=([a-zA-Z\-0-9_]*)/", $item->link, $result)) - { - $value = Text::sprintf('COM_MENUS_TYPE_UNEXISTING', $result[1]); - } - else - { - $value = Text::_('COM_MENUS_TYPE_UNKNOWN'); - } - } - break; - } - - $item->item_type = $value; - $item->protected = $item->menutype == 'main'; - } - - // Levels filter. - $options = array(); - $options[] = HTMLHelper::_('select.option', '1', Text::_('J1')); - $options[] = HTMLHelper::_('select.option', '2', Text::_('J2')); - $options[] = HTMLHelper::_('select.option', '3', Text::_('J3')); - $options[] = HTMLHelper::_('select.option', '4', Text::_('J4')); - $options[] = HTMLHelper::_('select.option', '5', Text::_('J5')); - $options[] = HTMLHelper::_('select.option', '6', Text::_('J6')); - $options[] = HTMLHelper::_('select.option', '7', Text::_('J7')); - $options[] = HTMLHelper::_('select.option', '8', Text::_('J8')); - $options[] = HTMLHelper::_('select.option', '9', Text::_('J9')); - $options[] = HTMLHelper::_('select.option', '10', Text::_('J10')); - - $this->f_levels = $options; - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In menu associations modal we need to remove language filter if forcing a language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - } - } - - // Allow a system plugin to insert dynamic menu types to the list shown in menus: - Factory::getApplication()->triggerEvent('onBeforeRenderMenuItems', array($this)); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $menutypeId = (int) $this->state->get('menutypeid'); - - $canDo = ContentHelper::getActions('com_menus', 'menu', (int) $menutypeId); - $user = Factory::getUser(); - - // Get the menu title - $menuTypeTitle = $this->get('State')->get('menutypetitle'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($menuTypeTitle) - { - ToolbarHelper::title(Text::sprintf('COM_MENUS_VIEW_ITEMS_MENU_TITLE', $menuTypeTitle), 'list menumgr'); - } - else - { - ToolbarHelper::title(Text::_('COM_MENUS_VIEW_ITEMS_ALL_TITLE'), 'list menumgr'); - } - - if ($canDo->get('core.create')) - { - $toolbar->addNew('item.add'); - } - - $protected = $this->state->get('filter.menutype') == 'main'; - - if (($canDo->get('core.edit.state') || Factory::getUser()->authorise('core.admin')) && !$protected - || $canDo->get('core.edit.state') && $this->state->get('filter.client_id') == 0) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state') && !$protected) - { - $childBar->publish('items.publish')->listCheck(true); - - $childBar->unpublish('items.unpublish')->listCheck(true); - } - - if (Factory::getUser()->authorise('core.admin') && !$protected) - { - $childBar->checkin('items.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - if ($this->state->get('filter.client_id') == 0) - { - $childBar->makeDefault('items.setDefault')->listCheck(true); - } - - if (!$protected) - { - $childBar->trash('items.trash')->listCheck(true); - } - } - - // Add a batch button - if (!$protected && $user->authorise('core.create', 'com_menus') - && $user->authorise('core.edit', 'com_menus') - && $user->authorise('core.edit.state', 'com_menus')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (Factory::getUser()->authorise('core.admin')) - { - $toolbar->standardButton('refresh') - ->text('JTOOLBAR_REBUILD') - ->task('items.rebuild'); - } - - if (!$protected && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('items.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_menus'); - } - - $toolbar->help('Menus:_Items'); - } + /** + * Array used for displaying the levels filter + * + * @var \stdClass[] + * @since 4.0.0 + */ + protected $f_levels; + + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Ordering of the items + * + * @var array + * @since 4.0.0 + */ + protected $ordering; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $lang = Factory::getLanguage(); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->total = $this->get('Total'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->ordering = array(); + + // Preprocess the list of items to find ordering divisions. + foreach ($this->items as $item) { + $this->ordering[$item->parent_id][] = $item->id; + + // Item type text + switch ($item->type) { + case 'url': + $value = Text::_('COM_MENUS_TYPE_EXTERNAL_URL'); + break; + + case 'alias': + $value = Text::_('COM_MENUS_TYPE_ALIAS'); + break; + + case 'separator': + $value = Text::_('COM_MENUS_TYPE_SEPARATOR'); + break; + + case 'heading': + $value = Text::_('COM_MENUS_TYPE_HEADING'); + break; + + case 'container': + $value = Text::_('COM_MENUS_TYPE_CONTAINER'); + break; + + case 'component': + default: + // Load language + $lang->load($item->componentname . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($item->componentname . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->componentname); + + if (!empty($item->componentname)) { + $titleParts = array(); + $titleParts[] = Text::_($item->componentname); + $vars = null; + + parse_str($item->link, $vars); + + if (isset($vars['view'])) { + // Attempt to load the view xml file. + $file = JPATH_SITE . '/components/' . $item->componentname . '/views/' . $vars['view'] . '/metadata.xml'; + + if (!is_file($file)) { + $file = JPATH_SITE . '/components/' . $item->componentname . '/view/' . $vars['view'] . '/metadata.xml'; + } + + if (is_file($file) && $xml = simplexml_load_file($file)) { + // Look for the first view node off of the root node. + if ($view = $xml->xpath('view[1]')) { + // Add view title if present. + if (!empty($view[0]['title'])) { + $viewTitle = trim((string) $view[0]['title']); + + // Check if the key is valid. Needed due to B/C so we don't show untranslated keys. This check should be removed with Joomla 4. + if ($lang->hasKey($viewTitle)) { + $titleParts[] = Text::_($viewTitle); + } + } + } + } + + $vars['layout'] = $vars['layout'] ?? 'default'; + + // Attempt to load the layout xml file. + // If Alternative Menu Item, get template folder for layout file + if (strpos($vars['layout'], ':') > 0) { + // Use template folder for layout file + $temp = explode(':', $vars['layout']); + $file = JPATH_SITE . '/templates/' . $temp[0] . '/html/' . $item->componentname . '/' . $vars['view'] . '/' . $temp[1] . '.xml'; + + // Load template language file + $lang->load('tpl_' . $temp[0] . '.sys', JPATH_SITE) + || $lang->load('tpl_' . $temp[0] . '.sys', JPATH_SITE . '/templates/' . $temp[0]); + } else { + $base = $this->state->get('filter.client_id') == 0 ? JPATH_SITE : JPATH_ADMINISTRATOR; + + // Get XML file from component folder for standard layouts + $file = $base . '/components/' . $item->componentname . '/tmpl/' . $vars['view'] + . '/' . $vars['layout'] . '.xml'; + + if (!file_exists($file)) { + $file = $base . '/components/' . $item->componentname . '/views/' + . $vars['view'] . '/tmpl/' . $vars['layout'] . '.xml'; + + if (!file_exists($file)) { + $file = $base . '/components/' . $item->componentname . '/view/' + . $vars['view'] . '/tmpl/' . $vars['layout'] . '.xml'; + } + } + } + + if (is_file($file) && $xml = simplexml_load_file($file)) { + // Look for the first view node off of the root node. + if ($layout = $xml->xpath('layout[1]')) { + if (!empty($layout[0]['title'])) { + $titleParts[] = Text::_(trim((string) $layout[0]['title'])); + } + } + + if (!empty($layout[0]->message[0])) { + $item->item_type_desc = Text::_(trim((string) $layout[0]->message[0])); + } + } + + unset($xml); + + // Special case if neither a view nor layout title is found + if (count($titleParts) == 1) { + $titleParts[] = $vars['view']; + } + } + + $value = implode(' » ', $titleParts); + } else { + if (preg_match("/^index.php\?option=([a-zA-Z\-0-9_]*)/", $item->link, $result)) { + $value = Text::sprintf('COM_MENUS_TYPE_UNEXISTING', $result[1]); + } else { + $value = Text::_('COM_MENUS_TYPE_UNKNOWN'); + } + } + break; + } + + $item->item_type = $value; + $item->protected = $item->menutype == 'main'; + } + + // Levels filter. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', Text::_('J1')); + $options[] = HTMLHelper::_('select.option', '2', Text::_('J2')); + $options[] = HTMLHelper::_('select.option', '3', Text::_('J3')); + $options[] = HTMLHelper::_('select.option', '4', Text::_('J4')); + $options[] = HTMLHelper::_('select.option', '5', Text::_('J5')); + $options[] = HTMLHelper::_('select.option', '6', Text::_('J6')); + $options[] = HTMLHelper::_('select.option', '7', Text::_('J7')); + $options[] = HTMLHelper::_('select.option', '8', Text::_('J8')); + $options[] = HTMLHelper::_('select.option', '9', Text::_('J9')); + $options[] = HTMLHelper::_('select.option', '10', Text::_('J10')); + + $this->f_levels = $options; + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In menu associations modal we need to remove language filter if forcing a language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + } + } + + // Allow a system plugin to insert dynamic menu types to the list shown in menus: + Factory::getApplication()->triggerEvent('onBeforeRenderMenuItems', array($this)); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $menutypeId = (int) $this->state->get('menutypeid'); + + $canDo = ContentHelper::getActions('com_menus', 'menu', (int) $menutypeId); + $user = $this->getCurrentUser(); + + // Get the menu title + $menuTypeTitle = $this->get('State')->get('menutypetitle'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($menuTypeTitle) { + ToolbarHelper::title(Text::sprintf('COM_MENUS_VIEW_ITEMS_MENU_TITLE', $menuTypeTitle), 'list menumgr'); + } else { + ToolbarHelper::title(Text::_('COM_MENUS_VIEW_ITEMS_ALL_TITLE'), 'list menumgr'); + } + + if ($canDo->get('core.create')) { + $toolbar->addNew('item.add'); + } + + $protected = $this->state->get('filter.menutype') == 'main'; + + if ( + ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) && !$protected + || $canDo->get('core.edit.state') && $this->state->get('filter.client_id') == 0 + ) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state') && !$protected) { + $childBar->publish('items.publish')->listCheck(true); + + $childBar->unpublish('items.unpublish')->listCheck(true); + } + + if ($this->getCurrentUser()->authorise('core.admin') && !$protected) { + $childBar->checkin('items.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + if ($this->state->get('filter.client_id') == 0) { + $childBar->makeDefault('items.setDefault')->listCheck(true); + } + + if (!$protected) { + $childBar->trash('items.trash')->listCheck(true); + } + } + + // Add a batch button + if ( + !$protected && $user->authorise('core.create', 'com_menus') + && $user->authorise('core.edit', 'com_menus') + && $user->authorise('core.edit.state', 'com_menus') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + $toolbar->standardButton('refresh') + ->text('JTOOLBAR_REBUILD') + ->task('items.rebuild'); + } + + if (!$protected && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('items.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_menus'); + } + + $toolbar->help('Menus:_Items'); + } } diff --git a/code/administrator/components/com_menus/src/View/Menu/HtmlView.php b/code/administrator/components/com_menus/src/View/Menu/HtmlView.php index 4ceeff9d..e3fa7bba 100644 --- a/code/administrator/components/com_menus/src/View/Menu/HtmlView.php +++ b/code/administrator/components/com_menus/src/View/Menu/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - $this->canDo = ContentHelper::getActions('com_menus', 'menu', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - $this->addToolbar(); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $input = Factory::getApplication()->input; - $input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - - ToolbarHelper::title(Text::_($isNew ? 'COM_MENUS_VIEW_NEW_MENU_TITLE' : 'COM_MENUS_VIEW_EDIT_MENU_TITLE'), 'list menu'); - - $toolbarButtons = []; - - // If a new item, can save the item. Allow users with edit permissions to apply changes to prevent returning to grid. - if ($isNew && $this->canDo->get('core.create')) - { - if ($this->canDo->get('core.edit')) - { - ToolbarHelper::apply('menu.apply'); - } - - $toolbarButtons[] = ['save', 'menu.save']; - } - - // If user can edit, can save the item. - if (!$isNew && $this->canDo->get('core.edit')) - { - ToolbarHelper::apply('menu.apply'); - - $toolbarButtons[] = ['save', 'menu.save']; - } - - // If the user can create new items, allow them to see Save & New - if ($this->canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'menu.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if ($isNew) - { - ToolbarHelper::cancel('menu.cancel'); - } - else - { - ToolbarHelper::cancel('menu.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Menus:_Edit'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + */ + protected $canDo; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + $this->canDo = ContentHelper::getActions('com_menus', 'menu', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + $this->addToolbar(); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $input = Factory::getApplication()->input; + $input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + + ToolbarHelper::title(Text::_($isNew ? 'COM_MENUS_VIEW_NEW_MENU_TITLE' : 'COM_MENUS_VIEW_EDIT_MENU_TITLE'), 'list menu'); + + $toolbarButtons = []; + + // If a new item, can save the item. Allow users with edit permissions to apply changes to prevent returning to grid. + if ($isNew && $this->canDo->get('core.create')) { + if ($this->canDo->get('core.edit')) { + ToolbarHelper::apply('menu.apply'); + } + + $toolbarButtons[] = ['save', 'menu.save']; + } + + // If user can edit, can save the item. + if (!$isNew && $this->canDo->get('core.edit')) { + ToolbarHelper::apply('menu.apply'); + + $toolbarButtons[] = ['save', 'menu.save']; + } + + // If the user can create new items, allow them to see Save & New + if ($this->canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'menu.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if ($isNew) { + ToolbarHelper::cancel('menu.cancel'); + } else { + ToolbarHelper::cancel('menu.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Menus:_Edit'); + } } diff --git a/code/administrator/components/com_menus/src/View/Menu/XmlView.php b/code/administrator/components/com_menus/src/View/Menu/XmlView.php index 29b3512d..d0b304b0 100644 --- a/code/administrator/components/com_menus/src/View/Menu/XmlView.php +++ b/code/administrator/components/com_menus/src/View/Menu/XmlView.php @@ -1,4 +1,5 @@ input->getCmd('menutype'); - - if ($menutype) - { - $root = MenusHelper::getMenuItems($menutype, true); - } - - if (!$root->hasChildren()) - { - Log::add(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), Log::WARNING, 'jerror'); - - $app->redirect(Route::_('index.php?option=com_menus&view=menus', false)); - - return; - } - - $this->items = $root->getChildren(true); - - $xml = new \SimpleXMLElement('' - ); - - foreach ($this->items as $item) - { - $this->addXmlChild($xml, $item); - } - - if (headers_sent($file, $line)) - { - Log::add("Headers already sent at $file:$line.", Log::ERROR, 'jerror'); - - return; - } - - header('content-type: application/xml'); - header('content-disposition: attachment; filename="' . $menutype . '.xml"'); - header("Cache-Control: no-cache, must-revalidate"); - header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); - - $dom = new \DOMDocument; - $dom->preserveWhiteSpace = true; - $dom->formatOutput = true; - $dom->loadXML($xml->asXML()); - - echo $dom->saveXML(); - - $app->close(); - } - - /** - * Add a child node to the xml - * - * @param \SimpleXMLElement $xml The current XML node which would become the parent to the new node - * @param \stdClass $item The menuitem object to create the child XML node from - * - * @return void - * - * @since 3.8.0 - */ - protected function addXmlChild($xml, $item) - { - $node = $xml->addChild('menuitem'); - - $node['type'] = $item->type; - - if ($item->title) - { - $node['title'] = htmlentities($item->title, ENT_XML1); - } - - if ($item->link) - { - $node['link'] = $item->link; - } - - if ($item->element) - { - $node['element'] = $item->element; - } - - if (isset($item->class) && $item->class) - { - $node['class'] = htmlentities($item->class, ENT_XML1); - } - - if ($item->access) - { - $node['access'] = $item->access; - } - - if ($item->browserNav) - { - $node['target'] = '_blank'; - } - - if (count($item->getParams())) - { - $hideitems = $item->getParams()->get('hideitems'); - - if ($hideitems) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - $query - ->select($db->quoteName('e.element')) - ->from($db->quoteName('#__extensions', 'e')) - ->join('INNER', $db->quoteName('#__menu', 'm'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) - ->whereIn($db->quoteName('m.id'), $hideitems); - - $hideitems = $db->setQuery($query)->loadColumn(); - - $item->getParams()->set('hideitems', $hideitems); - } - - $node->addChild('params', htmlentities((string) $item->getParams(), ENT_XML1)); - } - - if (isset($item->submenu)) - { - foreach ($item->submenu as $sub) - { - $this->addXmlChild($node, $sub); - } - } - } + /** + * @var \stdClass[] + * + * @since 3.8.0 + */ + protected $items; + + /** + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.8.0 + */ + protected $state; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.8.0 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $menutype = $app->input->getCmd('menutype'); + + if ($menutype) { + $root = MenusHelper::getMenuItems($menutype, true); + } + + if (!$root->hasChildren()) { + Log::add(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), Log::WARNING, 'jerror'); + + $app->redirect(Route::_('index.php?option=com_menus&view=menus', false)); + + return; + } + + $this->items = $root->getChildren(true); + + $xml = new \SimpleXMLElement(''); + + foreach ($this->items as $item) { + $this->addXmlChild($xml, $item); + } + + if (headers_sent($file, $line)) { + Log::add("Headers already sent at $file:$line.", Log::ERROR, 'jerror'); + + return; + } + + header('content-type: application/xml'); + header('content-disposition: attachment; filename="' . $menutype . '.xml"'); + header("Cache-Control: no-cache, must-revalidate"); + header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); + + $dom = new \DOMDocument(); + $dom->preserveWhiteSpace = true; + $dom->formatOutput = true; + $dom->loadXML($xml->asXML()); + + echo $dom->saveXML(); + + $app->close(); + } + + /** + * Add a child node to the xml + * + * @param \SimpleXMLElement $xml The current XML node which would become the parent to the new node + * @param \stdClass $item The menuitem object to create the child XML node from + * + * @return void + * + * @since 3.8.0 + */ + protected function addXmlChild($xml, $item) + { + $node = $xml->addChild('menuitem'); + + $node['type'] = $item->type; + + if ($item->title) { + $node['title'] = htmlentities($item->title, ENT_XML1); + } + + if ($item->link) { + $node['link'] = $item->link; + } + + if ($item->element) { + $node['element'] = $item->element; + } + + if (isset($item->class) && $item->class) { + $node['class'] = htmlentities($item->class, ENT_XML1); + } + + if ($item->access) { + $node['access'] = $item->access; + } + + if ($item->browserNav) { + $node['target'] = '_blank'; + } + + if ($item->getParams() && $hideitems = $item->getParams()->get('hideitems')) { + $item->getParams()->set('hideitems', $this->getModel('Menu')->getExtensionElementsForMenuItems($hideitems)); + + $node->addChild('params', htmlentities((string) $item->getParams(), ENT_XML1)); + } + + if (isset($item->submenu)) { + foreach ($item->submenu as $sub) { + $this->addXmlChild($node, $sub); + } + } + } } diff --git a/code/administrator/components/com_menus/src/View/Menus/HtmlView.php b/code/administrator/components/com_menus/src/View/Menus/HtmlView.php index e3ca25e2..949db95d 100644 --- a/code/administrator/components/com_menus/src/View/Menus/HtmlView.php +++ b/code/administrator/components/com_menus/src/View/Menus/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->modules = $this->get('Modules'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - - if ($this->getLayout() == 'default') - { - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_menus'); - - ToolbarHelper::title(Text::_('COM_MENUS_VIEW_MENUS_TITLE'), 'list menumgr'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('menu.add'); - } - - if ($canDo->get('core.delete')) - { - ToolbarHelper::divider(); - ToolbarHelper::deleteList('COM_MENUS_MENU_CONFIRM_DELETE', 'menus.delete', 'JTOOLBAR_DELETE'); - } - - if ($canDo->get('core.admin') && $this->state->get('client_id') == 1) - { - ToolbarHelper::custom('menu.exportXml', 'download', '', 'COM_MENUS_MENU_EXPORT_BUTTON', true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::divider(); - ToolbarHelper::preferences('com_menus'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Menus'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * List of all mod_mainmenu modules collated by menutype + * + * @var array + */ + protected $modules; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->modules = $this->get('Modules'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + + if ($this->getLayout() == 'default') { + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_menus'); + + ToolbarHelper::title(Text::_('COM_MENUS_VIEW_MENUS_TITLE'), 'list menumgr'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('menu.add'); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::divider(); + ToolbarHelper::deleteList('COM_MENUS_MENU_CONFIRM_DELETE', 'menus.delete', 'JTOOLBAR_DELETE'); + } + + if ($canDo->get('core.admin') && $this->state->get('client_id') == 1) { + ToolbarHelper::custom('menu.exportXml', 'download', '', 'COM_MENUS_MENU_EXPORT_BUTTON', true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::divider(); + ToolbarHelper::preferences('com_menus'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Menus'); + } } diff --git a/code/administrator/components/com_menus/src/View/Menutypes/HtmlView.php b/code/administrator/components/com_menus/src/View/Menutypes/HtmlView.php index 5844c40e..1796cbce 100644 --- a/code/administrator/components/com_menus/src/View/Menutypes/HtmlView.php +++ b/code/administrator/components/com_menus/src/View/Menutypes/HtmlView.php @@ -1,4 +1,5 @@ recordId = $app->input->getInt('recordId'); - - $types = $this->get('TypeOptions'); - - $this->addCustomTypes($types); - - $sortedTypes = array(); - - foreach ($types as $name => $list) - { - $tmp = array(); - - foreach ($list as $item) - { - $tmp[Text::_($item->title)] = $item; - } - - uksort($tmp, 'strcasecmp'); - $sortedTypes[Text::_($name)] = $tmp; - } - - uksort($sortedTypes, 'strcasecmp'); - - $this->types = $sortedTypes; - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.0 - */ - protected function addToolbar() - { - // Add page title - ToolbarHelper::title(Text::_('COM_MENUS'), 'list menumgr'); - - // Get the toolbar object instance - $bar = Toolbar::getInstance('toolbar'); - - // Cancel - $title = Text::_('JTOOLBAR_CANCEL'); - $dhtml = ""; - $bar->appendButton('Custom', $dhtml, 'new'); - } - - /** - * Method to add system link types to the link types array - * - * @param array $types The list of link types - * - * @return void - * - * @since 3.7.0 - */ - protected function addCustomTypes(&$types) - { - if (empty($types)) - { - $types = array(); - } - - // Adding System Links - $list = array(); - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_EXTERNAL_URL'; - $o->type = 'url'; - $o->description = 'COM_MENUS_TYPE_EXTERNAL_URL_DESC'; - $o->request = null; - $list[] = $o; - - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_ALIAS'; - $o->type = 'alias'; - $o->description = 'COM_MENUS_TYPE_ALIAS_DESC'; - $o->request = null; - $list[] = $o; - - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_SEPARATOR'; - $o->type = 'separator'; - $o->description = 'COM_MENUS_TYPE_SEPARATOR_DESC'; - $o->request = null; - $list[] = $o; - - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_HEADING'; - $o->type = 'heading'; - $o->description = 'COM_MENUS_TYPE_HEADING_DESC'; - $o->request = null; - $list[] = $o; - - if ($this->get('state')->get('client_id') == 1) - { - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_CONTAINER'; - $o->type = 'container'; - $o->description = 'COM_MENUS_TYPE_CONTAINER_DESC'; - $o->request = null; - $list[] = $o; - } - - $types['COM_MENUS_TYPE_SYSTEM'] = $list; - } + $bar->appendButton('Custom', $dhtml, 'new'); + } + + /** + * Method to add system link types to the link types array + * + * @param array $types The list of link types + * + * @return void + * + * @since 3.7.0 + */ + protected function addCustomTypes(&$types) + { + if (empty($types)) { + $types = array(); + } + + // Adding System Links + $list = array(); + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_EXTERNAL_URL'; + $o->type = 'url'; + $o->description = 'COM_MENUS_TYPE_EXTERNAL_URL_DESC'; + $o->request = null; + $list[] = $o; + + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_ALIAS'; + $o->type = 'alias'; + $o->description = 'COM_MENUS_TYPE_ALIAS_DESC'; + $o->request = null; + $list[] = $o; + + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_SEPARATOR'; + $o->type = 'separator'; + $o->description = 'COM_MENUS_TYPE_SEPARATOR_DESC'; + $o->request = null; + $list[] = $o; + + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_HEADING'; + $o->type = 'heading'; + $o->description = 'COM_MENUS_TYPE_HEADING_DESC'; + $o->request = null; + $list[] = $o; + + if ($this->get('state')->get('client_id') == 1) { + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_CONTAINER'; + $o->type = 'container'; + $o->description = 'COM_MENUS_TYPE_CONTAINER_DESC'; + $o->request = null; + $list[] = $o; + } + + $types['COM_MENUS_TYPE_SYSTEM'] = $list; + } } diff --git a/code/administrator/components/com_menus/tmpl/item/edit.php b/code/administrator/components/com_menus/tmpl/item/edit.php index dc9f5991..644dcca1 100644 --- a/code/administrator/components/com_menus/tmpl/item/edit.php +++ b/code/administrator/components/com_menus/tmpl/item/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_menus.admin-item-edit'); + ->useScript('form.validate') + ->useScript('com_menus.admin-item-edit'); $assoc = Associations::isEnabled(); $input = Factory::getApplication()->input; @@ -40,166 +41,135 @@ $lang = Factory::getLanguage()->getTag(); // Load mod_menu.ini file when client is administrator -if ($clientId === 1) -{ - Factory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR); +if ($clientId === 1) { + Factory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR); } ?> - - - - item->id != 0) : ?> -
-
-
-
- -
-
- -
-
-
-
- - -
- - 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
-
- form->renderField('type'); - - if ($this->item->type == 'alias') - { - echo $this->form->renderField('aliasoptions', 'params'); - } - - if ($this->item->type == 'separator') - { - echo $this->form->renderField('text_separator', 'params'); - } - - echo $this->form->renderFieldset('request'); - - if ($this->item->type == 'url') - { - $this->form->setFieldAttribute('link', 'readonly', 'false'); - $this->form->setFieldAttribute('link', 'required', 'true'); - } - - echo $this->form->renderField('link'); - - if ($this->item->type == 'alias') - { - echo $this->form->renderField('alias_redirect', 'params'); - } - - echo $this->form->renderField('browserNav'); - echo $this->form->renderField('template_style_id'); - - if (!$isModal && $this->item->type == 'container') - { - echo $this->loadTemplate('container'); - } - ?> -
-
- fields = array( - 'id', - 'client_id', - 'menutype', - 'parent_id', - 'menuordering', - 'published', - 'publish_up', - 'publish_down', - 'home', - 'access', - 'language', - 'note', - ); - - if ($this->item->type != 'component') - { - $this->fields = array_diff($this->fields, array('home')); - $this->form->setFieldAttribute('publish_up', 'showon', ''); - $this->form->setFieldAttribute('publish_down', 'showon', ''); - } - ?> - fields = array( - 'id', - 'client_id', - 'menutype', - 'parent_id', - 'menuordering', - 'published', - 'home', - 'publish_up', - 'publish_down', - 'access', - 'language', - 'note', - ); - - if ($this->item->type != 'component') - { - $this->fields = array_diff($this->fields, array('home')); - $this->form->setFieldAttribute('publish_up', 'showon', ''); - $this->form->setFieldAttribute('publish_down', 'showon', ''); - } - - echo LayoutHelper::render('joomla.edit.global', $this); ?> -
-
- - - fieldsets = array(); - $this->ignore_fieldsets = array('aliasoptions', 'request', 'item_associations'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - state->get('item.client_id') != 1) : ?> - -
- -
- -
-
- - - - - - modules)) : ?> - -
- -
- loadTemplate('modules'); ?> -
-
- - - - -
- - - - - form->getInput('component_id'); ?> - - + + + + item->id != 0) : ?> +
+
+
+
+ +
+
+ +
+
+
+
+ + +
+ + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+
+ form->renderField('type'); + + if ($this->item->type == 'alias') { + echo $this->form->renderField('aliasoptions', 'params'); + } + + if ($this->item->type == 'separator') { + echo $this->form->renderField('text_separator', 'params'); + } + + echo $this->form->renderFieldset('request'); + + if ($this->item->type == 'url') { + $this->form->setFieldAttribute('link', 'readonly', 'false'); + $this->form->setFieldAttribute('link', 'required', 'true'); + } + + echo $this->form->renderField('link'); + + if ($this->item->type == 'alias') { + echo $this->form->renderField('alias_redirect', 'params'); + } + + echo $this->form->renderField('browserNav'); + echo $this->form->renderField('template_style_id'); + + if (!$isModal && $this->item->type == 'container') { + echo $this->loadTemplate('container'); + } + ?> +
+
+ fields = array( + 'id', + 'client_id', + 'menutype', + 'parent_id', + 'menuordering', + 'published', + 'home', + 'publish_up', + 'publish_down', + 'access', + 'language', + 'note', + ); + + if ($this->item->type != 'component') { + $this->fields = array_diff($this->fields, array('home')); + $this->form->setFieldAttribute('publish_up', 'showon', ''); + $this->form->setFieldAttribute('publish_down', 'showon', ''); + } + + echo LayoutHelper::render('joomla.edit.global', $this); ?> +
+
+ + + fieldsets = array(); + $this->ignore_fieldsets = array('aliasoptions', 'request', 'item_associations'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + state->get('item.client_id') != 1) : ?> + +
+ +
+ +
+
+ + + + + + modules)) : ?> + +
+ +
+ loadTemplate('modules'); ?> +
+
+ + + + +
+ + + + + form->getInput('component_id'); ?> + + diff --git a/code/administrator/components/com_menus/tmpl/item/edit_container.php b/code/administrator/components/com_menus/tmpl/item/edit_container.php index 09d6ef94..53f7d070 100644 --- a/code/administrator/components/com_menus/tmpl/item/edit_container.php +++ b/code/administrator/components/com_menus/tmpl/item/edit_container.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -18,92 +20,86 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('joomla.treeselectmenu') - ->useStyle('com_menus.admin-item-edit-container') - ->useScript('com_menus.admin-item-edit-container'); + ->useStyle('com_menus.admin-item-edit-container') + ->useScript('com_menus.admin-item-edit-container'); ?> diff --git a/code/administrator/components/com_menus/tmpl/item/edit_modules.php b/code/administrator/components/com_menus/tmpl/item/edit_modules.php index 2088aa58..d8f7ae83 100644 --- a/code/administrator/components/com_menus/tmpl/item/edit_modules.php +++ b/code/administrator/components/com_menus/tmpl/item/edit_modules.php @@ -1,4 +1,5 @@ levels as $key => $value) -{ - $allLevels[$value->id] = $value->title; +foreach ($this->levels as $key => $value) { + $allLevels[$value->id] = $value->title; } $this->document->addScriptOptions('menus-edit-modules', ['viewLevels' => $allLevels, 'itemId' => $this->item->id]); @@ -23,26 +23,26 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useStyle('com_menus.admin-item-edit-modules') - ->useScript('com_menus.admin-item-edit-modules'); + ->useScript('com_menus.admin-item-edit-modules'); // Set up the bootstrap modal that will be used for all module editors echo HTMLHelper::_( - 'bootstrap.renderModal', - 'moduleEditModal', - array( - 'title' => Text::_('COM_MENUS_EDIT_MODULE_SETTINGS'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'footer' => '' - . '' - . '', - ) + 'bootstrap.renderModal', + 'moduleEditModal', + array( + 'title' => Text::_('COM_MENUS_EDIT_MODULE_SETTINGS'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'footer' => '' + . '' + . '', + ) ); ?> @@ -53,97 +53,97 @@ echo LayoutHelper::render('joomla.menu.edit_modules', $this); ?> - - - - - - - - - - - - modules as $i => &$module) : ?> - menuid)) : ?> - except || $module->menuid < 0) : ?> - - - - - - - - published) : ?> - - - - - - - - - - - - - + + + + + + + + + + + + modules as $i => &$module) : ?> + menuid)) : ?> + except || $module->menuid < 0) : ?> + + + + + + + + published) : ?> + + + + + + + + + + + + +
- -
- - - - - - - - - -
- - - escape($module->access_title); ?> - - escape($module->position); ?> - - published) : ?> - - - - - - - - -
+ +
+ + + + + + + + + +
+ + + escape($module->access_title); ?> + + escape($module->position); ?> + + published) : ?> + + + + + + + + +
diff --git a/code/administrator/components/com_menus/tmpl/item/modal.php b/code/administrator/components/com_menus/tmpl/item/modal.php index 2a62e7aa..f5c2253f 100644 --- a/code/administrator/components/com_menus/tmpl/item/modal.php +++ b/code/administrator/components/com_menus/tmpl/item/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/code/administrator/components/com_menus/tmpl/items/default.php b/code/administrator/components/com_menus/tmpl/items/default.php index e8a78cae..33900dd7 100644 --- a/code/administrator/components/com_menus/tmpl/items/default.php +++ b/code/administrator/components/com_menus/tmpl/items/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $app = Factory::getApplication(); @@ -31,10 +33,9 @@ $saveOrder = ($listOrder == 'a.lft' && strtolower($listDirn) == 'asc'); $menuType = (string) $app->getUserState('com_menus.items.menutype', ''); -if ($saveOrder && $menuType && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_menus&task=items.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && $menuType && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_menus&task=items.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $assoc = Associations::isEnabled() && $this->state->get('filter.client_id') == 0; @@ -42,248 +43,241 @@ ?>
-
-
-
- $this, 'options' => array('selectorFieldName' => 'menutype'))); ?> - items)) : ?> - - - - - - - - - - - - state->get('filter.client_id') == 0) : ?> - - - state->get('filter.client_id') == 0) : ?> - - - - - - state->get('filter.client_id') == 0) && (Multilanguage::isEnabled())) : ?> - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="false"> - items as $i => $item) : - $orderkey = array_search($item->id, $this->ordering[$item->parent_id]); - $canCreate = $user->authorise('core.create', 'com_menus.menu.' . $item->menutype_id); - $canEdit = $user->authorise('core.edit', 'com_menus.menu.' . $item->menutype_id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_menus.menu.' . $item->menutype_id) && $canCheckin; + id="adminForm"> +
+
+
+ $this, 'options' => array('selectorFieldName' => 'menutype'))); ?> + items)) : ?> +
+ + + + + + + + + + + state->get('filter.client_id') == 0) : ?> + + + state->get('filter.client_id') == 0) : ?> + + + + + + state->get('filter.client_id') == 0) && (Multilanguage::isEnabled())) : ?> + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + items as $i => $item) : + $orderkey = array_search($item->id, $this->ordering[$item->parent_id]); + $canCreate = $user->authorise('core.create', 'com_menus.menu.' . $item->menutype_id); + $canEdit = $user->authorise('core.edit', 'com_menus.menu.' . $item->menutype_id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_menus.menu.' . $item->menutype_id) && $canCheckin; - // Get the parents of item for sorting - if ($item->level > 1) - { - $parentsStr = ''; - $_currentParentId = $item->parent_id; - $parentsStr = ' ' . $_currentParentId; + // Get the parents of item for sorting + if ($item->level > 1) { + $parentsStr = ''; + $_currentParentId = $item->parent_id; + $parentsStr = ' ' . $_currentParentId; - for ($j = 0; $j < $item->level; $j++) - { - foreach ($this->ordering as $k => $v) - { - $v = implode('-', $v); - $v = '-' . $v . '-'; + for ($j = 0; $j < $item->level; $j++) { + foreach ($this->ordering as $k => $v) { + $v = implode('-', $v); + $v = '-' . $v . '-'; - if (strpos($v, '-' . $_currentParentId . '-') !== false) - { - $parentsStr .= ' ' . $k; - $_currentParentId = $k; - break; - } - } - } - } - else - { - $parentsStr = ''; - } - ?> - - - - + + + - - - - - state->get('filter.client_id') == 0) : ?> - - - state->get('filter.client_id') == 0) : ?> - - - - - - state->get('filter.client_id') == 0 && Multilanguage::isEnabled()) : ?> - - - - - - - + if (!$canChange) { + $iconClass = ' inactive'; + } elseif (!$saveOrder) { + $iconClass = ' inactive" title="' . Text::_('JORDERINGDISABLED'); + } + ?> + + + + + + + + + + published, $i, 'items.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + + + $item->level)); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'items.', $canCheckin); ?> + + protected) : ?> + + escape($item->title); ?> + + escape($item->title); ?> + + params); ?> +
+ + + type != 'url') : ?> + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + + type == 'url' && $item->note) : ?> + escape($item->note)); ?> + + +
+
+ + + escape($item->item_type); ?> + +
+ type === 'component' && !$item->enabled) : ?> +
+ + enabled === null ? 'JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND' : 'COM_MENUS_LABEL_DISABLED'); ?> + +
+ + + + escape($item->menutype_title ?: ucwords($item->menutype)); ?> + + state->get('filter.client_id') == 0) : ?> + + type == 'component') : ?> + language == '*' || $item->home == '0') : ?> + home, $i, 'items.', ($item->language != '*' || !$item->home) && $canChange && !$item->protected, 'cb', null, 'icon-home', 'icon-circle'); ?> + + + language_image) : ?> + language_image . '.gif', $item->language_title, array('title' => Text::sprintf('COM_MENUS_GRID_UNSET_LANGUAGE', $item->language_title)), true); ?> + + language; ?> + + + + language_image) : ?> + language_image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> + + language; ?> + + + + + + state->get('filter.client_id') == 0) : ?> + + escape($item->access_level); ?> + + + + + association) : ?> + id); ?> + + + + state->get('filter.client_id') == 0 && Multilanguage::isEnabled()) : ?> + + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_menus') || $user->authorise('core.edit', 'com_menus')) : ?> - Text::_('COM_MENUS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer') - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_menus') || $user->authorise('core.edit', 'com_menus')) : ?> + Text::_('COM_MENUS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer') + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - -
-
-
+ + + + + +
diff --git a/code/administrator/components/com_menus/tmpl/items/default_batch_body.php b/code/administrator/components/com_menus/tmpl/items/default_batch_body.php index acb8b8e9..8a0c950e 100644 --- a/code/administrator/components/com_menus/tmpl/items/default_batch_body.php +++ b/code/administrator/components/com_menus/tmpl/items/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -15,73 +17,72 @@ use Joomla\CMS\Layout\LayoutHelper; $options = [ - HTMLHelper::_('select.option', 'c', Text::_('JLIB_HTML_BATCH_COPY')), - HTMLHelper::_('select.option', 'm', Text::_('JLIB_HTML_BATCH_MOVE')) + HTMLHelper::_('select.option', 'c', Text::_('JLIB_HTML_BATCH_COPY')), + HTMLHelper::_('select.option', 'm', Text::_('JLIB_HTML_BATCH_MOVE')) ]; $published = (int) $this->state->get('filter.published'); $clientId = (int) $this->state->get('filter.client_id'); $menuType = Factory::getApplication()->getUserState('com_menus.items.menutype', ''); -if ($clientId == 1) -{ - /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = $this->document->getWebAssetManager(); - $wa->useScript('com_menus.batch-body'); - $wa->useScript('joomla.batch-copymove'); +if ($clientId == 1) { + /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = $this->document->getWebAssetManager(); + $wa->useScript('com_menus.batch-body'); + $wa->useScript('joomla.batch-copymove'); } ?>
- - -
- -
-
- -
-
- -
-
- -
-
-
- -
- = 0) : ?> -
-
- - -
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ = 0) : ?> +
+
+ + +
-
- - -
-
- +
+ + +
+
+ - -

- -
- -
-

-
- + +

+ +
+ +
+

+
+
diff --git a/code/administrator/components/com_menus/tmpl/items/default_batch_footer.php b/code/administrator/components/com_menus/tmpl/items/default_batch_footer.php index 6ce0e9df..4f2df096 100644 --- a/code/administrator/components/com_menus/tmpl/items/default_batch_footer.php +++ b/code/administrator/components/com_menus/tmpl/items/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -16,10 +18,10 @@ $menuType = Factory::getApplication()->getUserState('com_menus.items.menutype', ''); ?> -= 0 && $clientId == 1)): ?> - += 0 && $clientId == 1)) : ?> + diff --git a/code/administrator/components/com_menus/tmpl/items/modal.php b/code/administrator/components/com_menus/tmpl/items/modal.php index f4c6afef..e5920758 100644 --- a/code/administrator/components/com_menus/tmpl/items/modal.php +++ b/code/administrator/components/com_menus/tmpl/items/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -35,162 +35,155 @@ $link = 'index.php?option=com_menus&view=items&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; $multilang = Multilanguage::isEnabled(); -if (!empty($editor)) -{ - // This view is used also in com_menus. Load the xtd script only if the editor is set! - $this->document->addScriptOptions('xtd-menus', array('editor' => $editor)); - $onclick = "jSelectMenuItem"; - $link = 'index.php?option=com_menus&view=items&layout=modal&tmpl=component&editor=' . $editor . '&' . Session::getFormToken() . '=1'; +if (!empty($editor)) { + // This view is used also in com_menus. Load the xtd script only if the editor is set! + $this->document->addScriptOptions('xtd-menus', array('editor' => $editor)); + $onclick = "jSelectMenuItem"; + $link = 'index.php?option=com_menus&view=items&layout=modal&tmpl=component&editor=' . $editor . '&' . Session::getFormToken() . '=1'; } ?>
-
- $this, 'options' => array('selectorFieldName' => 'menutype'))); ?> + + $this, 'options' => array('selectorFieldName' => 'menutype'))); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - items as $i => $item) : ?> - type, array('separator', 'heading', 'alias', 'url', 'container')); ?> - language && $multilang) - { - if ($item->language !== '*') - { - $language = $item->language; - } - else - { - $language = ''; - } - } - elseif (!$multilang) - { - $language = ''; - } - ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - -
- published, $i, 'items.', false, 'cb', $item->publish_up, $item->publish_down); ?> - - $item->level)); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - - params); ?> -
- - - note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - - -
-
- - - escape($item->item_type); ?> - -
- type === 'component' && !$item->enabled) : ?> -
- - enabled === null ? 'JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND' : 'COM_MENUS_LABEL_DISABLED'); ?> - -
- -
- escape($item->menutype_title); ?> - - type == 'component') : ?> - language == '*' || $item->home == '0') : ?> - home, $i, 'items.', ($item->language != '*' || !$item->home) && false && !$item->protected, 'cb', null, 'home', 'circle'); ?> - - language_image) : ?> - language_image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> - - language; ?> - - - - - escape($item->access_level); ?> - - language == '') : ?> - - language == '*') : ?> - - - - - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + type, array('separator', 'heading', 'alias', 'url', 'container')); ?> + language && $multilang) { + if ($item->language !== '*') { + $language = $item->language; + } else { + $language = ''; + } + } elseif (!$multilang) { + $language = ''; + } + ?> + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + +
+ published, $i, 'items.', false, 'cb', $item->publish_up, $item->publish_down); ?> + + $item->level)); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + + params); ?> +
+ + + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + + +
+
+ + + escape($item->item_type); ?> + +
+ type === 'component' && !$item->enabled) : ?> +
+ + enabled === null ? 'JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND' : 'COM_MENUS_LABEL_DISABLED'); ?> + +
+ +
+ escape($item->menutype_title); ?> + + type == 'component') : ?> + language == '*' || $item->home == '0') : ?> + home, $i, 'items.', ($item->language != '*' || !$item->home) && false && !$item->protected, 'cb', null, 'home', 'circle'); ?> + + language_image) : ?> + language_image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> + + language; ?> + + + + + escape($item->access_level); ?> + + language == '') : ?> + + language == '*') : ?> + + + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - - + + + + + -
+
diff --git a/code/administrator/components/com_menus/tmpl/menu/edit.php b/code/administrator/components/com_menus/tmpl/menu/edit.php index 7959fda2..186b659d 100644 --- a/code/administrator/components/com_menus/tmpl/menu/edit.php +++ b/code/administrator/components/com_menus/tmpl/menu/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('keepalive') + ->useScript('form.validate'); Text::script('ERROR'); ?>
- + -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> - + -
- +
+ -
-
- form->renderField('menutype'); +
+
+ form->renderField('menutype'); - echo $this->form->renderField('description'); + echo $this->form->renderField('description'); - echo $this->form->renderField('client_id'); + echo $this->form->renderField('client_id'); - echo $this->form->renderField('preset'); - ?> -
-
-
+ echo $this->form->renderField('preset'); + ?> +
+
+ - + - canDo->get('core.admin')) : ?> - -
- -
- form->getInput('rules'); ?> -
-
- - + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + - - - - + + + +
diff --git a/code/administrator/components/com_menus/tmpl/menus/default.php b/code/administrator/components/com_menus/tmpl/menus/default.php index 6ce6fc32..09cc2388 100644 --- a/code/administrator/components/com_menus/tmpl/menus/default.php +++ b/code/administrator/components/com_menus/tmpl/menus/default.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -15,6 +17,12 @@ use Joomla\CMS\Router\Route; use Joomla\CMS\Uri\Uri; +/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ +$wa = $this->document->getWebAssetManager(); +$wa->useScript('table.columns') + ->useScript('multiselect') + ->useScript('com_menus.admin-menus'); + $uri = Uri::getInstance(); $return = base64_encode($uri); $user = Factory::getUser(); @@ -23,248 +31,240 @@ $modMenuId = (int) $this->get('ModMenuId'); $itemIds = []; -foreach ($this->items as $item) -{ - if ($user->authorise('core.edit', 'com_menus')) - { - $itemIds[] = $item->id; - } +foreach ($this->items as $item) { + if ($user->authorise('core.edit', 'com_menus')) { + $itemIds[] = $item->id; + } } $this->document->addScriptOptions('menus-default', ['items' => $itemIds]); - -/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ -$wa = $this->document->getWebAssetManager(); -$wa->useScript('multiselect') - ->useScript('com_menus.admin-menus'); - ?>
-
-
-
- $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - items as $i => $item) : - $canEdit = $user->authorise('core.edit', 'com_menus.menu.' . (int) $item->id); - $canManageItems = $user->authorise('core.manage', 'com_menus.menu.' . (int) $item->id); - ?> - - - - - - - - - - - - - +
+
+
+ $this, 'options' => array('filterButton' => false))); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + items as $i => $item) : + $canEdit = $user->authorise('core.edit', 'com_menus.menu.' . (int) $item->id); + $canManageItems = $user->authorise('core.manage', 'com_menus.menu.' . (int) $item->id); + ?> + + + + + + + + + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
-
-
+ + + +
+
+
diff --git a/code/administrator/components/com_menus/tmpl/menutypes/default.php b/code/administrator/components/com_menus/tmpl/menutypes/default.php index 9a5c7efe..a0a32a9d 100644 --- a/code/administrator/components/com_menus/tmpl/menutypes/default.php +++ b/code/administrator/components/com_menus/tmpl/menutypes/default.php @@ -1,4 +1,5 @@ 'slide1')); ?> - - types as $name => $list) : ?> - -
- $item) : ?> - $this->recordId, 'title' => $item->type ?? $item->title, 'request' => $item->request); ?> - - -
- -
- - description); ?> - -
- -
- - + + types as $name => $list) : ?> + +
+ $item) : ?> + $this->recordId, 'title' => $item->type ?? $item->title, 'request' => $item->request); ?> + + +
+ +
+ + description); ?> + +
+ +
+ + com_messages Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_messages/services/provider.php b/code/administrator/components/com_messages/services/provider.php index d4c60841..14c008ec 100644 --- a/code/administrator/components/com_messages/services/provider.php +++ b/code/administrator/components/com_messages/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Messages')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Messages')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Messages')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Messages')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MessagesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MessagesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_messages/src/Controller/ConfigController.php b/code/administrator/components/com_messages/src/Controller/ConfigController.php index cf76dd1d..7ee361b1 100644 --- a/code/administrator/components/com_messages/src/Controller/ConfigController.php +++ b/code/administrator/components/com_messages/src/Controller/ConfigController.php @@ -1,4 +1,5 @@ checkToken(); - - $model = $this->getModel('Config'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - $data = $model->validate($form, $data); - - // Check for validation errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Redirect back to the main list. - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - - return false; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Redirect back to the main list. - $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED', $model->getError()), 'warning'); - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - - return false; - } - - // Redirect to the list screen. - $this->setMessage(Text::_('COM_MESSAGES_CONFIG_SAVED')); - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - - return true; - } - - /** - * Cancel operation. - * - * @return void - * - * @since 4.0.0 - */ - public function cancel() - { - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - } + /** + * Method to save a record. + * + * @return boolean + * + * @since 1.6 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + $model = $this->getModel('Config'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + $data = $model->validate($form, $data); + + // Check for validation errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Redirect back to the main list. + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + + return false; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Redirect back to the main list. + $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED', $model->getError()), 'warning'); + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + + return false; + } + + // Redirect to the list screen. + $this->setMessage(Text::_('COM_MESSAGES_CONFIG_SAVED')); + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + + return true; + } + + /** + * Cancel operation. + * + * @return void + * + * @since 4.0.0 + */ + public function cancel() + { + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + } } diff --git a/code/administrator/components/com_messages/src/Controller/DisplayController.php b/code/administrator/components/com_messages/src/Controller/DisplayController.php index f7115d29..6d73258f 100644 --- a/code/administrator/components/com_messages/src/Controller/DisplayController.php +++ b/code/administrator/components/com_messages/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'messages'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'messages'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'message' && $layout == 'edit' && !$this->checkEditId('com_messages.edit.message', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'message' && $layout == 'edit' && !$this->checkEditId('com_messages.edit.message', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/code/administrator/components/com_messages/src/Controller/MessageController.php b/code/administrator/components/com_messages/src/Controller/MessageController.php index 44b83617..eb3b1bda 100644 --- a/code/administrator/components/com_messages/src/Controller/MessageController.php +++ b/code/administrator/components/com_messages/src/Controller/MessageController.php @@ -1,4 +1,5 @@ input->getInt('reply_id')) - { - $this->setRedirect('index.php?option=com_messages&view=message&layout=edit&reply_id=' . $replyId); - } - else - { - $this->setMessage(Text::_('COM_MESSAGES_INVALID_REPLY_ID')); - $this->setRedirect('index.php?option=com_messages&view=messages'); - } - } + /** + * Reply to an existing message. + * + * This is a simple redirect to the compose form. + * + * @return void + * + * @since 1.6 + */ + public function reply() + { + if ($replyId = $this->input->getInt('reply_id')) { + $this->setRedirect('index.php?option=com_messages&view=message&layout=edit&reply_id=' . $replyId); + } else { + $this->setMessage(Text::_('COM_MESSAGES_INVALID_REPLY_ID')); + $this->setRedirect('index.php?option=com_messages&view=messages'); + } + } } diff --git a/code/administrator/components/com_messages/src/Controller/MessagesController.php b/code/administrator/components/com_messages/src/Controller/MessagesController.php index 0d160b86..a9fde7eb 100644 --- a/code/administrator/components/com_messages/src/Controller/MessagesController.php +++ b/code/administrator/components/com_messages/src/Controller/MessagesController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Message', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_messages/src/Extension/MessagesComponent.php b/code/administrator/components/com_messages/src/Extension/MessagesComponent.php index ab09ea87..c35d45d3 100644 --- a/code/administrator/components/com_messages/src/Extension/MessagesComponent.php +++ b/code/administrator/components/com_messages/src/Extension/MessagesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('messages', new Messages); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('messages', new Messages()); + } } diff --git a/code/administrator/components/com_messages/src/Field/MessageStatesField.php b/code/administrator/components/com_messages/src/Field/MessageStatesField.php index 5e5dbfdc..2f2b302c 100644 --- a/code/administrator/components/com_messages/src/Field/MessageStatesField.php +++ b/code/administrator/components/com_messages/src/Field/MessageStatesField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('id') - ->from('#__usergroups'); - $db->setQuery($query); + /** + * Method to get the filtering groups (null means no filtering) + * + * @return array|null array of filtering groups or null. + * + * @since 1.6 + */ + protected function getGroups() + { + // Compute usergroups + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('id') + ->from('#__usergroups'); + $db->setQuery($query); - try - { - $groups = $db->loadColumn(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'notice'); + try { + $groups = $db->loadColumn(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'notice'); - return null; - } + return null; + } - foreach ($groups as $i => $group) - { - if (Access::checkGroup($group, 'core.admin')) - { - continue; - } + foreach ($groups as $i => $group) { + if (Access::checkGroup($group, 'core.admin')) { + continue; + } - if (!Access::checkGroup($group, 'core.manage', 'com_messages')) - { - unset($groups[$i]); - continue; - } + if (!Access::checkGroup($group, 'core.manage', 'com_messages')) { + unset($groups[$i]); + continue; + } - if (!Access::checkGroup($group, 'core.login.admin')) - { - unset($groups[$i]); - } - } + if (!Access::checkGroup($group, 'core.login.admin')) { + unset($groups[$i]); + } + } - return array_values($groups); - } + return array_values($groups); + } - /** - * Method to get the users to exclude from the list of users - * - * @return array|null array of users to exclude or null to to not exclude them - * - * @since 1.6 - */ - protected function getExcluded() - { - return array(Factory::getUser()->id); - } + /** + * Method to get the users to exclude from the list of users + * + * @return array|null array of users to exclude or null to to not exclude them + * + * @since 1.6 + */ + protected function getExcluded() + { + return array(Factory::getUser()->id); + } } diff --git a/code/administrator/components/com_messages/src/Helper/MessagesHelper.php b/code/administrator/components/com_messages/src/Helper/MessagesHelper.php index 28598ba7..4620bdc5 100644 --- a/code/administrator/components/com_messages/src/Helper/MessagesHelper.php +++ b/code/administrator/components/com_messages/src/Helper/MessagesHelper.php @@ -1,4 +1,5 @@ setState('user.id', $user->get('id')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_messages'); - $this->setState('params', $params); - } - - /** - * Method to get a single record. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function &getItem() - { - $item = new CMSObject; - $userid = (int) $this->getState('user.id'); - - $db = $this->getDbo(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('cfg_name'), - $db->quoteName('cfg_value'), - ] - ) - ->from($db->quoteName('#__messages_cfg')) - ->where($db->quoteName('user_id') . ' = :userid') - ->bind(':userid', $userid, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - foreach ($rows as $row) - { - $item->set($row->cfg_name, $row->cfg_value); - } - - $this->preprocessData('com_messages.config', $item); - - return $item; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_messages.config', 'config', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $db = $this->getDbo(); - - if ($userId = (int) $this->getState('user.id')) - { - $query = $db->getQuery(true) - ->delete($db->quoteName('#__messages_cfg')) - ->where($db->quoteName('user_id') . ' = :userid') - ->bind(':userid', $userId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (count($data)) - { - $query = $db->getQuery(true) - ->insert($db->quoteName('#__messages_cfg')) - ->columns( - [ - $db->quoteName('user_id'), - $db->quoteName('cfg_name'), - $db->quoteName('cfg_value'), - ] - ); - - foreach ($data as $k => $v) - { - $query->values( - implode( - ',', - $query->bindArray( - [$userId , $k, $v], - [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] - ) - ) - ); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - return true; - } - else - { - $this->setError('COM_MESSAGES_ERR_INVALID_USER'); - - return false; - } - } + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $user = Factory::getUser(); + + $this->setState('user.id', $user->get('id')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_messages'); + $this->setState('params', $params); + } + + /** + * Method to get a single record. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function &getItem() + { + $item = new CMSObject(); + $userid = (int) $this->getState('user.id'); + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('cfg_name'), + $db->quoteName('cfg_value'), + ] + ) + ->from($db->quoteName('#__messages_cfg')) + ->where($db->quoteName('user_id') . ' = :userid') + ->bind(':userid', $userid, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + foreach ($rows as $row) { + $item->set($row->cfg_name, $row->cfg_value); + } + + $this->preprocessData('com_messages.config', $item); + + return $item; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_messages.config', 'config', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $db = $this->getDatabase(); + + if ($userId = (int) $this->getState('user.id')) { + $query = $db->getQuery(true) + ->delete($db->quoteName('#__messages_cfg')) + ->where($db->quoteName('user_id') . ' = :userid') + ->bind(':userid', $userId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (count($data)) { + $query = $db->getQuery(true) + ->insert($db->quoteName('#__messages_cfg')) + ->columns( + [ + $db->quoteName('user_id'), + $db->quoteName('cfg_name'), + $db->quoteName('cfg_value'), + ] + ); + + foreach ($data as $k => $v) { + $query->values( + implode( + ',', + $query->bindArray( + [$userId , $k, $v], + [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] + ) + ) + ); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + return true; + } else { + $this->setError('COM_MESSAGES_ERR_INVALID_USER'); + + return false; + } + } } diff --git a/code/administrator/components/com_messages/src/Model/MessageModel.php b/code/administrator/components/com_messages/src/Model/MessageModel.php index 4be880b2..2c3c0a8f 100644 --- a/code/administrator/components/com_messages/src/Model/MessageModel.php +++ b/code/administrator/components/com_messages/src/Model/MessageModel.php @@ -1,4 +1,5 @@ input; - - $user = Factory::getUser(); - $this->setState('user.id', $user->get('id')); - - $messageId = (int) $input->getInt('message_id'); - $this->setState('message.id', $messageId); - - $replyId = (int) $input->getInt('reply_id'); - $this->setState('reply.id', $replyId); - } - - /** - * Check that recipient user is the one trying to delete and then call parent delete method - * - * @param array &$pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 3.1 - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - $user = Factory::getUser(); - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ($table->user_id_to != $user->id) - { - // Prune items that you can't change. - unset($pks[$i]); - - try - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'warning'); - } - - return false; - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - return parent::delete($pks); - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - if (!isset($this->item)) - { - if ($this->item = parent::getItem($pk)) - { - // Invalid message_id returns 0 - if ($this->item->user_id_to === '0') - { - $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - - // Prime required properties. - if (empty($this->item->message_id)) - { - // Prepare data for a new record. - if ($replyId = (int) $this->getState('reply.id')) - { - // If replying to a message, preload some data. - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName(['subject', 'user_id_from', 'user_id_to'])) - ->from($db->quoteName('#__messages')) - ->where($db->quoteName('message_id') . ' = :messageid') - ->bind(':messageid', $replyId, ParameterType::INTEGER); - - try - { - $message = $db->setQuery($query)->loadObject(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (!$message || $message->user_id_to != Factory::getUser()->id) - { - $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - - $this->item->set('user_id_to', $message->user_id_from); - $re = Text::_('COM_MESSAGES_RE'); - - if (stripos($message->subject, $re) !== 0) - { - $this->item->set('subject', $re . ' ' . $message->subject); - } - } - } - elseif ($this->item->user_id_to != Factory::getUser()->id) - { - $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - else - { - // Mark message read - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__messages')) - ->set($db->quoteName('state') . ' = 1') - ->where($db->quoteName('message_id') . ' = :messageid') - ->bind(':messageid', $this->item->message_id, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - } - } - - // Get the user name for an existing message. - if ($this->item->user_id_from && $fromUser = new User($this->item->user_id_from)) - { - $this->item->set('from_user_name', $fromUser->name); - } - } - - return $this->item; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_messages.message', 'message', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_messages.edit.message.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_messages.message', $data); - - return $data; - } - - /** - * Checks that the current user matches the message recipient and calls the parent publish method - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function publish(&$pks, $value = 1) - { - $user = Factory::getUser(); - $table = $this->getTable(); - $pks = (array) $pks; - - // Check that the recipient matches the current user - foreach ($pks as $i => $pk) - { - $table->reset(); - - if ($table->load($pk)) - { - if ($table->user_id_to != $user->id) - { - // Prune items that you can't change. - unset($pks[$i]); - - try - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'warning'); - } - - return false; - } - } - } - - return parent::publish($pks, $value); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $table = $this->getTable(); - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Assign empty values. - if (empty($table->user_id_from)) - { - $table->user_id_from = Factory::getUser()->get('id'); - } - - if ((int) $table->date_time == 0) - { - $table->date_time = Factory::getDate()->toSql(); - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Load the user details (already valid from table check). - $toUser = User::getInstance($table->user_id_to); - - // Check if recipient can access com_messages. - if (!$toUser->authorise('core.login.admin') || !$toUser->authorise('core.manage', 'com_messages')) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_RECIPIENT_NOT_AUTHORISED')); - - return false; - } - - // Load the recipient user configuration. - $model = $this->bootComponent('com_messages') - ->getMVCFactory()->createModel('Config', 'Administrator', ['ignore_request' => true]); - $model->setState('user.id', $table->user_id_to); - $config = $model->getItem(); - - if (empty($config)) - { - $this->setError($model->getError()); - - return false; - } - - if ($config->get('lock', false)) - { - $this->setError(Text::_('COM_MESSAGES_ERR_SEND_FAILED')); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - $key = $table->getKeyName(); - - if (isset($table->$key)) - { - $this->setState($this->getName() . '.id', $table->$key); - } - - if ($config->get('mail_on_new', true)) - { - $fromUser = User::getInstance($table->user_id_from); - $debug = Factory::getApplication()->get('debug_lang'); - $default_language = ComponentHelper::getParams('com_languages')->get('administrator'); - $lang = Language::getInstance($toUser->getParam('admin_language', $default_language), $debug); - $lang->load('com_messages', JPATH_ADMINISTRATOR); - - // Build the email subject and message - $app = Factory::getApplication(); - $linkMode = $app->get('force_ssl', 0) >= 1 ? Route::TLS_FORCE : Route::TLS_IGNORE; - $sitename = $app->get('sitename'); - $fromName = $fromUser->get('name'); - $siteURL = Route::link( - 'administrator', - 'index.php?option=com_messages&view=message&message_id=' . $table->message_id, - false, - $linkMode, - true - ); - $subject = html_entity_decode($table->subject, ENT_COMPAT, 'UTF-8'); - $message = strip_tags(html_entity_decode($table->message, ENT_COMPAT, 'UTF-8')); - - // Send the email - $mailer = new MailTemplate('com_messages.new_message', $lang->getTag()); - $data = [ - 'subject' => $subject, - 'message' => $message, - 'fromname' => $fromName, - 'sitename' => $sitename, - 'siteurl' => $siteURL, - 'fromemail' => $fromUser->email, - 'toname' => $toUser->name, - 'toemail' => $toUser->email - ]; - $mailer->addTemplateData($data); - $mailer->setReplyTo($fromUser->email, $fromUser->name); - $mailer->addRecipient($toUser->email, $toUser->name); - - try - { - $mailer->send(); - } - catch (MailDisabledException | phpMailerException $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); - - return false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); - - return false; - } - } - } - - return true; - } - - /** - * Sends a message to the site's super users - * - * @param string $subject The message subject - * @param string $message The message - * - * @return boolean - * - * @since 3.9.0 - */ - public function notifySuperUsers($subject, $message, $fromUser = null) - { - $db = $this->getDbo(); - - try - { - /** @var Asset $table */ - $table = Table::getInstance('Asset'); - $rootId = $table->getRootId(); - - /** @var Rule[] $rules */ - $rules = Access::getAssetRules($rootId)->getData(); - $rawGroups = $rules['core.admin']->getData(); - - if (empty($rawGroups)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_MISSING_ROOT_ASSET_GROUPS')); - - return false; - } - - $groups = array(); - - foreach ($rawGroups as $g => $enabled) - { - if ($enabled) - { - $groups[] = $g; - } - } - - if (empty($groups)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_NO_GROUPS_SET_AS_SUPER_USER')); - - return false; - } - - $query = $db->getQuery(true) - ->select($db->quoteName('map.user_id')) - ->from($db->quoteName('#__user_usergroup_map', 'map')) - ->join('LEFT', - $db->quoteName('#__users', 'u'), - $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id') - ) - ->whereIn($db->quoteName('map.group_id'), $groups) - ->where($db->quoteName('u.block') . ' = 0') - ->where($db->quoteName('u.sendEmail') . ' = 1'); - - $userIDs = $db->setQuery($query)->loadColumn(0); - - if (empty($userIDs)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_NO_USERS_SET_AS_SUPER_USER')); - - return false; - } - - foreach ($userIDs as $id) - { - /* - * All messages must have a valid from user, we have use cases where an unauthenticated user may trigger this - * so we will set the from user as the to user - */ - $data = [ - 'user_id_from' => $id, - 'user_id_to' => $id, - 'subject' => $subject, - 'message' => $message, - ]; - - if (!$this->save($data)) - { - return false; - } - } - - return true; - } - catch (\Exception $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - } + /** + * Message + * + * @var \stdClass + */ + protected $item; + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + parent::populateState(); + + $input = Factory::getApplication()->input; + + $user = Factory::getUser(); + $this->setState('user.id', $user->get('id')); + + $messageId = (int) $input->getInt('message_id'); + $this->setState('message.id', $messageId); + + $replyId = (int) $input->getInt('reply_id'); + $this->setState('reply.id', $replyId); + } + + /** + * Check that recipient user is the one trying to delete and then call parent delete method + * + * @param array &$pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 3.1 + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + $user = Factory::getUser(); + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ($table->user_id_to != $user->id) { + // Prune items that you can't change. + unset($pks[$i]); + + try { + Log::add(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'warning'); + } + + return false; + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + return parent::delete($pks); + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + if (!isset($this->item)) { + if ($this->item = parent::getItem($pk)) { + // Invalid message_id returns 0 + if ($this->item->user_id_to === '0') { + $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } + + // Prime required properties. + if (empty($this->item->message_id)) { + // Prepare data for a new record. + if ($replyId = (int) $this->getState('reply.id')) { + // If replying to a message, preload some data. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['subject', 'user_id_from', 'user_id_to'])) + ->from($db->quoteName('#__messages')) + ->where($db->quoteName('message_id') . ' = :messageid') + ->bind(':messageid', $replyId, ParameterType::INTEGER); + + try { + $message = $db->setQuery($query)->loadObject(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (!$message || $message->user_id_to != Factory::getUser()->id) { + $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } + + $this->item->set('user_id_to', $message->user_id_from); + $re = Text::_('COM_MESSAGES_RE'); + + if (stripos($message->subject, $re) !== 0) { + $this->item->set('subject', $re . ' ' . $message->subject); + } + } + } elseif ($this->item->user_id_to != Factory::getUser()->id) { + $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } else { + // Mark message read + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__messages')) + ->set($db->quoteName('state') . ' = 1') + ->where($db->quoteName('message_id') . ' = :messageid') + ->bind(':messageid', $this->item->message_id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + } + } + + // Get the user name for an existing message. + if ($this->item->user_id_from && $fromUser = new User($this->item->user_id_from)) { + $this->item->set('from_user_name', $fromUser->name); + } + } + + return $this->item; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_messages.message', 'message', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_messages.edit.message.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_messages.message', $data); + + return $data; + } + + /** + * Checks that the current user matches the message recipient and calls the parent publish method + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function publish(&$pks, $value = 1) + { + $user = Factory::getUser(); + $table = $this->getTable(); + $pks = (array) $pks; + + // Check that the recipient matches the current user + foreach ($pks as $i => $pk) { + $table->reset(); + + if ($table->load($pk)) { + if ($table->user_id_to != $user->id) { + // Prune items that you can't change. + unset($pks[$i]); + + try { + Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'warning'); + } + + return false; + } + } + } + + return parent::publish($pks, $value); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $table = $this->getTable(); + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Assign empty values. + if (empty($table->user_id_from)) { + $table->user_id_from = Factory::getUser()->get('id'); + } + + if ((int) $table->date_time == 0) { + $table->date_time = Factory::getDate()->toSql(); + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Load the user details (already valid from table check). + $toUser = User::getInstance($table->user_id_to); + + // Check if recipient can access com_messages. + if (!$toUser->authorise('core.login.admin') || !$toUser->authorise('core.manage', 'com_messages')) { + $this->setError(Text::_('COM_MESSAGES_ERROR_RECIPIENT_NOT_AUTHORISED')); + + return false; + } + + // Load the recipient user configuration. + $model = $this->bootComponent('com_messages') + ->getMVCFactory()->createModel('Config', 'Administrator', ['ignore_request' => true]); + $model->setState('user.id', $table->user_id_to); + $config = $model->getItem(); + + if (empty($config)) { + $this->setError($model->getError()); + + return false; + } + + if ($config->get('lock', false)) { + $this->setError(Text::_('COM_MESSAGES_ERR_SEND_FAILED')); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + $key = $table->getKeyName(); + + if (isset($table->$key)) { + $this->setState($this->getName() . '.id', $table->$key); + } + + if ($config->get('mail_on_new', true)) { + $fromUser = User::getInstance($table->user_id_from); + $debug = Factory::getApplication()->get('debug_lang'); + $default_language = ComponentHelper::getParams('com_languages')->get('administrator'); + $lang = Language::getInstance($toUser->getParam('admin_language', $default_language), $debug); + $lang->load('com_messages', JPATH_ADMINISTRATOR); + + // Build the email subject and message + $app = Factory::getApplication(); + $linkMode = $app->get('force_ssl', 0) >= 1 ? Route::TLS_FORCE : Route::TLS_IGNORE; + $sitename = $app->get('sitename'); + $fromName = $fromUser->get('name'); + $siteURL = Route::link( + 'administrator', + 'index.php?option=com_messages&view=message&message_id=' . $table->message_id, + false, + $linkMode, + true + ); + $subject = html_entity_decode($table->subject, ENT_COMPAT, 'UTF-8'); + $message = strip_tags(html_entity_decode($table->message, ENT_COMPAT, 'UTF-8')); + + // Send the email + $mailer = new MailTemplate('com_messages.new_message', $lang->getTag()); + $data = [ + 'subject' => $subject, + 'message' => $message, + 'fromname' => $fromName, + 'sitename' => $sitename, + 'siteurl' => $siteURL, + 'fromemail' => $fromUser->email, + 'toname' => $toUser->name, + 'toemail' => $toUser->email + ]; + $mailer->addTemplateData($data); + $mailer->setReplyTo($fromUser->email, $fromUser->name); + $mailer->addRecipient($toUser->email, $toUser->name); + + try { + $mailer->send(); + } catch (MailDisabledException | phpMailerException $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); + + return false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); + + return false; + } + } + } + + return true; + } + + /** + * Sends a message to the site's super users + * + * @param string $subject The message subject + * @param string $message The message + * + * @return boolean + * + * @since 3.9.0 + */ + public function notifySuperUsers($subject, $message, $fromUser = null) + { + $db = $this->getDatabase(); + + try { + /** @var Asset $table */ + $table = Table::getInstance('Asset'); + $rootId = $table->getRootId(); + + /** @var Rule[] $rules */ + $rules = Access::getAssetRules($rootId)->getData(); + $rawGroups = $rules['core.admin']->getData(); + + if (empty($rawGroups)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_MISSING_ROOT_ASSET_GROUPS')); + + return false; + } + + $groups = array(); + + foreach ($rawGroups as $g => $enabled) { + if ($enabled) { + $groups[] = $g; + } + } + + if (empty($groups)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_NO_GROUPS_SET_AS_SUPER_USER')); + + return false; + } + + $query = $db->getQuery(true) + ->select($db->quoteName('map.user_id')) + ->from($db->quoteName('#__user_usergroup_map', 'map')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'u'), + $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id') + ) + ->whereIn($db->quoteName('map.group_id'), $groups) + ->where($db->quoteName('u.block') . ' = 0') + ->where($db->quoteName('u.sendEmail') . ' = 1'); + + $userIDs = $db->setQuery($query)->loadColumn(0); + + if (empty($userIDs)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_NO_USERS_SET_AS_SUPER_USER')); + + return false; + } + + foreach ($userIDs as $id) { + /* + * All messages must have a valid from user, we have use cases where an unauthenticated user may trigger this + * so we will set the from user as the to user + */ + $data = [ + 'user_id_from' => $id, + 'user_id_to' => $id, + 'subject' => $subject, + 'message' => $message, + ]; + + if (!$this->save($data)) { + return false; + } + } + + return true; + } catch (\Exception $exception) { + $this->setError($exception->getMessage()); + + return false; + } + } } diff --git a/code/administrator/components/com_messages/src/Model/MessagesModel.php b/code/administrator/components/com_messages/src/Model/MessagesModel.php index 582d2753..a02bf67c 100644 --- a/code/administrator/components/com_messages/src/Model/MessagesModel.php +++ b/code/administrator/components/com_messages/src/Model/MessagesModel.php @@ -1,4 +1,5 @@ getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - $id = (int) $user->get('id'); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a') . '.*', - $db->quoteName('u.name', 'user_from'), - ] - ) - ); - $query->from($db->quoteName('#__messages', 'a')); - - // Join over the users for message owner. - $query->join('INNER', - $db->quoteName('#__users', 'u'), - $db->quoteName('u.id') . ' = ' . $db->quoteName('a.user_id_from') - ) - ->where($db->quoteName('a.user_id_to') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - // Filter by published state. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif ($state !== '*') - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - } - - // Filter by search in subject or message. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.subject') . ' LIKE :subject', - $db->quoteName('a.message') . ' LIKE :message', - ], - 'OR' - ) - ->bind(':subject', $search) - ->bind(':message', $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.date_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); - - return $query; - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'message_id', 'a.id', + 'subject', 'a.subject', + 'state', 'a.state', + 'user_id_from', 'a.user_id_from', + 'user_id_to', 'a.user_id_to', + 'date_time', 'a.date_time', + 'priority', 'a.priority', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.date_time', $direction = 'desc') + { + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + $id = (int) $user->get('id'); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a') . '.*', + $db->quoteName('u.name', 'user_from'), + ] + ) + ); + $query->from($db->quoteName('#__messages', 'a')); + + // Join over the users for message owner. + $query->join( + 'INNER', + $db->quoteName('#__users', 'u'), + $db->quoteName('u.id') . ' = ' . $db->quoteName('a.user_id_from') + ) + ->where($db->quoteName('a.user_id_to') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + // Filter by published state. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif ($state !== '*') { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + } + + // Filter by search in subject or message. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.subject') . ' LIKE :subject', + $db->quoteName('a.message') . ' LIKE :message', + ], + 'OR' + ) + ->bind(':subject', $search) + ->bind(':message', $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.date_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); + + return $query; + } + + /** + * Purge the messages table of old messages for the given user ID. + * + * @param int $userId The user id + * + * @return void + * + * @since 4.2.0 + */ + public function purge(int $userId): void + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['cfg_name', 'cfg_value'])) + ->from($db->quoteName('#__messages_cfg')) + ->where( + [ + $db->quoteName('user_id') . ' = :userId', + $db->quoteName('cfg_name') . ' = ' . $db->quote('auto_purge'), + ] + ) + ->bind(':userId', $userId, ParameterType::INTEGER); + + $db->setQuery($query); + $config = $db->loadObject(); + + // Default is 7 days + $purge = 7; + + // Check if auto_purge value set + if (\is_object($config) && $config->cfg_name === 'auto_purge') { + $purge = $config->cfg_value; + } + + // If purge value is not 0, then allow purging of old messages + if ($purge > 0) { + // Purge old messages at day set in message configuration + $past = Factory::getDate(time() - $purge * 86400)->toSql(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__messages')) + ->where( + [ + $db->quoteName('date_time') . ' < :past', + $db->quoteName('user_id_to') . ' = :userId', + ] + ) + ->bind(':past', $past) + ->bind(':userId', $userId, ParameterType::INTEGER); + + $db->setQuery($query); + $db->execute(); + } + } } diff --git a/code/administrator/components/com_messages/src/Service/HTML/Messages.php b/code/administrator/components/com_messages/src/Service/HTML/Messages.php index 17be8a32..1c10ce15 100644 --- a/code/administrator/components/com_messages/src/Service/HTML/Messages.php +++ b/code/administrator/components/com_messages/src/Service/HTML/Messages.php @@ -1,4 +1,5 @@ array('trash', 'messages.unpublish', 'JTRASHED', 'COM_MESSAGES_MARK_AS_UNREAD'), - 1 => array('publish', 'messages.unpublish', 'COM_MESSAGES_OPTION_READ', 'COM_MESSAGES_MARK_AS_UNREAD'), - 0 => array('unpublish', 'messages.publish', 'COM_MESSAGES_OPTION_UNREAD', 'COM_MESSAGES_MARK_AS_READ'), - ); - - $state = ArrayHelper::getValue($states, (int) $value, $states[0]); - $icon = $state[0]; - - if ($canChange) - { - $html = ''; - } - - return $html; - } + /** + * Get the HTML code of the state switcher + * + * @param int $i Row number + * @param int $value The state value + * @param boolean $canChange Can the user change the state? + * + * @return string + * + * @since 3.4 + */ + public function status($i, $value = 0, $canChange = false) + { + // Array of image, task, title, action. + $states = array( + -2 => array('trash', 'messages.unpublish', 'JTRASHED', 'COM_MESSAGES_MARK_AS_UNREAD'), + 1 => array('publish', 'messages.unpublish', 'COM_MESSAGES_OPTION_READ', 'COM_MESSAGES_MARK_AS_UNREAD'), + 0 => array('unpublish', 'messages.publish', 'COM_MESSAGES_OPTION_UNREAD', 'COM_MESSAGES_MARK_AS_READ'), + ); + + $state = ArrayHelper::getValue($states, (int) $value, $states[0]); + $icon = $state[0]; + + if ($canChange) { + $html = ''; + } + + return $html; + } } diff --git a/code/administrator/components/com_messages/src/Table/MessageTable.php b/code/administrator/components/com_messages/src/Table/MessageTable.php index a750d7d7..961bcd88 100644 --- a/code/administrator/components/com_messages/src/Table/MessageTable.php +++ b/code/administrator/components/com_messages/src/Table/MessageTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - } - - /** - * Validation and filtering. - * - * @return boolean - * - * @since 1.5 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Check the to and from users. - $user = new User($this->user_id_from); - - if (empty($user->id)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_FROM_USER')); - - return false; - } - - $user = new User($this->user_id_to); - - if (empty($user->id)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_TO_USER')); - - return false; - } - - if (empty($this->subject)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_SUBJECT')); - - return false; - } - - if (empty($this->message)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_MESSAGE')); - - return false; - } - - return true; - } + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 1.5 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__messages', 'message_id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Validation and filtering. + * + * @return boolean + * + * @since 1.5 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Check the to and from users. + $user = new User($this->user_id_from); + + if (empty($user->id)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_FROM_USER')); + + return false; + } + + $user = new User($this->user_id_to); + + if (empty($user->id)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_TO_USER')); + + return false; + } + + if (empty($this->subject)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_SUBJECT')); + + return false; + } + + if (empty($this->message)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_MESSAGE')); + + return false; + } + + return true; + } } diff --git a/code/administrator/components/com_messages/src/View/Config/HtmlView.php b/code/administrator/components/com_messages/src/View/Config/HtmlView.php index c4b9e792..707ce417 100644 --- a/code/administrator/components/com_messages/src/View/Config/HtmlView.php +++ b/code/administrator/components/com_messages/src/View/Config/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Bind the record to the form. - $this->form->bind($this->item); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - ToolbarHelper::title(Text::_('COM_MESSAGES_TOOLBAR_MY_SETTINGS'), 'envelope'); - - ToolbarHelper::apply('config.save', 'JSAVE'); - - ToolbarHelper::cancel('config.cancel', 'JCANCEL'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Bind the record to the form. + $this->form->bind($this->item); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + ToolbarHelper::title(Text::_('COM_MESSAGES_TOOLBAR_MY_SETTINGS'), 'envelope'); + + ToolbarHelper::apply('config.save', 'JSAVE'); + + ToolbarHelper::cancel('config.cancel', 'JCANCEL'); + } } diff --git a/code/administrator/components/com_messages/src/View/Message/HtmlView.php b/code/administrator/components/com_messages/src/View/Message/HtmlView.php index 8d6a13ca..64a9a87a 100644 --- a/code/administrator/components/com_messages/src/View/Message/HtmlView.php +++ b/code/administrator/components/com_messages/src/View/Message/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - elseif ($this->getLayout() !== 'edit' && empty($this->item->message_id)) - { - throw new GenericDataException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } elseif ($this->getLayout() !== 'edit' && empty($this->item->message_id)) { + throw new GenericDataException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - parent::display($tpl); - $this->addToolbar(); - } + parent::display($tpl); + $this->addToolbar(); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $app = Factory::getApplication(); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $app = Factory::getApplication(); - if ($this->getLayout() == 'edit') - { - $app->input->set('hidemainmenu', true); - ToolbarHelper::title(Text::_('COM_MESSAGES_WRITE_PRIVATE_MESSAGE'), 'envelope-open-text new-privatemessage'); - ToolbarHelper::custom('message.save', 'envelope', '', 'COM_MESSAGES_TOOLBAR_SEND', false); - ToolbarHelper::cancel('message.cancel'); - ToolbarHelper::help('Private_Messages:_Write'); - } - else - { - ToolbarHelper::title(Text::_('COM_MESSAGES_VIEW_PRIVATE_MESSAGE'), 'envelope inbox'); - $sender = User::getInstance($this->item->user_id_from); + if ($this->getLayout() == 'edit') { + $app->input->set('hidemainmenu', true); + ToolbarHelper::title(Text::_('COM_MESSAGES_WRITE_PRIVATE_MESSAGE'), 'envelope-open-text new-privatemessage'); + ToolbarHelper::custom('message.save', 'envelope', '', 'COM_MESSAGES_TOOLBAR_SEND', false); + ToolbarHelper::cancel('message.cancel'); + ToolbarHelper::help('Private_Messages:_Write'); + } else { + ToolbarHelper::title(Text::_('COM_MESSAGES_VIEW_PRIVATE_MESSAGE'), 'envelope inbox'); + $sender = User::getInstance($this->item->user_id_from); - if ($sender->id !== $app->getIdentity()->get('id') && ($sender->authorise('core.admin') - || $sender->authorise('core.manage', 'com_messages') && $sender->authorise('core.login.admin')) - && $app->getIdentity()->authorise('core.manage', 'com_users') - ) - { - ToolbarHelper::custom('message.reply', 'redo', '', 'COM_MESSAGES_TOOLBAR_REPLY', false); - } + if ( + $sender->id !== $app->getIdentity()->get('id') && ($sender->authorise('core.admin') + || $sender->authorise('core.manage', 'com_messages') && $sender->authorise('core.login.admin')) + && $app->getIdentity()->authorise('core.manage', 'com_users') + ) { + ToolbarHelper::custom('message.reply', 'redo', '', 'COM_MESSAGES_TOOLBAR_REPLY', false); + } - ToolbarHelper::cancel('message.cancel'); - ToolbarHelper::help('Private_Messages:_Read'); - } - } + ToolbarHelper::cancel('message.cancel'); + ToolbarHelper::help('Private_Messages:_Read'); + } + } } diff --git a/code/administrator/components/com_messages/src/View/Messages/HtmlView.php b/code/administrator/components/com_messages/src/View/Messages/HtmlView.php index ec7d978d..7221bbf7 100644 --- a/code/administrator/components/com_messages/src/View/Messages/HtmlView.php +++ b/code/administrator/components/com_messages/src/View/Messages/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $canDo = ContentHelper::getActions('com_messages'); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_MESSAGES_MANAGER_MESSAGES'), 'envelope inbox'); - - // Only display the New button if the user has the access level to create a message and if they have access to the list of users - if ($canDo->get('core.create') && $user->authorise('core.manage', 'com_users')) - { - $toolbar->addNew('message.add'); - } - - if (!$this->isEmptyState && $canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('messages.publish') - ->text('COM_MESSAGES_TOOLBAR_MARK_AS_READ') - ->listCheck(true); - - $childBar->unpublish('messages.unpublish') - ->text('COM_MESSAGES_TOOLBAR_MARK_AS_UNREAD') - ->listCheck(true); - - if ($this->state->get('filter.state') != -2) - { - $childBar->trash('messages.trash')->listCheck(true); - } - } - - $toolbar->appendButton('Link', 'cog', 'COM_MESSAGES_TOOLBAR_MY_SETTINGS', 'index.php?option=com_messages&view=config'); - ToolbarHelper::divider(); - - if (!$this->isEmptyState && $this->state->get('filter.state') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('messages.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin')) - { - $toolbar->preferences('com_messages'); - } - - $toolbar->help('Private_Messages'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $canDo = ContentHelper::getActions('com_messages'); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_MESSAGES_MANAGER_MESSAGES'), 'envelope inbox'); + + // Only display the New button if the user has the access level to create a message and if they have access to the list of users + if ($canDo->get('core.create') && $user->authorise('core.manage', 'com_users')) { + $toolbar->addNew('message.add'); + } + + if (!$this->isEmptyState && $canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('messages.publish') + ->text('COM_MESSAGES_TOOLBAR_MARK_AS_READ') + ->listCheck(true); + + $childBar->unpublish('messages.unpublish') + ->text('COM_MESSAGES_TOOLBAR_MARK_AS_UNREAD') + ->listCheck(true); + + if ($this->state->get('filter.state') != -2) { + $childBar->trash('messages.trash')->listCheck(true); + } + } + + $toolbar->appendButton('Link', 'cog', 'COM_MESSAGES_TOOLBAR_MY_SETTINGS', 'index.php?option=com_messages&view=config'); + ToolbarHelper::divider(); + + if (!$this->isEmptyState && $this->state->get('filter.state') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('messages.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin')) { + $toolbar->preferences('com_messages'); + } + + $toolbar->help('Private_Messages'); + } } diff --git a/code/administrator/components/com_messages/tmpl/config/default.php b/code/administrator/components/com_messages/tmpl/config/default.php index 1c3d3dba..862c2a06 100644 --- a/code/administrator/components/com_messages/tmpl/config/default.php +++ b/code/administrator/components/com_messages/tmpl/config/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
-
-
-
-
- - form->renderField('lock'); ?> - form->renderField('mail_on_new'); ?> - form->renderField('auto_purge'); ?> -
-
-
-
+
+
+
+
+ + form->renderField('lock'); ?> + form->renderField('mail_on_new'); ?> + form->renderField('auto_purge'); ?> +
+
+
+
- - + +
diff --git a/code/administrator/components/com_messages/tmpl/message/default.php b/code/administrator/components/com_messages/tmpl/message/default.php index ce8277f9..7cb2ee34 100644 --- a/code/administrator/components/com_messages/tmpl/message/default.php +++ b/code/administrator/components/com_messages/tmpl/message/default.php @@ -1,4 +1,5 @@
-
-
-
-
-
- -
-
- item->get('from_user_name'); ?> -
-
-
-
- -
-
- item->date_time, Text::_('DATE_FORMAT_LC2')); ?> -
-
-
-
- -
-
- item->subject; ?> -
-
-
-
- -
-
- item->message; ?> -
-
- - - -
-
-
+
+
+
+
+
+ +
+
+ item->get('from_user_name'); ?> +
+
+
+
+ +
+
+ item->date_time, Text::_('DATE_FORMAT_LC2')); ?> +
+
+
+
+ +
+
+ item->subject; ?> +
+
+
+
+ +
+
+ item->message; ?> +
+
+ + + +
+
+
diff --git a/code/administrator/components/com_messages/tmpl/message/edit.php b/code/administrator/components/com_messages/tmpl/message/edit.php index 2ddcf004..00b33c96 100644 --- a/code/administrator/components/com_messages/tmpl/message/edit.php +++ b/code/administrator/components/com_messages/tmpl/message/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
-
-
-
-
- form->getLabel('user_id_to'); ?> - form->getInput('user_id_to'); ?> -
-
- form->getLabel('subject'); ?> - form->getInput('subject'); ?> -
-
- form->getLabel('message'); ?> - form->getInput('message'); ?> -
-
-
-
- - +
+
+
+
+ form->getLabel('user_id_to'); ?> + form->getInput('user_id_to'); ?> +
+
+ form->getLabel('subject'); ?> + form->getInput('subject'); ?> +
+
+ form->getLabel('message'); ?> + form->getInput('message'); ?> +
+
+
+
+ +
diff --git a/code/administrator/components/com_messages/tmpl/messages/default.php b/code/administrator/components/com_messages/tmpl/messages/default.php index 57ab907e..0572dfdc 100644 --- a/code/administrator/components/com_messages/tmpl/messages/default.php +++ b/code/administrator/components/com_messages/tmpl/messages/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - items as $i => $item) : - $canChange = $user->authorise('core.edit.state', 'com_messages'); - ?> - - - - - - - - - -
- , - , - -
- - - - - - - - - -
- message_id, false, 'cid', 'cb', $item->subject); ?> - - - escape($item->subject); ?> - - state, $canChange); ?> - - user_from; ?> - - date_time, Text::_('DATE_FORMAT_LC2')); ?> -
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + items as $i => $item) : + $canChange = $user->authorise('core.edit.state', 'com_messages'); + ?> + + + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ message_id, false, 'cid', 'cb', $item->subject); ?> + + + escape($item->subject); ?> + + state, $canChange); ?> + + user_from; ?> + + date_time, Text::_('DATE_FORMAT_LC2')); ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - -
- - - -
-
+ +
+ + + +
+
diff --git a/code/administrator/components/com_messages/tmpl/messages/emptystate.php b/code/administrator/components/com_messages/tmpl/messages/emptystate.php index bd872cff..43a4b1a7 100644 --- a/code/administrator/components/com_messages/tmpl/messages/emptystate.php +++ b/code/administrator/components/com_messages/tmpl/messages/emptystate.php @@ -1,4 +1,5 @@ 'COM_MESSAGES', - 'formURL' => 'index.php?option=com_messages&view=messages', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Private_Messages', - 'icon' => 'icon-envelope inbox', + 'textPrefix' => 'COM_MESSAGES', + 'formURL' => 'index.php?option=com_messages&view=messages', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Private_Messages', + 'icon' => 'icon-envelope inbox', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_messages') - && Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_users')) -{ - $displayData['createURL'] = 'index.php?option=com_messages&task=message.add'; +if ( + Factory::getApplication()->getIdentity()->authorise('core.create', 'com_messages') + && Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_users') +) { + $displayData['createURL'] = 'index.php?option=com_messages&task=message.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_modules/helpers/modules.php b/code/administrator/components/com_modules/helpers/modules.php index a2419e69..a4436970 100644 --- a/code/administrator/components/com_modules/helpers/modules.php +++ b/code/administrator/components/com_modules/helpers/modules.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Modules component helper. @@ -18,5 +23,4 @@ */ abstract class ModulesHelper extends \Joomla\Component\Modules\Administrator\Helper\ModulesHelper { - } diff --git a/code/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php b/code/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php index 2af5c7d5..671d77c7 100644 --- a/code/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php +++ b/code/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php @@ -1,4 +1,5 @@ escape(Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_OPTIONS')) . '" ', + 'class="' . $class . '"', + ' allow-custom', + ' search-placeholder="' . $this->escape(Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_OPTIONS')) . '" ', ); $selectAttr = array( - $disabled ? 'disabled' : '', - $readonly ? 'readonly' : '', - strlen($hint) ? 'placeholder="' . $this->escape($hint) . '"' : '', - $onchange ? ' onchange="' . $onchange . '"' : '', - $autofocus ? ' autofocus' : '', + $disabled ? 'disabled' : '', + $readonly ? 'readonly' : '', + strlen($hint) ? 'placeholder="' . $this->escape($hint) . '"' : '', + $onchange ? ' onchange="' . $onchange . '"' : '', + $autofocus ? ' autofocus' : '', ); -if ($required) -{ - $selectAttr[] = ' required class="required"'; - $attributes[] = ' required'; +if ($required) { + $selectAttr[] = ' required class="required"'; + $attributes[] = ' required'; } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> > $id, - 'list.select' => $value, - 'list.attr' => implode(' ', $selectAttr), - ) - ); -?> + echo HTMLHelper::_('select.groupedlist', $positions, $name, array( + 'id' => $id, + 'list.select' => $value, + 'list.attr' => implode(' ', $selectAttr), + )); + ?> diff --git a/code/administrator/components/com_modules/layouts/toolbar/cancelselect.php b/code/administrator/components/com_modules/layouts/toolbar/cancelselect.php index b94d855f..76370608 100644 --- a/code/administrator/components/com_modules/layouts/toolbar/cancelselect.php +++ b/code/administrator/components/com_modules/layouts/toolbar/cancelselect.php @@ -1,4 +1,5 @@ - + diff --git a/code/administrator/components/com_modules/modules.xml b/code/administrator/components/com_modules/modules.xml index 77bb3560..ce6c284a 100644 --- a/code/administrator/components/com_modules/modules.xml +++ b/code/administrator/components/com_modules/modules.xml @@ -2,7 +2,7 @@ com_modules Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_modules/services/provider.php b/code/administrator/components/com_modules/services/provider.php index 4998d4d4..e6241919 100644 --- a/code/administrator/components/com_modules/services/provider.php +++ b/code/administrator/components/com_modules/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Modules')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Modules')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Modules')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Modules')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new ModulesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new ModulesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_modules/src/Controller/DisplayController.php b/code/administrator/components/com_modules/src/Controller/DisplayController.php index f6a2c97a..6ea12f68 100644 --- a/code/administrator/components/com_modules/src/Controller/DisplayController.php +++ b/code/administrator/components/com_modules/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('layout', 'edit'); - $id = $this->input->getInt('id'); - - // Verify client - $clientId = $this->input->post->getInt('client_id'); - - if (!is_null($clientId)) - { - $uri = Uri::getInstance(); - - if ((int) $uri->getVar('client_id') !== (int) $clientId) - { - $this->setRedirect(Route::_('index.php?option=com_modules&view=modules&client_id=' . $clientId, false)); - - return false; - } - } - - // Check for edit form. - if ($layout == 'edit' && !$this->checkEditId('com_modules.edit.module', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_modules&view=modules&client_id=' . $this->input->getInt('client_id'), false)); - - return false; - } - - // Check custom administrator menu modules - if (ModuleHelper::isAdminMultilang()) - { - $languages = LanguageHelper::getInstalledLanguages(1, true); - $langCodes = array(); - - foreach ($languages as $language) - { - if (isset($language->metadata['nativeName'])) - { - $languageName = $language->metadata['nativeName']; - } - else - { - $languageName = $language->metadata['name']; - } - - $langCodes[$language->metadata['tag']] = $languageName; - } - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - $query->select($db->quoteName('m.language')) - ->from($db->quoteName('#__modules', 'm')) - ->where($db->quoteName('m.module') . ' = ' . $db->quote('mod_menu')) - ->where($db->quoteName('m.published') . ' = 1') - ->where($db->quoteName('m.client_id') . ' = 1') - ->group($db->quoteName('m.language')); - - $mLanguages = $db->setQuery($query)->loadColumn(); - - // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. - if (!in_array('*', $mLanguages) && count($langMissing = array_diff(array_keys($langCodes), $mLanguages))) - { - $langMissing = array_intersect_key($langCodes, array_flip($langMissing)); - - $this->app->enqueueMessage(Text::sprintf('JMENU_MULTILANG_WARNING_MISSING_MODULES', implode(', ', $langMissing)), 'warning'); - } - } - - return parent::display(); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'modules'; + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array|boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()} + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $layout = $this->input->get('layout', 'edit'); + $id = $this->input->getInt('id'); + + // Verify client + $clientId = $this->input->post->getInt('client_id'); + + if (!is_null($clientId)) { + $uri = Uri::getInstance(); + + if ((int) $uri->getVar('client_id') !== (int) $clientId) { + $this->setRedirect(Route::_('index.php?option=com_modules&view=modules&client_id=' . $clientId, false)); + + return false; + } + } + + // Check for edit form. + if ($layout == 'edit' && !$this->checkEditId('com_modules.edit.module', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_modules&view=modules&client_id=' . $this->input->getInt('client_id'), false)); + + return false; + } + + // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. + $factory = $this->app->bootComponent('menus')->getMVCFactory(); + + if ($langMissing = $factory->createModel('Menus', 'Administrator')->getMissingModuleLanguages()) { + $this->app->enqueueMessage(Text::sprintf('JMENU_MULTILANG_WARNING_MISSING_MODULES', implode(', ', $langMissing)), 'warning'); + } + + return parent::display(); + } } diff --git a/code/administrator/components/com_modules/src/Controller/ModuleController.php b/code/administrator/components/com_modules/src/Controller/ModuleController.php index ef53b80a..61e3bcb1 100644 --- a/code/administrator/components/com_modules/src/Controller/ModuleController.php +++ b/code/administrator/components/com_modules/src/Controller/ModuleController.php @@ -1,4 +1,5 @@ app; - - // Get the result of the parent method. If an error, just return it. - $result = parent::add(); - - if ($result instanceof \Exception) - { - return $result; - } - - // Look for the Extension ID. - $extensionId = $this->input->get('eid', 0, 'int'); - - if (empty($extensionId)) - { - $redirectUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . '&layout=edit'; - - $this->setRedirect(Route::_($redirectUrl, false)); - - $app->enqueueMessage(Text::_('COM_MODULES_ERROR_INVALID_EXTENSION'), 'warning'); - } - - $app->setUserState('com_modules.add.module.extension_id', $extensionId); - $app->setUserState('com_modules.add.module.params', null); - - // Parameters could be coming in for a new item, so let's set them. - $params = $this->input->get('params', array(), 'array'); - $app->setUserState('com_modules.add.module.params', $params); - } - - /** - * Override parent cancel method to reset the add module state. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 1.6 - */ - public function cancel($key = null) - { - $result = parent::cancel(); - - $this->app->setUserState('com_modules.add.module.extension_id', null); - $this->app->setUserState('com_modules.add.module.params', null); - - if ($return = $this->input->get('return', '', 'BASE64')) - { - $return = base64_decode($return); - - // Don't redirect to an external URL. - if (!Uri::isInternal($return)) - { - $return = Uri::base(); - } - - $this->app->redirect($return); - } - - return $result; - } - - /** - * Override parent allowSave method. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowSave($data, $key = 'id') - { - // Use custom position if selected - if (isset($data['custom_position'])) - { - if (empty($data['position'])) - { - $data['position'] = $data['custom_position']; - } - - unset($data['custom_position']); - } - - return parent::allowSave($data, $key); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.2 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Initialise variables. - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - - // Zero record (id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($this->app->getIdentity()->authorise('core.edit', 'com_modules.module.' . $recordId)) - { - return true; - } - - return false; - } - - /** - * Method to run batch operations. - * - * @param string $model The model - * - * @return boolean True on success. - * - * @since 1.7 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Module', 'Administrator', array()); - - // Preset the redirect - $redirectUrl = 'index.php?option=com_modules&view=modules' . $this->getRedirectToListAppend(); - - $this->setRedirect(Route::_($redirectUrl, false)); - - return parent::batch($model); - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 1.6 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $task = $this->getTask(); - - switch ($task) - { - case 'save2new': - $this->app->setUserState('com_modules.add.module.extension_id', $model->getState('module.extension_id')); - break; - - default: - $this->app->setUserState('com_modules.add.module.extension_id', null); - break; - } - - $this->app->setUserState('com_modules.add.module.params', null); - } - - /** - * Method to save a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key - * - * @return boolean True if successful, false otherwise. - */ - public function save($key = null, $urlVar = null) - { - $this->checkToken(); - - if ($this->app->getDocument()->getType() == 'json') - { - $model = $this->getModel(); - $data = $this->input->post->get('jform', array(), 'array'); - $item = $model->getItem($this->input->get('id')); - $properties = $item->getProperties(); - - if (isset($data['params'])) - { - unset($properties['params']); - } - - // Replace changed properties - $data = array_replace_recursive($properties, $data); - - if (!empty($data['assigned'])) - { - $data['assigned'] = array_map('abs', $data['assigned']); - } - - // Add new data to input before process by parent save() - $this->input->post->set('jform', $data); - - // Add path of forms directory - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/models/forms'); - } - - return parent::save($key, $urlVar); - - } - - /** - * Method to get the other modules in the same position - * - * @return string The data for the Ajax request. - * - * @since 3.6.3 - */ - public function orderPosition() - { - $app = $this->app; - - // Send json mime type. - $app->mimeType = 'application/json'; - $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); - $app->sendHeaders(); - - // Check if user token is valid. - if (!Session::checkToken('get')) - { - $app->enqueueMessage(Text::_('JINVALID_TOKEN_NOTICE'), 'error'); - echo new JsonResponse; - $app->close(); - } - - $clientId = $this->input->getValue('client_id'); - $position = $this->input->getValue('position'); - $moduleId = $this->input->getValue('module_id'); - - // Access check. - if (!$this->app->getIdentity()->authorise('core.create', 'com_modules') - && !$this->app->getIdentity()->authorise('core.edit.state', 'com_modules') - && ($moduleId && !$this->app->getIdentity()->authorise('core.edit.state', 'com_modules.module.' . $moduleId))) - { - $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); - echo new JsonResponse; - $app->close(); - } - - $db = Factory::getDbo(); - $clientId = (int) $clientId; - $query = $db->getQuery(true) - ->select($db->quoteName(['position', 'ordering', 'title'])) - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->where($db->quoteName('position') . ' = :position') - ->order($db->quoteName('ordering')) - ->bind(':clientid', $clientId, ParameterType::INTEGER) - ->bind(':position', $position); - - $db->setQuery($query); - - try - { - $orders = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return ''; - } - - $orders2 = array(); - $n = count($orders); - - if ($n > 0) - { - for ($i = 0; $i < $n; $i++) - { - if (!isset($orders2[$orders[$i]->position])) - { - $orders2[$orders[$i]->position] = 0; - } - - $orders2[$orders[$i]->position]++; - $ord = $orders2[$orders[$i]->position]; - $title = Text::sprintf('COM_MODULES_OPTION_ORDER_POSITION', $ord, htmlspecialchars($orders[$i]->title, ENT_QUOTES, 'UTF-8')); - - $html[] = $orders[$i]->position . ',' . $ord . ',' . $title; - } - } - else - { - $html[] = $position . ',' . 1 . ',' . Text::_('JNONE'); - } - - echo new JsonResponse($html); - $app->close(); - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - $append .= '&client_id=' . $this->input->getInt('client_id'); - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&client_id=' . $this->input->getInt('client_id'); - - return $append; - } + /** + * Override parent add method. + * + * @return \Exception|void True if the record can be added, a \Exception object if not. + * + * @since 1.6 + */ + public function add() + { + $app = $this->app; + + // Get the result of the parent method. If an error, just return it. + $result = parent::add(); + + if ($result instanceof \Exception) { + return $result; + } + + // Look for the Extension ID. + $extensionId = $this->input->get('eid', 0, 'int'); + + if (empty($extensionId)) { + $redirectUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . '&layout=edit'; + + $this->setRedirect(Route::_($redirectUrl, false)); + + $app->enqueueMessage(Text::_('COM_MODULES_ERROR_INVALID_EXTENSION'), 'warning'); + } + + $app->setUserState('com_modules.add.module.extension_id', $extensionId); + $app->setUserState('com_modules.add.module.params', null); + + // Parameters could be coming in for a new item, so let's set them. + $params = $this->input->get('params', array(), 'array'); + $app->setUserState('com_modules.add.module.params', $params); + } + + /** + * Override parent cancel method to reset the add module state. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 1.6 + */ + public function cancel($key = null) + { + $result = parent::cancel(); + + $this->app->setUserState('com_modules.add.module.extension_id', null); + $this->app->setUserState('com_modules.add.module.params', null); + + if ($return = $this->input->get('return', '', 'BASE64')) { + $return = base64_decode($return); + + // Don't redirect to an external URL. + if (!Uri::isInternal($return)) { + $return = Uri::base(); + } + + $this->app->redirect($return); + } + + return $result; + } + + /** + * Override parent allowSave method. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowSave($data, $key = 'id') + { + // Use custom position if selected + if (isset($data['custom_position'])) { + if (empty($data['position'])) { + $data['position'] = $data['custom_position']; + } + + unset($data['custom_position']); + } + + return parent::allowSave($data, $key); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.2 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Initialise variables. + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + + // Zero record (id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($this->app->getIdentity()->authorise('core.edit', 'com_modules.module.' . $recordId)) { + return true; + } + + return false; + } + + /** + * Method to run batch operations. + * + * @param string $model The model + * + * @return boolean True on success. + * + * @since 1.7 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Module', 'Administrator', array()); + + // Preset the redirect + $redirectUrl = 'index.php?option=com_modules&view=modules' . $this->getRedirectToListAppend(); + + $this->setRedirect(Route::_($redirectUrl, false)); + + return parent::batch($model); + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 1.6 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $task = $this->getTask(); + + switch ($task) { + case 'save2new': + $this->app->setUserState('com_modules.add.module.extension_id', $model->getState('module.extension_id')); + break; + + default: + $this->app->setUserState('com_modules.add.module.extension_id', null); + break; + } + + $this->app->setUserState('com_modules.add.module.params', null); + } + + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * + * @return boolean True if successful, false otherwise. + */ + public function save($key = null, $urlVar = null) + { + $this->checkToken(); + + if ($this->app->getDocument()->getType() == 'json') { + $model = $this->getModel(); + $data = $this->input->post->get('jform', array(), 'array'); + $item = $model->getItem($this->input->get('id')); + $properties = $item->getProperties(); + + if (isset($data['params'])) { + unset($properties['params']); + } + + // Replace changed properties + $data = array_replace_recursive($properties, $data); + + if (!empty($data['assigned'])) { + $data['assigned'] = array_map('abs', $data['assigned']); + } + + // Add new data to input before process by parent save() + $this->input->post->set('jform', $data); + + // Add path of forms directory + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/models/forms'); + } + + return parent::save($key, $urlVar); + } + + /** + * Method to get the other modules in the same position + * + * @return string The data for the Ajax request. + * + * @since 3.6.3 + */ + public function orderPosition() + { + $app = $this->app; + + // Send json mime type. + $app->mimeType = 'application/json'; + $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); + $app->sendHeaders(); + + // Check if user token is valid. + if (!Session::checkToken('get')) { + $app->enqueueMessage(Text::_('JINVALID_TOKEN_NOTICE'), 'error'); + echo new JsonResponse(); + $app->close(); + } + + $clientId = $this->input->getValue('client_id'); + $position = $this->input->getValue('position'); + $moduleId = $this->input->getValue('module_id'); + + // Access check. + if ( + !$this->app->getIdentity()->authorise('core.create', 'com_modules') + && !$this->app->getIdentity()->authorise('core.edit.state', 'com_modules') + && ($moduleId && !$this->app->getIdentity()->authorise('core.edit.state', 'com_modules.module.' . $moduleId)) + ) { + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); + echo new JsonResponse(); + $app->close(); + } + + $db = Factory::getDbo(); + $clientId = (int) $clientId; + $query = $db->getQuery(true) + ->select($db->quoteName(['position', 'ordering', 'title'])) + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->where($db->quoteName('position') . ' = :position') + ->order($db->quoteName('ordering')) + ->bind(':clientid', $clientId, ParameterType::INTEGER) + ->bind(':position', $position); + + $db->setQuery($query); + + try { + $orders = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return ''; + } + + $orders2 = array(); + $n = count($orders); + + if ($n > 0) { + for ($i = 0; $i < $n; $i++) { + if (!isset($orders2[$orders[$i]->position])) { + $orders2[$orders[$i]->position] = 0; + } + + $orders2[$orders[$i]->position]++; + $ord = $orders2[$orders[$i]->position]; + $title = Text::sprintf('COM_MODULES_OPTION_ORDER_POSITION', $ord, htmlspecialchars($orders[$i]->title, ENT_QUOTES, 'UTF-8')); + + $html[] = $orders[$i]->position . ',' . $ord . ',' . $title; + } + } else { + $html[] = $position . ',' . 1 . ',' . Text::_('JNONE'); + } + + echo new JsonResponse($html); + $app->close(); + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $append .= '&client_id=' . $this->input->getInt('client_id'); + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&client_id=' . $this->input->getInt('client_id'); + + return $append; + } } diff --git a/code/administrator/components/com_modules/src/Controller/ModulesController.php b/code/administrator/components/com_modules/src/Controller/ModulesController.php index 005e5e8f..2b9fa119 100644 --- a/code/administrator/components/com_modules/src/Controller/ModulesController.php +++ b/code/administrator/components/com_modules/src/Controller/ModulesController.php @@ -1,4 +1,5 @@ checkToken(); - - $pks = (array) $this->input->post->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $pks = array_filter($pks); - - try - { - if (empty($pks)) - { - throw new \Exception(Text::_('COM_MODULES_ERROR_NO_MODULES_SELECTED')); - } - - $model = $this->getModel(); - $model->duplicate($pks); - $this->setMessage(Text::plural('COM_MODULES_N_MODULES_DUPLICATED', count($pks))); - } - catch (\Exception $e) - { - $this->app->enqueueMessage($e->getMessage(), 'warning'); - } - - $this->setRedirect('index.php?option=com_modules&view=modules' . $this->getRedirectToListAppend()); - } - - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.6 - */ - public function getModel($name = 'Module', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to get the number of frontend modules - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Modules'); - - $model->setState('filter.state', 1); - $model->setState('filter.client_id', 0); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_MODULES_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_MODULES_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&client_id=' . $this->input->getInt('client_id'); - - return $append; - } + /** + * Method to clone an existing module. + * + * @return void + * + * @since 1.6 + */ + public function duplicate() + { + // Check for request forgeries + $this->checkToken(); + + $pks = (array) $this->input->post->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $pks = array_filter($pks); + + try { + if (empty($pks)) { + throw new \Exception(Text::_('COM_MODULES_ERROR_NO_MODULES_SELECTED')); + } + + $model = $this->getModel(); + $model->duplicate($pks); + $this->setMessage(Text::plural('COM_MODULES_N_MODULES_DUPLICATED', count($pks))); + } catch (\Exception $e) { + $this->app->enqueueMessage($e->getMessage(), 'warning'); + } + + $this->setRedirect('index.php?option=com_modules&view=modules' . $this->getRedirectToListAppend()); + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Module', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to get the number of frontend modules + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Modules'); + + $model->setState('filter.state', 1); + $model->setState('filter.client_id', 0); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_MODULES_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_MODULES_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&client_id=' . $this->input->getInt('client_id'); + + return $append; + } } diff --git a/code/administrator/components/com_modules/src/Extension/ModulesComponent.php b/code/administrator/components/com_modules/src/Extension/ModulesComponent.php index 167c8b8f..b48cd9dd 100644 --- a/code/administrator/components/com_modules/src/Extension/ModulesComponent.php +++ b/code/administrator/components/com_modules/src/Extension/ModulesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('modules', new Modules); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('modules', new Modules()); + } } diff --git a/code/administrator/components/com_modules/src/Field/ModulesModuleField.php b/code/administrator/components/com_modules/src/Field/ModulesModuleField.php index f748ff6f..0ac0f8ec 100644 --- a/code/administrator/components/com_modules/src/Field/ModulesModuleField.php +++ b/code/administrator/components/com_modules/src/Field/ModulesModuleField.php @@ -1,4 +1,5 @@ $name; - } + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 4.0.0 + */ + public function __get($name) + { + switch ($name) { + case 'client': + return $this->$name; + } - return parent::__get($name); - } + return parent::__get($name); + } - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 4.0.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'client': - $this->$name = (string) $value; - break; + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 4.0.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'client': + $this->$name = (string) $value; + break; - default: - parent::__set($name, $value); - } - } + default: + parent::__set($name, $value); + } + } - /** - * Method to attach a Form object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $result = parent::setup($element, $value, $group); + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); - if ($result === true) - { - $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; - } + if ($result === true) { + $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; + } - return $result; - } + return $result; + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 3.4.2 - */ - public function getOptions() - { - $clientId = $this->client === 'administrator' ? 1 : 0; - $options = ModulesHelper::getModules($clientId); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.4.2 + */ + public function getOptions() + { + $clientId = $this->client === 'administrator' ? 1 : 0; + $options = ModulesHelper::getModules($clientId); - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/code/administrator/components/com_modules/src/Field/ModulesPositionField.php b/code/administrator/components/com_modules/src/Field/ModulesPositionField.php index e7e73d25..d5f28f6e 100644 --- a/code/administrator/components/com_modules/src/Field/ModulesPositionField.php +++ b/code/administrator/components/com_modules/src/Field/ModulesPositionField.php @@ -1,4 +1,5 @@ $name; - } + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 4.0.0 + */ + public function __get($name) + { + switch ($name) { + case 'client': + return $this->$name; + } - return parent::__get($name); - } + return parent::__get($name); + } - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 4.0.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'client': - $this->$name = (string) $value; - break; + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 4.0.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'client': + $this->$name = (string) $value; + break; - default: - parent::__set($name, $value); - } - } + default: + parent::__set($name, $value); + } + } - /** - * Method to attach a Form object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $result = parent::setup($element, $value, $group); + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); - if ($result === true) - { - $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; - } + if ($result === true) { + $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; + } - return $result; - } + return $result; + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 3.4.2 - */ - public function getOptions() - { - $clientId = $this->client === 'administrator' ? 1 : 0; - $options = ModulesHelper::getPositions($clientId); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.4.2 + */ + public function getOptions() + { + $clientId = $this->client === 'administrator' ? 1 : 0; + $options = ModulesHelper::getPositions($clientId); - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/code/administrator/components/com_modules/src/Field/ModulesPositioneditField.php b/code/administrator/components/com_modules/src/Field/ModulesPositioneditField.php index 040f5f3d..211270fc 100644 --- a/code/administrator/components/com_modules/src/Field/ModulesPositioneditField.php +++ b/code/administrator/components/com_modules/src/Field/ModulesPositioneditField.php @@ -1,4 +1,5 @@ $name; - } - - return parent::__get($name); - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 4.0.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'client': - $this->$name = (string) $value; - break; - - default: - parent::__set($name, $value); - } - } - - /** - * Method to attach a Form object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $result = parent::setup($element, $value, $group); - - if ($result === true) - { - $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; - } - - return $result; - } - - /** - * Method to get the field input markup. - * - * @return string The field input markup. - * - * @since 4.0.0 - */ - protected function getInput() - { - $data = $this->getLayoutData(); - - $clientId = $this->client === 'administrator' ? 1 : 0; - $positions = HTMLHelper::_('modules.positions', $clientId, 1, $this->value); - - $data['client'] = $clientId; - $data['positions'] = $positions; - - $renderer = $this->getRenderer($this->layout); - $renderer->setComponent('com_modules'); - $renderer->setClient(1); - - return $renderer->render($data); - } + /** + * The form field type. + * + * @var string + * @since 4.0.0 + */ + protected $type = 'ModulesPositionedit'; + + /** + * Name of the layout being used to render the field + * + * @var string + * @since 4.0.0 + */ + protected $layout = 'joomla.form.field.modulespositionedit'; + + /** + * Client name. + * + * @var string + * @since 4.0.0 + */ + protected $client; + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 4.0.0 + */ + public function __get($name) + { + switch ($name) { + case 'client': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 4.0.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'client': + $this->$name = (string) $value; + break; + + default: + parent::__set($name, $value); + } + } + + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); + + if ($result === true) { + $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; + } + + return $result; + } + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 4.0.0 + */ + protected function getInput() + { + $data = $this->getLayoutData(); + + $clientId = $this->client === 'administrator' ? 1 : 0; + $positions = HTMLHelper::_('modules.positions', $clientId, 1, $this->value); + + $data['client'] = $clientId; + $data['positions'] = $positions; + + $renderer = $this->getRenderer($this->layout); + $renderer->setComponent('com_modules'); + $renderer->setClient(1); + + return $renderer->render($data); + } } diff --git a/code/administrator/components/com_modules/src/Helper/ModulesHelper.php b/code/administrator/components/com_modules/src/Helper/ModulesHelper.php index 6657d2f1..646dbb53 100644 --- a/code/administrator/components/com_modules/src/Helper/ModulesHelper.php +++ b/code/administrator/components/com_modules/src/Helper/ModulesHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT ' . $db->quoteName('position')) - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->order($db->quoteName('position')) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $positions = $db->loadColumn(); - $positions = is_array($positions) ? $positions : array(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return; - } - - // Build the list - $options = array(); - - foreach ($positions as $position) - { - if (!$position && !$editPositions) - { - $options[] = HTMLHelper::_('select.option', 'none', Text::_('COM_MODULES_NONE')); - } - elseif (!$position) - { - $options[] = HTMLHelper::_('select.option', '', Text::_('COM_MODULES_NONE')); - } - else - { - $options[] = HTMLHelper::_('select.option', $position, $position); - } - } - - return $options; - } - - /** - * Return a list of templates - * - * @param integer $clientId Client ID - * @param string $state State - * @param string $template Template name - * - * @return array List of templates - */ - public static function getTemplates($clientId = 0, $state = '', $template = '') - { - $db = Factory::getDbo(); - $clientId = (int) $clientId; - - // Get the database object and a new query object. - $query = $db->getQuery(true); - - // Build the query. - $query->select($db->quoteName(['element', 'name', 'enabled'])) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->where($db->quoteName('type') . ' = ' . $db->quote('template')); - - if ($state != '') - { - $query->where($db->quoteName('enabled') . ' = :state') - ->bind(':state', $state); - } - - if ($template != '') - { - $query->where($db->quoteName('element') . ' = :element') - ->bind(':element', $template); - } - - $query->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Set the query and load the templates. - $db->setQuery($query); - $templates = $db->loadObjectList('element'); - - return $templates; - } - - /** - * Get a list of the unique modules installed in the client application. - * - * @param int $clientId The client id. - * - * @return array Array of unique modules - */ - public static function getModules($clientId) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('element AS value, name AS text') - ->from('#__extensions as e') - ->where('e.client_id = ' . (int) $clientId) - ->where('type = ' . $db->quote('module')) - ->join('LEFT', '#__modules as m ON m.module=e.element AND m.client_id=e.client_id') - ->where('m.module IS NOT NULL') - ->group('element,name'); - - $db->setQuery($query); - $modules = $db->loadObjectList(); - $lang = Factory::getLanguage(); - - foreach ($modules as $i => $module) - { - $extension = $module->value; - $path = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - $source = $path . "/modules/$extension"; - $lang->load("$extension.sys", $path) - || $lang->load("$extension.sys", $source); - $modules[$i]->text = Text::_($module->text); - } - - $modules = ArrayHelper::sortObjects($modules, 'text', 1, true, true); - - return $modules; - } - - /** - * Get a list of the assignment options for modules to menus. - * - * @param int $clientId The client id. - * - * @return array - */ - public static function getAssignmentOptions($clientId) - { - $options = array(); - $options[] = HTMLHelper::_('select.option', '0', 'COM_MODULES_OPTION_MENU_ALL'); - $options[] = HTMLHelper::_('select.option', '-', 'COM_MODULES_OPTION_MENU_NONE'); - - if ($clientId == 0) - { - $options[] = HTMLHelper::_('select.option', '1', 'COM_MODULES_OPTION_MENU_INCLUDE'); - $options[] = HTMLHelper::_('select.option', '-1', 'COM_MODULES_OPTION_MENU_EXCLUDE'); - } - - return $options; - } - - /** - * Return a translated module position name - * - * @param integer $clientId Application client id 0: site | 1: admin - * @param string $template Template name - * @param string $position Position name - * - * @return string Return a translated position name - * - * @since 3.0 - */ - public static function getTranslatedModulePosition($clientId, $template, $position) - { - // Template translation - $lang = Factory::getLanguage(); - $path = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - - $loaded = $lang->getPaths('tpl_' . $template . '.sys'); - - // Only load the template's language file if it hasn't been already - if (!$loaded) - { - $lang->load('tpl_' . $template . '.sys', $path, null, false, false) - || $lang->load('tpl_' . $template . '.sys', $path . '/templates/' . $template, null, false, false) - || $lang->load('tpl_' . $template . '.sys', $path, $lang->getDefault(), false, false) - || $lang->load('tpl_' . $template . '.sys', $path . '/templates/' . $template, $lang->getDefault(), false, false); - } - - $langKey = strtoupper('TPL_' . $template . '_POSITION_' . $position); - $text = Text::_($langKey); - - // Avoid untranslated strings - if (!self::isTranslatedText($langKey, $text)) - { - // Modules component translation - $langKey = strtoupper('COM_MODULES_POSITION_' . $position); - $text = Text::_($langKey); - - // Avoid untranslated strings - if (!self::isTranslatedText($langKey, $text)) - { - // Try to humanize the position name - $text = ucfirst(preg_replace('/^' . $template . '\-/', '', $position)); - $text = ucwords(str_replace(array('-', '_'), ' ', $text)); - } - } - - return $text; - } - - /** - * Check if the string was translated - * - * @param string $langKey Language file text key - * @param string $text The "translated" text to be checked - * - * @return boolean Return true for translated text - * - * @since 3.0 - */ - public static function isTranslatedText($langKey, $text) - { - return $text !== $langKey; - } - - /** - * Create and return a new Option - * - * @param string $value The option value [optional] - * @param string $text The option text [optional] - * - * @return object The option as an object (\stdClass instance) - * - * @since 3.0 - */ - public static function createOption($value = '', $text = '') - { - if (empty($text)) - { - $text = $value; - } - - $option = new \stdClass; - $option->value = $value; - $option->text = $text; - - return $option; - } - - /** - * Create and return a new Option Group - * - * @param string $label Value and label for group [optional] - * @param array $options Array of options to insert into group [optional] - * - * @return array Return the new group as an array - * - * @since 3.0 - */ - public static function createOptionGroup($label = '', $options = array()) - { - $group = array(); - $group['value'] = $label; - $group['text'] = $label; - $group['items'] = $options; - - return $group; - } + /** + * Get a list of filter options for the state of a module. + * + * @return array An array of \JHtmlOption elements. + */ + public static function getStateOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JPUBLISHED')); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JUNPUBLISHED')); + $options[] = HTMLHelper::_('select.option', '-2', Text::_('JTRASHED')); + $options[] = HTMLHelper::_('select.option', '*', Text::_('JALL')); + + return $options; + } + + /** + * Get a list of filter options for the application clients. + * + * @return array An array of \JHtmlOption elements. + */ + public static function getClientOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR')); + + return $options; + } + + /** + * Get a list of modules positions + * + * @param integer $clientId Client ID + * @param boolean $editPositions Allow to edit the positions + * + * @return array A list of positions + */ + public static function getPositions($clientId, $editPositions = false) + { + $db = Factory::getDbo(); + $clientId = (int) $clientId; + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('position')) + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->order($db->quoteName('position')) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $positions = $db->loadColumn(); + $positions = is_array($positions) ? $positions : array(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return; + } + + // Build the list + $options = array(); + + foreach ($positions as $position) { + if (!$position && !$editPositions) { + $options[] = HTMLHelper::_('select.option', 'none', Text::_('COM_MODULES_NONE')); + } elseif (!$position) { + $options[] = HTMLHelper::_('select.option', '', Text::_('COM_MODULES_NONE')); + } else { + $options[] = HTMLHelper::_('select.option', $position, $position); + } + } + + return $options; + } + + /** + * Return a list of templates + * + * @param integer $clientId Client ID + * @param string $state State + * @param string $template Template name + * + * @return array List of templates + */ + public static function getTemplates($clientId = 0, $state = '', $template = '') + { + $db = Factory::getDbo(); + $clientId = (int) $clientId; + + // Get the database object and a new query object. + $query = $db->getQuery(true); + + // Build the query. + $query->select($db->quoteName(['element', 'name', 'enabled'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->where($db->quoteName('type') . ' = ' . $db->quote('template')); + + if ($state != '') { + $query->where($db->quoteName('enabled') . ' = :state') + ->bind(':state', $state); + } + + if ($template != '') { + $query->where($db->quoteName('element') . ' = :element') + ->bind(':element', $template); + } + + $query->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Set the query and load the templates. + $db->setQuery($query); + $templates = $db->loadObjectList('element'); + + return $templates; + } + + /** + * Get a list of the unique modules installed in the client application. + * + * @param int $clientId The client id. + * + * @return array Array of unique modules + */ + public static function getModules($clientId) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('element AS value, name AS text') + ->from('#__extensions as e') + ->where('e.client_id = ' . (int) $clientId) + ->where('type = ' . $db->quote('module')) + ->join('LEFT', '#__modules as m ON m.module=e.element AND m.client_id=e.client_id') + ->where('m.module IS NOT NULL') + ->group('element,name'); + + $db->setQuery($query); + $modules = $db->loadObjectList(); + $lang = Factory::getLanguage(); + + foreach ($modules as $i => $module) { + $extension = $module->value; + $path = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + $source = $path . "/modules/$extension"; + $lang->load("$extension.sys", $path) + || $lang->load("$extension.sys", $source); + $modules[$i]->text = Text::_($module->text); + } + + $modules = ArrayHelper::sortObjects($modules, 'text', 1, true, true); + + return $modules; + } + + /** + * Get a list of the assignment options for modules to menus. + * + * @param int $clientId The client id. + * + * @return array + */ + public static function getAssignmentOptions($clientId) + { + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', 'COM_MODULES_OPTION_MENU_ALL'); + $options[] = HTMLHelper::_('select.option', '-', 'COM_MODULES_OPTION_MENU_NONE'); + + if ($clientId == 0) { + $options[] = HTMLHelper::_('select.option', '1', 'COM_MODULES_OPTION_MENU_INCLUDE'); + $options[] = HTMLHelper::_('select.option', '-1', 'COM_MODULES_OPTION_MENU_EXCLUDE'); + } + + return $options; + } + + /** + * Return a translated module position name + * + * @param integer $clientId Application client id 0: site | 1: admin + * @param string $template Template name + * @param string $position Position name + * + * @return string Return a translated position name + * + * @since 3.0 + */ + public static function getTranslatedModulePosition($clientId, $template, $position) + { + // Template translation + $lang = Factory::getLanguage(); + $path = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + + $loaded = $lang->getPaths('tpl_' . $template . '.sys'); + + // Only load the template's language file if it hasn't been already + if (!$loaded) { + $lang->load('tpl_' . $template . '.sys', $path, null, false, false) + || $lang->load('tpl_' . $template . '.sys', $path . '/templates/' . $template, null, false, false) + || $lang->load('tpl_' . $template . '.sys', $path, $lang->getDefault(), false, false) + || $lang->load('tpl_' . $template . '.sys', $path . '/templates/' . $template, $lang->getDefault(), false, false); + } + + $langKey = strtoupper('TPL_' . $template . '_POSITION_' . $position); + $text = Text::_($langKey); + + // Avoid untranslated strings + if (!self::isTranslatedText($langKey, $text)) { + // Modules component translation + $langKey = strtoupper('COM_MODULES_POSITION_' . $position); + $text = Text::_($langKey); + + // Avoid untranslated strings + if (!self::isTranslatedText($langKey, $text)) { + // Try to humanize the position name + $text = ucfirst(preg_replace('/^' . $template . '\-/', '', $position)); + $text = ucwords(str_replace(array('-', '_'), ' ', $text)); + } + } + + return $text; + } + + /** + * Check if the string was translated + * + * @param string $langKey Language file text key + * @param string $text The "translated" text to be checked + * + * @return boolean Return true for translated text + * + * @since 3.0 + */ + public static function isTranslatedText($langKey, $text) + { + return $text !== $langKey; + } + + /** + * Create and return a new Option + * + * @param string $value The option value [optional] + * @param string $text The option text [optional] + * + * @return object The option as an object (\stdClass instance) + * + * @since 3.0 + */ + public static function createOption($value = '', $text = '') + { + if (empty($text)) { + $text = $value; + } + + $option = new \stdClass(); + $option->value = $value; + $option->text = $text; + + return $option; + } + + /** + * Create and return a new Option Group + * + * @param string $label Value and label for group [optional] + * @param array $options Array of options to insert into group [optional] + * + * @return array Return the new group as an array + * + * @since 3.0 + */ + public static function createOptionGroup($label = '', $options = array()) + { + $group = array(); + $group['value'] = $label; + $group['text'] = $label; + $group['items'] = $options; + + return $group; + } } diff --git a/code/administrator/components/com_modules/src/Model/ModuleModel.php b/code/administrator/components/com_modules/src/Model/ModuleModel.php index 59e67784..7bfbccae 100644 --- a/code/administrator/components/com_modules/src/Model/ModuleModel.php +++ b/code/administrator/components/com_modules/src/Model/ModuleModel.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage', - ); - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - */ - public function __construct($config = array()) - { - $config = array_merge( - array( - 'event_after_delete' => 'onExtensionAfterDelete', - 'event_after_save' => 'onExtensionAfterSave', - 'event_before_delete' => 'onExtensionBeforeDelete', - 'event_before_save' => 'onExtensionBeforeSave', - 'events_map' => array( - 'save' => 'extension', - 'delete' => 'extension' - ) - ), $config - ); - - parent::__construct($config); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - - if (!$pk) - { - if ($extensionId = (int) $app->getUserState('com_modules.add.module.extension_id')) - { - $this->setState('extension.id', $extensionId); - } - } - - $this->setState('module.id', $pk); - - // Load the parameters. - $params = ComponentHelper::getParams('com_modules'); - $this->setState('params', $params); - } - - /** - * Batch copy modules to a new position or current. - * - * @param integer $value The new value matching a module position. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - protected function batchCopy($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - $table = $this->getTable(); - $newIds = array(); - - foreach ($pks as $pk) - { - if ($user->authorise('core.create', 'com_modules')) - { - $table->reset(); - $table->load($pk); - - // Set the new position - if ($value == 'noposition') - { - $position = ''; - } - elseif ($value == 'nochange') - { - $position = $table->position; - } - else - { - $position = $value; - } - - $table->position = $position; - - // Copy of the Asset ID - $oldAssetId = $table->asset_id; - - // Alter the title if necessary - $data = $this->generateNewTitle(0, $table->title, $table->position); - $table->title = $data['0']; - - // Reset the ID because we are making a copy - $table->id = 0; - - // Unpublish the new module - $table->published = 0; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Get the new item ID - $newId = $table->get('id'); - - // Add the new ID to the array - $newIds[$pk] = $newId; - - // Now we need to handle the module assignments - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('menuid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $pk, ParameterType::INTEGER); - $db->setQuery($query); - $menus = $db->loadColumn(); - - // Insert the new records into the table - foreach ($menus as $i => $menu) - { - $query->clear() - ->insert($db->quoteName('#__modules_menu')) - ->columns($db->quoteName(['moduleid', 'menuid'])) - ->values(implode(', ', [':newid' . $i, ':menu' . $i])) - ->bind(':newid' . $i, $newId, ParameterType::INTEGER) - ->bind(':menu' . $i, $menu, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } - - // Copy rules - $query->clear() - ->update($db->quoteName('#__assets', 't')) - ->join('INNER', $db->quoteName('#__assets', 's') . - ' ON ' . $db->quoteName('s.id') . ' = ' . $oldAssetId - ) - ->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules')) - ->where($db->quoteName('t.id') . ' = ' . $table->asset_id); - - $db->setQuery($query)->execute(); - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return $newIds; - } - - /** - * Batch move modules to a new position or current. - * - * @param integer $value The new value matching a module position. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - protected function batchMove($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - $table = $this->getTable(); - - foreach ($pks as $pk) - { - if ($user->authorise('core.edit', 'com_modules')) - { - $table->reset(); - $table->load($pk); - - // Set the new position - if ($value == 'noposition') - { - $position = ''; - } - elseif ($value == 'nochange') - { - $position = $table->position; - } - else - { - $position = $value; - } - - $table->position = $position; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can have its state edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 3.2 - */ - protected function canEditState($record) - { - // Check for existing module. - if (!empty($record->id)) - { - return Factory::getUser()->authorise('core.edit.state', 'com_modules.module.' . (int) $record->id); - } - - // Default to component settings if module not known. - return parent::canEditState($record); - } - - /** - * Method to delete rows. - * - * @param array &$pks An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function delete(&$pks) - { - $app = Factory::getApplication(); - $pks = (array) $pks; - $user = Factory::getUser(); - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - // Include the plugins for the on delete events. - PluginHelper::importPlugin($this->events_map['delete']); - - // Iterate the items to delete each one. - foreach ($pks as $pk) - { - if ($table->load($pk)) - { - // Access checks. - if (!$user->authorise('core.delete', 'com_modules.module.' . (int) $pk) || $table->published != -2) - { - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - - return; - } - - // Trigger the before delete event. - $result = $app->triggerEvent($this->event_before_delete, array($context, $table)); - - if (in_array(false, $result, true) || !$table->delete($pk)) - { - throw new \Exception($table->getError()); - } - else - { - // Delete the menu assignments - $pk = (int) $pk; - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $pk, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Trigger the after delete event. - $app->triggerEvent($this->event_after_delete, array($context, $table)); - } - - // Clear module cache - parent::cleanCache($table->module); - } - else - { - throw new \Exception($table->getError()); - } - } - - // Clear modules cache - $this->cleanCache(); - - return true; - } - - /** - * Method to duplicate modules. - * - * @param array &$pks An array of primary key IDs. - * - * @return boolean Boolean true on success - * - * @since 1.6 - * @throws \Exception - */ - public function duplicate(&$pks) - { - $user = Factory::getUser(); - $db = $this->getDbo(); - - // Access checks. - if (!$user->authorise('core.create', 'com_modules')) - { - throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED')); - } - - $table = $this->getTable(); - - foreach ($pks as $pk) - { - if ($table->load($pk, true)) - { - // Reset the id to create a new record. - $table->id = 0; - - // Alter the title. - $m = null; - - if (preg_match('#\((\d+)\)$#', $table->title, $m)) - { - $table->title = preg_replace('#\(\d+\)$#', '(' . ($m[1] + 1) . ')', $table->title); - } - - $data = $this->generateNewTitle(0, $table->title, $table->position); - $table->title = $data[0]; - - // Unpublish duplicate module - $table->published = 0; - - if (!$table->check() || !$table->store()) - { - throw new \Exception($table->getError()); - } - - $pk = (int) $pk; - $query = $db->getQuery(true) - ->select($db->quoteName('menuid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $pk, ParameterType::INTEGER); - - $db->setQuery($query); - $rows = $db->loadColumn(); - - foreach ($rows as $menuid) - { - $tuples[] = (int) $table->id . ',' . (int) $menuid; - } - } - else - { - throw new \Exception($table->getError()); - } - } - - if (!empty($tuples)) - { - // Module-Menu Mapping: Do it in one query - $query = $db->getQuery(true) - ->insert($db->quoteName('#__modules_menu')) - ->columns($db->quoteName(array('moduleid', 'menuid'))) - ->values($tuples); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - - // Clear modules cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the title. - * - * @param integer $categoryId The id of the category. Not used here. - * @param string $title The title. - * @param string $position The position. - * - * @return array Contains the modified title. - * - * @since 2.5 - */ - protected function generateNewTitle($categoryId, $title, $position) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('position' => $position, 'title' => $title))) - { - $title = StringHelper::increment($title); - } - - return array($title); - } - - /** - * Method to get the client object - * - * @return void - * - * @since 1.6 - */ - public function &getClient() - { - return $this->_client; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // The folder and element vars are passed when saving the form. - if (empty($data)) - { - $item = $this->getItem(); - $clientId = $item->client_id; - $module = $item->module; - $id = $item->id; - } - else - { - $clientId = ArrayHelper::getValue($data, 'client_id'); - $module = ArrayHelper::getValue($data, 'module'); - $id = ArrayHelper::getValue($data, 'id'); - } - - // Add the default fields directory - $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - Form::addFieldPath($baseFolder . '/modules/' . $module . '/field'); - - // These variables are used to add data from the plugin XML files. - $this->setState('item.client_id', $clientId); - $this->setState('item.module', $module); - - // Get the form. - if ($clientId == 1) - { - $form = $this->loadForm('com_modules.module.admin', 'moduleadmin', array('control' => 'jform', 'load_data' => $loadData), true); - - // Display language field to filter admin custom menus per language - if (!ModuleHelper::isAdminMultilang()) - { - $form->setFieldAttribute('language', 'type', 'hidden'); - } - } - else - { - $form = $this->loadForm('com_modules.module', 'module', array('control' => 'jform', 'load_data' => $loadData), true); - } - - if (empty($form)) - { - return false; - } - - $user = Factory::getUser(); - - /** - * Check for existing module - * Modify the form based on Edit State access controls. - */ - if ($id != 0 && (!$user->authorise('core.edit.state', 'com_modules.module.' . (int) $id)) - || ($id == 0 && !$user->authorise('core.edit.state', 'com_modules')) ) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('publish_up', 'disabled', 'true'); - $form->setFieldAttribute('publish_down', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - $form->setFieldAttribute('publish_up', 'filter', 'unset'); - $form->setFieldAttribute('publish_down', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $app = Factory::getApplication(); - - // Check the session for previously entered form data. - $data = $app->getUserState('com_modules.edit.module.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Module Position, Language, Access Level) in edit form if those have been selected in Module Manager - if (!$data->id) - { - $clientId = $app->input->getInt('client_id', 0); - $filters = (array) $app->getUserState('com_modules.modules.' . $clientId . '.filter'); - $data->set('published', $app->input->getInt('published', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null))); - $data->set('position', $app->input->getInt('position', (!empty($filters['position']) ? $filters['position'] : null))); - $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); - $data->set('access', $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access')))); - } - - // Avoid to delete params of a second module opened in a new browser tab while new one is not saved yet. - if (empty($data->params)) - { - // This allows us to inject parameter settings into a new module. - $params = $app->getUserState('com_modules.add.module.params'); - - if (is_array($params)) - { - $data->set('params', $params); - } - } - } - - $this->preprocessData('com_modules.module', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? (int) $pk : (int) $this->getState('module.id'); - $db = $this->getDbo(); - - if (!isset($this->_cache[$pk])) - { - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($pk); - - // Check for a table object error. - if ($return === false && $error = $table->getError()) - { - $this->setError($error); - - return false; - } - - // Check if we are creating a new extension. - if (empty($pk)) - { - if ($extensionId = (int) $this->getState('extension.id')) - { - $query = $db->getQuery(true) - ->select($db->quoteName(['element', 'client_id'])) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('extension_id') . ' = :extensionid') - ->where($db->quoteName('type') . ' = ' . $db->quote('module')) - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $extension = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (empty($extension)) - { - $this->setError('COM_MODULES_ERROR_CANNOT_FIND_MODULE'); - - return false; - } - - // Extension found, prime some module values. - $table->module = $extension->element; - $table->client_id = $extension->client_id; - } - else - { - Factory::getApplication()->redirect(Route::_('index.php?option=com_modules&view=modules', false)); - - return false; - } - } - - // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. - $properties = $table->getProperties(1); - $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); - - // Convert the params field to an array. - $registry = new Registry($table->params); - $this->_cache[$pk]->params = $registry->toArray(); - - // Determine the page assignment mode. - $query = $db->getQuery(true) - ->select($db->quoteName('menuid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $pk, ParameterType::INTEGER); - $db->setQuery($query); - $assigned = $db->loadColumn(); - - if (empty($pk)) - { - // If this is a new module, assign to all pages. - $assignment = 0; - } - elseif (empty($assigned)) - { - // For an existing module it is assigned to none. - $assignment = '-'; - } - else - { - if ($assigned[0] > 0) - { - $assignment = 1; - } - elseif ($assigned[0] < 0) - { - $assignment = -1; - } - else - { - $assignment = 0; - } - } - - $this->_cache[$pk]->assigned = $assigned; - $this->_cache[$pk]->assignment = $assignment; - - // Get the module XML. - $client = ApplicationHelper::getClientInfo($table->client_id); - $path = Path::clean($client->path . '/modules/' . $table->module . '/' . $table->module . '.xml'); - - if (file_exists($path)) - { - $this->_cache[$pk]->xml = simplexml_load_file($path); - } - else - { - $this->_cache[$pk]->xml = null; - } - } - - return $this->_cache[$pk]; - } - - /** - * Get the necessary data to load an item help screen. - * - * @return object An object with key, url, and local properties for loading the item help screen. - * - * @since 1.6 - */ - public function getHelp() - { - return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); - } - - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'Module', $prefix = 'JTable', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param Table $table The database object - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - $table->title = htmlspecialchars_decode($table->title, ENT_QUOTES); - $table->position = trim($table->position); - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $lang = Factory::getLanguage(); - $clientId = $this->getState('item.client_id'); - $module = $this->getState('item.module'); - - $client = ApplicationHelper::getClientInfo($clientId); - $formFile = Path::clean($client->path . '/modules/' . $module . '/' . $module . '.xml'); - - // Load the core and/or local language file(s). - $lang->load($module, $client->path) - || $lang->load($module, $client->path . '/modules/' . $module); - - if (file_exists($formFile)) - { - // Get the module form. - if (!$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/extension/help'); - - if (!empty($help)) - { - $helpKey = trim((string) $help[0]['key']); - $helpURL = trim((string) $help[0]['url']); - - $this->helpKey = $helpKey ?: $this->helpKey; - $this->helpURL = $helpURL ?: $this->helpURL; - } - } - - // Load the default advanced params - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/models/forms'); - $form->loadFile('advanced', false); - - // Load chrome specific params for global files - $chromePath = JPATH_SITE . '/layouts/chromes'; - $chromeFormFiles = Folder::files($chromePath, '.*\.xml'); - - if ($chromeFormFiles) - { - Form::addFormPath($chromePath); - - foreach ($chromeFormFiles as $formFile) - { - $form->loadFile(basename($formFile, '.xml'), false); - } - } - - // Load chrome specific params for template files - $templates = ModulesHelper::getTemplates($clientId); - - foreach ($templates as $template) - { - $chromePath = $client->path . '/templates/' . $template->element . '/html/layouts/chromes'; - - // Skip if there is no chrome folder in that template. - if (!is_dir($chromePath)) - { - continue; - } - - $chromeFormFiles = Folder::files($chromePath, '.*\.xml'); - - if ($chromeFormFiles) - { - Form::addFormPath($chromePath); - - foreach ($chromeFormFiles as $formFile) - { - $form->loadFile(basename($formFile, '.xml'), false); - } - } - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Loads ContentHelper for filters before validating data. - * - * @param object $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the group(defaults to null). - * - * @return mixed Array of filtered data if valid, false otherwise. - * - * @since 1.1 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_modules')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $input = Factory::getApplication()->input; - $table = $this->getTable(); - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('module.id'); - $isNew = true; - $context = $this->option . '.' . $this->name; - - // Include the plugins for the save event. - PluginHelper::importPlugin($this->events_map['save']); - - // Load the row if saving an existing record. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - // Alter the title and published state for Save as Copy - if ($input->get('task') == 'save2copy') - { - $orig_table = clone $this->getTable(); - $orig_table->load((int) $input->getInt('id')); - $data['published'] = 0; - - if ($data['title'] == $orig_table->title) - { - $data['title'] = StringHelper::increment($data['title']); - } - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Prepare the row for saving - $this->prepareTable($table); - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Process the menu link mappings. - $assignment = $data['assignment'] ?? 0; - - $table->id = (int) $table->id; - - // Delete old module to menu item associations - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $table->id, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // If the assignment is numeric, then something is selected (otherwise it's none). - if (is_numeric($assignment)) - { - // Variable is numeric, but could be a string. - $assignment = (int) $assignment; - - // Logic check: if no module excluded then convert to display on all. - if ($assignment == -1 && empty($data['assigned'])) - { - $assignment = 0; - } - - // Check needed to stop a module being assigned to `All` - // and other menu items resulting in a module being displayed twice. - if ($assignment === 0) - { - // Assign new module to `all` menu item associations. - $query->clear() - ->insert($db->quoteName('#__modules_menu')) - ->columns($db->quoteName(['moduleid', 'menuid'])) - ->values(implode(', ', [':moduleid', 0])) - ->bind(':moduleid', $table->id, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - elseif (!empty($data['assigned'])) - { - // Get the sign of the number. - $sign = $assignment < 0 ? -1 : 1; - - $query->clear() - ->insert($db->quoteName('#__modules_menu')) - ->columns($db->quoteName(array('moduleid', 'menuid'))); - - foreach ($data['assigned'] as &$pk) - { - $query->values((int) $table->id . ',' . (int) $pk * $sign); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); - - // Compute the extension id of this module in case the controller wants it. - $query->clear() - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions', 'e')) - ->join( - 'LEFT', - $db->quoteName('#__modules', 'm') . ' ON ' . $db->quoteName('e.client_id') . ' = ' . (int) $table->client_id . - ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('m.module') - ) - ->where($db->quoteName('m.id') . ' = :id') - ->bind(':id', $table->id, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $extensionId = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - $this->setState('module.extension_id', $extensionId); - $this->setState('module.id', $table->id); - - // Clear modules cache - $this->cleanCache(); - - // Clean module cache - parent::cleanCache($table->module); - - return true; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('client_id') . ' = ' . (int) $table->client_id, - $this->_db->quoteName('position') . ' = ' . $this->_db->quote($table->position), - ]; - } - - /** - * Custom clean cache method for different clients - * - * @param string $group The name of the plugin group to import (defaults to null). - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_modules'); - } + /** + * The type alias for this content type. + * + * @var string + * @since 3.4 + */ + public $typeAlias = 'com_modules.module'; + + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_MODULES'; + + /** + * @var string The help screen key for the module. + * @since 1.6 + */ + protected $helpKey = ''; + + /** + * @var string The help screen base URL for the module. + * @since 1.6 + */ + protected $helpURL; + + /** + * Batch copy/move command. If set to false, + * the batch copy/move command is not supported + * + * @var string + */ + protected $batch_copymove = 'position_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage', + ); + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + */ + public function __construct($config = array()) + { + $config = array_merge( + array( + 'event_after_delete' => 'onExtensionAfterDelete', + 'event_after_save' => 'onExtensionAfterSave', + 'event_before_delete' => 'onExtensionBeforeDelete', + 'event_before_save' => 'onExtensionBeforeSave', + 'events_map' => array( + 'save' => 'extension', + 'delete' => 'extension' + ) + ), + $config + ); + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + + if (!$pk) { + if ($extensionId = (int) $app->getUserState('com_modules.add.module.extension_id')) { + $this->setState('extension.id', $extensionId); + } + } + + $this->setState('module.id', $pk); + + // Load the parameters. + $params = ComponentHelper::getParams('com_modules'); + $this->setState('params', $params); + } + + /** + * Batch copy modules to a new position or current. + * + * @param integer $value The new value matching a module position. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + protected function batchCopy($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + $table = $this->getTable(); + $newIds = array(); + + foreach ($pks as $pk) { + if ($user->authorise('core.create', 'com_modules')) { + $table->reset(); + $table->load($pk); + + // Set the new position + if ($value == 'noposition') { + $position = ''; + } elseif ($value == 'nochange') { + $position = $table->position; + } else { + $position = $value; + } + + $table->position = $position; + + // Copy of the Asset ID + $oldAssetId = $table->asset_id; + + // Alter the title if necessary + $data = $this->generateNewTitle(0, $table->title, $table->position); + $table->title = $data['0']; + + // Reset the ID because we are making a copy + $table->id = 0; + + // Unpublish the new module + $table->published = 0; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Get the new item ID + $newId = $table->get('id'); + + // Add the new ID to the array + $newIds[$pk] = $newId; + + // Now we need to handle the module assignments + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('menuid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $pk, ParameterType::INTEGER); + $db->setQuery($query); + $menus = $db->loadColumn(); + + // Insert the new records into the table + foreach ($menus as $i => $menu) { + $query->clear() + ->insert($db->quoteName('#__modules_menu')) + ->columns($db->quoteName(['moduleid', 'menuid'])) + ->values(implode(', ', [':newid' . $i, ':menu' . $i])) + ->bind(':newid' . $i, $newId, ParameterType::INTEGER) + ->bind(':menu' . $i, $menu, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + } + + // Copy rules + $query->clear() + ->update($db->quoteName('#__assets', 't')) + ->join('INNER', $db->quoteName('#__assets', 's') . + ' ON ' . $db->quoteName('s.id') . ' = ' . $oldAssetId) + ->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules')) + ->where($db->quoteName('t.id') . ' = ' . $table->asset_id); + + $db->setQuery($query)->execute(); + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return $newIds; + } + + /** + * Batch move modules to a new position or current. + * + * @param integer $value The new value matching a module position. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + protected function batchMove($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + $table = $this->getTable(); + + foreach ($pks as $pk) { + if ($user->authorise('core.edit', 'com_modules')) { + $table->reset(); + $table->load($pk); + + // Set the new position + if ($value == 'noposition') { + $position = ''; + } elseif ($value == 'nochange') { + $position = $table->position; + } else { + $position = $value; + } + + $table->position = $position; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can have its state edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 3.2 + */ + protected function canEditState($record) + { + // Check for existing module. + if (!empty($record->id)) { + return Factory::getUser()->authorise('core.edit.state', 'com_modules.module.' . (int) $record->id); + } + + // Default to component settings if module not known. + return parent::canEditState($record); + } + + /** + * Method to delete rows. + * + * @param array &$pks An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function delete(&$pks) + { + $app = Factory::getApplication(); + $pks = (array) $pks; + $user = Factory::getUser(); + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + // Include the plugins for the on delete events. + PluginHelper::importPlugin($this->events_map['delete']); + + // Iterate the items to delete each one. + foreach ($pks as $pk) { + if ($table->load($pk)) { + // Access checks. + if (!$user->authorise('core.delete', 'com_modules.module.' . (int) $pk) || $table->published != -2) { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + + return; + } + + // Trigger the before delete event. + $result = $app->triggerEvent($this->event_before_delete, array($context, $table)); + + if (in_array(false, $result, true) || !$table->delete($pk)) { + throw new \Exception($table->getError()); + } else { + // Delete the menu assignments + $pk = (int) $pk; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $pk, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Trigger the after delete event. + $app->triggerEvent($this->event_after_delete, array($context, $table)); + } + + // Clear module cache + parent::cleanCache($table->module); + } else { + throw new \Exception($table->getError()); + } + } + + // Clear modules cache + $this->cleanCache(); + + return true; + } + + /** + * Method to duplicate modules. + * + * @param array &$pks An array of primary key IDs. + * + * @return boolean Boolean true on success + * + * @since 1.6 + * @throws \Exception + */ + public function duplicate(&$pks) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Access checks. + if (!$user->authorise('core.create', 'com_modules')) { + throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED')); + } + + $table = $this->getTable(); + + foreach ($pks as $pk) { + if ($table->load($pk, true)) { + // Reset the id to create a new record. + $table->id = 0; + + // Alter the title. + $m = null; + + if (preg_match('#\((\d+)\)$#', $table->title, $m)) { + $table->title = preg_replace('#\(\d+\)$#', '(' . ($m[1] + 1) . ')', $table->title); + } + + $data = $this->generateNewTitle(0, $table->title, $table->position); + $table->title = $data[0]; + + // Unpublish duplicate module + $table->published = 0; + + if (!$table->check() || !$table->store()) { + throw new \Exception($table->getError()); + } + + $pk = (int) $pk; + $query = $db->getQuery(true) + ->select($db->quoteName('menuid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $pk, ParameterType::INTEGER); + + $db->setQuery($query); + $rows = $db->loadColumn(); + + foreach ($rows as $menuid) { + $tuples[] = (int) $table->id . ',' . (int) $menuid; + } + } else { + throw new \Exception($table->getError()); + } + } + + if (!empty($tuples)) { + // Module-Menu Mapping: Do it in one query + $query = $db->getQuery(true) + ->insert($db->quoteName('#__modules_menu')) + ->columns($db->quoteName(array('moduleid', 'menuid'))) + ->values($tuples); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } + + // Clear modules cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the title. + * + * @param integer $categoryId The id of the category. Not used here. + * @param string $title The title. + * @param string $position The position. + * + * @return array Contains the modified title. + * + * @since 2.5 + */ + protected function generateNewTitle($categoryId, $title, $position) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('position' => $position, 'title' => $title))) { + $title = StringHelper::increment($title); + } + + return array($title); + } + + /** + * Method to get the client object + * + * @return void + * + * @since 1.6 + */ + public function &getClient() + { + return $this->_client; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // The folder and element vars are passed when saving the form. + if (empty($data)) { + $item = $this->getItem(); + $clientId = $item->client_id; + $module = $item->module; + $id = $item->id; + } else { + $clientId = ArrayHelper::getValue($data, 'client_id'); + $module = ArrayHelper::getValue($data, 'module'); + $id = ArrayHelper::getValue($data, 'id'); + } + + // Add the default fields directory + $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + Form::addFieldPath($baseFolder . '/modules/' . $module . '/field'); + + // These variables are used to add data from the plugin XML files. + $this->setState('item.client_id', $clientId); + $this->setState('item.module', $module); + + // Get the form. + if ($clientId == 1) { + $form = $this->loadForm('com_modules.module.admin', 'moduleadmin', array('control' => 'jform', 'load_data' => $loadData), true); + + // Display language field to filter admin custom menus per language + if (!ModuleHelper::isAdminMultilang()) { + $form->setFieldAttribute('language', 'type', 'hidden'); + } + } else { + $form = $this->loadForm('com_modules.module', 'module', array('control' => 'jform', 'load_data' => $loadData), true); + } + + if (empty($form)) { + return false; + } + + $user = Factory::getUser(); + + /** + * Check for existing module + * Modify the form based on Edit State access controls. + */ + if ( + $id != 0 && (!$user->authorise('core.edit.state', 'com_modules.module.' . (int) $id)) + || ($id == 0 && !$user->authorise('core.edit.state', 'com_modules')) + ) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $app = Factory::getApplication(); + + // Check the session for previously entered form data. + $data = $app->getUserState('com_modules.edit.module.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Module Position, Language, Access Level) in edit form if those have been selected in Module Manager + if (!$data->id) { + $clientId = $app->input->getInt('client_id', 0); + $filters = (array) $app->getUserState('com_modules.modules.' . $clientId . '.filter'); + $data->set('published', $app->input->getInt('published', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null))); + $data->set('position', $app->input->getInt('position', (!empty($filters['position']) ? $filters['position'] : null))); + $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); + $data->set('access', $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access')))); + } + + // Avoid to delete params of a second module opened in a new browser tab while new one is not saved yet. + if (empty($data->params)) { + // This allows us to inject parameter settings into a new module. + $params = $app->getUserState('com_modules.add.module.params'); + + if (is_array($params)) { + $data->set('params', $params); + } + } + } + + $this->preprocessData('com_modules.module', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? (int) $pk : (int) $this->getState('module.id'); + $db = $this->getDatabase(); + + if (!isset($this->_cache[$pk])) { + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($pk); + + // Check for a table object error. + if ($return === false && $error = $table->getError()) { + $this->setError($error); + + return false; + } + + // Check if we are creating a new extension. + if (empty($pk)) { + if ($extensionId = (int) $this->getState('extension.id')) { + $query = $db->getQuery(true) + ->select($db->quoteName(['element', 'client_id'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('extension_id') . ' = :extensionid') + ->where($db->quoteName('type') . ' = ' . $db->quote('module')) + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $extension = $db->loadObject(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (empty($extension)) { + $this->setError('COM_MODULES_ERROR_CANNOT_FIND_MODULE'); + + return false; + } + + // Extension found, prime some module values. + $table->module = $extension->element; + $table->client_id = $extension->client_id; + } else { + Factory::getApplication()->redirect(Route::_('index.php?option=com_modules&view=modules', false)); + + return false; + } + } + + // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. + $properties = $table->getProperties(1); + $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); + + // Convert the params field to an array. + $registry = new Registry($table->params); + $this->_cache[$pk]->params = $registry->toArray(); + + // Determine the page assignment mode. + $query = $db->getQuery(true) + ->select($db->quoteName('menuid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $pk, ParameterType::INTEGER); + $db->setQuery($query); + $assigned = $db->loadColumn(); + + if (empty($pk)) { + // If this is a new module, assign to all pages. + $assignment = 0; + } elseif (empty($assigned)) { + // For an existing module it is assigned to none. + $assignment = '-'; + } else { + if ($assigned[0] > 0) { + $assignment = 1; + } elseif ($assigned[0] < 0) { + $assignment = -1; + } else { + $assignment = 0; + } + } + + $this->_cache[$pk]->assigned = $assigned; + $this->_cache[$pk]->assignment = $assignment; + + // Get the module XML. + $client = ApplicationHelper::getClientInfo($table->client_id); + $path = Path::clean($client->path . '/modules/' . $table->module . '/' . $table->module . '.xml'); + + if (file_exists($path)) { + $this->_cache[$pk]->xml = simplexml_load_file($path); + } else { + $this->_cache[$pk]->xml = null; + } + } + + return $this->_cache[$pk]; + } + + /** + * Get the necessary data to load an item help screen. + * + * @return object An object with key, url, and local properties for loading the item help screen. + * + * @since 1.6 + */ + public function getHelp() + { + return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); + } + + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'Module', $prefix = 'JTable', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param Table $table The database object + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + $table->title = htmlspecialchars_decode($table->title, ENT_QUOTES); + $table->position = trim($table->position); + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $lang = Factory::getLanguage(); + $clientId = $this->getState('item.client_id'); + $module = $this->getState('item.module'); + + $client = ApplicationHelper::getClientInfo($clientId); + $formFile = Path::clean($client->path . '/modules/' . $module . '/' . $module . '.xml'); + + // Load the core and/or local language file(s). + $lang->load($module, $client->path) + || $lang->load($module, $client->path . '/modules/' . $module); + + if (file_exists($formFile)) { + // Get the module form. + if (!$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/extension/help'); + + if (!empty($help)) { + $helpKey = trim((string) $help[0]['key']); + $helpURL = trim((string) $help[0]['url']); + + $this->helpKey = $helpKey ?: $this->helpKey; + $this->helpURL = $helpURL ?: $this->helpURL; + } + } + + // Load the default advanced params + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/models/forms'); + $form->loadFile('advanced', false); + + // Load chrome specific params for global files + $chromePath = JPATH_SITE . '/layouts/chromes'; + $chromeFormFiles = Folder::files($chromePath, '.*\.xml'); + + if ($chromeFormFiles) { + Form::addFormPath($chromePath); + + foreach ($chromeFormFiles as $formFile) { + $form->loadFile(basename($formFile, '.xml'), false); + } + } + + // Load chrome specific params for template files + $templates = ModulesHelper::getTemplates($clientId); + + foreach ($templates as $template) { + $chromePath = $client->path . '/templates/' . $template->element . '/html/layouts/chromes'; + + // Skip if there is no chrome folder in that template. + if (!is_dir($chromePath)) { + continue; + } + + $chromeFormFiles = Folder::files($chromePath, '.*\.xml'); + + if ($chromeFormFiles) { + Form::addFormPath($chromePath); + + foreach ($chromeFormFiles as $formFile) { + $form->loadFile(basename($formFile, '.xml'), false); + } + } + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Loads ContentHelper for filters before validating data. + * + * @param object $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the group(defaults to null). + * + * @return mixed Array of filtered data if valid, false otherwise. + * + * @since 1.1 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_modules')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $input = Factory::getApplication()->input; + $table = $this->getTable(); + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('module.id'); + $isNew = true; + $context = $this->option . '.' . $this->name; + + // Include the plugins for the save event. + PluginHelper::importPlugin($this->events_map['save']); + + // Load the row if saving an existing record. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + // Alter the title and published state for Save as Copy + if ($input->get('task') == 'save2copy') { + $orig_table = clone $this->getTable(); + $orig_table->load((int) $input->getInt('id')); + $data['published'] = 0; + + if ($data['title'] == $orig_table->title) { + $data['title'] = StringHelper::increment($data['title']); + } + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Process the menu link mappings. + $assignment = $data['assignment'] ?? 0; + + $table->id = (int) $table->id; + + // Delete old module to menu item associations + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $table->id, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // If the assignment is numeric, then something is selected (otherwise it's none). + if (is_numeric($assignment)) { + // Variable is numeric, but could be a string. + $assignment = (int) $assignment; + + // Logic check: if no module excluded then convert to display on all. + if ($assignment == -1 && empty($data['assigned'])) { + $assignment = 0; + } + + // Check needed to stop a module being assigned to `All` + // and other menu items resulting in a module being displayed twice. + if ($assignment === 0) { + // Assign new module to `all` menu item associations. + $query->clear() + ->insert($db->quoteName('#__modules_menu')) + ->columns($db->quoteName(['moduleid', 'menuid'])) + ->values(implode(', ', [':moduleid', 0])) + ->bind(':moduleid', $table->id, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } elseif (!empty($data['assigned'])) { + // Get the sign of the number. + $sign = $assignment < 0 ? -1 : 1; + + $query->clear() + ->insert($db->quoteName('#__modules_menu')) + ->columns($db->quoteName(array('moduleid', 'menuid'))); + + foreach ($data['assigned'] as &$pk) { + $query->values((int) $table->id . ',' . (int) $pk * $sign); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); + + // Compute the extension id of this module in case the controller wants it. + $query->clear() + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions', 'e')) + ->join( + 'LEFT', + $db->quoteName('#__modules', 'm') . ' ON ' . $db->quoteName('e.client_id') . ' = ' . (int) $table->client_id . + ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('m.module') + ) + ->where($db->quoteName('m.id') . ' = :id') + ->bind(':id', $table->id, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $extensionId = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + $this->setState('module.extension_id', $extensionId); + $this->setState('module.id', $table->id); + + // Clear modules cache + $this->cleanCache(); + + // Clean module cache + parent::cleanCache($table->module); + + return true; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('client_id') . ' = ' . (int) $table->client_id, + $db->quoteName('position') . ' = ' . $db->quote($table->position), + ]; + } + + /** + * Custom clean cache method for different clients + * + * @param string $group The name of the plugin group to import (defaults to null). + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_modules'); + } } diff --git a/code/administrator/components/com_modules/src/Model/ModulesModel.php b/code/administrator/components/com_modules/src/Model/ModulesModel.php index 6509d025..b0b71c22 100644 --- a/code/administrator/components/com_modules/src/Model/ModulesModel.php +++ b/code/administrator/components/com_modules/src/Model/ModulesModel.php @@ -1,4 +1,5 @@ input->get('layout', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout) - { - $this->context .= '.' . $layout; - } - - // Make context client aware - $this->context .= '.' . $app->input->get->getInt('client_id', 0); - - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.position', $this->getUserStateFromRequest($this->context . '.filter.position', 'filter_position', '', 'string')); - $this->setState('filter.module', $this->getUserStateFromRequest($this->context . '.filter.module', 'filter_module', '', 'string')); - $this->setState('filter.menuitem', $this->getUserStateFromRequest($this->context . '.filter.menuitem', 'filter_menuitem', '', 'cmd')); - $this->setState('filter.access', $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '', 'cmd')); - - // If in modal layout on the frontend, state and language are always forced. - if ($app->isClient('site') && $layout === 'modal') - { - $this->setState('filter.language', 'current'); - $this->setState('filter.state', 1); - } - // If in backend (modal or not) we get the same fields from the user request. - else - { - $this->setState('filter.language', $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string')); - } - - // Special case for the client id. - if ($app->isClient('site') || $layout === 'modal') - { - $this->setState('client_id', 0); - $clientId = 0; - } - else - { - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = (!in_array($clientId, array(0, 1))) ? 0 : $clientId; - $this->setState('client_id', $clientId); - } - - // Use a different filter file when client is administrator - if ($clientId == 1) - { - $this->filterFormName = 'filter_modulesadmin'; - } - - // Load the parameters. - $params = ComponentHelper::getParams('com_modules'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.position'); - $id .= ':' . $this->getState('filter.module'); - $id .= ':' . $this->getState('filter.menuitem'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.language'); - - return parent::getStoreId($id); - } - - /** - * Returns an object list - * - * @param DatabaseQuery $query The query - * @param int $limitstart Offset - * @param int $limit The number of records - * - * @return array - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $listOrder = $this->getState('list.ordering', 'a.position'); - $listDirn = $this->getState('list.direction', 'asc'); - - // If ordering by fields that need translate we need to sort the array of objects after translating them. - if (in_array($listOrder, array('pages', 'name'))) - { - // Fetch the results. - $this->_db->setQuery($query); - $result = $this->_db->loadObjectList(); - - // Translate the results. - $this->translate($result); - - // Sort the array of translated objects. - $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, true, true); - - // Process pagination. - $total = count($result); - $this->cache[$this->getStoreId('getTotal')] = $total; - - if ($total < $limitstart) - { - $limitstart = 0; - $this->setState('list.start', 0); - } - - return array_slice($result, $limitstart, $limit ?: null); - } - - // If ordering by fields that doesn't need translate just order the query. - if ($listOrder === 'a.ordering') - { - $query->order($this->_db->quoteName('a.position') . ' ASC') - ->order($this->_db->quoteName($listOrder) . ' ' . $this->_db->escape($listDirn)); - } - elseif ($listOrder === 'a.position') - { - $query->order($this->_db->quoteName($listOrder) . ' ' . $this->_db->escape($listDirn)) - ->order($this->_db->quoteName('a.ordering') . ' ASC'); - } - else - { - $query->order($this->_db->quoteName($listOrder) . ' ' . $this->_db->escape($listDirn)); - } - - // Process pagination. - $result = parent::_getList($query, $limitstart, $limit); - - // Translate the results. - $this->translate($result); - - return $result; - } - - /** - * Translate a list of objects - * - * @param array &$items The array of objects - * - * @return array The array of translated objects - */ - protected function translate(&$items) - { - $lang = Factory::getLanguage(); - $clientPath = $this->getState('client_id') ? JPATH_ADMINISTRATOR : JPATH_SITE; - - foreach ($items as $item) - { - $extension = $item->module; - $source = $clientPath . "/modules/$extension"; - $lang->load("$extension.sys", $clientPath) - || $lang->load("$extension.sys", $source); - $item->name = Text::_($item->name); - - if (is_null($item->pages)) - { - $item->pages = Text::_('JNONE'); - } - elseif ($item->pages < 0) - { - $item->pages = Text::_('COM_MODULES_ASSIGNED_VARIES_EXCEPT'); - } - elseif ($item->pages > 0) - { - $item->pages = Text::_('COM_MODULES_ASSIGNED_VARIES_ONLY'); - } - else - { - $item->pages = Text::_('JALL'); - } - } - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.title, a.note, a.position, a.module, a.language,' . - 'a.checked_out, a.checked_out_time, a.published AS published, e.enabled AS enabled, a.access, a.ordering, a.publish_up, a.publish_down' - ) - ); - - // From modules table. - $query->from($db->quoteName('#__modules', 'a')); - - // Join over the language - $query->select($db->quoteName('l.title', 'language_title')) - ->select($db->quoteName('l.image', 'language_image')) - ->join('LEFT', $db->quoteName('#__languages', 'l') . ' ON ' . $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join('LEFT', $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Join over the asset groups. - $query->select($db->quoteName('ag.title', 'access_level')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')); - - // Join over the module menus - $query->select('MIN(mm.menuid) AS pages') - ->join('LEFT', $db->quoteName('#__modules_menu', 'mm') . ' ON ' . $db->quoteName('mm.moduleid') . ' = ' . $db->quoteName('a.id')); - - // Join over the extensions - $query->select($db->quoteName('e.name', 'name')) - ->join('LEFT', $db->quoteName('#__extensions', 'e') . ' ON ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('a.module')); - - // Group (careful with PostgreSQL) - $query->group( - 'a.id, a.title, a.note, a.position, a.module, a.language, a.checked_out, ' - . 'a.checked_out_time, a.published, a.access, a.ordering, l.title, l.image, uc.name, ag.title, e.name, ' - . 'l.lang_code, uc.id, ag.id, mm.moduleid, e.element, a.publish_up, a.publish_down, e.enabled' - ); - - // Filter by client. - $clientId = (int) $this->getState('client_id'); - $query->where($db->quoteName('a.client_id') . ' = :aclientid') - ->where($db->quoteName('e.client_id') . ' = :eclientid') - ->bind(':aclientid', $clientId, ParameterType::INTEGER) - ->bind(':eclientid', $clientId, ParameterType::INTEGER); - - // Filter by current user access level. - $user = Factory::getUser(); - - // Get the current user for authorisation checks - if ($user->authorise('core.admin') !== true) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Filter by published state. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.published') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif ($state === '') - { - $query->whereIn($db->quoteName('a.published'), [0, 1]); - } - - // Filter by position. - if ($position = $this->getState('filter.position')) - { - $position = ($position === 'none') ? '' : $position; - $query->where($db->quoteName('a.position') . ' = :position') - ->bind(':position', $position); - } - - // Filter by module. - if ($module = $this->getState('filter.module')) - { - $query->where($db->quoteName('a.module') . ' = :module') - ->bind(':module', $module); - } - - // Filter by menuitem id (only for site client). - if ((int) $clientId === 0 && $menuItemId = $this->getState('filter.menuitem')) - { - // If user selected the modules not assigned to any page (menu item). - if ((int) $menuItemId === -1) - { - $query->having('MIN(' . $db->quoteName('mm.menuid') . ') IS NULL'); - } - // If user selected the modules assigned to some particular page (menu item). - else - { - // Modules in "All" pages. - $subQuery1 = $db->getQuery(true); - $subQuery1->select('MIN(' . $db->quoteName('menuid') . ')') - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = ' . $db->quoteName('a.id')); - - // Modules in "Selected" pages that have the chosen menu item id. - $menuItemId = (int) $menuItemId; - $minusMenuItemId = $menuItemId * -1; - $subQuery2 = $db->getQuery(true); - $subQuery2->select($db->quoteName('moduleid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('menuid') . ' = :menuitemid2'); - - // Modules in "All except selected" pages that doesn't have the chosen menu item id. - $subQuery3 = $db->getQuery(true); - $subQuery3->select($db->quoteName('moduleid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('menuid') . ' = :menuitemid3'); - - // Filter by modules assigned to the selected menu item. - $query->where('( + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see \JController + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'published', 'a.published', 'state', + 'access', 'a.access', + 'ag.title', 'access_level', + 'ordering', 'a.ordering', + 'module', 'a.module', + 'language', 'a.language', + 'l.title', 'language_title', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'client_id', 'a.client_id', + 'position', 'a.position', + 'pages', + 'name', 'e.name', + 'menuitem', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.position', $direction = 'asc') + { + $app = Factory::getApplication(); + + $layout = $app->input->get('layout', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout) { + $this->context .= '.' . $layout; + } + + // Make context client aware + $this->context .= '.' . $app->input->get->getInt('client_id', 0); + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.position', $this->getUserStateFromRequest($this->context . '.filter.position', 'filter_position', '', 'string')); + $this->setState('filter.module', $this->getUserStateFromRequest($this->context . '.filter.module', 'filter_module', '', 'string')); + $this->setState('filter.menuitem', $this->getUserStateFromRequest($this->context . '.filter.menuitem', 'filter_menuitem', '', 'cmd')); + $this->setState('filter.access', $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '', 'cmd')); + + // If in modal layout on the frontend, state and language are always forced. + if ($app->isClient('site') && $layout === 'modal') { + $this->setState('filter.language', 'current'); + $this->setState('filter.state', 1); + } else { + // If in backend (modal or not) we get the same fields from the user request. + $this->setState('filter.language', $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string')); + } + + // Special case for the client id. + if ($app->isClient('site') || $layout === 'modal') { + $this->setState('client_id', 0); + $clientId = 0; + } else { + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = (!in_array($clientId, array(0, 1))) ? 0 : $clientId; + $this->setState('client_id', $clientId); + } + + // Use a different filter file when client is administrator + if ($clientId == 1) { + $this->filterFormName = 'filter_modulesadmin'; + } + + // Load the parameters. + $params = ComponentHelper::getParams('com_modules'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.position'); + $id .= ':' . $this->getState('filter.module'); + $id .= ':' . $this->getState('filter.menuitem'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.language'); + + return parent::getStoreId($id); + } + + /** + * Returns an object list + * + * @param DatabaseQuery $query The query + * @param int $limitstart Offset + * @param int $limit The number of records + * + * @return array + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $listOrder = $this->getState('list.ordering', 'a.position'); + $listDirn = $this->getState('list.direction', 'asc'); + + $db = $this->getDatabase(); + + // If ordering by fields that need translate we need to sort the array of objects after translating them. + if (in_array($listOrder, array('pages', 'name'))) { + // Fetch the results. + $db->setQuery($query); + $result = $db->loadObjectList(); + + // Translate the results. + $this->translate($result); + + // Sort the array of translated objects. + $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, true, true); + + // Process pagination. + $total = count($result); + $this->cache[$this->getStoreId('getTotal')] = $total; + + if ($total < $limitstart) { + $limitstart = 0; + $this->setState('list.start', 0); + } + + return array_slice($result, $limitstart, $limit ?: null); + } + + // If ordering by fields that doesn't need translate just order the query. + if ($listOrder === 'a.ordering') { + $query->order($db->quoteName('a.position') . ' ASC') + ->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + } elseif ($listOrder === 'a.position') { + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)) + ->order($db->quoteName('a.ordering') . ' ASC'); + } else { + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + } + + // Process pagination. + $result = parent::_getList($query, $limitstart, $limit); + + // Translate the results. + $this->translate($result); + + return $result; + } + + /** + * Translate a list of objects + * + * @param array &$items The array of objects + * + * @return array The array of translated objects + */ + protected function translate(&$items) + { + $lang = Factory::getLanguage(); + $clientPath = $this->getState('client_id') ? JPATH_ADMINISTRATOR : JPATH_SITE; + + foreach ($items as $item) { + $extension = $item->module; + $source = $clientPath . "/modules/$extension"; + $lang->load("$extension.sys", $clientPath) + || $lang->load("$extension.sys", $source); + $item->name = Text::_($item->name); + + if (is_null($item->pages)) { + $item->pages = Text::_('JNONE'); + } elseif ($item->pages < 0) { + $item->pages = Text::_('COM_MODULES_ASSIGNED_VARIES_EXCEPT'); + } elseif ($item->pages > 0) { + $item->pages = Text::_('COM_MODULES_ASSIGNED_VARIES_ONLY'); + } else { + $item->pages = Text::_('JALL'); + } + } + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.title, a.note, a.position, a.module, a.language,' . + 'a.checked_out, a.checked_out_time, a.published AS published, e.enabled AS enabled, a.access, a.ordering, a.publish_up, a.publish_down' + ) + ); + + // From modules table. + $query->from($db->quoteName('#__modules', 'a')); + + // Join over the language + $query->select($db->quoteName('l.title', 'language_title')) + ->select($db->quoteName('l.image', 'language_image')) + ->join('LEFT', $db->quoteName('#__languages', 'l') . ' ON ' . $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join('LEFT', $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Join over the asset groups. + $query->select($db->quoteName('ag.title', 'access_level')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')); + + // Join over the module menus + $query->select('MIN(mm.menuid) AS pages') + ->join('LEFT', $db->quoteName('#__modules_menu', 'mm') . ' ON ' . $db->quoteName('mm.moduleid') . ' = ' . $db->quoteName('a.id')); + + // Join over the extensions + $query->select($db->quoteName('e.name', 'name')) + ->join('LEFT', $db->quoteName('#__extensions', 'e') . ' ON ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('a.module')); + + // Group (careful with PostgreSQL) + $query->group( + 'a.id, a.title, a.note, a.position, a.module, a.language, a.checked_out, ' + . 'a.checked_out_time, a.published, a.access, a.ordering, l.title, l.image, uc.name, ag.title, e.name, ' + . 'l.lang_code, uc.id, ag.id, mm.moduleid, e.element, a.publish_up, a.publish_down, e.enabled' + ); + + // Filter by client. + $clientId = (int) $this->getState('client_id'); + $query->where($db->quoteName('a.client_id') . ' = :aclientid') + ->where($db->quoteName('e.client_id') . ' = :eclientid') + ->bind(':aclientid', $clientId, ParameterType::INTEGER) + ->bind(':eclientid', $clientId, ParameterType::INTEGER); + + // Filter by current user access level. + $user = Factory::getUser(); + + // Get the current user for authorisation checks + if ($user->authorise('core.admin') !== true) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Filter by published state. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.published') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif ($state === '') { + $query->whereIn($db->quoteName('a.published'), [0, 1]); + } + + // Filter by position. + if ($position = $this->getState('filter.position')) { + $position = ($position === 'none') ? '' : $position; + $query->where($db->quoteName('a.position') . ' = :position') + ->bind(':position', $position); + } + + // Filter by module. + if ($module = $this->getState('filter.module')) { + $query->where($db->quoteName('a.module') . ' = :module') + ->bind(':module', $module); + } + + // Filter by menuitem id (only for site client). + if ((int) $clientId === 0 && $menuItemId = $this->getState('filter.menuitem')) { + // If user selected the modules not assigned to any page (menu item). + if ((int) $menuItemId === -1) { + $query->having('MIN(' . $db->quoteName('mm.menuid') . ') IS NULL'); + } else { + // If user selected the modules assigned to some particular page (menu item). + // Modules in "All" pages. + $subQuery1 = $db->getQuery(true); + $subQuery1->select('MIN(' . $db->quoteName('menuid') . ')') + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = ' . $db->quoteName('a.id')); + + // Modules in "Selected" pages that have the chosen menu item id. + $menuItemId = (int) $menuItemId; + $minusMenuItemId = $menuItemId * -1; + $subQuery2 = $db->getQuery(true); + $subQuery2->select($db->quoteName('moduleid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('menuid') . ' = :menuitemid2'); + + // Modules in "All except selected" pages that doesn't have the chosen menu item id. + $subQuery3 = $db->getQuery(true); + $subQuery3->select($db->quoteName('moduleid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('menuid') . ' = :menuitemid3'); + + // Filter by modules assigned to the selected menu item. + $query->where('( (' . $subQuery1 . ') = 0 OR ((' . $subQuery1 . ') > 0 AND ' . $db->quoteName('a.id') . ' IN (' . $subQuery2 . ')) OR ((' . $subQuery1 . ') < 0 AND ' . $db->quoteName('a.id') . ' NOT IN (' . $subQuery3 . ')) - )' - ); - $query->bind(':menuitemid2', $menuItemId, ParameterType::INTEGER); - $query->bind(':menuitemid3', $minusMenuItemId, ParameterType::INTEGER); - } - } - - // Filter by search in title or note or id:. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . StringHelper::strtolower($search) . '%'; - $query->extendWhere( - 'AND', - [ - 'LOWER(' . $db->quoteName('a.title') . ') LIKE :title', - 'LOWER(' . $db->quoteName('a.note') . ') LIKE :note', - ], - 'OR' - ) - ->bind(':title', $search) - ->bind(':note', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - if ($language === 'current') - { - $language = [Factory::getLanguage()->getTag(), '*']; - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - else - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - } - - return $query; - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $clientId = (int) $this->getState('client_id'); - - $query->where($this->_db->quoteName('a.client_id') . ' = :client_id') - ->bind(':client_id', $clientId, ParameterType::INTEGER); - - return $query; - } + )'); + $query->bind(':menuitemid2', $menuItemId, ParameterType::INTEGER); + $query->bind(':menuitemid3', $minusMenuItemId, ParameterType::INTEGER); + } + } + + // Filter by search in title or note or id:. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . StringHelper::strtolower($search) . '%'; + $query->extendWhere( + 'AND', + [ + 'LOWER(' . $db->quoteName('a.title') . ') LIKE :title', + 'LOWER(' . $db->quoteName('a.note') . ') LIKE :note', + ], + 'OR' + ) + ->bind(':title', $search) + ->bind(':note', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + if ($language === 'current') { + $language = [Factory::getLanguage()->getTag(), '*']; + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } else { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + } + + return $query; + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $clientId = (int) $this->getState('client_id'); + + $query->where($this->getDatabase()->quoteName('a.client_id') . ' = :client_id') + ->bind(':client_id', $clientId, ParameterType::INTEGER); + + return $query; + } } diff --git a/code/administrator/components/com_modules/src/Model/PositionsModel.php b/code/administrator/components/com_modules/src/Model/PositionsModel.php index de4ecb06..5015c6cd 100644 --- a/code/administrator/components/com_modules/src/Model/PositionsModel.php +++ b/code/administrator/components/com_modules/src/Model/PositionsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); - $this->setState('filter.search', $search); - - $state = $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string'); - $this->setState('filter.state', $state); - - $template = $this->getUserStateFromRequest($this->context . '.filter.template', 'filter_template', '', 'string'); - $this->setState('filter.template', $template); - - $type = $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string'); - $this->setState('filter.type', $type); - - // Special case for the client id. - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = (!in_array((int) $clientId, array (0, 1))) ? 0 : (int) $clientId; - $this->setState('client_id', $clientId); - - // Load the parameters. - $params = ComponentHelper::getParams('com_modules'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getItems() - { - if (!isset($this->items)) - { - $lang = Factory::getLanguage(); - $search = $this->getState('filter.search'); - $state = $this->getState('filter.state'); - $clientId = $this->getState('client_id'); - $filter_template = $this->getState('filter.template'); - $type = $this->getState('filter.type'); - $ordering = $this->getState('list.ordering'); - $direction = $this->getState('list.direction'); - $limitstart = $this->getState('list.start'); - $limit = $this->getState('list.limit'); - $client = ApplicationHelper::getClientInfo($clientId); - - if ($type != 'template') - { - $clientId = (int) $clientId; - - // Get the database object and a new query object. - $query = $this->_db->getQuery(true) - ->select('DISTINCT ' . $this->_db->quoteName('position', 'value')) - ->from($this->_db->quoteName('#__modules')) - ->where($this->_db->quoteName('client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - if ($search) - { - $search = '%' . str_replace(' ', '%', trim($search), true) . '%'; - $query->where($this->_db->quoteName('position') . ' LIKE :position') - ->bind(':position', $search); - } - - $this->_db->setQuery($query); - - try - { - $positions = $this->_db->loadObjectList('value'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - foreach ($positions as $value => $position) - { - $positions[$value] = array(); - } - } - else - { - $positions = array(); - } - - // Load the positions from the installed templates. - foreach (ModulesHelper::getTemplates($clientId) as $template) - { - $path = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml'); - - if (file_exists($path)) - { - $xml = simplexml_load_file($path); - - if (isset($xml->positions[0])) - { - $lang->load('tpl_' . $template->element . '.sys', $client->path) - || $lang->load('tpl_' . $template->element . '.sys', $client->path . '/templates/' . $template->element); - - foreach ($xml->positions[0] as $position) - { - $value = (string) $position['value']; - $label = (string) $position; - - if (!$value) - { - $value = $label; - $label = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'TPL_' . $template->element . '_POSITION_' . $value); - $altlabel = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'COM_MODULES_POSITION_' . $value); - - if (!$lang->hasKey($label) && $lang->hasKey($altlabel)) - { - $label = $altlabel; - } - } - - if ($type == 'user' || ($state != '' && $state != $template->enabled)) - { - unset($positions[$value]); - } - elseif (preg_match(chr(1) . $search . chr(1) . 'i', $value) && ($filter_template == '' || $filter_template == $template->element)) - { - if (!isset($positions[$value])) - { - $positions[$value] = array(); - } - - $positions[$value][$template->name] = $label; - } - } - } - } - } - - $this->total = count($positions); - - if ($limitstart >= $this->total) - { - $limitstart = $limitstart < $limit ? 0 : $limitstart - $limit; - $this->setState('list.start', $limitstart); - } - - if ($ordering == 'value') - { - if ($direction == 'asc') - { - ksort($positions); - } - else - { - krsort($positions); - } - } - else - { - if ($direction == 'asc') - { - asort($positions); - } - else - { - arsort($positions); - } - } - - $this->items = array_slice($positions, $limitstart, $limit ?: null); - } - - return $this->items; - } - - /** - * Method to get the total number of items. - * - * @return integer The total number of items. - * - * @since 1.6 - */ - public function getTotal() - { - if (!isset($this->total)) - { - $this->getItems(); - } - - return $this->total; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see \JController + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'value', + 'templates', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'ordering', $direction = 'asc') + { + // Load the filter state. + $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); + $this->setState('filter.search', $search); + + $state = $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string'); + $this->setState('filter.state', $state); + + $template = $this->getUserStateFromRequest($this->context . '.filter.template', 'filter_template', '', 'string'); + $this->setState('filter.template', $template); + + $type = $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string'); + $this->setState('filter.type', $type); + + // Special case for the client id. + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = (!in_array((int) $clientId, array (0, 1))) ? 0 : (int) $clientId; + $this->setState('client_id', $clientId); + + // Load the parameters. + $params = ComponentHelper::getParams('com_modules'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getItems() + { + if (!isset($this->items)) { + $lang = Factory::getLanguage(); + $search = $this->getState('filter.search'); + $state = $this->getState('filter.state'); + $clientId = $this->getState('client_id'); + $filter_template = $this->getState('filter.template'); + $type = $this->getState('filter.type'); + $ordering = $this->getState('list.ordering'); + $direction = $this->getState('list.direction'); + $limitstart = $this->getState('list.start'); + $limit = $this->getState('list.limit'); + $client = ApplicationHelper::getClientInfo($clientId); + + if ($type != 'template') { + $clientId = (int) $clientId; + + // Get the database object and a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('position', 'value')) + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + if ($search) { + $search = '%' . str_replace(' ', '%', trim($search), true) . '%'; + $query->where($db->quoteName('position') . ' LIKE :position') + ->bind(':position', $search); + } + + $db->setQuery($query); + + try { + $positions = $db->loadObjectList('value'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + foreach ($positions as $value => $position) { + $positions[$value] = array(); + } + } else { + $positions = array(); + } + + // Load the positions from the installed templates. + foreach (ModulesHelper::getTemplates($clientId) as $template) { + $path = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml'); + + if (file_exists($path)) { + $xml = simplexml_load_file($path); + + if (isset($xml->positions[0])) { + $lang->load('tpl_' . $template->element . '.sys', $client->path) + || $lang->load('tpl_' . $template->element . '.sys', $client->path . '/templates/' . $template->element); + + foreach ($xml->positions[0] as $position) { + $value = (string) $position['value']; + $label = (string) $position; + + if (!$value) { + $value = $label; + $label = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'TPL_' . $template->element . '_POSITION_' . $value); + $altlabel = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'COM_MODULES_POSITION_' . $value); + + if (!$lang->hasKey($label) && $lang->hasKey($altlabel)) { + $label = $altlabel; + } + } + + if ($type == 'user' || ($state != '' && $state != $template->enabled)) { + unset($positions[$value]); + } elseif (preg_match(chr(1) . $search . chr(1) . 'i', $value) && ($filter_template == '' || $filter_template == $template->element)) { + if (!isset($positions[$value])) { + $positions[$value] = array(); + } + + $positions[$value][$template->name] = $label; + } + } + } + } + } + + $this->total = count($positions); + + if ($limitstart >= $this->total) { + $limitstart = $limitstart < $limit ? 0 : $limitstart - $limit; + $this->setState('list.start', $limitstart); + } + + if ($ordering == 'value') { + if ($direction == 'asc') { + ksort($positions); + } else { + krsort($positions); + } + } else { + if ($direction == 'asc') { + asort($positions); + } else { + arsort($positions); + } + } + + $this->items = array_slice($positions, $limitstart, $limit ?: null); + } + + return $this->items; + } + + /** + * Method to get the total number of items. + * + * @return integer The total number of items. + * + * @since 1.6 + */ + public function getTotal() + { + if (!isset($this->total)) { + $this->getItems(); + } + + return $this->total; + } } diff --git a/code/administrator/components/com_modules/src/Model/SelectModel.php b/code/administrator/components/com_modules/src/Model/SelectModel.php index 70257eae..25b7ff3c 100644 --- a/code/administrator/components/com_modules/src/Model/SelectModel.php +++ b/code/administrator/components/com_modules/src/Model/SelectModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest('com_modules.modules.client_id', 'client_id', 0); - $this->setState('client_id', (int) $clientId); - - // Load the parameters. - $params = ComponentHelper::getParams('com_modules'); - $this->setState('params', $params); - - // Manually set limits to get all modules. - $this->setState('list.limit', 0); - $this->setState('list.start', 0); - $this->setState('list.ordering', 'a.name'); - $this->setState('list.direction', 'ASC'); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.extension_id, a.name, a.element AS module' - ) - ); - $query->from($db->quoteName('#__extensions', 'a')); - - // Filter by module - $query->where($db->quoteName('a.type') . ' = ' . $db->quote('module')); - - // Filter by client. - $clientId = (int) $this->getState('client_id'); - $query->where($db->quoteName('a.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Filter by enabled - $query->where($db->quoteName('a.enabled') . ' = 1'); - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to get a list of items. - * - * @return mixed An array of objects on success, false on failure. - */ - public function getItems() - { - // Get the list of items from the database. - $items = parent::getItems(); - - $client = ApplicationHelper::getClientInfo($this->getState('client_id', 0)); - $lang = Factory::getLanguage(); - - // Loop through the results to add the XML metadata, - // and load language support. - foreach ($items as &$item) - { - $path = Path::clean($client->path . '/modules/' . $item->module . '/' . $item->module . '.xml'); - - if (file_exists($path)) - { - $item->xml = simplexml_load_file($path); - } - else - { - $item->xml = null; - } - - // 1.5 Format; Core files or language packs then - // 1.6 3PD Extension Support - $lang->load($item->module . '.sys', $client->path) - || $lang->load($item->module . '.sys', $client->path . '/modules/' . $item->module); - $item->name = Text::_($item->name); - - if (isset($item->xml) && $text = trim($item->xml->description)) - { - $item->desc = Text::_($text); - } - else - { - $item->desc = Text::_('COM_MODULES_NODESCRIPTION'); - } - } - - $items = ArrayHelper::sortObjects($items, 'name', 1, true, true); - - // @todo: Use the cached XML from the extensions table? - - return $items; - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + + // Load the filter state. + $clientId = $app->getUserStateFromRequest('com_modules.modules.client_id', 'client_id', 0); + $this->setState('client_id', (int) $clientId); + + // Load the parameters. + $params = ComponentHelper::getParams('com_modules'); + $this->setState('params', $params); + + // Manually set limits to get all modules. + $this->setState('list.limit', 0); + $this->setState('list.start', 0); + $this->setState('list.ordering', 'a.name'); + $this->setState('list.direction', 'ASC'); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.extension_id, a.name, a.element AS module' + ) + ); + $query->from($db->quoteName('#__extensions', 'a')); + + // Filter by module + $query->where($db->quoteName('a.type') . ' = ' . $db->quote('module')); + + // Filter by client. + $clientId = (int) $this->getState('client_id'); + $query->where($db->quoteName('a.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Filter by enabled + $query->where($db->quoteName('a.enabled') . ' = 1'); + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to get a list of items. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + // Get the list of items from the database. + $items = parent::getItems(); + + $client = ApplicationHelper::getClientInfo($this->getState('client_id', 0)); + $lang = Factory::getLanguage(); + + // Loop through the results to add the XML metadata, + // and load language support. + foreach ($items as &$item) { + $path = Path::clean($client->path . '/modules/' . $item->module . '/' . $item->module . '.xml'); + + if (file_exists($path)) { + $item->xml = simplexml_load_file($path); + } else { + $item->xml = null; + } + + // 1.5 Format; Core files or language packs then + // 1.6 3PD Extension Support + $lang->load($item->module . '.sys', $client->path) + || $lang->load($item->module . '.sys', $client->path . '/modules/' . $item->module); + $item->name = Text::_($item->name); + + if (isset($item->xml) && $text = trim($item->xml->description)) { + $item->desc = Text::_($text); + } else { + $item->desc = Text::_('COM_MODULES_NODESCRIPTION'); + } + } + + $items = ArrayHelper::sortObjects($items, 'name', 1, true, true); + + // @todo: Use the cached XML from the extensions table? + + return $items; + } } diff --git a/code/administrator/components/com_modules/src/Service/HTML/Modules.php b/code/administrator/components/com_modules/src/Service/HTML/Modules.php index 050833f2..68869371 100644 --- a/code/administrator/components/com_modules/src/Service/HTML/Modules.php +++ b/code/administrator/components/com_modules/src/Service/HTML/Modules.php @@ -1,4 +1,5 @@ element, $template->name); - } - - return $options; - } - - /** - * Builds an array of template type options - * - * @return array - */ - public function types() - { - $options = array(); - $options[] = HTMLHelper::_('select.option', 'user', 'COM_MODULES_OPTION_POSITION_USER_DEFINED'); - $options[] = HTMLHelper::_('select.option', 'template', 'COM_MODULES_OPTION_POSITION_TEMPLATE_DEFINED'); - - return $options; - } - - /** - * Builds an array of template state options - * - * @return array - */ - public function templateStates() - { - $options = array(); - $options[] = HTMLHelper::_('select.option', '1', 'JENABLED'); - $options[] = HTMLHelper::_('select.option', '0', 'JDISABLED'); - - return $options; - } - - /** - * Returns a published state on a grid - * - * @param integer $value The state value. - * @param integer $i The row index - * @param boolean $enabled An optional setting for access control on the action. - * @param string $checkbox An optional prefix for checkboxes. - * - * @return string The Html code - * - * @see HTMLHelperJGrid::state - * @since 1.7.1 - */ - public function state($value, $i, $enabled = true, $checkbox = 'cb') - { - $states = array( - 1 => array( - 'unpublish', - 'COM_MODULES_EXTENSION_PUBLISHED_ENABLED', - 'COM_MODULES_HTML_UNPUBLISH_ENABLED', - 'COM_MODULES_EXTENSION_PUBLISHED_ENABLED', - true, - 'publish', - 'publish', - ), - 0 => array( - 'publish', - 'COM_MODULES_EXTENSION_UNPUBLISHED_ENABLED', - 'COM_MODULES_HTML_PUBLISH_ENABLED', - 'COM_MODULES_EXTENSION_UNPUBLISHED_ENABLED', - true, - 'unpublish', - 'unpublish', - ), - -1 => array( - 'unpublish', - 'COM_MODULES_EXTENSION_PUBLISHED_DISABLED', - 'COM_MODULES_HTML_UNPUBLISH_DISABLED', - 'COM_MODULES_EXTENSION_PUBLISHED_DISABLED', - true, - 'warning', - 'warning', - ), - -2 => array( - 'publish', - 'COM_MODULES_EXTENSION_UNPUBLISHED_DISABLED', - 'COM_MODULES_HTML_PUBLISH_DISABLED', - 'COM_MODULES_EXTENSION_UNPUBLISHED_DISABLED', - true, - 'unpublish', - 'unpublish', - ), - ); - - return HTMLHelper::_('jgrid.state', $states, $value, $i, 'modules.', $enabled, true, $checkbox); - } - - /** - * Display a batch widget for the module position selector. - * - * @param integer $clientId The client ID. - * @param integer $state The state of the module (enabled, unenabled, trashed). - * @param string $selectedPosition The currently selected position for the module. - * - * @return string The necessary positions for the widget. - * - * @since 2.5 - */ - public function positions($clientId, $state = 1, $selectedPosition = '') - { - $templates = array_keys(ModulesHelper::getTemplates($clientId, $state)); - $templateGroups = array(); - - // Add an empty value to be able to deselect a module position - $option = ModulesHelper::createOption('', Text::_('COM_MODULES_NONE')); - $templateGroups[''] = ModulesHelper::createOptionGroup('', array($option)); - - // Add positions from templates - $isTemplatePosition = false; - - foreach ($templates as $template) - { - $options = array(); - - $positions = TemplatesHelper::getPositions($clientId, $template); - - if (is_array($positions)) - { - foreach ($positions as $position) - { - $text = ModulesHelper::getTranslatedModulePosition($clientId, $template, $position) . ' [' . $position . ']'; - $options[] = ModulesHelper::createOption($position, $text); - - if (!$isTemplatePosition && $selectedPosition === $position) - { - $isTemplatePosition = true; - } - } - - $options = ArrayHelper::sortObjects($options, 'text'); - } - - $templateGroups[$template] = ModulesHelper::createOptionGroup(ucfirst($template), $options); - } - - // Add custom position to options - $customGroupText = Text::_('COM_MODULES_CUSTOM_POSITION'); - $editPositions = true; - $customPositions = ModulesHelper::getPositions($clientId, $editPositions); - - $app = Factory::getApplication(); - - $position = $app->getUserState('com_modules.modules.' . $clientId . '.filter.position'); - - if ($position) - { - $customPositions[] = HTMLHelper::_('select.option', $position); - - $customPositions = array_unique($customPositions, SORT_REGULAR); - } - - $templateGroups[$customGroupText] = ModulesHelper::createOptionGroup($customGroupText, $customPositions); - - return $templateGroups; - } - - /** - * Get a select with the batch action options - * - * @return void - */ - public function batchOptions() - { - // Create the copy/move options. - $options = array( - HTMLHelper::_('select.option', 'c', Text::_('JLIB_HTML_BATCH_COPY')), - HTMLHelper::_('select.option', 'm', Text::_('JLIB_HTML_BATCH_MOVE')) - ); - - echo HTMLHelper::_('select.radiolist', $options, 'batch[move_copy]', '', 'value', 'text', 'm'); - } - - /** - * Method to get the field options. - * - * @param integer $clientId The client ID - * - * @return array The field option objects. - * - * @since 2.5 - */ - public function positionList($clientId = 0) - { - $clientId = (int) $clientId; - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('position', 'value')) - ->select($db->quoteName('position', 'text')) - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->order($db->quoteName('position')) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Pop the first item off the array if it's blank - if (count($options)) - { - if (strlen($options[0]->text) < 1) - { - array_shift($options); - } - } - - return $options; - } + /** + * Builds an array of template options + * + * @param integer $clientId The client id. + * @param string $state The state of the template. + * + * @return array + */ + public function templates($clientId = 0, $state = '') + { + $options = array(); + $templates = ModulesHelper::getTemplates($clientId, $state); + + foreach ($templates as $template) { + $options[] = HTMLHelper::_('select.option', $template->element, $template->name); + } + + return $options; + } + + /** + * Builds an array of template type options + * + * @return array + */ + public function types() + { + $options = array(); + $options[] = HTMLHelper::_('select.option', 'user', 'COM_MODULES_OPTION_POSITION_USER_DEFINED'); + $options[] = HTMLHelper::_('select.option', 'template', 'COM_MODULES_OPTION_POSITION_TEMPLATE_DEFINED'); + + return $options; + } + + /** + * Builds an array of template state options + * + * @return array + */ + public function templateStates() + { + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', 'JENABLED'); + $options[] = HTMLHelper::_('select.option', '0', 'JDISABLED'); + + return $options; + } + + /** + * Returns a published state on a grid + * + * @param integer $value The state value. + * @param integer $i The row index + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * + * @return string The Html code + * + * @see HTMLHelperJGrid::state + * @since 1.7.1 + */ + public function state($value, $i, $enabled = true, $checkbox = 'cb') + { + $states = array( + 1 => array( + 'unpublish', + 'COM_MODULES_EXTENSION_PUBLISHED_ENABLED', + 'COM_MODULES_HTML_UNPUBLISH_ENABLED', + 'COM_MODULES_EXTENSION_PUBLISHED_ENABLED', + true, + 'publish', + 'publish', + ), + 0 => array( + 'publish', + 'COM_MODULES_EXTENSION_UNPUBLISHED_ENABLED', + 'COM_MODULES_HTML_PUBLISH_ENABLED', + 'COM_MODULES_EXTENSION_UNPUBLISHED_ENABLED', + true, + 'unpublish', + 'unpublish', + ), + -1 => array( + 'unpublish', + 'COM_MODULES_EXTENSION_PUBLISHED_DISABLED', + 'COM_MODULES_HTML_UNPUBLISH_DISABLED', + 'COM_MODULES_EXTENSION_PUBLISHED_DISABLED', + true, + 'warning', + 'warning', + ), + -2 => array( + 'publish', + 'COM_MODULES_EXTENSION_UNPUBLISHED_DISABLED', + 'COM_MODULES_HTML_PUBLISH_DISABLED', + 'COM_MODULES_EXTENSION_UNPUBLISHED_DISABLED', + true, + 'unpublish', + 'unpublish', + ), + ); + + return HTMLHelper::_('jgrid.state', $states, $value, $i, 'modules.', $enabled, true, $checkbox); + } + + /** + * Display a batch widget for the module position selector. + * + * @param integer $clientId The client ID. + * @param integer $state The state of the module (enabled, unenabled, trashed). + * @param string $selectedPosition The currently selected position for the module. + * + * @return string The necessary positions for the widget. + * + * @since 2.5 + */ + public function positions($clientId, $state = 1, $selectedPosition = '') + { + $templates = array_keys(ModulesHelper::getTemplates($clientId, $state)); + $templateGroups = array(); + + // Add an empty value to be able to deselect a module position + $option = ModulesHelper::createOption('', Text::_('COM_MODULES_NONE')); + $templateGroups[''] = ModulesHelper::createOptionGroup('', array($option)); + + // Add positions from templates + $isTemplatePosition = false; + + foreach ($templates as $template) { + $options = array(); + + $positions = TemplatesHelper::getPositions($clientId, $template); + + if (is_array($positions)) { + foreach ($positions as $position) { + $text = ModulesHelper::getTranslatedModulePosition($clientId, $template, $position) . ' [' . $position . ']'; + $options[] = ModulesHelper::createOption($position, $text); + + if (!$isTemplatePosition && $selectedPosition === $position) { + $isTemplatePosition = true; + } + } + + $options = ArrayHelper::sortObjects($options, 'text'); + } + + $templateGroups[$template] = ModulesHelper::createOptionGroup(ucfirst($template), $options); + } + + // Add custom position to options + $customGroupText = Text::_('COM_MODULES_CUSTOM_POSITION'); + $editPositions = true; + $customPositions = ModulesHelper::getPositions($clientId, $editPositions); + + $app = Factory::getApplication(); + + $position = $app->getUserState('com_modules.modules.' . $clientId . '.filter.position'); + + if ($position) { + $customPositions[] = HTMLHelper::_('select.option', $position); + + $customPositions = array_unique($customPositions, SORT_REGULAR); + } + + $templateGroups[$customGroupText] = ModulesHelper::createOptionGroup($customGroupText, $customPositions); + + return $templateGroups; + } + + /** + * Get a select with the batch action options + * + * @return void + */ + public function batchOptions() + { + // Create the copy/move options. + $options = array( + HTMLHelper::_('select.option', 'c', Text::_('JLIB_HTML_BATCH_COPY')), + HTMLHelper::_('select.option', 'm', Text::_('JLIB_HTML_BATCH_MOVE')) + ); + + echo HTMLHelper::_('select.radiolist', $options, 'batch[move_copy]', '', 'value', 'text', 'm'); + } + + /** + * Method to get the field options. + * + * @param integer $clientId The client ID + * + * @return array The field option objects. + * + * @since 2.5 + * + * @deprecated 5.0 Will be removed with no replacement + */ + public function positionList($clientId = 0) + { + $clientId = (int) $clientId; + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('position', 'value')) + ->select($db->quoteName('position', 'text')) + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->order($db->quoteName('position')) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Pop the first item off the array if it's blank + if (count($options)) { + if (strlen($options[0]->text) < 1) { + array_shift($options); + } + } + + return $options; + } } diff --git a/code/administrator/components/com_modules/src/View/Module/HtmlView.php b/code/administrator/components/com_modules/src/View/Module/HtmlView.php index 98b3f402..f8143d3f 100644 --- a/code/administrator/components/com_modules/src/View/Module/HtmlView.php +++ b/code/administrator/components/com_modules/src/View/Module/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - $this->canDo = ContentHelper::getActions('com_modules', 'module', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = Factory::getUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); - $canDo = $this->canDo; - - ToolbarHelper::title(Text::sprintf('COM_MODULES_MANAGER_MODULE', Text::_($this->item->module)), 'cube module'); - - // For new records, check the create permission. - if ($isNew && $canDo->get('core.create')) - { - ToolbarHelper::apply('module.apply'); - - ToolbarHelper::saveGroup( - [ - ['save', 'module.save'], - ['save2new', 'module.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('module.cancel'); - } - else - { - $toolbarButtons = []; - - // Can't save the record if it's checked out. - if (!$checkedOut) - { - // Since it's an existing record, check the edit permission. - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('module.apply'); - - $toolbarButtons[] = ['save', 'module.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'module.save2new']; - } - } - } - - // If checked out, we can still save - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'module.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('module.cancel', 'JTOOLBAR_CLOSE'); - } - - // Get the help information for the menu item. - $lang = Factory::getLanguage(); - - $help = $this->get('Help'); - - if ($lang->hasKey($help->url)) - { - $debug = $lang->setDebug(false); - $url = Text::_($help->url); - $lang->setDebug($debug); - } - else - { - $url = null; - } - - ToolbarHelper::inlinehelp(); - ToolbarHelper::help($help->key, false, $url); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->canDo = ContentHelper::getActions('com_modules', 'module', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); + $canDo = $this->canDo; + + ToolbarHelper::title(Text::sprintf('COM_MODULES_MANAGER_MODULE', Text::_($this->item->module)), 'cube module'); + + // For new records, check the create permission. + if ($isNew && $canDo->get('core.create')) { + ToolbarHelper::apply('module.apply'); + + ToolbarHelper::saveGroup( + [ + ['save', 'module.save'], + ['save2new', 'module.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('module.cancel'); + } else { + $toolbarButtons = []; + + // Can't save the record if it's checked out. + if (!$checkedOut) { + // Since it's an existing record, check the edit permission. + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('module.apply'); + + $toolbarButtons[] = ['save', 'module.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'module.save2new']; + } + } + } + + // If checked out, we can still save + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'module.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('module.cancel', 'JTOOLBAR_CLOSE'); + } + + // Get the help information for the menu item. + $lang = Factory::getLanguage(); + + $help = $this->get('Help'); + + if ($lang->hasKey($help->url)) { + $debug = $lang->setDebug(false); + $url = Text::_($help->url); + $lang->setDebug($debug); + } else { + $url = null; + } + + ToolbarHelper::inlinehelp(); + ToolbarHelper::help($help->key, false, $url); + } } diff --git a/code/administrator/components/com_modules/src/View/Modules/HtmlView.php b/code/administrator/components/com_modules/src/View/Modules/HtmlView.php index 698fed37..de283925 100644 --- a/code/administrator/components/com_modules/src/View/Modules/HtmlView.php +++ b/code/administrator/components/com_modules/src/View/Modules/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->clientId = $this->state->get('client_id'); - - if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - /** - * The code below make sure the remembered position will be available from filter dropdown even if there are no - * modules available for this position. This will make the UI less confusing for users in case there is only one - * module in the selected position and user: - * 1. Edit the module, change it to new position, save it and come back to Modules Management Screen - * 2. Or move that module to new position using Batch action - */ - if (count($this->items) === 0 && $this->state->get('filter.position')) - { - $selectedPosition = $this->state->get('filter.position'); - $positionField = $this->filterForm->getField('position', 'filter'); - - $positionExists = false; - - foreach ($positionField->getOptions() as $option) - { - if ($option->value === $selectedPosition) - { - $positionExists = true; - break; - } - } - - if ($positionExists === false) - { - $positionField->addOption($selectedPosition, ['value' => $selectedPosition]); - } - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // We do not need the Language filter when modules are not filtered - if ($this->clientId == 1 && !ModuleHelper::isAdminMultilang()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - // We don't need the toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - // If in modal layout. - else - { - // Client id selector should not exist. - $this->filterForm->removeField('client_id', ''); - - // If in the frontend state and language should not activate the search tools. - if (Factory::getApplication()->isClient('site')) - { - unset($this->activeFilters['state']); - unset($this->activeFilters['language']); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $canDo = ContentHelper::getActions('com_modules'); - $user = Factory::getUser(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($state->get('client_id') == 1) - { - ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_ADMIN'), 'cube module'); - } - else - { - ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_SITE'), 'cube module'); - } - - if ($canDo->get('core.create')) - { - $toolbar->standardButton('new', 'JTOOLBAR_NEW') - ->onclick("location.href='index.php?option=com_modules&view=select&client_id=" . $this->state->get('client_id', 0) . "'"); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || Factory::getUser()->authorise('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('modules.publish')->listCheck(true); - - $childBar->unpublish('modules.unpublish')->listCheck(true); - } - - if (Factory::getUser()->authorise('core.admin')) - { - $childBar->checkin('modules.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - $childBar->trash('modules.trash')->listCheck(true); - } - - // Add a batch button - if ($user->authorise('core.create', 'com_modules') && $user->authorise('core.edit', 'com_modules') - && $user->authorise('core.edit.state', 'com_modules')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - - if ($canDo->get('core.create')) - { - $childBar->standardButton('copy') - ->text('JTOOLBAR_DUPLICATE') - ->task('modules.duplicate') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && ($state->get('filter.state') == -2 && $canDo->get('core.delete'))) - { - $toolbar->delete('modules.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin')) - { - $toolbar->preferences('com_modules'); - } - - $toolbar->help('Modules'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->clientId = $this->state->get('client_id'); + + if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + /** + * The code below make sure the remembered position will be available from filter dropdown even if there are no + * modules available for this position. This will make the UI less confusing for users in case there is only one + * module in the selected position and user: + * 1. Edit the module, change it to new position, save it and come back to Modules Management Screen + * 2. Or move that module to new position using Batch action + */ + if (count($this->items) === 0 && $this->state->get('filter.position')) { + $selectedPosition = $this->state->get('filter.position'); + $positionField = $this->filterForm->getField('position', 'filter'); + + $positionExists = false; + + foreach ($positionField->getOptions() as $option) { + if ($option->value === $selectedPosition) { + $positionExists = true; + break; + } + } + + if ($positionExists === false) { + $positionField->addOption($selectedPosition, ['value' => $selectedPosition]); + } + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We do not need the Language filter when modules are not filtered + if ($this->clientId == 1 && !ModuleHelper::isAdminMultilang()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + // We don't need the toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // If in modal layout. + // Client id selector should not exist. + $this->filterForm->removeField('client_id', ''); + + // If in the frontend state and language should not activate the search tools. + if (Factory::getApplication()->isClient('site')) { + unset($this->activeFilters['state']); + unset($this->activeFilters['language']); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $canDo = ContentHelper::getActions('com_modules'); + $user = $this->getCurrentUser(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($state->get('client_id') == 1) { + ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_ADMIN'), 'cube module'); + } else { + ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_SITE'), 'cube module'); + } + + if ($canDo->get('core.create')) { + $toolbar->standardButton('new', 'JTOOLBAR_NEW') + ->onclick("location.href='index.php?option=com_modules&view=select&client_id=" . $this->state->get('client_id', 0) . "'"); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('modules.publish')->listCheck(true); + + $childBar->unpublish('modules.unpublish')->listCheck(true); + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + $childBar->checkin('modules.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + $childBar->trash('modules.trash')->listCheck(true); + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_modules') && $user->authorise('core.edit', 'com_modules') + && $user->authorise('core.edit.state', 'com_modules') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + + if ($canDo->get('core.create')) { + $childBar->standardButton('copy') + ->text('JTOOLBAR_DUPLICATE') + ->task('modules.duplicate') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && ($state->get('filter.state') == -2 && $canDo->get('core.delete'))) { + $toolbar->delete('modules.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin')) { + $toolbar->preferences('com_modules'); + } + + $toolbar->help('Modules'); + } } diff --git a/code/administrator/components/com_modules/src/View/Select/HtmlView.php b/code/administrator/components/com_modules/src/View/Select/HtmlView.php index 30840056..1ead5caf 100644 --- a/code/administrator/components/com_modules/src/View/Select/HtmlView.php +++ b/code/administrator/components/com_modules/src/View/Select/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->modalLink = ''; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $clientId = (int) $state->get('client_id', 0); - - // Add page title - ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_SITE'), 'cube module'); - - if ($clientId === 1) - { - ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_ADMIN'), 'cube module'); - } - - // Get the toolbar object instance - $bar = Toolbar::getInstance('toolbar'); - - // Instantiate a new FileLayout instance and render the layout - $layout = new FileLayout('toolbar.cancelselect'); - - $bar->appendButton('Custom', $layout->render(array('client_id' => $clientId)), 'new'); - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * A suffix for links for modal use + * + * @var string + */ + protected $modalLink; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->modalLink = ''; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $clientId = (int) $state->get('client_id', 0); + + // Add page title + ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_SITE'), 'cube module'); + + if ($clientId === 1) { + ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_ADMIN'), 'cube module'); + } + + // Get the toolbar object instance + $bar = Toolbar::getInstance('toolbar'); + + // Instantiate a new FileLayout instance and render the layout + $layout = new FileLayout('toolbar.cancelselect'); + + $bar->appendButton('Custom', $layout->render(array('client_id' => $clientId)), 'new'); + } } diff --git a/code/administrator/components/com_modules/tmpl/module/edit.php b/code/administrator/components/com_modules/tmpl/module/edit.php index 982bbf00..0dbdb468 100644 --- a/code/administrator/components/com_modules/tmpl/module/edit.php +++ b/code/administrator/components/com_modules/tmpl/module/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_modules.admin-module-edit'); + ->useScript('form.validate') + ->useScript('com_modules.admin-module-edit'); $input = Factory::getApplication()->input; @@ -54,151 +54,144 @@
- - -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> - - - -
-
- item->xml) : ?> - item->xml->description) : ?> -

- item->xml) - { - echo ($text = (string) $this->item->xml->name) ? Text::_($text) : $this->item->module; - } - else - { - echo Text::_('COM_MODULES_ERR_XML'); - } - ?> -

-
- - item->client_id == 0 ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); ?> - -
-
- fieldset = 'description'; - $short_description = Text::_($this->item->xml->description); - $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); - - if (!$long_description) - { - $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); - - if (strlen($truncated) > 500) - { - $long_description = $short_description; - $short_description = HTMLHelper::_('string.truncate', $truncated, 250); - - if ($short_description == $long_description) - { - $long_description = ''; - } - } - } - ?> -

- -

- - - -

- -
- - -
- - -
- - form->getInput($hasContentFieldName); - } - $this->fieldset = 'basic'; - $html = LayoutHelper::render('joomla.edit.fieldset', $this); - echo $html ? '
' . $html : ''; - ?> -
-
- fields = array( - 'showtitle', - 'position', - 'published', - 'publish_up', - 'publish_down', - 'access', - 'ordering', - 'language', - 'note' - ); - - ?> - item->client_id == 0) : ?> - - - - -
-
- - - - -
-
- -
-
- - - - item->client_id == 0) : ?> - -
- -
- loadTemplate('assignment'); ?> -
-
- - - - fieldsets = array(); - $this->ignore_fieldsets = array('basic', 'description'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - canDo->get('core.admin')) : ?> - -
- -
- form->getInput('rules'); ?> -
-
- - - - - - - - - form->getInput('module'); ?> - form->getInput('client_id'); ?> -
+ + +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + + +
+
+ item->xml) : ?> + item->xml->description) : ?> +

+ item->xml) { + echo ($text = (string) $this->item->xml->name) ? Text::_($text) : $this->item->module; + } else { + echo Text::_('COM_MODULES_ERR_XML'); + } + ?> +

+
+ + item->client_id == 0 ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); ?> + +
+
+ fieldset = 'description'; + $short_description = Text::_($this->item->xml->description); + $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); + + if (!$long_description) { + $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); + + if (strlen($truncated) > 500) { + $long_description = $short_description; + $short_description = HTMLHelper::_('string.truncate', $truncated, 250); + + if ($short_description == $long_description) { + $long_description = ''; + } + } + } + ?> +

+ +

+ + + +

+ +
+ + +
+ + +
+ + form->getInput($hasContentFieldName); + } + $this->fieldset = 'basic'; + $html = LayoutHelper::render('joomla.edit.fieldset', $this); + echo $html ? '
' . $html : ''; + ?> +
+
+ fields = array( + 'showtitle', + 'position', + 'published', + 'publish_up', + 'publish_down', + 'access', + 'ordering', + 'language', + 'note' + ); + + ?> + item->client_id == 0) : ?> + + + + +
+
+ + + + +
+
+ +
+
+ + + + item->client_id == 0) : ?> + +
+ +
+ loadTemplate('assignment'); ?> +
+
+ + + + fieldsets = array(); + $this->ignore_fieldsets = array('basic', 'description'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + + + + + + form->getInput('module'); ?> + form->getInput('client_id'); ?> +
diff --git a/code/administrator/components/com_modules/tmpl/module/edit_assignment.php b/code/administrator/components/com_modules/tmpl/module/edit_assignment.php index bcd98ef2..311e1999 100644 --- a/code/administrator/components/com_modules/tmpl/module/edit_assignment.php +++ b/code/administrator/components/com_modules/tmpl/module/edit_assignment.php @@ -1,4 +1,5 @@ document->getWebAssetManager() - ->useScript('joomla.treeselectmenu') - ->useScript('com_modules.admin-module-edit-assignment'); + ->useScript('joomla.treeselectmenu') + ->useScript('com_modules.admin-module-edit-assignment'); ?>
- -
- -
+ +
+ +
+ + diff --git a/code/administrator/components/com_modules/tmpl/module/modal.php b/code/administrator/components/com_modules/tmpl/module/modal.php index bb8088a0..b90680e7 100644 --- a/code/administrator/components/com_modules/tmpl/module/modal.php +++ b/code/administrator/components/com_modules/tmpl/module/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/code/administrator/components/com_modules/tmpl/modules/default.php b/code/administrator/components/com_modules/tmpl/modules/default.php index d45f5c49..5d2e16ec 100644 --- a/code/administrator/components/com_modules/tmpl/modules/default.php +++ b/code/administrator/components/com_modules/tmpl/modules/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $clientId = (int) $this->state->get('client_id', 0); $user = Factory::getUser(); @@ -28,191 +30,192 @@ $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = ($listOrder == 'a.ordering'); -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_modules&task=modules.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_modules&task=modules.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
- $this)); ?> - total > 0) : ?> - - - - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="false"> - items as $i => $item) : - $ordering = ($listOrder == 'a.ordering'); - $canCreate = $user->authorise('core.create', 'com_modules'); - $canEdit = $user->authorise('core.edit', 'com_modules.module.' . $item->id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id')|| is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_modules.module.' . $item->id) && $canCheckin; - ?> - - - - - + + + + + + + + + + + + + + + +
- , - , - -
- - - - - - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - enabled > 0) : ?> - published, $i, 'modules.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - - - - - - - -
- checked_out) : ?> - editor, $item->checked_out_time, 'modules.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - +
+ $this)); ?> + total > 0) : ?> + + + + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + items as $i => $item) : + $ordering = ($listOrder == 'a.ordering'); + $canCreate = $user->authorise('core.create', 'com_modules'); + $canEdit = $user->authorise('core.edit', 'com_modules.module.' . $item->id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_modules.module.' . $item->id) && $canCheckin; + ?> + + + + + - - - - - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + enabled > 0) : ?> + published, $i, 'modules.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + + + + + + + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'modules.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + - note)) : ?> -
- escape($item->note)); ?> -
- -
-
- position) : ?> - - position; ?> - - - - - - - - name; ?> - - pages; ?> - - escape($item->access_level); ?> - - - - language == ''):?> - - language == '*'):?> - - - escape($item->language); ?> - - - id; ?> -
+ note)) : ?> +
+ escape($item->note)); ?> +
+ +
+
+ position) : ?> + + position; ?> + + + + + + + + name; ?> + + pages; ?> + + escape($item->access_level); ?> + + + + language == '') :?> + + language == '*') :?> + + + escape($item->language); ?> + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - authorise('core.create', 'com_modules') - && $user->authorise('core.edit', 'com_modules') - && $user->authorise('core.edit.state', 'com_modules')) : ?> - Text::_('COM_MODULES_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - - - -
+ + authorise('core.create', 'com_modules') + && $user->authorise('core.edit', 'com_modules') + && $user->authorise('core.edit.state', 'com_modules') + ) : ?> + Text::_('COM_MODULES_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + + + +
diff --git a/code/administrator/components/com_modules/tmpl/modules/default_batch_body.php b/code/administrator/components/com_modules/tmpl/modules/default_batch_body.php index 2a52e9c4..4683baed 100644 --- a/code/administrator/components/com_modules/tmpl/modules/default_batch_body.php +++ b/code/administrator/components/com_modules/tmpl/modules/default_batch_body.php @@ -1,4 +1,5 @@ 'batch-position-id', + 'id' => 'batch-position-id', ); Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); $this->document->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select') - ->useScript('joomla.batch-copymove'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select') + ->useScript('joomla.batch-copymove'); ?>
-

-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- = 0) : ?> -
-
- -
- - - -
- -
-
-
- -
-
+

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ +
+ + + +
+ +
+
+
+ +
+
diff --git a/code/administrator/components/com_modules/tmpl/modules/default_batch_footer.php b/code/administrator/components/com_modules/tmpl/modules/default_batch_footer.php index 44f8345a..0a684547 100644 --- a/code/administrator/components/com_modules/tmpl/modules/default_batch_footer.php +++ b/code/administrator/components/com_modules/tmpl/modules/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/code/administrator/components/com_modules/tmpl/modules/emptystate.php b/code/administrator/components/com_modules/tmpl/modules/emptystate.php index 323715f6..f0b2b02e 100644 --- a/code/administrator/components/com_modules/tmpl/modules/emptystate.php +++ b/code/administrator/components/com_modules/tmpl/modules/emptystate.php @@ -1,4 +1,5 @@ 'COM_MODULES', - 'formURL' => 'index.php?option=com_modules&view=select&client_id=' . $this->clientId, - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Module', - 'icon' => 'icon-cube module', - // Although it is (almost) impossible to get to this page with no created Administrator Modules, we add this for completeness. - 'title' => Text::_('COM_MODULES_EMPTYSTATE_TITLE_' . ($this->clientId ? 'ADMINISTRATOR' : 'SITE')), + 'textPrefix' => 'COM_MODULES', + 'formURL' => 'index.php?option=com_modules&view=select&client_id=' . $this->clientId, + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Module', + 'icon' => 'icon-cube module', + // Although it is (almost) impossible to get to this page with no created Administrator Modules, we add this for completeness. + 'title' => Text::_('COM_MODULES_EMPTYSTATE_TITLE_' . ($this->clientId ? 'ADMINISTRATOR' : 'SITE')), ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_modules')) -{ - $displayData['createURL'] = 'index.php?option=com_modules&view=select&client_id=' . $this->clientId; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_modules')) { + $displayData['createURL'] = 'index.php?option=com_modules&view=select&client_id=' . $this->clientId; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_modules/tmpl/modules/modal.php b/code/administrator/components/com_modules/tmpl/modules/modal.php index 972919a7..3c24c77f 100644 --- a/code/administrator/components/com_modules/tmpl/modules/modal.php +++ b/code/administrator/components/com_modules/tmpl/modules/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if (Factory::getApplication()->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -30,109 +30,108 @@ $editor = Factory::getApplication()->input->get('editor', '', 'cmd'); $link = 'index.php?option=com_modules&view=modules&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; -if (!empty($editor)) -{ - $link .= '&editor=' . $editor; +if (!empty($editor)) { + $link .= '&editor=' . $editor; } ?>
-
+ - $this)); ?> + $this)); ?> - total > 0) : ?> - - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - foreach ($this->items as $i => $item) : - ?> - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - -
- - - - - - escape($item->title); ?> - - - position) : ?> - escape($item->position); ?> - - - - - name; ?> - - pages; ?> - - escape($item->access_level); ?> - - - - id; ?> -
+ total > 0) : ?> + + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + foreach ($this->items as $i => $item) : + ?> + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + +
+ + + + + + escape($item->title); ?> + + + position) : ?> + escape($item->position); ?> + + + + + name; ?> + + pages; ?> + + escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - + + + + -
+
diff --git a/code/administrator/components/com_modules/tmpl/select/default.php b/code/administrator/components/com_modules/tmpl/select/default.php index 344db412..7e779d42 100644 --- a/code/administrator/components/com_modules/tmpl/select/default.php +++ b/code/administrator/components/com_modules/tmpl/select/default.php @@ -1,4 +1,5 @@ useScript('com_modules.admin-module-search'); if ($function) : - $wa->useScript('com_modules.admin-select-modal'); + $wa->useScript('com_modules.admin-select-modal'); endif; ?>
-
-
- -
- -
- -
-
-
-
+
+
+ +
+ +
+ +
+
+
+
-
-
- - -
-

- -

-
- items as &$item) : ?> - - state->get('client_id', 0) . $this->modalLink . '&eid=' . $item->extension_id; ?> - escape($item->name); ?> - escape(strip_tags($item->desc)), 200); ?> - -
-

-

- -

-
- - - -
- -
-
+
+
+ + +
+

+ +

+
+ items as &$item) : ?> + + state->get('client_id', 0) . $this->modalLink . '&eid=' . $item->extension_id; ?> + escape($item->name); ?> + escape(strip_tags($item->desc)), 200); ?> + +
+

+

+ +

+
+ + + +
+ +
+
diff --git a/code/administrator/components/com_modules/tmpl/select/modal.php b/code/administrator/components/com_modules/tmpl/select/modal.php index 2e6998d6..25ac8d0a 100644 --- a/code/administrator/components/com_modules/tmpl/select/modal.php +++ b/code/administrator/components/com_modules/tmpl/select/modal.php @@ -1,4 +1,5 @@ modalLink = '&tmpl=component&view=module&layout=modal'; ?>
- setLayout('default'); ?> - loadTemplate(); ?> + setLayout('default'); ?> + loadTemplate(); ?>
diff --git a/code/administrator/components/com_newsfeeds/helpers/newsfeeds.php b/code/administrator/components/com_newsfeeds/helpers/newsfeeds.php index 608aeabc..0e34683f 100644 --- a/code/administrator/components/com_newsfeeds/helpers/newsfeeds.php +++ b/code/administrator/components/com_newsfeeds/helpers/newsfeeds.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Newsfeeds component helper. diff --git a/code/administrator/components/com_newsfeeds/newsfeeds.xml b/code/administrator/components/com_newsfeeds/newsfeeds.xml index e84b30e0..68af2e9c 100644 --- a/code/administrator/components/com_newsfeeds/newsfeeds.xml +++ b/code/administrator/components/com_newsfeeds/newsfeeds.xml @@ -2,7 +2,7 @@ com_newsfeeds Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_newsfeeds/services/provider.php b/code/administrator/components/com_newsfeeds/services/provider.php index 3374c725..0382efe8 100644 --- a/code/administrator/components/com_newsfeeds/services/provider.php +++ b/code/administrator/components/com_newsfeeds/services/provider.php @@ -1,4 +1,5 @@ set(AssociationExtensionInterface::class, new AssociationsHelper); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->set(AssociationExtensionInterface::class, new AssociationsHelper()); - $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Newsfeeds')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Newsfeeds')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Newsfeeds')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Newsfeeds')); + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Newsfeeds')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Newsfeeds')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Newsfeeds')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Newsfeeds')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new NewsfeedsComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new NewsfeedsComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_newsfeeds/src/Controller/AjaxController.php b/code/administrator/components/com_newsfeeds/src/Controller/AjaxController.php index 34487acb..48d340af 100644 --- a/code/administrator/components/com_newsfeeds/src/Controller/AjaxController.php +++ b/code/administrator/components/com_newsfeeds/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->getInt('assocId', 0); + /** + * Method to fetch associations of a newsfeed + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the newsfeed whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return null + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', (int) $assocId); + $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', (int) $assocId); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - $newsfeedsTable = $this->factory->createTable('Newsfeed', 'Administrator'); + // Add the title to each of the associated records + $newsfeedsTable = $this->factory->createTable('Newsfeed', 'Administrator'); - foreach ($associations as $lang => $association) - { - $newsfeedsTable->load($association->id); - $associations[$lang]->title = $newsfeedsTable->name; - } + foreach ($associations as $lang => $association) { + $newsfeedsTable->load($association->id); + $associations[$lang]->title = $newsfeedsTable->name; + } - $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/code/administrator/components/com_newsfeeds/src/Controller/DisplayController.php b/code/administrator/components/com_newsfeeds/src/Controller/DisplayController.php index 7eeabbb7..5de73268 100644 --- a/code/administrator/components/com_newsfeeds/src/Controller/DisplayController.php +++ b/code/administrator/components/com_newsfeeds/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'newsfeeds'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'newsfeeds'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'newsfeed' && $layout == 'edit' && !$this->checkEditId('com_newsfeeds.edit.newsfeed', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'newsfeed' && $layout == 'edit' && !$this->checkEditId('com_newsfeeds.edit.newsfeed', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_newsfeeds&view=newsfeeds', false)); + $this->setRedirect(Route::_('index.php?option=com_newsfeeds&view=newsfeeds', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/code/administrator/components/com_newsfeeds/src/Controller/NewsfeedController.php b/code/administrator/components/com_newsfeeds/src/Controller/NewsfeedController.php index 0ef1f405..f1cd23bb 100644 --- a/code/administrator/components/com_newsfeeds/src/Controller/NewsfeedController.php +++ b/code/administrator/components/com_newsfeeds/src/Controller/NewsfeedController.php @@ -1,4 +1,5 @@ input->getInt('filter_category_id'), 'int'); - $allow = null; - - if ($categoryId) - { - // If the category has been passed in the URL check it. - $allow = $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); - } - - if ($allow === null) - { - // In the absence of better information, revert to the component permissions. - return parent::allowAdd($data); - } - else - { - return $allow; - } - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - - // Since there is no asset tracking, fallback to the component permissions. - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Get the item. - $item = $this->getModel()->getItem($recordId); - - // Since there is no item, return false. - if (empty($item)) - { - return false; - } - - $user = $this->app->getIdentity(); - - // Check if can edit own core.edit.own. - $canEditOwn = $user->authorise('core.edit.own', $this->option . '.category.' . (int) $item->catid) && $item->created_by == $user->id; - - // Check the category core.edit permissions. - return $canEditOwn || $user->authorise('core.edit', $this->option . '.category.' . (int) $item->catid); - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Newsfeed', '', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_newsfeeds&view=newsfeeds' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } + use VersionableControllerTrait; + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('filter_category_id'), 'int'); + $allow = null; + + if ($categoryId) { + // If the category has been passed in the URL check it. + $allow = $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); + } + + if ($allow === null) { + // In the absence of better information, revert to the component permissions. + return parent::allowAdd($data); + } else { + return $allow; + } + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + + // Since there is no asset tracking, fallback to the component permissions. + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Get the item. + $item = $this->getModel()->getItem($recordId); + + // Since there is no item, return false. + if (empty($item)) { + return false; + } + + $user = $this->app->getIdentity(); + + // Check if can edit own core.edit.own. + $canEditOwn = $user->authorise('core.edit.own', $this->option . '.category.' . (int) $item->catid) && $item->created_by == $user->id; + + // Check the category core.edit permissions. + return $canEditOwn || $user->authorise('core.edit', $this->option . '.category.' . (int) $item->catid); + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Newsfeed', '', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_newsfeeds&view=newsfeeds' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } } diff --git a/code/administrator/components/com_newsfeeds/src/Controller/NewsfeedsController.php b/code/administrator/components/com_newsfeeds/src/Controller/NewsfeedsController.php index 72bbd630..2c6fc408 100644 --- a/code/administrator/components/com_newsfeeds/src/Controller/NewsfeedsController.php +++ b/code/administrator/components/com_newsfeeds/src/Controller/NewsfeedsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Newsfeed', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_newsfeeds/src/Extension/NewsfeedsComponent.php b/code/administrator/components/com_newsfeeds/src/Extension/NewsfeedsComponent.php index a9226c38..63bd515a 100644 --- a/code/administrator/components/com_newsfeeds/src/Extension/NewsfeedsComponent.php +++ b/code/administrator/components/com_newsfeeds/src/Extension/NewsfeedsComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('newsfeedsadministrator', new AdministratorService); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('newsfeedsadministrator', new AdministratorService()); + } - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return $section === 'category' ? 'categories' : 'newsfeeds'; - } + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return $section === 'category' ? 'categories' : 'newsfeeds'; + } - /** - * Returns the state column for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getStateColumnForSection(string $section = null) - { - return 'published'; - } + /** + * Returns the state column for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getStateColumnForSection(string $section = null) + { + return 'published'; + } } diff --git a/code/administrator/components/com_newsfeeds/src/Field/Modal/NewsfeedField.php b/code/administrator/components/com_newsfeeds/src/Field/Modal/NewsfeedField.php index 7faa0193..3eb8e8cb 100644 --- a/code/administrator/components/com_newsfeeds/src/Field/Modal/NewsfeedField.php +++ b/code/administrator/components/com_newsfeeds/src/Field/Modal/NewsfeedField.php @@ -1,4 +1,5 @@ element['new'] == 'true'); - $allowEdit = ((string) $this->element['edit'] == 'true'); - $allowClear = ((string) $this->element['clear'] != 'false'); - $allowSelect = ((string) $this->element['select'] != 'false'); - $allowPropagate = ((string) $this->element['propagate'] == 'true'); - - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language - Factory::getLanguage()->load('com_newsfeeds', JPATH_ADMINISTRATOR); - - // The active newsfeed id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Newsfeed_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Modal_Newsfeed'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $allowNew = ((string) $this->element['new'] == 'true'); + $allowEdit = ((string) $this->element['edit'] == 'true'); + $allowClear = ((string) $this->element['clear'] != 'false'); + $allowSelect = ((string) $this->element['select'] != 'false'); + $allowPropagate = ((string) $this->element['propagate'] == 'true'); + + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language + Factory::getLanguage()->load('com_newsfeeds', JPATH_ADMINISTRATOR); + + // The active newsfeed id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Newsfeed_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectNewsfeed_" . $this->id . " = function (id, title, object) { window.processModalSelect('Newsfeed', '" . $this->id . "', id, title, '', object); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkNewsfeeds = 'index.php?option=com_newsfeeds&view=newsfeeds&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $linkNewsfeed = 'index.php?option=com_newsfeeds&view=newsfeed&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $modalTitle = Text::_('COM_NEWSFEEDS_SELECT_A_FEED'); - - if (isset($this->element['language'])) - { - $linkNewsfeeds .= '&forcedLanguage=' . $this->element['language']; - $linkNewsfeed .= '&forcedLanguage=' . $this->element['language']; - $modalTitle .= ' — ' . $this->element['label']; - } - - $urlSelect = $linkNewsfeeds . '&function=jSelectNewsfeed_' . $this->id; - $urlEdit = $linkNewsfeed . '&task=newsfeed.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkNewsfeed . '&task=newsfeed.add'; - - if ($value) - { - $id = (int) $value; - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__newsfeeds')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - $title = empty($title) ? Text::_('COM_NEWSFEEDS_SELECT_A_FEED') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current newsfeed display field. - $html = ''; - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select newsfeed button - if ($allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New newsfeed button - if ($allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit newsfeed button - if ($allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear newsfeed button - if ($allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate newsfeed button - if ($allowPropagate && count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectNewsfeed_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - // Select newsfeed modal - if ($allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New newsfeed modal - if ($allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_NEWSFEEDS_NEW_NEWSFEED'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit newsfeed modal. - if ($allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_NEWSFEEDS_EDIT_NEWSFEED'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Add class='required' for client side validation - $class = $this->required ? ' class="required modal-value"' : ''; - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.4 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkNewsfeeds = 'index.php?option=com_newsfeeds&view=newsfeeds&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $linkNewsfeed = 'index.php?option=com_newsfeeds&view=newsfeed&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $modalTitle = Text::_('COM_NEWSFEEDS_SELECT_A_FEED'); + + if (isset($this->element['language'])) { + $linkNewsfeeds .= '&forcedLanguage=' . $this->element['language']; + $linkNewsfeed .= '&forcedLanguage=' . $this->element['language']; + $modalTitle .= ' — ' . $this->element['label']; + } + + $urlSelect = $linkNewsfeeds . '&function=jSelectNewsfeed_' . $this->id; + $urlEdit = $linkNewsfeed . '&task=newsfeed.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkNewsfeed . '&task=newsfeed.add'; + + if ($value) { + $id = (int) $value; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__newsfeeds')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + $title = empty($title) ? Text::_('COM_NEWSFEEDS_SELECT_A_FEED') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current newsfeed display field. + $html = ''; + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select newsfeed button + if ($allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New newsfeed button + if ($allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit newsfeed button + if ($allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear newsfeed button + if ($allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate newsfeed button + if ($allowPropagate && count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectNewsfeed_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + // Select newsfeed modal + if ($allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New newsfeed modal + if ($allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_NEWSFEEDS_NEW_NEWSFEED'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit newsfeed modal. + if ($allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_NEWSFEEDS_EDIT_NEWSFEED'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Add class='required' for client side validation + $class = $this->required ? ' class="required modal-value"' : ''; + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.4 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/code/administrator/components/com_newsfeeds/src/Field/NewsfeedsField.php b/code/administrator/components/com_newsfeeds/src/Field/NewsfeedsField.php index 6aeda151..274beda6 100644 --- a/code/administrator/components/com_newsfeeds/src/Field/NewsfeedsField.php +++ b/code/administrator/components/com_newsfeeds/src/Field/NewsfeedsField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select( - [ - $db->quoteName('id', 'value'), - $db->quoteName('name', 'text'), - ] - ) - ->from($db->quoteName('#__newsfeeds', 'a')) - ->order($db->quoteName('a.name')); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id', 'value'), + $db->quoteName('name', 'text'), + ] + ) + ->from($db->quoteName('#__newsfeeds', 'a')) + ->order($db->quoteName('a.name')); - // Get the options. - $db->setQuery($query); + // Get the options. + $db->setQuery($query); - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); - return $options; - } + return $options; + } } diff --git a/code/administrator/components/com_newsfeeds/src/Helper/AssociationsHelper.php b/code/administrator/components/com_newsfeeds/src/Helper/AssociationsHelper.php index 85c90141..6381048d 100644 --- a/code/administrator/components/com_newsfeeds/src/Helper/AssociationsHelper.php +++ b/code/administrator/components/com_newsfeeds/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getType($typeName); - - $context = $this->extension . '.item'; - $catidField = 'catid'; - - if ($typeName === 'category') - { - $context = 'com_categories.item'; - $catidField = ''; - } - - // Get the associations. - $associations = Associations::getAssociations( - $this->extension, - $type['tables']['a'], - $context, - $id, - 'id', - 'alias', - $catidField - ); - - return $associations; - } - - /** - * Get item information - * - * @param string $typeName The item type - * @param int $id The id of item for which we need the associated items - * - * @return Table|null - * - * @since 3.7.0 - */ - public function getItem($typeName, $id) - { - if (empty($id)) - { - return null; - } - - $table = null; - - switch ($typeName) - { - case 'newsfeed': - $table = Table::getInstance('NewsfeedTable', 'Joomla\\Component\\Newsfeeds\\Administrator\\Table\\'); - break; - - case 'category': - $table = Table::getInstance('Category'); - break; - } - - if (empty($table)) - { - return null; - } - - $table->load($id); - - return $table; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - if (in_array($typeName, $this->itemTypes)) - { - switch ($typeName) - { - case 'newsfeed': - $fields['title'] = 'a.name'; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['category'] = true; - $support['save2copy'] = true; - - $tables = array( - 'a' => '#__newsfeeds' - ); - $title = 'newsfeed'; - break; - - case 'category': - $fields['created_user_id'] = 'a.created_user_id'; - $fields['ordering'] = 'a.lft'; - $fields['level'] = 'a.level'; - $fields['catid'] = ''; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['level'] = true; - - $tables = array( - 'a' => '#__categories' - ); - - $title = 'category'; - break; - } - } - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_newsfeeds'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array('newsfeed', 'category'); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = true; + + /** + * Method to get the associations for a given item. + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 4.0.0 + */ + public function getAssociationsForItem($id = 0, $view = null) + { + return AssociationHelper::getAssociations($id, $view); + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociations($typeName, $id) + { + $type = $this->getType($typeName); + + $context = $this->extension . '.item'; + $catidField = 'catid'; + + if ($typeName === 'category') { + $context = 'com_categories.item'; + $catidField = ''; + } + + // Get the associations. + $associations = Associations::getAssociations( + $this->extension, + $type['tables']['a'], + $context, + $id, + 'id', + 'alias', + $catidField + ); + + return $associations; + } + + /** + * Get item information + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return Table|null + * + * @since 3.7.0 + */ + public function getItem($typeName, $id) + { + if (empty($id)) { + return null; + } + + $table = null; + + switch ($typeName) { + case 'newsfeed': + $table = Table::getInstance('NewsfeedTable', 'Joomla\\Component\\Newsfeeds\\Administrator\\Table\\'); + break; + + case 'category': + $table = Table::getInstance('Category'); + break; + } + + if (empty($table)) { + return null; + } + + $table->load($id); + + return $table; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + if (in_array($typeName, $this->itemTypes)) { + switch ($typeName) { + case 'newsfeed': + $fields['title'] = 'a.name'; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['category'] = true; + $support['save2copy'] = true; + + $tables = array( + 'a' => '#__newsfeeds' + ); + $title = 'newsfeed'; + break; + + case 'category': + $fields['created_user_id'] = 'a.created_user_id'; + $fields['ordering'] = 'a.lft'; + $fields['level'] = 'a.level'; + $fields['catid'] = ''; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['level'] = true; + + $tables = array( + 'a' => '#__categories' + ); + + $title = 'category'; + break; + } + } + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } } diff --git a/code/administrator/components/com_newsfeeds/src/Helper/NewsfeedsHelper.php b/code/administrator/components/com_newsfeeds/src/Helper/NewsfeedsHelper.php index 55dc24d5..dcdee332 100644 --- a/code/administrator/components/com_newsfeeds/src/Helper/NewsfeedsHelper.php +++ b/code/administrator/components/com_newsfeeds/src/Helper/NewsfeedsHelper.php @@ -1,4 +1,5 @@ getQuery(true); - $query->select( - [ - $db->quoteName('published', 'state'), - 'COUNT(*) AS ' . $db->quoteName('count'), - ] - ) - ->from($db->quoteName('#__newsfeeds')) - ->where($db->quoteName('catid') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER) - ->group($db->quoteName('state')); - $db->setQuery($query); - - foreach ($items as $item) - { - $item->count_trashed = 0; - $item->count_archived = 0; - $item->count_unpublished = 0; - $item->count_published = 0; - - $id = (int) $item->id; - $newfeeds = $db->loadObjectList(); - - foreach ($newfeeds as $newsfeed) - { - if ($newsfeed->state == 1) - { - $item->count_published = $newsfeed->count; - } - - if ($newsfeed->state == 0) - { - $item->count_unpublished = $newsfeed->count; - } - - if ($newsfeed->state == 2) - { - $item->count_archived = $newsfeed->count; - } - - if ($newsfeed->state == -2) - { - $item->count_trashed = $newsfeed->count; - } - } - } - - return $items; - } - - /** - * Adds Count Items for Tag Manager. - * - * @param \stdClass[] &$items The newsfeed tag objects - * @param string $extension The name of the active view. - * - * @return \stdClass[] - * - * @since 3.6 - */ - public static function countTagItems(&$items, $extension) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $parts = explode('.', $extension); - $section = null; - - if (count($parts) > 1) - { - $section = $parts[1]; - } - - $query->select( - [ - $db->quoteName('published', 'state'), - 'COUNT(*) AS ' . $db->quoteName('count'), - ] - ) - ->from($db->quoteName('#__contentitem_tag_map', 'ct')); - - if ($section === 'category') - { - $query->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('ct.content_item_id') . ' = ' . $db->quoteName('c.id')); - } - else - { - $query->join('LEFT', $db->quoteName('#__newsfeeds', 'c'), $db->quoteName('ct.content_item_id') . ' = ' . $db->quoteName('c.id')); - } - - $query->where( - [ - $db->quoteName('ct.tag_id') . ' = :id', - $db->quoteName('ct.type_alias') . ' = :extension', - ] - ) - ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':extension', $extension) - ->group($db->quoteName('state')); - - $db->setQuery($query); - - foreach ($items as $item) - { - $item->count_trashed = 0; - $item->count_archived = 0; - $item->count_unpublished = 0; - $item->count_published = 0; - - // Update ID used in database query. - $id = (int) $item->id; - $newsfeeds = $db->loadObjectList(); - - foreach ($newsfeeds as $newsfeed) - { - if ($newsfeed->state == 1) - { - $item->count_published = $newsfeed->count; - } - - if ($newsfeed->state == 0) - { - $item->count_unpublished = $newsfeed->count; - } - - if ($newsfeed->state == 2) - { - $item->count_archived = $newsfeed->count; - } - - if ($newsfeed->state == -2) - { - $item->count_trashed = $newsfeed->count; - } - } - } - - return $items; - } + /** + * Name of the extension + * + * @var string + */ + public static $extension = 'com_newsfeeds'; + + /** + * Adds Count Items for Category Manager. + * + * @param \stdClass[] &$items The banner category objects + * + * @return \stdClass[] + * + * @since 3.5 + */ + public static function countItems(&$items) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('published', 'state'), + 'COUNT(*) AS ' . $db->quoteName('count'), + ] + ) + ->from($db->quoteName('#__newsfeeds')) + ->where($db->quoteName('catid') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER) + ->group($db->quoteName('state')); + $db->setQuery($query); + + foreach ($items as $item) { + $item->count_trashed = 0; + $item->count_archived = 0; + $item->count_unpublished = 0; + $item->count_published = 0; + + $id = (int) $item->id; + $newfeeds = $db->loadObjectList(); + + foreach ($newfeeds as $newsfeed) { + if ($newsfeed->state == 1) { + $item->count_published = $newsfeed->count; + } + + if ($newsfeed->state == 0) { + $item->count_unpublished = $newsfeed->count; + } + + if ($newsfeed->state == 2) { + $item->count_archived = $newsfeed->count; + } + + if ($newsfeed->state == -2) { + $item->count_trashed = $newsfeed->count; + } + } + } + + return $items; + } + + /** + * Adds Count Items for Tag Manager. + * + * @param \stdClass[] &$items The newsfeed tag objects + * @param string $extension The name of the active view. + * + * @return \stdClass[] + * + * @since 3.6 + */ + public static function countTagItems(&$items, $extension) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $parts = explode('.', $extension); + $section = null; + + if (count($parts) > 1) { + $section = $parts[1]; + } + + $query->select( + [ + $db->quoteName('published', 'state'), + 'COUNT(*) AS ' . $db->quoteName('count'), + ] + ) + ->from($db->quoteName('#__contentitem_tag_map', 'ct')); + + if ($section === 'category') { + $query->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('ct.content_item_id') . ' = ' . $db->quoteName('c.id')); + } else { + $query->join('LEFT', $db->quoteName('#__newsfeeds', 'c'), $db->quoteName('ct.content_item_id') . ' = ' . $db->quoteName('c.id')); + } + + $query->where( + [ + $db->quoteName('ct.tag_id') . ' = :id', + $db->quoteName('ct.type_alias') . ' = :extension', + ] + ) + ->bind(':id', $id, ParameterType::INTEGER) + ->bind(':extension', $extension) + ->group($db->quoteName('state')); + + $db->setQuery($query); + + foreach ($items as $item) { + $item->count_trashed = 0; + $item->count_archived = 0; + $item->count_unpublished = 0; + $item->count_published = 0; + + // Update ID used in database query. + $id = (int) $item->id; + $newsfeeds = $db->loadObjectList(); + + foreach ($newsfeeds as $newsfeed) { + if ($newsfeed->state == 1) { + $item->count_published = $newsfeed->count; + } + + if ($newsfeed->state == 0) { + $item->count_unpublished = $newsfeed->count; + } + + if ($newsfeed->state == 2) { + $item->count_archived = $newsfeed->count; + } + + if ($newsfeed->state == -2) { + $item->count_trashed = $newsfeed->count; + } + } + } + + return $items; + } } diff --git a/code/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php b/code/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php index b971f8bd..2a5fef90 100644 --- a/code/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php +++ b/code/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php @@ -1,4 +1,5 @@ id) || $record->published != -2) - { - return false; - } - - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.delete', 'com_newsfeed.category.' . (int) $record->catid); - } - - return parent::canDelete($record); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.edit.state', 'com_newsfeeds.category.' . (int) $record->catid); - } - - return parent::canEditState($record); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_newsfeeds.newsfeed', 'newsfeed', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('publish_up', 'disabled', 'true'); - $form->setFieldAttribute('publish_down', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - $form->setFieldAttribute('publish_up', 'filter', 'unset'); - $form->setFieldAttribute('publish_down', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_newsfeeds.edit.newsfeed.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Prime some default values. - if ($this->getState('newsfeed.id') == 0) - { - $app = Factory::getApplication(); - $data->set('catid', $app->input->get('catid', $app->getUserState('com_newsfeeds.newsfeeds.filter.category_id'), 'int')); - } - } - - $this->preprocessData('com_newsfeeds.newsfeed', $data); - - return $data; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 3.0 - */ - public function save($data) - { - $input = Factory::getApplication()->input; - - // Create new category, if needed. - $createCategory = true; - - // If category ID is provided, check if it's valid. - if (is_numeric($data['catid']) && $data['catid']) - { - $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_newsfeeds'); - } - - // Save New Category - if ($createCategory && $this->canCreateCategory()) - { - $category = [ - // Remove #new# prefix, if exists. - 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], - 'parent_id' => 1, - 'extension' => 'com_newsfeeds', - 'language' => $data['language'], - 'published' => 1, - ]; - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - - // Create new category. - if (!$categoryModel->save($category)) - { - $this->setError($categoryModel->getError()); - - return false; - } - - // Get the Category ID. - $data['catid'] = $categoryModel->getState('category.id'); - } - - // Alter the name for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['name'] == $origTable->name) - { - list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); - $data['name'] = $name; - $data['alias'] = $alias; - } - else - { - if ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - $data['published'] = 0; - } - - return parent::save($data); - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - if ($item = parent::getItem($pk)) - { - // Convert the params field to an array. - $registry = new Registry($item->metadata); - $item->metadata = $registry->toArray(); - - // Convert the images field to an array. - $registry = new Registry($item->images); - $item->images = $registry->toArray(); - } - - // Load associated newsfeeds items - $assoc = Associations::isEnabled(); - - if ($assoc) - { - $item->associations = array(); - - if ($item->id != null) - { - $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $item->id); - - foreach ($associations as $tag => $association) - { - $item->associations[$tag] = $association->id; - } - } - } - - if (!empty($item->id)) - { - $item->tags = new TagsHelper; - $item->tags->getTagIds($item->id, 'com_newsfeeds.newsfeed'); - - // @todo: We probably don't need this in any client - but needs careful validation - if (!Factory::getApplication()->isClient('api')) - { - $item->metadata['tags'] = $item->tags; - } - } - - return $item; - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param \Joomla\CMS\Table\Table $table The table object - * - * @return void - */ - protected function prepareTable($table) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); - $table->alias = ApplicationHelper::stringURLSafe($table->alias, $table->language); - - if (empty($table->alias)) - { - $table->alias = ApplicationHelper::stringURLSafe($table->name, $table->language); - } - - if (empty($table->id)) - { - // Set the values - $table->created = $date->toSql(); - - // Set ordering to the last item if not set - if (empty($table->ordering)) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('MAX(' . $db->quoteName('ordering') . ')') - ->from($db->quoteName('#__newsfeeds')); - $db->setQuery($query); - $max = $db->loadResult(); - - $table->ordering = $max + 1; - } - } - else - { - // Set the values - $table->modified = $date->toSql(); - $table->modified_by = $user->get('id'); - } - - // Increment the content version number. - $table->version++; - } - - /** - * Method to change the published state of one or more records. - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function publish(&$pks, $value = 1) - { - $result = parent::publish($pks, $value); - - // Clean extra cache for newsfeeds - $this->cleanCache('feed_parser'); - - return $result; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('catid') . ' = ' . (int) $table->catid, - ]; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param Form $form The form object. - * @param array $data The data to be injected into the form - * @param string $group The plugin group to process - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - if ($this->canCreateCategory()) - { - $form->setFieldAttribute('catid', 'allowAdd', 'true'); - - // Add a prefix for categories created on the fly. - $form->setFieldAttribute('catid', 'customPrefix', '#new#'); - } - - // Association newsfeeds items - if (Associations::isEnabled()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (count($languages) > 1) - { - $addform = new \SimpleXMLElement('
'); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_newsfeed'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - } - - $form->load($addform, false); - } - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Is the user allowed to create an on the fly category? - * - * @return boolean - * - * @since 3.6.1 - */ - private function canCreateCategory() - { - return Factory::getUser()->authorise('core.create', 'com_newsfeeds'); - } + use VersionableModelTrait; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_newsfeeds.newsfeed'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_newsfeeds.item'; + + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_NEWSFEEDS'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.delete', 'com_newsfeed.category.' . (int) $record->catid); + } + + return parent::canDelete($record); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.edit.state', 'com_newsfeeds.category.' . (int) $record->catid); + } + + return parent::canEditState($record); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_newsfeeds.newsfeed', 'newsfeed', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_newsfeeds.edit.newsfeed.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Prime some default values. + if ($this->getState('newsfeed.id') == 0) { + $app = Factory::getApplication(); + $data->set('catid', $app->input->get('catid', $app->getUserState('com_newsfeeds.newsfeeds.filter.category_id'), 'int')); + } + } + + $this->preprocessData('com_newsfeeds.newsfeed', $data); + + return $data; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 3.0 + */ + public function save($data) + { + $input = Factory::getApplication()->input; + + // Create new category, if needed. + $createCategory = true; + + // If category ID is provided, check if it's valid. + if (is_numeric($data['catid']) && $data['catid']) { + $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_newsfeeds'); + } + + // Save New Category + if ($createCategory && $this->canCreateCategory()) { + $category = [ + // Remove #new# prefix, if exists. + 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], + 'parent_id' => 1, + 'extension' => 'com_newsfeeds', + 'language' => $data['language'], + 'published' => 1, + ]; + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + + // Create new category. + if (!$categoryModel->save($category)) { + $this->setError($categoryModel->getError()); + + return false; + } + + // Get the Category ID. + $data['catid'] = $categoryModel->getState('category.id'); + } + + // Alter the name for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['name'] == $origTable->name) { + list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); + $data['name'] = $name; + $data['alias'] = $alias; + } else { + if ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + $data['published'] = 0; + } + + return parent::save($data); + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + if ($item = parent::getItem($pk)) { + // Convert the params field to an array. + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + + // Convert the images field to an array. + $registry = new Registry($item->images); + $item->images = $registry->toArray(); + } + + // Load associated newsfeeds items + $assoc = Associations::isEnabled(); + + if ($assoc) { + $item->associations = array(); + + if ($item->id != null) { + $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $item->id); + + foreach ($associations as $tag => $association) { + $item->associations[$tag] = $association->id; + } + } + } + + if (!empty($item->id)) { + $item->tags = new TagsHelper(); + $item->tags->getTagIds($item->id, 'com_newsfeeds.newsfeed'); + + // @todo: We probably don't need this in any client - but needs careful validation + if (!Factory::getApplication()->isClient('api')) { + $item->metadata['tags'] = $item->tags; + } + } + + return $item; + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param \Joomla\CMS\Table\Table $table The table object + * + * @return void + */ + protected function prepareTable($table) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); + $table->alias = ApplicationHelper::stringURLSafe($table->alias, $table->language); + + if (empty($table->alias)) { + $table->alias = ApplicationHelper::stringURLSafe($table->name, $table->language); + } + + if (empty($table->id)) { + // Set the values + $table->created = $date->toSql(); + + // Set ordering to the last item if not set + if (empty($table->ordering)) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('MAX(' . $db->quoteName('ordering') . ')') + ->from($db->quoteName('#__newsfeeds')); + $db->setQuery($query); + $max = $db->loadResult(); + + $table->ordering = $max + 1; + } + } else { + // Set the values + $table->modified = $date->toSql(); + $table->modified_by = $user->get('id'); + } + + // Increment the content version number. + $table->version++; + } + + /** + * Method to change the published state of one or more records. + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function publish(&$pks, $value = 1) + { + $result = parent::publish($pks, $value); + + // Clean extra cache for newsfeeds + $this->cleanCache('feed_parser'); + + return $result; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, + ]; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param Form $form The form object. + * @param array $data The data to be injected into the form + * @param string $group The plugin group to process + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + if ($this->canCreateCategory()) { + $form->setFieldAttribute('catid', 'allowAdd', 'true'); + + // Add a prefix for categories created on the fly. + $form->setFieldAttribute('catid', 'customPrefix', '#new#'); + } + + // Association newsfeeds items + if (Associations::isEnabled()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_newsfeed'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + } + + $form->load($addform, false); + } + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Is the user allowed to create an on the fly category? + * + * @return boolean + * + * @since 3.6.1 + */ + private function canCreateCategory() + { + return Factory::getUser()->authorise('core.create', 'com_newsfeeds'); + } } diff --git a/code/administrator/components/com_newsfeeds/src/Model/NewsfeedsModel.php b/code/administrator/components/com_newsfeeds/src/Model/NewsfeedsModel.php index f4b0a7a0..b86f2631 100644 --- a/code/administrator/components/com_newsfeeds/src/Model/NewsfeedsModel.php +++ b/code/administrator/components/com_newsfeeds/src/Model/NewsfeedsModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - // Load the parameters. - $params = ComponentHelper::getParams('com_newsfeeds'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.category_id'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . $this->getState('filter.level'); - $id .= ':' . serialize($this->getState('filter.tag')); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.name'), - $db->quoteName('a.alias'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.catid'), - $db->quoteName('a.numarticles'), - $db->quoteName('a.cache_time'), - $db->quoteName('a.created_by'), - $db->quoteName('a.published'), - $db->quoteName('a.access'), - $db->quoteName('a.ordering'), - $db->quoteName('a.language'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - ] - ) - ) - ->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('uc.name', 'editor'), - $db->quoteName('ag.title', 'access_level'), - $db->quoteName('c.title', 'category_title'), - ] - ) - ->from($db->quoteName('#__newsfeeds', 'a')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')); - - // Join over the associations. - if (Associations::isEnabled()) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') - ->from($db->quoteName('#__associations', 'asso1')) - ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) - ->where( - [ - $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('asso1.context') . ' = ' . $db->quote('com_newsfeeds.item'), - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); - } - - // Filter by access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $query->whereIn($db->quoteName('a.access'), $user->getAuthorisedViewLevels()); - } - - // Filter by published state. - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.published') . ' IN (0, 1)'); - } - - // Filter by category. - $categoryId = $this->getState('filter.category_id'); - - if (is_numeric($categoryId)) - { - $categoryId = (int) $categoryId; - $query->where($db->quoteName('a.catid') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by search in title - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('a.name') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Filter by a single or group of tags. - $tag = $this->getState('filter.tag'); - - // Run simplified query when filtering by one tag. - if (\is_array($tag) && \count($tag) === 1) - { - $tag = $tag[0]; - } - - if ($tag && \is_array($tag)) - { - $tag = ArrayHelper::toInteger($tag); - - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', - $db->quoteName('type_alias') . ' = ' . $db->quote('com_newsfeeds.newsfeed'), - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ); - } - elseif ($tag = (int) $tag) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->where( - [ - $db->quoteName('tagmap.tag_id') . ' = :tag', - $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_newsfeeds.newsfeed'), - ] - ) - ->bind(':tag', $tag, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'a.name'); - $orderDirn = $this->state->get('list.direction', 'ASC'); - - if ($orderCol == 'a.ordering' || $orderCol == 'category_title') - { - $ordering = [ - $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), - $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), - ]; - } - else - { - $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); - } - - $query->order($ordering); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_id', 'category_title', + 'published', 'a.published', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'created_by', 'a.created_by', + 'ordering', 'a.ordering', + 'language', 'a.language', 'language_title', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'cache_time', 'a.cache_time', + 'numarticles', + 'tag', + 'level', 'c.level', + 'tag', + ); + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + // Load the parameters. + $params = ComponentHelper::getParams('com_newsfeeds'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.category_id'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . $this->getState('filter.level'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.name'), + $db->quoteName('a.alias'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.catid'), + $db->quoteName('a.numarticles'), + $db->quoteName('a.cache_time'), + $db->quoteName('a.created_by'), + $db->quoteName('a.published'), + $db->quoteName('a.access'), + $db->quoteName('a.ordering'), + $db->quoteName('a.language'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + ] + ) + ) + ->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('uc.name', 'editor'), + $db->quoteName('ag.title', 'access_level'), + $db->quoteName('c.title', 'category_title'), + ] + ) + ->from($db->quoteName('#__newsfeeds', 'a')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')); + + // Join over the associations. + if (Associations::isEnabled()) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') + ->from($db->quoteName('#__associations', 'asso1')) + ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) + ->where( + [ + $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('asso1.context') . ' = ' . $db->quote('com_newsfeeds.item'), + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); + } + + // Filter by access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $query->whereIn($db->quoteName('a.access'), $user->getAuthorisedViewLevels()); + } + + // Filter by published state. + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.published') . ' IN (0, 1)'); + } + + // Filter by category. + $categoryId = $this->getState('filter.category_id'); + + if (is_numeric($categoryId)) { + $categoryId = (int) $categoryId; + $query->where($db->quoteName('a.catid') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by search in title + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('a.name') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Filter by a single or group of tags. + $tag = $this->getState('filter.tag'); + + // Run simplified query when filtering by one tag. + if (\is_array($tag) && \count($tag) === 1) { + $tag = $tag[0]; + } + + if ($tag && \is_array($tag)) { + $tag = ArrayHelper::toInteger($tag); + + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', + $db->quoteName('type_alias') . ' = ' . $db->quote('com_newsfeeds.newsfeed'), + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ); + } elseif ($tag = (int) $tag) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->where( + [ + $db->quoteName('tagmap.tag_id') . ' = :tag', + $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_newsfeeds.newsfeed'), + ] + ) + ->bind(':tag', $tag, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'a.name'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + + if ($orderCol == 'a.ordering' || $orderCol == 'category_title') { + $ordering = [ + $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), + $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), + ]; + } else { + $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); + } + + $query->order($ordering); + + return $query; + } } diff --git a/code/administrator/components/com_newsfeeds/src/Service/HTML/AdministratorService.php b/code/administrator/components/com_newsfeeds/src/Service/HTML/AdministratorService.php index 0b620ba5..31d0373a 100644 --- a/code/administrator/components/com_newsfeeds/src/Service/HTML/AdministratorService.php +++ b/code/administrator/components/com_newsfeeds/src/Service/HTML/AdministratorService.php @@ -1,4 +1,5 @@ $associated) - { - $associations[$tag] = (int) $associated->id; - } + // Get the associations + if ($associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $newsfeedid)) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } - // Get the associated newsfeed items - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query - ->select( - [ - $db->quoteName('c.id'), - $db->quoteName('c.name', 'title'), - $db->quoteName('cat.title', 'category_title'), - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('l.lang_code'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__newsfeeds', 'c')) - ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) - ->where( - [ - $db->quoteName('c.id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')', - $db->quoteName('c.id') . ' != :id', - ] - ) - ->bind(':id', $newsfeedid, ParameterType::INTEGER); - $db->setQuery($query); + // Get the associated newsfeed items + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query + ->select( + [ + $db->quoteName('c.id'), + $db->quoteName('c.name', 'title'), + $db->quoteName('cat.title', 'category_title'), + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('l.lang_code'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__newsfeeds', 'c')) + ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) + ->where( + [ + $db->quoteName('c.id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')', + $db->quoteName('c.id') . ' != :id', + ] + ) + ->bind(':id', $newsfeedid, ParameterType::INTEGER); + $db->setQuery($query); - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_newsfeeds&task=newsfeed.edit&id=' . (int) $item->id); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); - $classes = 'badge bg-secondary'; + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_newsfeeds&task=newsfeed.edit&id=' . (int) $item->id); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); + $classes = 'badge bg-secondary'; - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } - $html = LayoutHelper::render('joomla.content.associations', $items); - } + $html = LayoutHelper::render('joomla.content.associations', $items); + } - return $html; - } + return $html; + } } diff --git a/code/administrator/components/com_newsfeeds/src/Table/NewsfeedTable.php b/code/administrator/components/com_newsfeeds/src/Table/NewsfeedTable.php index 6d54fe1a..0695082a 100644 --- a/code/administrator/components/com_newsfeeds/src/Table/NewsfeedTable.php +++ b/code/administrator/components/com_newsfeeds/src/Table/NewsfeedTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_newsfeeds.newsfeed'; - parent::__construct('#__newsfeeds', 'id', $db); - $this->setColumnAlias('title', 'name'); - } - - /** - * Overloaded check method to ensure data integrity. - * - * @return boolean True on success. - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Check for valid name. - if (trim($this->name) == '') - { - $this->setError(Text::_('COM_NEWSFEEDS_WARNING_PROVIDE_VALID_NAME')); - - return false; - } - - if (empty($this->alias)) - { - $this->alias = $this->name; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); - - if (trim(str_replace('-', '', $this->alias)) == '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - // Check for a valid category. - if (!$this->catid = (int) $this->catid) - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); - - return false; - } - - // Check the publish down date is not earlier than publish up. - if ((int) $this->publish_down > 0 && $this->publish_down < $this->publish_up) - { - $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); - - return false; - } - - // Clean up description -- eliminate quotes and <> brackets - if (!empty($this->metadesc)) - { - // Only process if not empty - $bad_characters = array("\"", '<', '>'); - $this->metadesc = StringHelper::str_ireplace($bad_characters, '', $this->metadesc); - } - - if (is_null($this->hits)) - { - $this->hits = 0; - } - - return true; - } - - /** - * Overridden \JTable::store to set modified data. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = $date->toSql(); - } - - if ($this->id) - { - // Existing item - $this->modified_by = $user->get('id'); - $this->modified = $date->toSql(); - } - else - { - // Field created_by can be set by the user, so we don't touch it if it's set. - if (empty($this->created_by)) - { - $this->created_by = $user->get('id'); - } - - if (!(int) $this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - } - - // Set publish_up, publish_down to null if not set - if (!$this->publish_up) - { - $this->publish_up = null; - } - - if (!$this->publish_down) - { - $this->publish_down = null; - } - - // Verify that the alias is unique - $table = Table::getInstance('NewsfeedTable', __NAMESPACE__ . '\\', array('dbo' => $this->_db)); - - if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_NEWSFEEDS_ERROR_UNIQUE_ALIAS')); - - return false; - } - - // Save links as punycode. - $this->link = PunycodeHelper::urlToPunycode($this->link); - - return parent::store($updateNulls); - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + use TaggableTableTrait; + + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Ensure the params, metadata and images are json encoded in the bind method + * + * @var array + * @since 3.3 + */ + protected $_jsonEncode = array('params', 'metadata', 'images'); + + /** + * Constructor + * + * @param DatabaseDriver $db A database connector object + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_newsfeeds.newsfeed'; + parent::__construct('#__newsfeeds', 'id', $db); + $this->setColumnAlias('title', 'name'); + } + + /** + * Overloaded check method to ensure data integrity. + * + * @return boolean True on success. + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Check for valid name. + if (trim($this->name) == '') { + $this->setError(Text::_('COM_NEWSFEEDS_WARNING_PROVIDE_VALID_NAME')); + + return false; + } + + if (empty($this->alias)) { + $this->alias = $this->name; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); + + if (trim(str_replace('-', '', $this->alias)) == '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + // Check for a valid category. + if (!$this->catid = (int) $this->catid) { + $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); + + return false; + } + + // Check the publish down date is not earlier than publish up. + if ((int) $this->publish_down > 0 && $this->publish_down < $this->publish_up) { + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); + + return false; + } + + // Clean up description -- eliminate quotes and <> brackets + if (!empty($this->metadesc)) { + // Only process if not empty + $bad_characters = array("\"", '<', '>'); + $this->metadesc = StringHelper::str_ireplace($bad_characters, '', $this->metadesc); + } + + if (is_null($this->hits)) { + $this->hits = 0; + } + + return true; + } + + /** + * Overridden \JTable::store to set modified data. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = $date->toSql(); + } + + if ($this->id) { + // Existing item + $this->modified_by = $user->get('id'); + $this->modified = $date->toSql(); + } else { + // Field created_by can be set by the user, so we don't touch it if it's set. + if (empty($this->created_by)) { + $this->created_by = $user->get('id'); + } + + if (!(int) $this->modified) { + $this->modified = $this->created; + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + } + + // Set publish_up, publish_down to null if not set + if (!$this->publish_up) { + $this->publish_up = null; + } + + if (!$this->publish_down) { + $this->publish_down = null; + } + + // Verify that the alias is unique + $table = Table::getInstance('NewsfeedTable', __NAMESPACE__ . '\\', array('dbo' => $this->_db)); + + if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_NEWSFEEDS_ERROR_UNIQUE_ALIAS')); + + return false; + } + + // Save links as punycode. + $this->link = PunycodeHelper::urlToPunycode($this->link); + + return parent::store($updateNulls); + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/code/administrator/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php b/code/administrator/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php index eb84cfcc..68651b6b 100644 --- a/code/administrator/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php +++ b/code/administrator/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); - - // Only allow to select tags with All language or with the forced language. - $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = Factory::getUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_newsfeeds', 'category', $this->item->catid); - - $title = $isNew ? Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEED_NEW') : Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEED_EDIT'); - ToolbarHelper::title($title, 'rss newsfeeds'); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if (!$checkedOut && ($canDo->get('core.edit') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0)) - { - ToolbarHelper::apply('newsfeed.apply'); - - $toolbarButtons[] = ['save', 'newsfeed.save']; - } - - if (!$checkedOut && count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) - { - $toolbarButtons[] = ['save2new', 'newsfeed.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'newsfeed.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('newsfeed.cancel'); - } - else - { - ToolbarHelper::cancel('newsfeed.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) - { - ToolbarHelper::versions('com_newsfeeds.newsfeed', $this->item->id); - } - } - - if (!$isNew && Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) - { - ToolbarHelper::custom('newsfeed.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('News_Feeds:_New_or_Edit'); - } + /** + * The item object for the newsfeed + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 1.6 + */ + protected $item; + + /** + * The form object for the newsfeed + * + * @var \Joomla\CMS\Form\Form + * + * @since 1.6 + */ + protected $form; + + /** + * The model state of the newsfeed + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 1.6 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_newsfeeds', 'category', $this->item->catid); + + $title = $isNew ? Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEED_NEW') : Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEED_EDIT'); + ToolbarHelper::title($title, 'rss newsfeeds'); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if (!$checkedOut && ($canDo->get('core.edit') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0)) { + ToolbarHelper::apply('newsfeed.apply'); + + $toolbarButtons[] = ['save', 'newsfeed.save']; + } + + if (!$checkedOut && count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) { + $toolbarButtons[] = ['save2new', 'newsfeed.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'newsfeed.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('newsfeed.cancel'); + } else { + ToolbarHelper::cancel('newsfeed.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) { + ToolbarHelper::versions('com_newsfeeds.newsfeed', $this->item->id); + } + } + + if (!$isNew && Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + ToolbarHelper::custom('newsfeed.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('News_Feeds:_New_or_Edit'); + } } diff --git a/code/administrator/components/com_newsfeeds/src/View/Newsfeeds/HtmlView.php b/code/administrator/components/com_newsfeeds/src/View/Newsfeeds/HtmlView.php index 0fbe696d..106ed99b 100644 --- a/code/administrator/components/com_newsfeeds/src/View/Newsfeeds/HtmlView.php +++ b/code/administrator/components/com_newsfeeds/src/View/Newsfeeds/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // We don't need toolbar in the modal layout. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In article associations modal we need to remove language filter if forcing a language. - // We also need to change the category filter to show show categories with All or the forced language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - - // One last changes needed is to change the category filter to just show categories with All language or with the forced language. - $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $canDo = ContentHelper::getActions('com_newsfeeds', 'category', $state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEEDS'), 'rss newsfeeds'); - - if ($canDo->get('core.create') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) - { - $toolbar->addNew('newsfeed.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('newsfeeds.publish')->listCheck(true); - $childBar->unpublish('newsfeeds.unpublish')->listCheck(true); - $childBar->archive('newsfeeds.archive')->listCheck(true); - - if ($user->authorise('core.admin')) - { - $childBar->checkin('newsfeeds.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') != -2) - { - $childBar->trash('newsfeeds.trash')->listCheck(true); - } - - // Add a batch button - if ($user->authorise('core.create', 'com_newsfeeds') - && $user->authorise('core.edit', 'com_newsfeeds') - && $user->authorise('core.edit.state', 'com_newsfeeds')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('newsfeeds.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($user->authorise('core.admin', 'com_newsfeeds') || $user->authorise('core.options', 'com_newsfeeds')) - { - $toolbar->preferences('com_newsfeeds'); - } - - $toolbar->help('News_Feeds'); - } + /** + * The list of newsfeeds + * + * @var CMSObject + * + * @since 1.6 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + * + * @since 1.6 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We don't need toolbar in the modal layout. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In article associations modal we need to remove language filter if forcing a language. + // We also need to change the category filter to show show categories with All or the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + + // One last changes needed is to change the category filter to just show categories with All language or with the forced language. + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $canDo = ContentHelper::getActions('com_newsfeeds', 'category', $state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEEDS'), 'rss newsfeeds'); + + if ($canDo->get('core.create') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) { + $toolbar->addNew('newsfeed.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('newsfeeds.publish')->listCheck(true); + $childBar->unpublish('newsfeeds.unpublish')->listCheck(true); + $childBar->archive('newsfeeds.archive')->listCheck(true); + + if ($user->authorise('core.admin')) { + $childBar->checkin('newsfeeds.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') != -2) { + $childBar->trash('newsfeeds.trash')->listCheck(true); + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_newsfeeds') + && $user->authorise('core.edit', 'com_newsfeeds') + && $user->authorise('core.edit.state', 'com_newsfeeds') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('newsfeeds.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_newsfeeds') || $user->authorise('core.options', 'com_newsfeeds')) { + $toolbar->preferences('com_newsfeeds'); + } + + $toolbar->help('News_Feeds'); + } } diff --git a/code/administrator/components/com_newsfeeds/tmpl/newsfeed/edit.php b/code/administrator/components/com_newsfeeds/tmpl/newsfeed/edit.php index 34dcd86d..dd09e694 100644 --- a/code/administrator/components/com_newsfeeds/tmpl/newsfeed/edit.php +++ b/code/administrator/components/com_newsfeeds/tmpl/newsfeed/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $input = $app->input; @@ -38,82 +39,82 @@ - - -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - item->id) ? Text::_('COM_NEWSFEEDS_NEW_NEWSFEED') : Text::_('COM_NEWSFEEDS_EDIT_NEWSFEED')); ?> -
-
-
- form->renderField('link'); ?> - form->renderField('description'); ?> -
-
-
- -
-
- - - -
-
-
- -
- form->getGroup('images') as $field) : ?> - renderField(); ?> - -
-
-
-
- loadTemplate('display'); ?> -
-
- - - - - -
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- - - - -
- -
- -
-
- - - - - - -
- - - + + +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + item->id) ? Text::_('COM_NEWSFEEDS_NEW_NEWSFEED') : Text::_('COM_NEWSFEEDS_EDIT_NEWSFEED')); ?> +
+
+
+ form->renderField('link'); ?> + form->renderField('description'); ?> +
+
+
+ +
+
+ + + +
+
+
+ +
+ form->getGroup('images') as $field) : ?> + renderField(); ?> + +
+
+
+
+ loadTemplate('display'); ?> +
+
+ + + + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + + + +
+ +
+ +
+
+ + + + + + +
+ + + diff --git a/code/administrator/components/com_newsfeeds/tmpl/newsfeed/edit_display.php b/code/administrator/components/com_newsfeeds/tmpl/newsfeed/edit_display.php index 9ba2e1a8..889b0677 100644 --- a/code/administrator/components/com_newsfeeds/tmpl/newsfeed/edit_display.php +++ b/code/administrator/components/com_newsfeeds/tmpl/newsfeed/edit_display.php @@ -1,4 +1,5 @@
- -
- -
+ +
+ +
diff --git a/code/administrator/components/com_newsfeeds/tmpl/newsfeed/modal.php b/code/administrator/components/com_newsfeeds/tmpl/newsfeed/modal.php index fcf15353..91f2a029 100644 --- a/code/administrator/components/com_newsfeeds/tmpl/newsfeed/modal.php +++ b/code/administrator/components/com_newsfeeds/tmpl/newsfeed/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/default.php b/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/default.php index a2b18335..237c18ad 100644 --- a/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/default.php +++ b/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -28,172 +30,173 @@ $saveOrder = $listOrder == 'a.ordering'; $assoc = Associations::isEnabled(); -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_newsfeeds&task=newsfeeds.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_newsfeeds&task=newsfeeds.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
-
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $ordering = ($listOrder == 'a.ordering'); - $canCreate = $user->authorise('core.create', 'com_newsfeeds.category.' . $item->catid); - $canEdit = $user->authorise('core.edit', 'com_newsfeeds.category.' . $item->catid); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', 'com_newsfeeds.category.' . $item->catid) && $item->created_by == $user->id; - $canChange = $user->authorise('core.edit.state', 'com_newsfeeds.category.' . $item->catid) && $canCheckin; - ?> - - - - - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->name); ?> - - - - - - - - - - published, $i, 'newsfeeds.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'newsfeeds.', $canCheckin); ?> - - - - escape($item->name); ?> - - escape($item->name); ?> - -
- escape($item->alias)); ?> -
-
- escape($item->category_title); ?> -
-
-
- escape($item->access_level); ?> - - numarticles; ?> - - cache_time; ?> - - association) : ?> - id); ?> - - - - - id; ?> -
+
+
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $ordering = ($listOrder == 'a.ordering'); + $canCreate = $user->authorise('core.create', 'com_newsfeeds.category.' . $item->catid); + $canEdit = $user->authorise('core.edit', 'com_newsfeeds.category.' . $item->catid); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', 'com_newsfeeds.category.' . $item->catid) && $item->created_by == $user->id; + $canChange = $user->authorise('core.edit.state', 'com_newsfeeds.category.' . $item->catid) && $canCheckin; + ?> + + + + + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->name); ?> + + + + + + + + + + published, $i, 'newsfeeds.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'newsfeeds.', $canCheckin); ?> + + + + escape($item->name); ?> + + escape($item->name); ?> + +
+ escape($item->alias)); ?> +
+
+ escape($item->category_title); ?> +
+
+
+ escape($item->access_level); ?> + + numarticles; ?> + + cache_time; ?> + + association) : ?> + id); ?> + + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_newsfeeds') - && $user->authorise('core.edit', 'com_newsfeeds') - && $user->authorise('core.edit.state', 'com_newsfeeds')) : ?> - Text::_('COM_NEWSFEEDS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - - - - -
-
-
+ + authorise('core.create', 'com_newsfeeds') + && $user->authorise('core.edit', 'com_newsfeeds') + && $user->authorise('core.edit.state', 'com_newsfeeds') + ) : ?> + Text::_('COM_NEWSFEEDS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + +
+
+
diff --git a/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_body.php b/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_body.php index 71e37298..0578eea2 100644 --- a/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_body.php +++ b/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Multilanguage; @@ -15,32 +17,32 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
-
- = 0) : ?> -
-
- 'com_newsfeeds']); ?> -
-
- -
-
- -
-
-
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ 'com_newsfeeds']); ?> +
+
+ +
+
+ +
+
+
diff --git a/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_footer.php b/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_footer.php index b8394dfd..f43ed8b7 100644 --- a/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_footer.php +++ b/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_footer.php @@ -1,4 +1,5 @@ diff --git a/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/emptystate.php b/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/emptystate.php index 64c1fd1b..234282c3 100644 --- a/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/emptystate.php +++ b/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/emptystate.php @@ -1,4 +1,5 @@ 'COM_NEWSFEEDS', - 'formURL' => 'index.php?option=com_newsfeeds&view=newsfeeds', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:News_Feeds', - 'icon' => 'icon-rss newsfeeds', + 'textPrefix' => 'COM_NEWSFEEDS', + 'formURL' => 'index.php?option=com_newsfeeds&view=newsfeeds', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:News_Feeds', + 'icon' => 'icon-rss newsfeeds', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_newsfeeds') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_newsfeeds&task=newsfeed.add'; +if ($user->authorise('core.create', 'com_newsfeeds') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_newsfeeds&task=newsfeed.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/modal.php b/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/modal.php index 6ab58193..c99d6372 100644 --- a/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/modal.php +++ b/code/administrator/components/com_newsfeeds/tmpl/newsfeeds/modal.php @@ -1,4 +1,5 @@
-
+ - $this)); ?> + $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - ?> - items as $i => $item) : ?> - language && $multilang) - { - $tag = strlen($item->language); - if ($tag == 5) - { - $lang = substr($item->language, 0, 2); - } - elseif ($tag == 6) - { - $lang = substr($item->language, 0, 3); - } - else { - $lang = ''; - } - } - elseif (!$multilang) - { - $lang = ''; - } - ?> - - - - - - - - - - - -
- , - , - -
- - - - - - - - - -
- - - - - - escape($item->name); ?> -
- escape($item->category_title); ?> -
-
- escape($item->access_level); ?> - - - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + ?> + items as $i => $item) : ?> + language && $multilang) { + $tag = strlen($item->language); + if ($tag == 5) { + $lang = substr($item->language, 0, 2); + } elseif ($tag == 6) { + $lang = substr($item->language, 0, 3); + } else { + $lang = ''; + } + } elseif (!$multilang) { + $lang = ''; + } + ?> + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ + + + + + escape($item->name); ?> +
+ escape($item->category_title); ?> +
+
+ escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - + + + + -
+
diff --git a/code/administrator/components/com_plugins/helpers/plugins.php b/code/administrator/components/com_plugins/helpers/plugins.php index a2da98ba..f7861e16 100644 --- a/code/administrator/components/com_plugins/helpers/plugins.php +++ b/code/administrator/components/com_plugins/helpers/plugins.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Plugins component helper. @@ -18,5 +23,4 @@ */ class PluginsHelper extends \Joomla\Component\Plugins\Administrator\Helper\PluginsHelper { - } diff --git a/code/administrator/components/com_plugins/plugins.xml b/code/administrator/components/com_plugins/plugins.xml index d3d8f971..dbb7c5d3 100644 --- a/code/administrator/components/com_plugins/plugins.xml +++ b/code/administrator/components/com_plugins/plugins.xml @@ -2,7 +2,7 @@ com_plugins Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_plugins/services/provider.php b/code/administrator/components/com_plugins/services/provider.php index a36ebdc9..44576bab 100644 --- a/code/administrator/components/com_plugins/services/provider.php +++ b/code/administrator/components/com_plugins/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Plugins')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Plugins')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Plugins')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Plugins')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_plugins/src/Controller/DisplayController.php b/code/administrator/components/com_plugins/src/Controller/DisplayController.php index bf6cbc91..5cfc7ab9 100644 --- a/code/administrator/components/com_plugins/src/Controller/DisplayController.php +++ b/code/administrator/components/com_plugins/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'plugins'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('extension_id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'plugins'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('extension_id'); - // Check for edit form. - if ($view == 'plugin' && $layout == 'edit' && !$this->checkEditId('com_plugins.edit.plugin', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'plugin' && $layout == 'edit' && !$this->checkEditId('com_plugins.edit.plugin', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_plugins&view=plugins', false)); + $this->setRedirect(Route::_('index.php?option=com_plugins&view=plugins', false)); - return false; - } + return false; + } - parent::display(); - } + parent::display(); + } } diff --git a/code/administrator/components/com_plugins/src/Controller/PluginController.php b/code/administrator/components/com_plugins/src/Controller/PluginController.php index 8585648f..d2edfbf9 100644 --- a/code/administrator/components/com_plugins/src/Controller/PluginController.php +++ b/code/administrator/components/com_plugins/src/Controller/PluginController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Plugins\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Plugins\Administrator\Controller; use Joomla\CMS\MVC\Controller\FormController; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Plugin controller class. * diff --git a/code/administrator/components/com_plugins/src/Controller/PluginsController.php b/code/administrator/components/com_plugins/src/Controller/PluginsController.php index aa0883e1..67bf3a12 100644 --- a/code/administrator/components/com_plugins/src/Controller/PluginsController.php +++ b/code/administrator/components/com_plugins/src/Controller/PluginsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Plugin', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } - /** - * Method to get the number of activated plugins - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Plugins'); + /** + * Method to get the number of activated plugins + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Plugins'); - $model->setState('filter.enabled', 1); + $model->setState('filter.enabled', 1); - $amount = (int) $model->getTotal(); + $amount = (int) $model->getTotal(); - $result = []; + $result = []; - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_PLUGINS_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_PLUGINS_N_QUICKICON', $amount); + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_PLUGINS_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_PLUGINS_N_QUICKICON', $amount); - echo new JsonResponse($result); - } + echo new JsonResponse($result); + } } diff --git a/code/administrator/components/com_plugins/src/Field/PluginElementField.php b/code/administrator/components/com_plugins/src/Field/PluginElementField.php index 4f50bc7a..10c73d76 100644 --- a/code/administrator/components/com_plugins/src/Field/PluginElementField.php +++ b/code/administrator/components/com_plugins/src/Field/PluginElementField.php @@ -1,4 +1,5 @@ form->getValue('folder'); + /** + * Builds the query for the ordering list. + * + * @return \Joomla\Database\DatabaseQuery The query for the ordering form field. + */ + protected function getQuery() + { + $db = $this->getDatabase(); + $folder = $this->form->getValue('folder'); - // Build the query for the ordering list. - $query = $db->getQuery(true) - ->select( - array( - $db->quoteName('ordering', 'value'), - $db->quoteName('name', 'text'), - $db->quoteName('type'), - $db->quote('folder'), - $db->quote('extension_id') - ) - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = :folder') - ->order($db->quoteName('ordering')) - ->bind(':folder', $folder); + // Build the query for the ordering list. + $query = $db->getQuery(true) + ->select( + array( + $db->quoteName('ordering', 'value'), + $db->quoteName('name', 'text'), + $db->quoteName('type'), + $db->quote('folder'), + $db->quote('extension_id') + ) + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = :folder') + ->order($db->quoteName('ordering')) + ->bind(':folder', $folder); - return $query; - } + return $query; + } - /** - * Retrieves the current Item's Id. - * - * @return integer The current item ID. - */ - protected function getItemId() - { - return (int) $this->form->getValue('extension_id'); - } + /** + * Retrieves the current Item's Id. + * + * @return integer The current item ID. + */ + protected function getItemId() + { + return (int) $this->form->getValue('extension_id'); + } } diff --git a/code/administrator/components/com_plugins/src/Helper/PluginsHelper.php b/code/administrator/components/com_plugins/src/Helper/PluginsHelper.php index 39bcf135..54f20a02 100644 --- a/code/administrator/components/com_plugins/src/Helper/PluginsHelper.php +++ b/code/administrator/components/com_plugins/src/Helper/PluginsHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT(folder) AS value, folder AS text') - ->from('#__extensions') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->order('folder'); - - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - return $options; - } - - /** - * Returns a list of elements filter options. - * - * @return string The HTML code for the select tag - */ - public static function elementOptions() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT(element) AS value, element AS text') - ->from('#__extensions') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->order('element'); - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - return $options; - } - - /** - * Parse the template file. - * - * @param string $templateBaseDir Base path to the template directory. - * @param string $templateDir Template directory. - * - * @return CMSObject|bool - */ - public function parseXMLTemplateFile($templateBaseDir, $templateDir) - { - $data = new CMSObject; - - // Check of the xml file exists. - $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); - - if (is_file($filePath)) - { - $xml = Installer::parseXMLInstallFile($filePath); - - if ($xml['type'] != 'template') - { - return false; - } - - foreach ($xml as $key => $value) - { - $data->set($key, $value); - } - } - - return $data; - } + public static $extension = 'com_plugins'; + + /** + * Returns an array of standard published state filter options. + * + * @return array The HTML code for the select tag + */ + public static function publishedOptions() + { + // Build the active state filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', 'JENABLED'); + $options[] = HTMLHelper::_('select.option', '0', 'JDISABLED'); + + return $options; + } + + /** + * Returns a list of folders filter options. + * + * @return string The HTML code for the select tag + */ + public static function folderOptions() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT(folder) AS value, folder AS text') + ->from('#__extensions') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->order('folder'); + + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + return $options; + } + + /** + * Returns a list of elements filter options. + * + * @return string The HTML code for the select tag + */ + public static function elementOptions() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT(element) AS value, element AS text') + ->from('#__extensions') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->order('element'); + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + return $options; + } + + /** + * Parse the template file. + * + * @param string $templateBaseDir Base path to the template directory. + * @param string $templateDir Template directory. + * + * @return CMSObject|bool + */ + public function parseXMLTemplateFile($templateBaseDir, $templateDir) + { + $data = new CMSObject(); + + // Check of the xml file exists. + $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); + + if (is_file($filePath)) { + $xml = Installer::parseXMLInstallFile($filePath); + + if ($xml['type'] != 'template') { + return false; + } + + foreach ($xml as $key => $value) { + $data->set($key, $value); + } + } + + return $data; + } } diff --git a/code/administrator/components/com_plugins/src/Model/PluginModel.php b/code/administrator/components/com_plugins/src/Model/PluginModel.php index 1cb39063..d5c4c8f5 100644 --- a/code/administrator/components/com_plugins/src/Model/PluginModel.php +++ b/code/administrator/components/com_plugins/src/Model/PluginModel.php @@ -1,4 +1,5 @@ 'onExtensionAfterSave', - 'event_before_save' => 'onExtensionBeforeSave', - 'events_map' => array( - 'save' => 'extension' - ) - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure. - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // The folder and element vars are passed when saving the form. - if (empty($data)) - { - $item = $this->getItem(); - $folder = $item->folder; - $element = $item->element; - } - else - { - $folder = ArrayHelper::getValue($data, 'folder', '', 'cmd'); - $element = ArrayHelper::getValue($data, 'element', '', 'cmd'); - } - - // Add the default fields directory - Form::addFieldPath(JPATH_PLUGINS . '/' . $folder . '/' . $element . '/field'); - - // These variables are used to add data from the plugin XML files. - $this->setState('item.folder', $folder); - $this->setState('item.element', $element); - - // Get the form. - $form = $this->loadForm('com_plugins.plugin', 'plugin', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('enabled', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('enabled', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_plugins.edit.plugin.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_plugins.plugin', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('plugin.id'); - - if (!isset($this->_cache[$pk])) - { - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load(array('extension_id' => $pk, 'type' => 'plugin')); - - // Check for a table object error. - if ($return === false) - { - return false; - } - - // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. - $properties = $table->getProperties(1); - $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); - - // Convert the params field to an array. - $registry = new Registry($table->params); - $this->_cache[$pk]->params = $registry->toArray(); - - // Get the plugin XML. - $path = Path::clean(JPATH_PLUGINS . '/' . $table->folder . '/' . $table->element . '/' . $table->element . '.xml'); - - if (file_exists($path)) - { - $this->_cache[$pk]->xml = simplexml_load_file($path); - } - else - { - $this->_cache[$pk]->xml = null; - } - } - - return $this->_cache[$pk]; - } - - /** - * Returns a reference to the Table object, always creating it. - * - * @param string $type The table type to instantiate. - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - */ - public function getTable($type = 'Extension', $prefix = 'JTable', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - // Execute the parent method. - parent::populateState(); - - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('extension_id'); - $this->setState('plugin.id', $pk); - } - - /** - * Preprocess the form. - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group Cache group name. - * - * @return mixed True if successful. - * - * @since 1.6 - * - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $folder = $this->getState('item.folder'); - $element = $this->getState('item.element'); - $lang = Factory::getLanguage(); - - // Load the core and/or local language sys file(s) for the ordering field. - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('element')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = :folder') - ->bind(':folder', $folder); - $db->setQuery($query); - $elements = $db->loadColumn(); - - foreach ($elements as $elementa) - { - $lang->load('plg_' . $folder . '_' . $elementa . '.sys', JPATH_ADMINISTRATOR) - || $lang->load('plg_' . $folder . '_' . $elementa . '.sys', JPATH_PLUGINS . '/' . $folder . '/' . $elementa); - } - - if (empty($folder) || empty($element)) - { - $app = Factory::getApplication(); - $app->redirect(Route::_('index.php?option=com_plugins&view=plugins', false)); - } - - $formFile = Path::clean(JPATH_PLUGINS . '/' . $folder . '/' . $element . '/' . $element . '.xml'); - - if (!file_exists($formFile)) - { - throw new \Exception(Text::sprintf('COM_PLUGINS_ERROR_FILE_NOT_FOUND', $element . '.xml')); - } - - // Load the core and/or local language file(s). - $lang->load('plg_' . $folder . '_' . $element, JPATH_ADMINISTRATOR) - || $lang->load('plg_' . $folder . '_' . $element, JPATH_PLUGINS . '/' . $folder . '/' . $element); - - if (file_exists($formFile)) - { - // Get the plugin form. - if (!$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/extension/help'); - - if (!empty($help)) - { - $helpKey = trim((string) $help[0]['key']); - $helpURL = trim((string) $help[0]['url']); - - $this->helpKey = $helpKey ?: $this->helpKey; - $this->helpURL = $helpURL ?: $this->helpURL; - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('type') . ' = ' . $this->_db->quote($table->type), - $this->_db->quoteName('folder') . ' = ' . $this->_db->quote($table->folder), - ]; - } - - /** - * Override method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - // Setup type. - $data['type'] = 'plugin'; - - return parent::save($data); - } - - /** - * Get the necessary data to load an item help screen. - * - * @return object An object with key, url, and local properties for loading the item help screen. - * - * @since 1.6 - */ - public function getHelp() - { - return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); - } - - /** - * Custom clean cache method, plugins are cached in 2 places for different clients. - * - * @param string $group Cache group name. - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_plugins'); - } + /** + * @var string The help screen key for the module. + * @since 1.6 + */ + protected $helpKey = 'Plugins:_Name_of_Plugin'; + + /** + * @var string The help screen base URL for the module. + * @since 1.6 + */ + protected $helpURL; + + /** + * @var array An array of cached plugin items. + * @since 1.6 + */ + protected $_cache; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_after_save' => 'onExtensionAfterSave', + 'event_before_save' => 'onExtensionBeforeSave', + 'events_map' => array( + 'save' => 'extension' + ) + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure. + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // The folder and element vars are passed when saving the form. + if (empty($data)) { + $item = $this->getItem(); + $folder = $item->folder; + $element = $item->element; + } else { + $folder = ArrayHelper::getValue($data, 'folder', '', 'cmd'); + $element = ArrayHelper::getValue($data, 'element', '', 'cmd'); + } + + // Add the default fields directory + Form::addFieldPath(JPATH_PLUGINS . '/' . $folder . '/' . $element . '/field'); + + // These variables are used to add data from the plugin XML files. + $this->setState('item.folder', $folder); + $this->setState('item.element', $element); + + // Get the form. + $form = $this->loadForm('com_plugins.plugin', 'plugin', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('enabled', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('enabled', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_plugins.edit.plugin.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_plugins.plugin', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('plugin.id'); + + $cacheId = $pk; + + if (\is_array($cacheId)) { + $cacheId = serialize($cacheId); + } + + if (!isset($this->_cache[$cacheId])) { + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load(\is_array($pk) ? $pk : ['extension_id' => $pk, 'type' => 'plugin']); + + // Check for a table object error. + if ($return === false) { + return false; + } + + // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. + $properties = $table->getProperties(1); + $this->_cache[$cacheId] = ArrayHelper::toObject($properties, CMSObject::class); + + // Convert the params field to an array. + $registry = new Registry($table->params); + $this->_cache[$cacheId]->params = $registry->toArray(); + + // Get the plugin XML. + $path = Path::clean(JPATH_PLUGINS . '/' . $table->folder . '/' . $table->element . '/' . $table->element . '.xml'); + + if (file_exists($path)) { + $this->_cache[$cacheId]->xml = simplexml_load_file($path); + } else { + $this->_cache[$cacheId]->xml = null; + } + } + + return $this->_cache[$cacheId]; + } + + /** + * Returns a reference to the Table object, always creating it. + * + * @param string $type The table type to instantiate. + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + */ + public function getTable($type = 'Extension', $prefix = 'JTable', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + // Execute the parent method. + parent::populateState(); + + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('extension_id'); + $this->setState('plugin.id', $pk); + } + + /** + * Preprocess the form. + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group Cache group name. + * + * @return mixed True if successful. + * + * @since 1.6 + * + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $folder = $this->getState('item.folder'); + $element = $this->getState('item.element'); + $lang = Factory::getLanguage(); + + // Load the core and/or local language sys file(s) for the ordering field. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('element')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = :folder') + ->bind(':folder', $folder); + $db->setQuery($query); + $elements = $db->loadColumn(); + + foreach ($elements as $elementa) { + $lang->load('plg_' . $folder . '_' . $elementa . '.sys', JPATH_ADMINISTRATOR) + || $lang->load('plg_' . $folder . '_' . $elementa . '.sys', JPATH_PLUGINS . '/' . $folder . '/' . $elementa); + } + + if (empty($folder) || empty($element)) { + $app = Factory::getApplication(); + $app->redirect(Route::_('index.php?option=com_plugins&view=plugins', false)); + } + + $formFile = Path::clean(JPATH_PLUGINS . '/' . $folder . '/' . $element . '/' . $element . '.xml'); + + if (!file_exists($formFile)) { + throw new \Exception(Text::sprintf('COM_PLUGINS_ERROR_FILE_NOT_FOUND', $element . '.xml')); + } + + // Load the core and/or local language file(s). + $lang->load('plg_' . $folder . '_' . $element, JPATH_ADMINISTRATOR) + || $lang->load('plg_' . $folder . '_' . $element, JPATH_PLUGINS . '/' . $folder . '/' . $element); + + if (file_exists($formFile)) { + // Get the plugin form. + if (!$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/extension/help'); + + if (!empty($help)) { + $helpKey = trim((string) $help[0]['key']); + $helpURL = trim((string) $help[0]['url']); + + $this->helpKey = $helpKey ?: $this->helpKey; + $this->helpURL = $helpURL ?: $this->helpURL; + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('type') . ' = ' . $db->quote($table->type), + $db->quoteName('folder') . ' = ' . $db->quote($table->folder), + ]; + } + + /** + * Override method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + // Setup type. + $data['type'] = 'plugin'; + + return parent::save($data); + } + + /** + * Get the necessary data to load an item help screen. + * + * @return object An object with key, url, and local properties for loading the item help screen. + * + * @since 1.6 + */ + public function getHelp() + { + return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); + } + + /** + * Custom clean cache method, plugins are cached in 2 places for different clients. + * + * @param string $group Cache group name. + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_plugins'); + } } diff --git a/code/administrator/components/com_plugins/src/Model/PluginsModel.php b/code/administrator/components/com_plugins/src/Model/PluginsModel.php index 0800c2be..019a29cd 100644 --- a/code/administrator/components/com_plugins/src/Model/PluginsModel.php +++ b/code/administrator/components/com_plugins/src/Model/PluginsModel.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Plugins\Administrator\Model; -\defined('_JEXEC') or die; +namespace Joomla\Component\Plugins\Administrator\Model; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; @@ -18,6 +18,10 @@ use Joomla\Database\ParameterType; use Joomla\Utilities\ArrayHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Methods supporting a list of plugin records. * @@ -25,280 +29,262 @@ */ class PluginsModel extends ListModel { - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * - * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel - * @since 3.2 - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null) - { - if (empty($config['filter_fields'])) - { - $config['filter_fields'] = array( - 'extension_id', 'a.extension_id', - 'name', 'a.name', - 'folder', 'a.folder', - 'element', 'a.element', - 'checked_out', 'a.checked_out', - 'checked_out_time', 'a.checked_out_time', - 'state', 'a.state', - 'enabled', 'a.enabled', - 'access', 'a.access', 'access_level', - 'ordering', 'a.ordering', - 'client_id', 'a.client_id', - ); - } - - parent::__construct($config, $factory); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'folder', $direction = 'asc') - { - // Load the parameters. - $params = ComponentHelper::getParams('com_plugins'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.enabled'); - $id .= ':' . $this->getState('filter.folder'); - $id .= ':' . $this->getState('filter.element'); - - return parent::getStoreId($id); - } - - /** - * Returns an object list. - * - * @param \Joomla\Database\DatabaseQuery $query A database query object. - * @param integer $limitstart Offset. - * @param integer $limit The number of records. - * - * @return array - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $search = $this->getState('filter.search'); - $ordering = $this->getState('list.ordering', 'ordering'); - - // If "Sort Table By:" is not set, set ordering to name - if ($ordering == '') - { - $ordering = 'name'; - } - - if ($ordering == 'name' || (!empty($search) && stripos($search, 'id:') !== 0)) - { - $this->_db->setQuery($query); - $result = $this->_db->loadObjectList(); - $this->translate($result); - - if (!empty($search)) - { - $escapedSearchString = $this->refineSearchStringToRegex($search, '/'); - - foreach ($result as $i => $item) - { - if (!preg_match("/$escapedSearchString/i", $item->name)) - { - unset($result[$i]); - } - } - } - - $orderingDirection = strtolower($this->getState('list.direction')); - $direction = ($orderingDirection == 'desc') ? -1 : 1; - $result = ArrayHelper::sortObjects($result, $ordering, $direction, true, true); - - $total = count($result); - $this->cache[$this->getStoreId('getTotal')] = $total; - - if ($total < $limitstart) - { - $limitstart = 0; - } - - $this->cache[$this->getStoreId('getStart')] = $limitstart; - - return array_slice($result, $limitstart, $limit ?: null); - } - else - { - if ($ordering == 'ordering') - { - $query->order('a.folder ASC'); - $ordering = 'a.ordering'; - } - - $query->order($this->_db->quoteName($ordering) . ' ' . $this->getState('list.direction')); - - if ($ordering == 'folder') - { - $query->order('a.ordering ASC'); - } - - $result = parent::_getList($query, $limitstart, $limit); - $this->translate($result); - - return $result; - } - } - - /** - * Translate a list of objects. - * - * @param array &$items The array of objects. - * - * @return array The array of translated objects. - */ - protected function translate(&$items) - { - $lang = Factory::getLanguage(); - - foreach ($items as &$item) - { - $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; - $extension = 'plg_' . $item->folder . '_' . $item->element; - $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($extension . '.sys', $source); - $item->name = Text::_($item->name); - } - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.extension_id , a.name, a.element, a.folder, a.checked_out, a.checked_out_time,' . - ' a.enabled, a.access, a.ordering, a.note' - ) - ) - ->from($db->quoteName('#__extensions') . ' AS a') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')); - - // Join over the users for the checked out user. - $query->select('uc.name AS editor') - ->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); - - // Join over the asset groups. - $query->select('ag.title AS access_level') - ->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Filter by published state. - $published = (string) $this->getState('filter.enabled'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.enabled') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->whereIn($db->quoteName('a.enabled'), [0, 1]); - } - - // Filter by state. - $query->where('a.state >= 0'); - - // Filter by folder. - if ($folder = $this->getState('filter.folder')) - { - $query->where($db->quoteName('a.folder') . ' = :folder') - ->bind(':folder', $folder); - } - - // Filter by element. - if ($element = $this->getState('filter.element')) - { - $query->where($db->quoteName('a.element') . ' = :element') - ->bind(':element', $element); - } - - // Filter by search in name or id. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.extension_id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - } - - return $query; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 3.5 - */ - protected function loadFormData() - { - $data = parent::loadFormData(); - - // Set the selected filter values for pages that use the Layouts for filtering - $data->list['sortTable'] = $this->state->get('list.ordering'); - $data->list['directionTable'] = $this->state->get('list.direction'); - - return $data; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'extension_id', 'a.extension_id', + 'name', 'a.name', + 'folder', 'a.folder', + 'element', 'a.element', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'state', 'a.state', + 'enabled', 'a.enabled', + 'access', 'a.access', 'access_level', + 'ordering', 'a.ordering', + 'client_id', 'a.client_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'folder', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_plugins'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.enabled'); + $id .= ':' . $this->getState('filter.folder'); + $id .= ':' . $this->getState('filter.element'); + + return parent::getStoreId($id); + } + + /** + * Returns an object list. + * + * @param \Joomla\Database\DatabaseQuery $query A database query object. + * @param integer $limitstart Offset. + * @param integer $limit The number of records. + * + * @return array + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $search = $this->getState('filter.search'); + $ordering = $this->getState('list.ordering', 'ordering'); + + // If "Sort Table By:" is not set, set ordering to name + if ($ordering == '') { + $ordering = 'name'; + } + + $db = $this->getDatabase(); + + if ($ordering == 'name' || (!empty($search) && stripos($search, 'id:') !== 0)) { + $db->setQuery($query); + $result = $db->loadObjectList(); + $this->translate($result); + + if (!empty($search)) { + $escapedSearchString = $this->refineSearchStringToRegex($search, '/'); + + foreach ($result as $i => $item) { + if (!preg_match("/$escapedSearchString/i", $item->name)) { + unset($result[$i]); + } + } + } + + $orderingDirection = strtolower($this->getState('list.direction')); + $direction = ($orderingDirection == 'desc') ? -1 : 1; + $result = ArrayHelper::sortObjects($result, $ordering, $direction, true, true); + + $total = count($result); + $this->cache[$this->getStoreId('getTotal')] = $total; + + if ($total < $limitstart) { + $limitstart = 0; + } + + $this->cache[$this->getStoreId('getStart')] = $limitstart; + + return array_slice($result, $limitstart, $limit ?: null); + } else { + if ($ordering == 'ordering') { + $query->order('a.folder ASC'); + $ordering = 'a.ordering'; + } + + $query->order($db->quoteName($ordering) . ' ' . $this->getState('list.direction')); + + if ($ordering == 'folder') { + $query->order('a.ordering ASC'); + } + + $result = parent::_getList($query, $limitstart, $limit); + $this->translate($result); + + return $result; + } + } + + /** + * Translate a list of objects. + * + * @param array &$items The array of objects. + * + * @return array The array of translated objects. + */ + protected function translate(&$items) + { + $lang = Factory::getLanguage(); + + foreach ($items as &$item) { + $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; + $extension = 'plg_' . $item->folder . '_' . $item->element; + $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($extension . '.sys', $source); + $item->name = Text::_($item->name); + } + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.extension_id , a.name, a.element, a.folder, a.checked_out, a.checked_out_time,' . + ' a.enabled, a.access, a.ordering, a.note' + ) + ) + ->from($db->quoteName('#__extensions') . ' AS a') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')); + + // Join over the users for the checked out user. + $query->select('uc.name AS editor') + ->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); + + // Join over the asset groups. + $query->select('ag.title AS access_level') + ->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Filter by published state. + $published = (string) $this->getState('filter.enabled'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.enabled') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->whereIn($db->quoteName('a.enabled'), [0, 1]); + } + + // Filter by state. + $query->where('a.state >= 0'); + + // Filter by folder. + if ($folder = $this->getState('filter.folder')) { + $query->where($db->quoteName('a.folder') . ' = :folder') + ->bind(':folder', $folder); + } + + // Filter by element. + if ($element = $this->getState('filter.element')) { + $query->where($db->quoteName('a.element') . ' = :element') + ->bind(':element', $element); + } + + // Filter by search in name or id. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.extension_id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } + } + + return $query; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 3.5 + */ + protected function loadFormData() + { + $data = parent::loadFormData(); + + // Set the selected filter values for pages that use the Layouts for filtering + $data->list['sortTable'] = $this->state->get('list.ordering'); + $data->list['directionTable'] = $this->state->get('list.direction'); + + return $data; + } } diff --git a/code/administrator/components/com_plugins/src/View/Plugin/HtmlView.php b/code/administrator/components/com_plugins/src/View/Plugin/HtmlView.php index 888a2303..5a922e7b 100644 --- a/code/administrator/components/com_plugins/src/View/Plugin/HtmlView.php +++ b/code/administrator/components/com_plugins/src/View/Plugin/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $canDo = ContentHelper::getActions('com_plugins'); - - ToolbarHelper::title(Text::sprintf('COM_PLUGINS_MANAGER_PLUGIN', Text::_($this->item->name)), 'plug plugin'); - - // If not checked out, can save the item. - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('plugin.apply'); - - ToolbarHelper::save('plugin.save'); - } - - ToolbarHelper::cancel('plugin.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::divider(); - - // Get the help information for the plugin item. - $lang = Factory::getLanguage(); - - $help = $this->get('Help'); - - if ($help->url && $lang->hasKey($help->url)) - { - $debug = $lang->setDebug(false); - $url = Text::_($help->url); - $lang->setDebug($debug); - } - else - { - $url = null; - } - - ToolbarHelper::inlinehelp(); - ToolbarHelper::help($help->key, false, $url); - } + /** + * The item object for the newsfeed + * + * @var CMSObject + */ + protected $item; + + /** + * The form object for the newsfeed + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The model state of the newsfeed + * + * @var CMSObject + */ + protected $state; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $canDo = ContentHelper::getActions('com_plugins'); + + ToolbarHelper::title(Text::sprintf('COM_PLUGINS_MANAGER_PLUGIN', Text::_($this->item->name)), 'plug plugin'); + + // If not checked out, can save the item. + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('plugin.apply'); + + ToolbarHelper::save('plugin.save'); + } + + ToolbarHelper::cancel('plugin.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::divider(); + + // Get the help information for the plugin item. + $lang = Factory::getLanguage(); + + $help = $this->get('Help'); + + if ($help->url && $lang->hasKey($help->url)) { + $debug = $lang->setDebug(false); + $url = Text::_($help->url); + $lang->setDebug($debug); + } else { + $url = null; + } + + ToolbarHelper::inlinehelp(); + ToolbarHelper::help($help->key, false, $url); + } } diff --git a/code/administrator/components/com_plugins/src/View/Plugins/HtmlView.php b/code/administrator/components/com_plugins/src/View/Plugins/HtmlView.php index dae68430..ef18432d 100644 --- a/code/administrator/components/com_plugins/src/View/Plugins/HtmlView.php +++ b/code/administrator/components/com_plugins/src/View/Plugins/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_plugins'); - - ToolbarHelper::title(Text::_('COM_PLUGINS_MANAGER_PLUGINS'), 'plug plugin'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.edit.state')) - { - $toolbar->publish('plugins.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $toolbar->unpublish('plugins.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - $toolbar->checkin('plugins.checkin')->listCheck(true); - } - - if ($canDo->get('core.admin')) - { - $toolbar->preferences('com_plugins'); - } - - $toolbar->help('Plugins'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_plugins'); + + ToolbarHelper::title(Text::_('COM_PLUGINS_MANAGER_PLUGINS'), 'plug plugin'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.edit.state')) { + $toolbar->publish('plugins.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $toolbar->unpublish('plugins.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + $toolbar->checkin('plugins.checkin')->listCheck(true); + } + + if ($canDo->get('core.admin')) { + $toolbar->preferences('com_plugins'); + } + + $toolbar->help('Plugins'); + } } diff --git a/code/administrator/components/com_plugins/tmpl/plugin/edit.php b/code/administrator/components/com_plugins/tmpl/plugin/edit.php index 06dae096..c97f8d19 100644 --- a/code/administrator/components/com_plugins/tmpl/plugin/edit.php +++ b/code/administrator/components/com_plugins/tmpl/plugin/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $this->fieldsets = $this->form->getFieldsets('params'); $this->useCoreUI = true; @@ -32,111 +33,105 @@ ?>
-
- - 'general', 'recall' => true, 'breakpoint' => 768]); ?> - - - -
-
- item->xml) : ?> - item->xml->description) : ?> -

- item->xml) - { - echo ($text = (string) $this->item->xml->name) ? Text::_($text) : $this->item->name; - } - else - { - echo Text::_('COM_PLUGINS_XML_ERR'); - } - ?> -

-
- - form->getValue('folder'); ?> - / - - form->getValue('element'); ?> - -
-
- fieldset = 'description'; - $short_description = Text::_($this->item->xml->description); - $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); - - if (!$long_description) - { - $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); - - if (strlen($truncated) > 500) - { - $long_description = $short_description; - $short_description = HTMLHelper::_('string.truncate', $truncated, 250); - - if ($short_description == $long_description) - { - $long_description = ''; - } - } - } - ?> -

- -

- - - -

- -
- - -
- - -
- - fieldset = 'basic'; - $html = LayoutHelper::render('joomla.edit.fieldset', $this); - echo $html ? '
' . $html : ''; - ?> -
-
- fields = array( - 'enabled', - 'access', - 'ordering', - 'folder', - 'element', - 'note', - ); ?> - -
-
- - - - - - - - - fieldsets = array(); - $this->ignore_fieldsets = array('basic', 'description'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - -
- - - +
+ + 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + + +
+
+ item->xml) : ?> + item->xml->description) : ?> +

+ item->xml) { + echo ($text = (string) $this->item->xml->name) ? Text::_($text) : $this->item->name; + } else { + echo Text::_('COM_PLUGINS_XML_ERR'); + } + ?> +

+
+ + form->getValue('folder'); ?> + / + + form->getValue('element'); ?> + +
+
+ fieldset = 'description'; + $short_description = Text::_($this->item->xml->description); + $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); + + if (!$long_description) { + $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); + + if (strlen($truncated) > 500) { + $long_description = $short_description; + $short_description = HTMLHelper::_('string.truncate', $truncated, 250); + + if ($short_description == $long_description) { + $long_description = ''; + } + } + } + ?> +

+ +

+ + + +

+ +
+ + +
+ + +
+ + fieldset = 'basic'; + $html = LayoutHelper::render('joomla.edit.fieldset', $this); + echo $html ? '
' . $html : ''; + ?> +
+
+ fields = array( + 'enabled', + 'access', + 'ordering', + 'folder', + 'element', + 'note', + ); ?> + +
+
+ + + + + + + + + fieldsets = array(); + $this->ignore_fieldsets = array('basic', 'description'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + +
+ + +
diff --git a/code/administrator/components/com_plugins/tmpl/plugin/modal.php b/code/administrator/components/com_plugins/tmpl/plugin/modal.php index 4c6534c5..aeff088d 100644 --- a/code/administrator/components/com_plugins/tmpl/plugin/modal.php +++ b/code/administrator/components/com_plugins/tmpl/plugin/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/code/administrator/components/com_plugins/tmpl/plugins/default.php b/code/administrator/components/com_plugins/tmpl/plugins/default.php index 5973a9d9..296766df 100644 --- a/code/administrator/components/com_plugins/tmpl/plugins/default.php +++ b/code/administrator/components/com_plugins/tmpl/plugins/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'ordering'; -if ($saveOrder) -{ - $saveOrderingUrl = 'index.php?option=com_plugins&task=plugins.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder) { + $saveOrderingUrl = 'index.php?option=com_plugins&task=plugins.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $ordering = ($listOrder == 'ordering'); - $canEdit = $user->authorise('core.edit', 'com_plugins'); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_plugins') && $canCheckin; - ?> - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - -
- extension_id, false, 'cid', 'cb', $item->name); ?> - - - - - - - - - - enabled, $i, 'plugins.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'plugins.', $canCheckin); ?> - - - - name; ?> - note)) : ?> -
- escape($item->note)); ?> -
- - - name; ?> - -
- escape($item->folder); ?> - - escape($item->element); ?> - - escape($item->access_level); ?> - - extension_id; ?> -
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $ordering = ($listOrder == 'ordering'); + $canEdit = $user->authorise('core.edit', 'com_plugins'); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_plugins') && $canCheckin; + ?> + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + +
+ extension_id, false, 'cid', 'cb', $item->name); ?> + + + + + + + + + + enabled, $i, 'plugins.', $canChange); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'plugins.', $canCheckin); ?> + + + + name; ?> + note)) : ?> +
+ escape($item->note)); ?> +
+ + + name; ?> + +
+ escape($item->folder); ?> + + escape($item->element); ?> + + escape($item->access_level); ?> + + extension_id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
+ + + +
diff --git a/code/administrator/components/com_postinstall/postinstall.xml b/code/administrator/components/com_postinstall/postinstall.xml index 8de2e873..49d4a487 100644 --- a/code/administrator/components/com_postinstall/postinstall.xml +++ b/code/administrator/components/com_postinstall/postinstall.xml @@ -2,7 +2,7 @@ com_postinstall Joomla! Project - September 2013 + 2013-09 (C) 2013 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_postinstall/services/provider.php b/code/administrator/components/com_postinstall/services/provider.php index eeb3f3a0..d0995305 100644 --- a/code/administrator/components/com_postinstall/services/provider.php +++ b/code/administrator/components/com_postinstall/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Postinstall')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Postinstall')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Postinstall')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Postinstall')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_postinstall/src/Controller/DisplayController.php b/code/administrator/components/com_postinstall/src/Controller/DisplayController.php index e9a6be0e..9c10746f 100644 --- a/code/administrator/components/com_postinstall/src/Controller/DisplayController.php +++ b/code/administrator/components/com_postinstall/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getIdentity()->authorise('core.manage', 'com_postinstall')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Messages'); - - echo new JsonResponse($model->getItemsCount()); - } + /** + * @var string The default view. + * @since 1.6 + */ + protected $default_view = 'messages'; + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_postinstall')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Messages'); + + echo new JsonResponse($model->getItemsCount()); + } } diff --git a/code/administrator/components/com_postinstall/src/Controller/MessageController.php b/code/administrator/components/com_postinstall/src/Controller/MessageController.php index a1684fbc..4c879b74 100644 --- a/code/administrator/components/com_postinstall/src/Controller/MessageController.php +++ b/code/administrator/components/com_postinstall/src/Controller/MessageController.php @@ -1,4 +1,5 @@ checkToken('get'); - - /** @var MessagesModel $model */ - $model = $this->getModel('Messages', '', array('ignore_request' => true)); - $eid = $this->input->getInt('eid'); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->resetMessages($eid); - - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } - - /** - * Unpublishes post-installation message of the specified extension. - * - * @return void - * - * @since 3.2 - */ - public function unpublish() - { - $model = $this->getModel('Messages', '', array('ignore_request' => true)); - - $id = $this->input->get('id'); - - $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->setState('published', 0); - $model->unpublishMessage($id); - - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } - - /** - * Executes the action associated with an item. - * - * @return void - * - * @since 3.2 - */ - public function action() - { - $this->checkToken('get'); - - $model = $this->getModel('Messages', '', array('ignore_request' => true)); - - $id = $this->input->get('id'); - - $item = $model->getItem($id); - - switch ($item->type) - { - case 'link': - $this->setRedirect($item->action); - - return; - - case 'action': - $helper = new PostinstallHelper; - $file = $helper->parsePath($item->action_file); - - if (File::exists($file)) - { - require_once $file; - - call_user_func($item->action); - } - break; - } - - $this->setRedirect('index.php?option=com_postinstall'); - } - - /** - * Hides all post-installation messages of the specified extension. - * - * @return void - * - * @since 3.8.7 - */ - public function hideAll() - { - $this->checkToken(); - - /** @var MessagesModel $model */ - $model = $this->getModel('Messages', '', array('ignore_request' => true)); - $eid = $this->input->getInt('eid'); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->hideMessages($eid); - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } + /** + * Resets all post-installation messages of the specified extension. + * + * @return void + * + * @since 3.2 + */ + public function reset() + { + $this->checkToken('get'); + + /** @var MessagesModel $model */ + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + $eid = $this->input->getInt('eid'); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->resetMessages($eid); + + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } + + /** + * Unpublishes post-installation message of the specified extension. + * + * @return void + * + * @since 3.2 + */ + public function unpublish() + { + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + + $id = $this->input->get('id'); + + $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->setState('published', 0); + $model->unpublishMessage($id); + + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } + + /** + * Re-Publishes an archived post-installation message of the specified extension. + * + * @return void + * + * @since 4.2.0 + */ + public function republish() + { + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + + $id = $this->input->get('id'); + + $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->setState('published', 1); + $model->republishMessage($id); + + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } + + /** + * Archives a published post-installation message of the specified extension. + * + * @return void + * + * @since 4.2.0 + */ + public function archive() + { + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + + $id = $this->input->get('id'); + + $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->setState('published', 2); + $model->archiveMessage($id); + + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } + + /** + * Executes the action associated with an item. + * + * @return void + * + * @since 3.2 + */ + public function action() + { + $this->checkToken('get'); + + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + + $id = $this->input->get('id'); + + $item = $model->getItem($id); + + switch ($item->type) { + case 'link': + $this->setRedirect($item->action); + + return; + + case 'action': + $helper = new PostinstallHelper(); + $file = $helper->parsePath($item->action_file); + + if (File::exists($file)) { + require_once $file; + + call_user_func($item->action); + } + break; + } + + $this->setRedirect('index.php?option=com_postinstall'); + } + + /** + * Hides all post-installation messages of the specified extension. + * + * @return void + * + * @since 3.8.7 + */ + public function hideAll() + { + $this->checkToken(); + + /** @var MessagesModel $model */ + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + $eid = $this->input->getInt('eid'); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->hideMessages($eid); + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } } diff --git a/code/administrator/components/com_postinstall/src/Helper/PostinstallHelper.php b/code/administrator/components/com_postinstall/src/Helper/PostinstallHelper.php index 8b10b66d..8cd4baed 100644 --- a/code/administrator/components/com_postinstall/src/Helper/PostinstallHelper.php +++ b/code/administrator/components/com_postinstall/src/Helper/PostinstallHelper.php @@ -1,4 +1,5 @@ input->getInt('eid'); - - if ($eid) - { - $this->setState('eid', $eid); - } - } - - /** - * Gets an item with the given id from the database - * - * @param integer $id The item id - * - * @return Object - * - * @since 3.2 - */ - public function getItem($id) - { - $db = $this->getDbo(); - $id = (int) $id; - - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('postinstall_message_id'), - $db->quoteName('extension_id'), - $db->quoteName('title_key'), - $db->quoteName('description_key'), - $db->quoteName('action_key'), - $db->quoteName('language_extension'), - $db->quoteName('language_client_id'), - $db->quoteName('type'), - $db->quoteName('action_file'), - $db->quoteName('action'), - $db->quoteName('condition_file'), - $db->quoteName('condition_method'), - $db->quoteName('version_introduced'), - $db->quoteName('enabled'), - ] - ) - ->from($db->quoteName('#__postinstall_messages')) - ->where($db->quoteName('postinstall_message_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - $result = $db->loadObject(); - - return $result; - } - - /** - * Unpublishes specified post-install message - * - * @param integer $id The message id - * - * @return void - */ - public function unpublishMessage($id) - { - $db = $this->getDbo(); - $id = (int) $id; - - $query = $db->getQuery(true); - $query - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 0') - ->where($db->quoteName('postinstall_message_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - Factory::getCache()->clean('com_postinstall'); - } - - /** - * Returns a list of messages from the #__postinstall_messages table - * - * @return array - * - * @since 3.2 - */ - public function getItems() - { - // Add a forced extension filtering to the list - $eid = (int) $this->getState('eid', $this->getJoomlaFilesExtensionId()); - $published = (int) $this->getState('published', 1); - - // Build a cache ID for the resulting data object - $cacheId = $eid . '.' . $published; - - $db = $this->getDbo(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('postinstall_message_id'), - $db->quoteName('extension_id'), - $db->quoteName('title_key'), - $db->quoteName('description_key'), - $db->quoteName('action_key'), - $db->quoteName('language_extension'), - $db->quoteName('language_client_id'), - $db->quoteName('type'), - $db->quoteName('action_file'), - $db->quoteName('action'), - $db->quoteName('condition_file'), - $db->quoteName('condition_method'), - $db->quoteName('version_introduced'), - $db->quoteName('enabled'), - ] - ) - ->from($db->quoteName('#__postinstall_messages')); - $query->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - - // Force filter only enabled messages - $query->where($db->quoteName('enabled') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - /** @var CallbackController $cache */ - $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class) - ->createCacheController('callback', ['defaultgroup' => 'com_postinstall']); - - $result = $cache->get(array($db, 'loadObjectList'), array(), md5($cacheId), false); - } - catch (\RuntimeException $e) - { - $app = Factory::getApplication(); - $app->getLogger()->warning( - Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), - array('category' => 'jerror') - ); - - return array(); - } - - $this->onProcessList($result); - - return $result; - } - - /** - * Returns a count of all enabled messages from the #__postinstall_messages table - * - * @return integer - * - * @since 4.0.0 - */ - public function getItemsCount() - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('language_extension'), - $db->quoteName('language_client_id'), - $db->quoteName('condition_file'), - $db->quoteName('condition_method'), - ] - ) - ->from($db->quoteName('#__postinstall_messages')); - - // Force filter only enabled messages - $query->where($db->quoteName('enabled') . ' = 1'); - $db->setQuery($query); - - try - { - /** @var CallbackController $cache */ - $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class) - ->createCacheController('callback', ['defaultgroup' => 'com_postinstall']); - - // Get the resulting data object for cache ID 'all.1' from com_postinstall group. - $result = $cache->get(array($db, 'loadObjectList'), array(), md5('all.1'), false); - } - catch (\RuntimeException $e) - { - $app = Factory::getApplication(); - $app->getLogger()->warning( - Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), - array('category' => 'jerror') - ); - - return 0; - } - - $this->onProcessList($result); - - return \count($result); - } - - /** - * Returns the name of an extension, as registered in the #__extensions table - * - * @param integer $eid The extension ID - * - * @return string The extension name - * - * @since 3.2 - */ - public function getExtensionName($eid) - { - // Load the extension's information from the database - $db = $this->getDbo(); - $eid = (int) $eid; - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('name'), - $db->quoteName('element'), - $db->quoteName('client_id'), - ] - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER) - ->setLimit(1); - - $db->setQuery($query); - - $extension = $db->loadObject(); - - if (!is_object($extension)) - { - return ''; - } - - // Load language files - $basePath = JPATH_ADMINISTRATOR; - - if ($extension->client_id == 0) - { - $basePath = JPATH_SITE; - } - - $lang = Factory::getApplication()->getLanguage(); - $lang->load($extension->element, $basePath); - - // Return the localised name - return Text::_(strtoupper($extension->name)); - } - - /** - * Resets all messages for an extension - * - * @param integer $eid The extension ID whose messages we'll reset - * - * @return mixed False if we fail, a db cursor otherwise - * - * @since 3.2 - */ - public function resetMessages($eid) - { - $db = $this->getDbo(); - $eid = (int) $eid; - - $query = $db->getQuery(true) - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - $db->setQuery($query); - - $result = $db->execute(); - Factory::getCache()->clean('com_postinstall'); - - return $result; - } - - /** - * Hides all messages for an extension - * - * @param integer $eid The extension ID whose messages we'll hide - * - * @return mixed False if we fail, a db cursor otherwise - * - * @since 3.8.7 - */ - public function hideMessages($eid) - { - $db = $this->getDbo(); - $eid = (int) $eid; - - $query = $db->getQuery(true) - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 0') - ->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - $db->setQuery($query); - - $result = $db->execute(); - Factory::getCache()->clean('com_postinstall'); - - return $result; - } - - /** - * List post-processing. This is used to run the programmatic display - * conditions against each list item and decide if we have to show it or - * not. - * - * Do note that this a core method of the RAD Layer which operates directly - * on the list it's being fed. A little touch of modern magic. - * - * @param array &$resultArray A list of items to process - * - * @return void - * - * @since 3.2 - */ - protected function onProcessList(&$resultArray) - { - $unset_keys = array(); - $language_extensions = array(); - - // Order the results DESC so the newest is on the top. - $resultArray = array_reverse($resultArray); - - foreach ($resultArray as $key => $item) - { - // Filter out messages based on dynamically loaded programmatic conditions. - if (!empty($item->condition_file) && !empty($item->condition_method)) - { - $helper = new PostinstallHelper; - $file = $helper->parsePath($item->condition_file); - - if (File::exists($file)) - { - require_once $file; - - $result = call_user_func($item->condition_method); - - if ($result === false) - { - $unset_keys[] = $key; - } - } - } - - // Load the necessary language files. - if (!empty($item->language_extension)) - { - $hash = $item->language_client_id . '-' . $item->language_extension; - - if (!in_array($hash, $language_extensions)) - { - $language_extensions[] = $hash; - Factory::getApplication()->getLanguage()->load($item->language_extension, $item->language_client_id == 0 ? JPATH_SITE : JPATH_ADMINISTRATOR); - } - } - } - - if (!empty($unset_keys)) - { - foreach ($unset_keys as $key) - { - unset($resultArray[$key]); - } - } - } - - /** - * Get the dropdown options for the list of component with post-installation messages - * - * @since 3.4 - * - * @return array Compatible with JHtmlSelect::genericList - */ - public function getComponentOptions() - { - $db = $this->getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__postinstall_messages')) - ->group($db->quoteName('extension_id')); - $db->setQuery($query); - $extension_ids = $db->loadColumn(); - - $options = array(); - - Factory::getApplication()->getLanguage()->load('files_joomla.sys', JPATH_SITE, null, false, false); - - foreach ($extension_ids as $eid) - { - $options[] = HTMLHelper::_('select.option', $eid, $this->getExtensionName($eid)); - } - - return $options; - } - - /** - * Adds or updates a post-installation message (PIM) definition. You can use this in your post-installation script using this code: - * - * require_once JPATH_LIBRARIES . '/fof/include.php'; - * FOFModel::getTmpInstance('Messages', 'PostinstallModel')->addPostInstallationMessage($options); - * - * The $options array contains the following mandatory keys: - * - * extension_id The numeric ID of the extension this message is for (see the #__extensions table) - * - * type One of message, link or action. Their meaning is: - * message Informative message. The user can dismiss it. - * link The action button links to a URL. The URL is defined in the action parameter. - * action A PHP action takes place when the action button is clicked. You need to specify the action_file - * (RAD path to the PHP file) and action (PHP function name) keys. See below for more information. - * - * title_key The Text language key for the title of this PIM. - * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_TITLE - * - * description_key The Text language key for the main body (description) of this PIM - * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_DESCRIPTION - * - * action_key The Text language key for the action button. Ignored and not required when type=message - * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_ACTION - * - * language_extension The extension name which holds the language keys used above. - * For example, com_foobar, mod_something, plg_system_whatever, tpl_mytemplate - * - * language_client_id Should we load the frontend (0) or backend (1) language keys? - * - * version_introduced Which was the version of your extension where this message appeared for the first time? - * Example: 3.2.1 - * - * enabled Must be 1 for this message to be enabled. If you omit it, it defaults to 1. - * - * condition_file The RAD path to a PHP file containing a PHP function which determines whether this message should be shown to - * the user. @see FOFTemplateUtils::parsePath() for RAD path format. Joomla! will include this file before calling - * the condition_method. - * Example: admin://components/com_foobar/helpers/postinstall.php - * - * condition_method The name of a PHP function which will be used to determine whether to show this message to the user. This must be - * a simple PHP user function (not a class method, static method etc) which returns true to show the message and false - * to hide it. This function is defined in the condition_file. - * Example: com_foobar_postinstall_messageone_condition - * - * When type=message no additional keys are required. - * - * When type=link the following additional keys are required: - * - * action The URL which will open when the user clicks on the PIM's action button - * Example: index.php?option=com_foobar&view=tools&task=installSampleData - * - * When type=action the following additional keys are required: - * - * action_file The RAD path to a PHP file containing a PHP function which performs the action of this PIM. @see FOFTemplateUtils::parsePath() - * for RAD path format. Joomla! will include this file before calling the function defined in the action key below. - * Example: admin://components/com_foobar/helpers/postinstall.php - * - * action The name of a PHP function which will be used to run the action of this PIM. This must be a simple PHP user function - * (not a class method, static method etc) which returns no result. - * Example: com_foobar_postinstall_messageone_action - * - * @param array $options See description - * - * @return $this - * - * @throws \Exception - */ - public function addPostInstallationMessage(array $options) - { - // Make sure there are options set - if (!is_array($options)) - { - throw new \Exception('Post-installation message definitions must be of type array', 500); - } - - // Initialise array keys - $defaultOptions = array( - 'extension_id' => '', - 'type' => '', - 'title_key' => '', - 'description_key' => '', - 'action_key' => '', - 'language_extension' => '', - 'language_client_id' => '', - 'action_file' => '', - 'action' => '', - 'condition_file' => '', - 'condition_method' => '', - 'version_introduced' => '', - 'enabled' => '1', - ); - - $options = array_merge($defaultOptions, $options); - - // Array normalisation. Removes array keys not belonging to a definition. - $defaultKeys = array_keys($defaultOptions); - $allKeys = array_keys($options); - $extraKeys = array_diff($allKeys, $defaultKeys); - - if (!empty($extraKeys)) - { - foreach ($extraKeys as $key) - { - unset($options[$key]); - } - } - - // Normalisation of integer values - $options['extension_id'] = (int) $options['extension_id']; - $options['language_client_id'] = (int) $options['language_client_id']; - $options['enabled'] = (int) $options['enabled']; - - // Normalisation of 0/1 values - foreach (array('language_client_id', 'enabled') as $key) - { - $options[$key] = $options[$key] ? 1 : 0; - } - - // Make sure there's an extension_id - if (!(int) $options['extension_id']) - { - throw new \Exception('Post-installation message definitions need an extension_id', 500); - } - - // Make sure there's a valid type - if (!in_array($options['type'], array('message', 'link', 'action'))) - { - throw new \Exception('Post-installation message definitions need to declare a type of message, link or action', 500); - } - - // Make sure there's a title key - if (empty($options['title_key'])) - { - throw new \Exception('Post-installation message definitions need a title key', 500); - } - - // Make sure there's a description key - if (empty($options['description_key'])) - { - throw new \Exception('Post-installation message definitions need a description key', 500); - } - - // If the type is anything other than message you need an action key - if (($options['type'] != 'message') && empty($options['action_key'])) - { - throw new \Exception('Post-installation message definitions need an action key when they are of type "' . $options['type'] . '"', 500); - } - - // You must specify the language extension - if (empty($options['language_extension'])) - { - throw new \Exception('Post-installation message definitions need to specify which extension contains their language keys', 500); - } - - // The action file and method are only required for the "action" type - if ($options['type'] == 'action') - { - if (empty($options['action_file'])) - { - throw new \Exception('Post-installation message definitions need an action file when they are of type "action"', 500); - } - - $helper = new PostinstallHelper; - $file_path = $helper->parsePath($options['action_file']); - - if (!@is_file($file_path)) - { - throw new \Exception('The action file ' . $options['action_file'] . ' of your post-installation message definition does not exist', 500); - } - - if (empty($options['action'])) - { - throw new \Exception('Post-installation message definitions need an action (function name) when they are of type "action"', 500); - } - } - - if ($options['type'] == 'link') - { - if (empty($options['link'])) - { - throw new \Exception('Post-installation message definitions need an action (URL) when they are of type "link"', 500); - } - } - - // The condition file and method are only required when the type is not "message" - if ($options['type'] != 'message') - { - if (empty($options['condition_file'])) - { - throw new \Exception('Post-installation message definitions need a condition file when they are of type "' . $options['type'] . '"', 500); - } - - $helper = new PostinstallHelper; - $file_path = $helper->parsePath($options['condition_file']); - - if (!@is_file($file_path)) - { - throw new \Exception('The condition file ' . $options['condition_file'] . ' of your post-installation message definition does not exist', 500); - } - - if (empty($options['condition_method'])) - { - throw new \Exception( - 'Post-installation message definitions need a condition method (function name) when they are of type "' - . $options['type'] . '"', - 500 - ); - } - } - - // Check if the definition exists - $table = $this->getTable(); - $tableName = $table->getTableName(); - $extensionId = (int) $options['extension_id']; - - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName($tableName)) - ->where( - [ - $db->quoteName('extension_id') . ' = :extensionId', - $db->quoteName('type') . ' = :type', - $db->quoteName('title_key') . ' = :titleKey', - ] - ) - ->bind(':extensionId', $extensionId, ParameterType::INTEGER) - ->bind(':type', $options['type']) - ->bind(':titleKey', $options['title_key']); - - $existingRow = $db->setQuery($query)->loadAssoc(); - - // Is the existing definition the same as the one we're trying to save? - if (!empty($existingRow)) - { - $same = true; - - foreach ($options as $k => $v) - { - if ($existingRow[$k] != $v) - { - $same = false; - break; - } - } - - // Trying to add the same row as the existing one; quit - if ($same) - { - return $this; - } - - // Otherwise it's not the same row. Remove the old row before insert a new one. - $query = $db->getQuery(true) - ->delete($db->quoteName($tableName)) - ->where( - [ - $db->quoteName('extension_id') . ' = :extensionId', - $db->quoteName('type') . ' = :type', - $db->quoteName('title_key') . ' = :titleKey', - ] - ) - ->bind(':extensionId', $extensionId, ParameterType::INTEGER) - ->bind(':type', $options['type']) - ->bind(':titleKey', $options['title_key']); - - $db->setQuery($query)->execute(); - } - - // Insert the new row - $options = (object) $options; - $db->insertObject($tableName, $options); - Factory::getCache()->clean('com_postinstall'); - - return $this; - } - - /** - * Returns the library extension ID. - * - * @return integer - * - * @since 4.0.0 - */ - public function getJoomlaFilesExtensionId() - { - return ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - } + /** + * Method to auto-populate the state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the + * configuration flag to ignore the request is set. + * + * @return void + * + * @note Calling getState in this method will result in recursion. + * @since 4.0.0 + */ + protected function populateState() + { + parent::populateState(); + + $eid = (int) Factory::getApplication()->input->getInt('eid'); + + if ($eid) { + $this->setState('eid', $eid); + } + } + + /** + * Gets an item with the given id from the database + * + * @param integer $id The item id + * + * @return Object + * + * @since 3.2 + */ + public function getItem($id) + { + $db = $this->getDatabase(); + $id = (int) $id; + + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('postinstall_message_id'), + $db->quoteName('extension_id'), + $db->quoteName('title_key'), + $db->quoteName('description_key'), + $db->quoteName('action_key'), + $db->quoteName('language_extension'), + $db->quoteName('language_client_id'), + $db->quoteName('type'), + $db->quoteName('action_file'), + $db->quoteName('action'), + $db->quoteName('condition_file'), + $db->quoteName('condition_method'), + $db->quoteName('version_introduced'), + $db->quoteName('enabled'), + ] + ) + ->from($db->quoteName('#__postinstall_messages')) + ->where($db->quoteName('postinstall_message_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + $result = $db->loadObject(); + + return $result; + } + + /** + * Unpublishes specified post-install message + * + * @param integer $id The message id + * + * @return void + */ + public function unpublishMessage($id) + { + $db = $this->getDatabase(); + $id = (int) $id; + + $query = $db->getQuery(true); + $query + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 0') + ->where($db->quoteName('postinstall_message_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + Factory::getCache()->clean('com_postinstall'); + } + + /** + * Archives specified post-install message + * + * @param integer $id The message id + * + * @return void + * + * @since 4.2.0 + */ + public function archiveMessage($id) + { + $db = $this->getDatabase(); + $id = (int) $id; + + $query = $db->getQuery(true); + $query + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 2') + ->where($db->quoteName('postinstall_message_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + Factory::getCache()->clean('com_postinstall'); + } + + /** + * Republishes specified post-install message + * + * @param integer $id The message id + * + * @return void + * + * @since 4.2.0 + */ + public function republishMessage($id) + { + $db = $this->getDatabase(); + $id = (int) $id; + + $query = $db->getQuery(true); + $query + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('postinstall_message_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + Factory::getCache()->clean('com_postinstall'); + } + + /** + * Returns a list of messages from the #__postinstall_messages table + * + * @return array + * + * @since 3.2 + */ + public function getItems() + { + // Add a forced extension filtering to the list + $eid = (int) $this->getState('eid', $this->getJoomlaFilesExtensionId()); + + // Build a cache ID for the resulting data object + $cacheId = 'postinstall_messages.' . $eid; + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('postinstall_message_id'), + $db->quoteName('extension_id'), + $db->quoteName('title_key'), + $db->quoteName('description_key'), + $db->quoteName('action_key'), + $db->quoteName('language_extension'), + $db->quoteName('language_client_id'), + $db->quoteName('type'), + $db->quoteName('action_file'), + $db->quoteName('action'), + $db->quoteName('condition_file'), + $db->quoteName('condition_method'), + $db->quoteName('version_introduced'), + $db->quoteName('enabled'), + ] + ) + ->from($db->quoteName('#__postinstall_messages')); + $query->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + + // Force filter only enabled messages + $query->whereIn($db->quoteName('enabled'), [1, 2]); + $db->setQuery($query); + + try { + /** @var CallbackController $cache */ + $cache = $this->getCacheControllerFactory()->createCacheController('callback', ['defaultgroup' => 'com_postinstall']); + + $result = $cache->get(array($db, 'loadObjectList'), array(), md5($cacheId), false); + } catch (\RuntimeException $e) { + $app = Factory::getApplication(); + $app->getLogger()->warning( + Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), + array('category' => 'jerror') + ); + + return array(); + } + + $this->onProcessList($result); + + return $result; + } + + /** + * Returns a count of all enabled messages from the #__postinstall_messages table + * + * @return integer + * + * @since 4.0.0 + */ + public function getItemsCount() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('language_extension'), + $db->quoteName('language_client_id'), + $db->quoteName('condition_file'), + $db->quoteName('condition_method'), + ] + ) + ->from($db->quoteName('#__postinstall_messages')); + + // Force filter only enabled messages + $query->where($db->quoteName('enabled') . ' = 1'); + $db->setQuery($query); + + try { + /** @var CallbackController $cache */ + $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class) + ->createCacheController('callback', ['defaultgroup' => 'com_postinstall']); + + // Get the resulting data object for cache ID 'all.1' from com_postinstall group. + $result = $cache->get(array($db, 'loadObjectList'), array(), md5('all.1'), false); + } catch (\RuntimeException $e) { + $app = Factory::getApplication(); + $app->getLogger()->warning( + Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), + array('category' => 'jerror') + ); + + return 0; + } + + $this->onProcessList($result); + + return \count($result); + } + + /** + * Returns the name of an extension, as registered in the #__extensions table + * + * @param integer $eid The extension ID + * + * @return string The extension name + * + * @since 3.2 + */ + public function getExtensionName($eid) + { + // Load the extension's information from the database + $db = $this->getDatabase(); + $eid = (int) $eid; + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('name'), + $db->quoteName('element'), + $db->quoteName('client_id'), + ] + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER) + ->setLimit(1); + + $db->setQuery($query); + + $extension = $db->loadObject(); + + if (!is_object($extension)) { + return ''; + } + + // Load language files + $basePath = JPATH_ADMINISTRATOR; + + if ($extension->client_id == 0) { + $basePath = JPATH_SITE; + } + + $lang = Factory::getApplication()->getLanguage(); + $lang->load($extension->element, $basePath); + + // Return the localised name + return Text::_(strtoupper($extension->name)); + } + + /** + * Resets all messages for an extension + * + * @param integer $eid The extension ID whose messages we'll reset + * + * @return mixed False if we fail, a db cursor otherwise + * + * @since 3.2 + */ + public function resetMessages($eid) + { + $db = $this->getDatabase(); + $eid = (int) $eid; + + $query = $db->getQuery(true) + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + $db->setQuery($query); + + $result = $db->execute(); + Factory::getCache()->clean('com_postinstall'); + + return $result; + } + + /** + * Hides all messages for an extension + * + * @param integer $eid The extension ID whose messages we'll hide + * + * @return mixed False if we fail, a db cursor otherwise + * + * @since 3.8.7 + */ + public function hideMessages($eid) + { + $db = $this->getDatabase(); + $eid = (int) $eid; + + $query = $db->getQuery(true) + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 0') + ->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + $db->setQuery($query); + + $result = $db->execute(); + Factory::getCache()->clean('com_postinstall'); + + return $result; + } + + /** + * List post-processing. This is used to run the programmatic display + * conditions against each list item and decide if we have to show it or + * not. + * + * Do note that this a core method of the RAD Layer which operates directly + * on the list it's being fed. A little touch of modern magic. + * + * @param array &$resultArray A list of items to process + * + * @return void + * + * @since 3.2 + */ + protected function onProcessList(&$resultArray) + { + $unset_keys = array(); + $language_extensions = array(); + + // Order the results DESC so the newest is on the top. + $resultArray = array_reverse($resultArray); + + foreach ($resultArray as $key => $item) { + // Filter out messages based on dynamically loaded programmatic conditions. + if (!empty($item->condition_file) && !empty($item->condition_method)) { + $helper = new PostinstallHelper(); + $file = $helper->parsePath($item->condition_file); + + if (File::exists($file)) { + require_once $file; + + $result = call_user_func($item->condition_method); + + if ($result === false) { + $unset_keys[] = $key; + } + } + } + + // Load the necessary language files. + if (!empty($item->language_extension)) { + $hash = $item->language_client_id . '-' . $item->language_extension; + + if (!in_array($hash, $language_extensions)) { + $language_extensions[] = $hash; + Factory::getApplication()->getLanguage()->load($item->language_extension, $item->language_client_id == 0 ? JPATH_SITE : JPATH_ADMINISTRATOR); + } + } + } + + if (!empty($unset_keys)) { + foreach ($unset_keys as $key) { + unset($resultArray[$key]); + } + } + } + + /** + * Get the dropdown options for the list of component with post-installation messages + * + * @since 3.4 + * + * @return array Compatible with JHtmlSelect::genericList + */ + public function getComponentOptions() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__postinstall_messages')) + ->group($db->quoteName('extension_id')); + $db->setQuery($query); + $extension_ids = $db->loadColumn(); + + $options = array(); + + Factory::getApplication()->getLanguage()->load('files_joomla.sys', JPATH_SITE, null, false, false); + + foreach ($extension_ids as $eid) { + $options[] = HTMLHelper::_('select.option', $eid, $this->getExtensionName($eid)); + } + + return $options; + } + + /** + * Adds or updates a post-installation message (PIM) definition. You can use this in your post-installation script using this code: + * + * require_once JPATH_LIBRARIES . '/fof/include.php'; + * FOFModel::getTmpInstance('Messages', 'PostinstallModel')->addPostInstallationMessage($options); + * + * The $options array contains the following mandatory keys: + * + * extension_id The numeric ID of the extension this message is for (see the #__extensions table) + * + * type One of message, link or action. Their meaning is: + * message Informative message. The user can dismiss it. + * link The action button links to a URL. The URL is defined in the action parameter. + * action A PHP action takes place when the action button is clicked. You need to specify the action_file + * (RAD path to the PHP file) and action (PHP function name) keys. See below for more information. + * + * title_key The Text language key for the title of this PIM. + * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_TITLE + * + * description_key The Text language key for the main body (description) of this PIM + * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_DESCRIPTION + * + * action_key The Text language key for the action button. Ignored and not required when type=message + * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_ACTION + * + * language_extension The extension name which holds the language keys used above. + * For example, com_foobar, mod_something, plg_system_whatever, tpl_mytemplate + * + * language_client_id Should we load the frontend (0) or backend (1) language keys? + * + * version_introduced Which was the version of your extension where this message appeared for the first time? + * Example: 3.2.1 + * + * enabled Must be 1 for this message to be enabled. If you omit it, it defaults to 1. + * + * condition_file The RAD path to a PHP file containing a PHP function which determines whether this message should be shown to + * the user. @see FOFTemplateUtils::parsePath() for RAD path format. Joomla! will include this file before calling + * the condition_method. + * Example: admin://components/com_foobar/helpers/postinstall.php + * + * condition_method The name of a PHP function which will be used to determine whether to show this message to the user. This must be + * a simple PHP user function (not a class method, static method etc) which returns true to show the message and false + * to hide it. This function is defined in the condition_file. + * Example: com_foobar_postinstall_messageone_condition + * + * When type=message no additional keys are required. + * + * When type=link the following additional keys are required: + * + * action The URL which will open when the user clicks on the PIM's action button + * Example: index.php?option=com_foobar&view=tools&task=installSampleData + * + * When type=action the following additional keys are required: + * + * action_file The RAD path to a PHP file containing a PHP function which performs the action of this PIM. @see FOFTemplateUtils::parsePath() + * for RAD path format. Joomla! will include this file before calling the function defined in the action key below. + * Example: admin://components/com_foobar/helpers/postinstall.php + * + * action The name of a PHP function which will be used to run the action of this PIM. This must be a simple PHP user function + * (not a class method, static method etc) which returns no result. + * Example: com_foobar_postinstall_messageone_action + * + * @param array $options See description + * + * @return $this + * + * @throws \Exception + */ + public function addPostInstallationMessage(array $options) + { + // Make sure there are options set + if (!is_array($options)) { + throw new \Exception('Post-installation message definitions must be of type array', 500); + } + + // Initialise array keys + $defaultOptions = array( + 'extension_id' => '', + 'type' => '', + 'title_key' => '', + 'description_key' => '', + 'action_key' => '', + 'language_extension' => '', + 'language_client_id' => '', + 'action_file' => '', + 'action' => '', + 'condition_file' => '', + 'condition_method' => '', + 'version_introduced' => '', + 'enabled' => '1', + ); + + $options = array_merge($defaultOptions, $options); + + // Array normalisation. Removes array keys not belonging to a definition. + $defaultKeys = array_keys($defaultOptions); + $allKeys = array_keys($options); + $extraKeys = array_diff($allKeys, $defaultKeys); + + if (!empty($extraKeys)) { + foreach ($extraKeys as $key) { + unset($options[$key]); + } + } + + // Normalisation of integer values + $options['extension_id'] = (int) $options['extension_id']; + $options['language_client_id'] = (int) $options['language_client_id']; + $options['enabled'] = (int) $options['enabled']; + + // Normalisation of 0/1 values + foreach (array('language_client_id', 'enabled') as $key) { + $options[$key] = $options[$key] ? 1 : 0; + } + + // Make sure there's an extension_id + if (!(int) $options['extension_id']) { + throw new \Exception('Post-installation message definitions need an extension_id', 500); + } + + // Make sure there's a valid type + if (!in_array($options['type'], array('message', 'link', 'action'))) { + throw new \Exception('Post-installation message definitions need to declare a type of message, link or action', 500); + } + + // Make sure there's a title key + if (empty($options['title_key'])) { + throw new \Exception('Post-installation message definitions need a title key', 500); + } + + // Make sure there's a description key + if (empty($options['description_key'])) { + throw new \Exception('Post-installation message definitions need a description key', 500); + } + + // If the type is anything other than message you need an action key + if (($options['type'] != 'message') && empty($options['action_key'])) { + throw new \Exception('Post-installation message definitions need an action key when they are of type "' . $options['type'] . '"', 500); + } + + // You must specify the language extension + if (empty($options['language_extension'])) { + throw new \Exception('Post-installation message definitions need to specify which extension contains their language keys', 500); + } + + // The action file and method are only required for the "action" type + if ($options['type'] == 'action') { + if (empty($options['action_file'])) { + throw new \Exception('Post-installation message definitions need an action file when they are of type "action"', 500); + } + + $helper = new PostinstallHelper(); + $file_path = $helper->parsePath($options['action_file']); + + if (!@is_file($file_path)) { + throw new \Exception('The action file ' . $options['action_file'] . ' of your post-installation message definition does not exist', 500); + } + + if (empty($options['action'])) { + throw new \Exception('Post-installation message definitions need an action (function name) when they are of type "action"', 500); + } + } + + if ($options['type'] == 'link') { + if (empty($options['link'])) { + throw new \Exception('Post-installation message definitions need an action (URL) when they are of type "link"', 500); + } + } + + // The condition file and method are only required when the type is not "message" + if ($options['type'] != 'message') { + if (empty($options['condition_file'])) { + throw new \Exception('Post-installation message definitions need a condition file when they are of type "' . $options['type'] . '"', 500); + } + + $helper = new PostinstallHelper(); + $file_path = $helper->parsePath($options['condition_file']); + + if (!@is_file($file_path)) { + throw new \Exception('The condition file ' . $options['condition_file'] . ' of your post-installation message definition does not exist', 500); + } + + if (empty($options['condition_method'])) { + throw new \Exception( + 'Post-installation message definitions need a condition method (function name) when they are of type "' + . $options['type'] . '"', + 500 + ); + } + } + + // Check if the definition exists + $table = $this->getTable(); + $tableName = $table->getTableName(); + $extensionId = (int) $options['extension_id']; + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName($tableName)) + ->where( + [ + $db->quoteName('extension_id') . ' = :extensionId', + $db->quoteName('type') . ' = :type', + $db->quoteName('title_key') . ' = :titleKey', + ] + ) + ->bind(':extensionId', $extensionId, ParameterType::INTEGER) + ->bind(':type', $options['type']) + ->bind(':titleKey', $options['title_key']); + + $existingRow = $db->setQuery($query)->loadAssoc(); + + // Is the existing definition the same as the one we're trying to save? + if (!empty($existingRow)) { + $same = true; + + foreach ($options as $k => $v) { + if ($existingRow[$k] != $v) { + $same = false; + break; + } + } + + // Trying to add the same row as the existing one; quit + if ($same) { + return $this; + } + + // Otherwise it's not the same row. Remove the old row before insert a new one. + $query = $db->getQuery(true) + ->delete($db->quoteName($tableName)) + ->where( + [ + $db->quoteName('extension_id') . ' = :extensionId', + $db->quoteName('type') . ' = :type', + $db->quoteName('title_key') . ' = :titleKey', + ] + ) + ->bind(':extensionId', $extensionId, ParameterType::INTEGER) + ->bind(':type', $options['type']) + ->bind(':titleKey', $options['title_key']); + + $db->setQuery($query)->execute(); + } + + // Insert the new row + $options = (object) $options; + $db->insertObject($tableName, $options); + Factory::getCache()->clean('com_postinstall'); + + return $this; + } + + /** + * Returns the library extension ID. + * + * @return integer + * + * @since 4.0.0 + */ + public function getJoomlaFilesExtensionId() + { + return ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + } } diff --git a/code/administrator/components/com_postinstall/src/View/Messages/HtmlView.php b/code/administrator/components/com_postinstall/src/View/Messages/HtmlView.php index 2163793b..ee9beddd 100644 --- a/code/administrator/components/com_postinstall/src/View/Messages/HtmlView.php +++ b/code/administrator/components/com_postinstall/src/View/Messages/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - - $this->items = $model->getItems(); - - if (!\count($this->items)) - { - $this->setLayout('emptystate'); - } - - $this->joomlaFilesExtensionId = $model->getJoomlaFilesExtensionId(); - $this->eid = (int) $model->getState('eid', $this->joomlaFilesExtensionId); - - if (empty($this->eid)) - { - $this->eid = $this->joomlaFilesExtensionId; - } - - $this->toolbar(); - - $this->token = Factory::getSession()->getFormToken(); - $this->extension_options = $model->getComponentOptions(); - - ToolbarHelper::title(Text::sprintf('COM_POSTINSTALL_MESSAGES_TITLE', $model->getExtensionName($this->eid)), 'bell'); - - parent::display($tpl); - } - - /** - * displays the toolbar - * - * @return void - * - * @since 3.6 - */ - private function toolbar() - { - $toolbar = Toolbar::getInstance('toolbar'); - - if (!empty($this->items)) - { - $toolbar->unpublish('message.hideAll', 'COM_POSTINSTALL_HIDE_ALL_MESSAGES'); - } - - // Options button. - if (Factory::getUser()->authorise('core.admin', 'com_postinstall')) - { - $toolbar->preferences('com_postinstall'); - $toolbar->help('Post-installation_Messages_for_Joomla_CMS'); - } - } + /** + * Executes before rendering the page for the Browse task. + * + * @param string $tpl Subtemplate to use + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + /** @var MessagesModel $model */ + $model = $this->getModel(); + + $this->items = $model->getItems(); + + if (!\count($this->items)) { + $this->setLayout('emptystate'); + } + + $this->joomlaFilesExtensionId = $model->getJoomlaFilesExtensionId(); + $this->eid = (int) $model->getState('eid', $this->joomlaFilesExtensionId); + + if (empty($this->eid)) { + $this->eid = $this->joomlaFilesExtensionId; + } + + $this->toolbar(); + + $this->token = Factory::getSession()->getFormToken(); + $this->extension_options = $model->getComponentOptions(); + + ToolbarHelper::title(Text::sprintf('COM_POSTINSTALL_MESSAGES_TITLE', $model->getExtensionName($this->eid)), 'bell'); + + parent::display($tpl); + } + + /** + * displays the toolbar + * + * @return void + * + * @since 3.6 + */ + private function toolbar() + { + $toolbar = Toolbar::getInstance('toolbar'); + + if (!empty($this->items)) { + $toolbar->unpublish('message.hideAll', 'COM_POSTINSTALL_HIDE_ALL_MESSAGES'); + } + + // Options button. + if ($this->getCurrentUser()->authorise('core.admin', 'com_postinstall')) { + $toolbar->preferences('com_postinstall'); + $toolbar->help('Post-installation_Messages_for_Joomla_CMS'); + } + } } diff --git a/code/administrator/components/com_postinstall/tmpl/messages/default.php b/code/administrator/components/com_postinstall/tmpl/messages/default.php index 0cd1ea64..84cf2892 100644 --- a/code/administrator/components/com_postinstall/tmpl/messages/default.php +++ b/code/administrator/components/com_postinstall/tmpl/messages/default.php @@ -1,4 +1,5 @@
- - - - - extension_options, 'eid', array('onchange' => 'this.form.submit()', 'class' => 'form-select'), 'value', 'text', $this->eid, 'eid'); ?> + + + + + extension_options, 'eid', array('onchange' => 'this.form.submit()', 'class' => 'form-select'), 'value', 'text', $this->eid, 'eid'); ?>
items as $item) : ?> -
-
-

title_key); ?>

-

- version_introduced); ?> -

-
- description_key); ?> - type !== 'message') : ?> - - action_key); ?> - - - getIdentity()->authorise('core.edit.state', 'com_postinstall')) : ?> - - - - -
-
-
+ enabled === 1) : ?> +
+
+

title_key); ?>

+

+ version_introduced); ?> +

+
+ description_key); ?> + type !== 'message') : ?> + + action_key); ?> + + + getIdentity()->authorise('core.edit.state', 'com_postinstall')) : ?> + + + + + + + +
+
+
+ enabled === 2) : ?> +
+
+

title_key); ?>

+
+ getIdentity()->authorise('core.edit.state', 'com_postinstall')) : ?> + + + + + + + +
+
+
+ diff --git a/code/administrator/components/com_postinstall/tmpl/messages/emptystate.php b/code/administrator/components/com_postinstall/tmpl/messages/emptystate.php index f58a1d4c..4c2b43ac 100644 --- a/code/administrator/components/com_postinstall/tmpl/messages/emptystate.php +++ b/code/administrator/components/com_postinstall/tmpl/messages/emptystate.php @@ -1,4 +1,5 @@
- - - - - extension_options, 'eid', array('onchange' => 'this.form.submit()', 'class' => 'form-select'), 'value', 'text', $this->eid, 'eid'); ?> + + + + + extension_options, 'eid', array('onchange' => 'this.form.submit()', 'class' => 'form-select'), 'value', 'text', $this->eid, 'eid'); ?>
'COM_POSTINSTALL', - 'formURL' => 'index.php?option=com_postinstall', - 'icon' => 'icon-bell', - 'createURL' => 'index.php?option=com_postinstall&view=messages&task=message.reset&eid=' . $this->eid . '&' . $this->token . '=1', + 'textPrefix' => 'COM_POSTINSTALL', + 'formURL' => 'index.php?option=com_postinstall', + 'icon' => 'icon-bell', + 'createURL' => 'index.php?option=com_postinstall&view=messages&task=message.reset&eid=' . $this->eid . '&' . $this->token . '=1', ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_privacy/privacy.xml b/code/administrator/components/com_privacy/privacy.xml index 57c51867..fb56c747 100644 --- a/code/administrator/components/com_privacy/privacy.xml +++ b/code/administrator/components/com_privacy/privacy.xml @@ -2,7 +2,7 @@ com_privacy Joomla! Project - May 2018 + 2018-05 (C) 2018 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_privacy/services/provider.php b/code/administrator/components/com_privacy/services/provider.php index fc62cfaf..d4c329ec 100644 --- a/code/administrator/components/com_privacy/services/provider.php +++ b/code/administrator/components/com_privacy/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Privacy')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Privacy')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Privacy')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Privacy')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Privacy')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Privacy')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new PrivacyComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new PrivacyComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_privacy/src/Controller/ConsentsController.php b/code/administrator/components/com_privacy/src/Controller/ConsentsController.php index 10976f3d..66ac0dc5 100644 --- a/code/administrator/components/com_privacy/src/Controller/ConsentsController.php +++ b/code/administrator/components/com_privacy/src/Controller/ConsentsController.php @@ -1,4 +1,5 @@ checkToken(); - - $ids = (array) $this->input->get('cid', [], 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), CMSApplication::MSG_ERROR); - } - else - { - /** @var ConsentsModel $model */ - $model = $this->getModel(); - - if (!$model->invalidate($ids)) - { - $this->setMessage($model->getError()); - } - else - { - $this->setMessage(Text::plural('COM_PRIVACY_N_CONSENTS_INVALIDATED', count($ids))); - } - } - - $this->setRedirect(Route::_('index.php?option=com_privacy&view=consents', false)); - } - - /** - * Method to invalidate all consents of a specific subject. - * - * @return void - * - * @since 3.9.0 - */ - public function invalidateAll() - { - // Check for request forgeries - $this->checkToken(); - - $filters = $this->input->get('filter', [], 'array'); - - $this->setRedirect(Route::_('index.php?option=com_privacy&view=consents', false)); - - if (isset($filters['subject']) && $filters['subject'] != '') - { - $subject = $filters['subject']; - } - else - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED')); - - return; - } - - /** @var ConsentsModel $model */ - $model = $this->getModel(); - - if (!$model->invalidateAll($subject)) - { - $this->setMessage($model->getError()); - } - - $this->setMessage(Text::_('COM_PRIVACY_CONSENTS_INVALIDATED_ALL')); - } + /** + * Method to invalidate specific consents. + * + * @return void + * + * @since 3.9.0 + */ + public function invalidate() + { + // Check for request forgeries + $this->checkToken(); + + $ids = (array) $this->input->get('cid', [], 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), CMSApplication::MSG_ERROR); + } else { + /** @var ConsentsModel $model */ + $model = $this->getModel(); + + if (!$model->invalidate($ids)) { + $this->setMessage($model->getError()); + } else { + $this->setMessage(Text::plural('COM_PRIVACY_N_CONSENTS_INVALIDATED', count($ids))); + } + } + + $this->setRedirect(Route::_('index.php?option=com_privacy&view=consents', false)); + } + + /** + * Method to invalidate all consents of a specific subject. + * + * @return void + * + * @since 3.9.0 + */ + public function invalidateAll() + { + // Check for request forgeries + $this->checkToken(); + + $filters = $this->input->get('filter', [], 'array'); + + $this->setRedirect(Route::_('index.php?option=com_privacy&view=consents', false)); + + if (isset($filters['subject']) && $filters['subject'] != '') { + $subject = $filters['subject']; + } else { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED')); + + return; + } + + /** @var ConsentsModel $model */ + $model = $this->getModel(); + + if (!$model->invalidateAll($subject)) { + $this->setMessage($model->getError()); + } + + $this->setMessage(Text::_('COM_PRIVACY_CONSENTS_INVALIDATED_ALL')); + } } diff --git a/code/administrator/components/com_privacy/src/Controller/DisplayController.php b/code/administrator/components/com_privacy/src/Controller/DisplayController.php index 33d3fbc8..2005caa7 100644 --- a/code/administrator/components/com_privacy/src/Controller/DisplayController.php +++ b/code/administrator/components/com_privacy/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', $this->default_view); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - $model = $this->getModel($vName); - $view->setModel($model, true); - - if ($vName === 'request') - { - // For the default layout, we need to also push the action logs model into the view - if ($lName === 'default') - { - $logsModel = $this->app->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlogs', 'Administrator', ['ignore_request' => true]); - - // Set default ordering for the context - $logsModel->setState('list.fullordering', 'a.log_date DESC'); - - // And push the model into the view - $view->setModel($logsModel, false); - } - - // For the edit layout, if mail sending is disabled then redirect back to the list view as the form is unusable in this state - if ($lName === 'edit' && !$this->app->get('mailonline', 1)) - { - $this->setRedirect( - Route::_('index.php?option=com_privacy&view=requests', false), - Text::_('COM_PRIVACY_WARNING_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED'), - 'warning' - ); - - return $this; - } - } - - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - - $view->display(); - } - - return $this; - } - - /** - * Fetch and report number urgent privacy requests in JSON format, for AJAX requests - * - * @return void - * - * @since 3.9.0 - */ - public function getNumberUrgentRequests() - { - // Check for a valid token. If invalid, send a 403 with the error message. - if (!Session::checkToken('get')) - { - $this->app->setHeader('status', 403, true); - $this->app->sendHeaders(); - echo new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403)); - $this->app->close(); - } - - /** @var RequestsModel $model */ - $model = $this->getModel('requests'); - $numberUrgentRequests = $model->getNumberUrgentRequests(); - - echo new JsonResponse(['number_urgent_requests' => $numberUrgentRequests]); - } + /** + * The default view. + * + * @var string + * @since 3.9.0 + */ + protected $default_view = 'requests'; + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return $this + * + * @since 3.9.0 + */ + public function display($cachable = false, $urlparams = []) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', $this->default_view); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + $model = $this->getModel($vName); + $view->setModel($model, true); + + if ($vName === 'request') { + // For the default layout, we need to also push the action logs model into the view + if ($lName === 'default') { + $logsModel = $this->app->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlogs', 'Administrator', ['ignore_request' => true]); + + // Set default ordering for the context + $logsModel->setState('list.fullordering', 'a.log_date DESC'); + + // And push the model into the view + $view->setModel($logsModel, false); + } + + // For the edit layout, if mail sending is disabled then redirect back to the list view as the form is unusable in this state + if ($lName === 'edit' && !$this->app->get('mailonline', 1)) { + $this->setRedirect( + Route::_('index.php?option=com_privacy&view=requests', false), + Text::_('COM_PRIVACY_WARNING_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED'), + 'warning' + ); + + return $this; + } + } + + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + + $view->display(); + } + + return $this; + } + + /** + * Fetch and report number urgent privacy requests in JSON format, for AJAX requests + * + * @return void + * + * @since 3.9.0 + */ + public function getNumberUrgentRequests() + { + // Check for a valid token. If invalid, send a 403 with the error message. + if (!Session::checkToken('get')) { + $this->app->setHeader('status', 403, true); + $this->app->sendHeaders(); + echo new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403)); + $this->app->close(); + } + + /** @var RequestsModel $model */ + $model = $this->getModel('requests'); + $numberUrgentRequests = $model->getNumberUrgentRequests(); + + echo new JsonResponse(['number_urgent_requests' => $numberUrgentRequests]); + } } diff --git a/code/administrator/components/com_privacy/src/Controller/RequestController.php b/code/administrator/components/com_privacy/src/Controller/RequestController.php index 2f4c55c3..f203f7f7 100644 --- a/code/administrator/components/com_privacy/src/Controller/RequestController.php +++ b/code/administrator/components/com_privacy/src/Controller/RequestController.php @@ -1,4 +1,5 @@ checkToken(); - - /** @var RequestModel $model */ - $model = $this->getModel(); - - /** @var RequestTable $table */ - $table = $model->getTable(); - - // Determine the name of the primary key for the data. - if (empty($key)) - { - $key = $table->getKeyName(); - } - - // To avoid data collisions the urlVar may be different from the primary key. - if (empty($urlVar)) - { - $urlVar = $key; - } - - $recordId = $this->input->getInt($urlVar); - - $item = $model->getItem($recordId); - - // Ensure this record can transition to the requested state - if (!$this->canTransition($item, '2')) - { - $this->setMessage(Text::_('COM_PRIVACY_ERROR_COMPLETE_TRANSITION_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Build the data array for the update - $data = [ - $key => $recordId, - 'status' => '2', - ]; - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Log the request completed - $model->logRequestCompleted($recordId); - - $this->setMessage(Text::_('COM_PRIVACY_REQUEST_COMPLETED')); - - $url = 'index.php?option=com_privacy&view=requests'; - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - - return true; - } - - /** - * Method to email the data export for a request. - * - * @return boolean - * - * @since 3.9.0 - */ - public function emailexport() - { - // Check for request forgeries. - $this->checkToken('get'); - - /** @var ExportModel $model */ - $model = $this->getModel('Export'); - - $recordId = $this->input->getUint('id'); - - if (!$model->emailDataExport($recordId)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('COM_PRIVACY_ERROR_EXPORT_EMAIL_FAILED', $model->getError()), 'error'); - } - else - { - $this->setMessage(Text::_('COM_PRIVACY_EXPORT_EMAILED')); - } - - $url = 'index.php?option=com_privacy&view=requests'; - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - - return true; - } - - /** - * Method to export the data for a request. - * - * @return $this - * - * @since 3.9.0 - */ - public function export() - { - $this->input->set('view', 'export'); - - return $this->display(); - } - - /** - * Method to invalidate a request. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean - * - * @since 3.9.0 - */ - public function invalidate($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - /** @var RequestModel $model */ - $model = $this->getModel(); - - /** @var RequestTable $table */ - $table = $model->getTable(); - - // Determine the name of the primary key for the data. - if (empty($key)) - { - $key = $table->getKeyName(); - } - - // To avoid data collisions the urlVar may be different from the primary key. - if (empty($urlVar)) - { - $urlVar = $key; - } - - $recordId = $this->input->getInt($urlVar); - - $item = $model->getItem($recordId); - - // Ensure this record can transition to the requested state - if (!$this->canTransition($item, '-1')) - { - $this->setMessage(Text::_('COM_PRIVACY_ERROR_INVALID_TRANSITION_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Build the data array for the update - $data = [ - $key => $recordId, - 'status' => '-1', - ]; - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Log the request invalidated - $model->logRequestInvalidated($recordId); - - $this->setMessage(Text::_('COM_PRIVACY_REQUEST_INVALIDATED')); - - $url = 'index.php?option=com_privacy&view=requests'; - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - - return true; - } - - /** - * Method to remove the user data for a privacy remove request. - * - * @return boolean - * - * @since 3.9.0 - */ - public function remove() - { - // Check for request forgeries. - $this->checkToken('request'); - - /** @var RemoveModel $model */ - $model = $this->getModel('Remove'); - - $recordId = $this->input->getUint('id'); - - if (!$model->removeDataForRequest($recordId)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('COM_PRIVACY_ERROR_REMOVE_DATA_FAILED', $model->getError()), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - $this->setMessage(Text::_('COM_PRIVACY_DATA_REMOVED')); - - $url = 'index.php?option=com_privacy&view=requests'; - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - - return true; - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.9.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = []) - { - // This hook only processes new items - if (!$model->getState($model->getName() . '.new', false)) - { - return; - } - - if (!$model->logRequestCreated($model->getState($model->getName() . '.id'))) - { - if ($error = $model->getError()) - { - $this->app->enqueueMessage($error, 'warning'); - } - } - - if (!$model->notifyUserAdminCreatedRequest($model->getState($model->getName() . '.id'))) - { - if ($error = $model->getError()) - { - $this->app->enqueueMessage($error, 'warning'); - } - } - else - { - $this->app->enqueueMessage(Text::_('COM_PRIVACY_MSG_CONFIRM_EMAIL_SENT_TO_USER')); - } - } - - /** - * Method to determine if an item can transition to the specified status. - * - * @param object $item The item being updated. - * @param string $newStatus The new status of the item. - * - * @return boolean - * - * @since 3.9.0 - */ - private function canTransition($item, $newStatus) - { - switch ($item->status) - { - case '0': - // A pending item can only move to invalid through this controller due to the requirement for a user to confirm the request - return $newStatus === '-1'; - - case '1': - // A confirmed item can be marked completed or invalid - return in_array($newStatus, ['-1', '2'], true); - - // An item which is already in an invalid or complete state cannot transition, likewise if we don't know the state don't change anything - case '-1': - case '2': - default: - return false; - } - } + /** + * Method to complete a request. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean + * + * @since 3.9.0 + */ + public function complete($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var RequestModel $model */ + $model = $this->getModel(); + + /** @var RequestTable $table */ + $table = $model->getTable(); + + // Determine the name of the primary key for the data. + if (empty($key)) { + $key = $table->getKeyName(); + } + + // To avoid data collisions the urlVar may be different from the primary key. + if (empty($urlVar)) { + $urlVar = $key; + } + + $recordId = $this->input->getInt($urlVar); + + $item = $model->getItem($recordId); + + // Ensure this record can transition to the requested state + if (!$this->canTransition($item, '2')) { + $this->setMessage(Text::_('COM_PRIVACY_ERROR_COMPLETE_TRANSITION_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Build the data array for the update + $data = [ + $key => $recordId, + 'status' => '2', + ]; + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Log the request completed + $model->logRequestCompleted($recordId); + + $this->setMessage(Text::_('COM_PRIVACY_REQUEST_COMPLETED')); + + $url = 'index.php?option=com_privacy&view=requests'; + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + + return true; + } + + /** + * Method to email the data export for a request. + * + * @return boolean + * + * @since 3.9.0 + */ + public function emailexport() + { + // Check for request forgeries. + $this->checkToken('get'); + + /** @var ExportModel $model */ + $model = $this->getModel('Export'); + + $recordId = $this->input->getUint('id'); + + if (!$model->emailDataExport($recordId)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('COM_PRIVACY_ERROR_EXPORT_EMAIL_FAILED', $model->getError()), 'error'); + } else { + $this->setMessage(Text::_('COM_PRIVACY_EXPORT_EMAILED')); + } + + $url = 'index.php?option=com_privacy&view=requests'; + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + + return true; + } + + /** + * Method to export the data for a request. + * + * @return $this + * + * @since 3.9.0 + */ + public function export() + { + $this->input->set('view', 'export'); + + return $this->display(); + } + + /** + * Method to invalidate a request. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean + * + * @since 3.9.0 + */ + public function invalidate($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var RequestModel $model */ + $model = $this->getModel(); + + /** @var RequestTable $table */ + $table = $model->getTable(); + + // Determine the name of the primary key for the data. + if (empty($key)) { + $key = $table->getKeyName(); + } + + // To avoid data collisions the urlVar may be different from the primary key. + if (empty($urlVar)) { + $urlVar = $key; + } + + $recordId = $this->input->getInt($urlVar); + + $item = $model->getItem($recordId); + + // Ensure this record can transition to the requested state + if (!$this->canTransition($item, '-1')) { + $this->setMessage(Text::_('COM_PRIVACY_ERROR_INVALID_TRANSITION_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Build the data array for the update + $data = [ + $key => $recordId, + 'status' => '-1', + ]; + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Log the request invalidated + $model->logRequestInvalidated($recordId); + + $this->setMessage(Text::_('COM_PRIVACY_REQUEST_INVALIDATED')); + + $url = 'index.php?option=com_privacy&view=requests'; + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + + return true; + } + + /** + * Method to remove the user data for a privacy remove request. + * + * @return boolean + * + * @since 3.9.0 + */ + public function remove() + { + // Check for request forgeries. + $this->checkToken('request'); + + /** @var RemoveModel $model */ + $model = $this->getModel('Remove'); + + $recordId = $this->input->getUint('id'); + + if (!$model->removeDataForRequest($recordId)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('COM_PRIVACY_ERROR_REMOVE_DATA_FAILED', $model->getError()), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + $this->setMessage(Text::_('COM_PRIVACY_DATA_REMOVED')); + + $url = 'index.php?option=com_privacy&view=requests'; + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + + return true; + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.9.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = []) + { + // This hook only processes new items + if (!$model->getState($model->getName() . '.new', false)) { + return; + } + + if (!$model->logRequestCreated($model->getState($model->getName() . '.id'))) { + if ($error = $model->getError()) { + $this->app->enqueueMessage($error, 'warning'); + } + } + + if (!$model->notifyUserAdminCreatedRequest($model->getState($model->getName() . '.id'))) { + if ($error = $model->getError()) { + $this->app->enqueueMessage($error, 'warning'); + } + } else { + $this->app->enqueueMessage(Text::_('COM_PRIVACY_MSG_CONFIRM_EMAIL_SENT_TO_USER')); + } + } + + /** + * Method to determine if an item can transition to the specified status. + * + * @param object $item The item being updated. + * @param string $newStatus The new status of the item. + * + * @return boolean + * + * @since 3.9.0 + */ + private function canTransition($item, $newStatus) + { + switch ($item->status) { + case '0': + // A pending item can only move to invalid through this controller due to the requirement for a user to confirm the request + return $newStatus === '-1'; + + case '1': + // A confirmed item can be marked completed or invalid + return in_array($newStatus, ['-1', '2'], true); + + // An item which is already in an invalid or complete state cannot transition, likewise if we don't know the state don't change anything + case '-1': + case '2': + default: + return false; + } + } } diff --git a/code/administrator/components/com_privacy/src/Controller/RequestsController.php b/code/administrator/components/com_privacy/src/Controller/RequestsController.php index 4b70cfab..8f916549 100644 --- a/code/administrator/components/com_privacy/src/Controller/RequestsController.php +++ b/code/administrator/components/com_privacy/src/Controller/RequestsController.php @@ -1,4 +1,5 @@ true]) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return BaseDatabaseModel|boolean Model object on success; otherwise false on failure. + * + * @since 3.9.0 + */ + public function getModel($name = 'Request', $prefix = 'Administrator', $config = ['ignore_request' => true]) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_privacy/src/Dispatcher/Dispatcher.php b/code/administrator/components/com_privacy/src/Dispatcher/Dispatcher.php index ea1e2921..189a0399 100644 --- a/code/administrator/components/com_privacy/src/Dispatcher/Dispatcher.php +++ b/code/administrator/components/com_privacy/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + /** + * Method to check component access permission + * + * @return void + */ + protected function checkAccess() + { + // Check the user has permission to access this component if in the backend + if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/code/administrator/components/com_privacy/src/Export/Domain.php b/code/administrator/components/com_privacy/src/Export/Domain.php index 5ed37948..22546ff1 100644 --- a/code/administrator/components/com_privacy/src/Export/Domain.php +++ b/code/administrator/components/com_privacy/src/Export/Domain.php @@ -1,4 +1,5 @@ items[] = $item; - } + /** + * Add an item to the domain + * + * @param Item $item The item to add + * + * @return void + * + * @since 3.9.0 + */ + public function addItem(Item $item) + { + $this->items[] = $item; + } - /** - * Get the domain's items - * - * @return Item[] - * - * @since 3.9.0 - */ - public function getItems() - { - return $this->items; - } + /** + * Get the domain's items + * + * @return Item[] + * + * @since 3.9.0 + */ + public function getItems() + { + return $this->items; + } } diff --git a/code/administrator/components/com_privacy/src/Export/Field.php b/code/administrator/components/com_privacy/src/Export/Field.php index 81c94dec..2d53de5d 100644 --- a/code/administrator/components/com_privacy/src/Export/Field.php +++ b/code/administrator/components/com_privacy/src/Export/Field.php @@ -1,4 +1,5 @@ fields[] = $field; - } + /** + * Add a field to the item + * + * @param Field $field The field to add + * + * @return void + * + * @since 3.9.0 + */ + public function addField(Field $field) + { + $this->fields[] = $field; + } - /** - * Get the item's fields - * - * @return Field[] - * - * @since 3.9.0 - */ - public function getFields() - { - return $this->fields; - } + /** + * Get the item's fields + * + * @return Field[] + * + * @since 3.9.0 + */ + public function getFields() + { + return $this->fields; + } } diff --git a/code/administrator/components/com_privacy/src/Extension/PrivacyComponent.php b/code/administrator/components/com_privacy/src/Extension/PrivacyComponent.php index d270d19b..a2e8dc8e 100644 --- a/code/administrator/components/com_privacy/src/Extension/PrivacyComponent.php +++ b/code/administrator/components/com_privacy/src/Extension/PrivacyComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('privacy', new Privacy); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('privacy', new Privacy()); + } } diff --git a/code/administrator/components/com_privacy/src/Field/RequeststatusField.php b/code/administrator/components/com_privacy/src/Field/RequeststatusField.php index 56bb22bd..0e276154 100644 --- a/code/administrator/components/com_privacy/src/Field/RequeststatusField.php +++ b/code/administrator/components/com_privacy/src/Field/RequeststatusField.php @@ -1,4 +1,5 @@ 'COM_PRIVACY_STATUS_INVALID', - '0' => 'COM_PRIVACY_STATUS_PENDING', - '1' => 'COM_PRIVACY_STATUS_CONFIRMED', - '2' => 'COM_PRIVACY_STATUS_COMPLETED', - ]; + /** + * Available statuses + * + * @var array + * @since 3.9.0 + */ + protected $predefinedOptions = [ + '-1' => 'COM_PRIVACY_STATUS_INVALID', + '0' => 'COM_PRIVACY_STATUS_PENDING', + '1' => 'COM_PRIVACY_STATUS_CONFIRMED', + '2' => 'COM_PRIVACY_STATUS_COMPLETED', + ]; } diff --git a/code/administrator/components/com_privacy/src/Field/RequesttypeField.php b/code/administrator/components/com_privacy/src/Field/RequesttypeField.php index 9d0cbacf..08b07ddc 100644 --- a/code/administrator/components/com_privacy/src/Field/RequesttypeField.php +++ b/code/administrator/components/com_privacy/src/Field/RequesttypeField.php @@ -1,4 +1,5 @@ 'COM_PRIVACY_HEADING_REQUEST_TYPE_TYPE_EXPORT', - 'remove' => 'COM_PRIVACY_HEADING_REQUEST_TYPE_TYPE_REMOVE', - ]; + /** + * Available types + * + * @var array + * @since 3.9.0 + */ + protected $predefinedOptions = [ + 'export' => 'COM_PRIVACY_HEADING_REQUEST_TYPE_TYPE_EXPORT', + 'remove' => 'COM_PRIVACY_HEADING_REQUEST_TYPE_TYPE_REMOVE', + ]; } diff --git a/code/administrator/components/com_privacy/src/Helper/PrivacyHelper.php b/code/administrator/components/com_privacy/src/Helper/PrivacyHelper.php index a8c0f9fa..325cfc48 100644 --- a/code/administrator/components/com_privacy/src/Helper/PrivacyHelper.php +++ b/code/administrator/components/com_privacy/src/Helper/PrivacyHelper.php @@ -1,4 +1,5 @@ '); + /** + * Render the data request as a XML document. + * + * @param Domain[] $exportData The data to be exported. + * + * @return string + * + * @since 3.9.0 + */ + public static function renderDataAsXml(array $exportData) + { + $export = new \SimpleXMLElement(''); - foreach ($exportData as $domain) - { - $xmlDomain = $export->addChild('domain'); - $xmlDomain->addAttribute('name', $domain->name); - $xmlDomain->addAttribute('description', $domain->description); + foreach ($exportData as $domain) { + $xmlDomain = $export->addChild('domain'); + $xmlDomain->addAttribute('name', $domain->name); + $xmlDomain->addAttribute('description', $domain->description); - foreach ($domain->getItems() as $item) - { - $xmlItem = $xmlDomain->addChild('item'); + foreach ($domain->getItems() as $item) { + $xmlItem = $xmlDomain->addChild('item'); - if ($item->id) - { - $xmlItem->addAttribute('id', $item->id); - } + if ($item->id) { + $xmlItem->addAttribute('id', $item->id); + } - foreach ($item->getFields() as $field) - { - $xmlItem->{$field->name} = $field->value; - } - } - } + foreach ($item->getFields() as $field) { + $xmlItem->{$field->name} = $field->value; + } + } + } - $dom = new \DOMDocument; - $dom->loadXML($export->asXML()); - $dom->formatOutput = true; + $dom = new \DOMDocument(); + $dom->loadXML($export->asXML()); + $dom->formatOutput = true; - return $dom->saveXML(); - } + return $dom->saveXML(); + } - /** - * Gets the privacyconsent system plugin extension id. - * - * @return integer The privacyconsent system plugin extension id. - * - * @since 3.9.2 - */ - public static function getPrivacyConsentPluginId() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('privacyconsent')); + /** + * Gets the privacyconsent system plugin extension id. + * + * @return integer The privacyconsent system plugin extension id. + * + * @since 3.9.2 + */ + public static function getPrivacyConsentPluginId() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('privacyconsent')); - $db->setQuery($query); + $db->setQuery($query); - return (int) $db->loadResult(); - } + return (int) $db->loadResult(); + } } diff --git a/code/administrator/components/com_privacy/src/Model/CapabilitiesModel.php b/code/administrator/components/com_privacy/src/Model/CapabilitiesModel.php index 98296574..dc5d8d00 100644 --- a/code/administrator/components/com_privacy/src/Model/CapabilitiesModel.php +++ b/code/administrator/components/com_privacy/src/Model/CapabilitiesModel.php @@ -1,4 +1,5 @@ [ - Text::_('COM_PRIVACY_CORE_CAPABILITY_SESSION_IP_ADDRESS_AND_COOKIE'), - Text::sprintf('COM_PRIVACY_CORE_CAPABILITY_LOGGING_IP_ADDRESS', $app->get('log_path', JPATH_ADMINISTRATOR . '/logs')), - Text::_('COM_PRIVACY_CORE_CAPABILITY_COMMUNICATION_WITH_JOOMLA_ORG'), - ], - ]; + $coreCapabilities = [ + Text::_('COM_PRIVACY_HEADING_CORE_CAPABILITIES') => [ + Text::_('COM_PRIVACY_CORE_CAPABILITY_SESSION_IP_ADDRESS_AND_COOKIE'), + Text::sprintf('COM_PRIVACY_CORE_CAPABILITY_LOGGING_IP_ADDRESS', $app->get('log_path', JPATH_ADMINISTRATOR . '/logs')), + Text::_('COM_PRIVACY_CORE_CAPABILITY_COMMUNICATION_WITH_JOOMLA_ORG'), + ], + ]; - /* - * We will search for capabilities from the following plugin groups: - * - * - Authentication: These plugins by design process user information and may have capabilities such as creating cookies - * - Captcha: These plugins may communicate information to third party systems - * - Installer: These plugins can add additional install capabilities to the Extension Manager, such as the Install from Web service - * - Privacy: These plugins are the primary integration point into this component - * - User: These plugins are intended to extend the user management system - * - * This is in addition to plugin groups which are imported before this method is triggered, generally this is the system group. - */ + /* + * We will search for capabilities from the following plugin groups: + * + * - Authentication: These plugins by design process user information and may have capabilities such as creating cookies + * - Captcha: These plugins may communicate information to third party systems + * - Installer: These plugins can add additional install capabilities to the Extension Manager, such as the Install from Web service + * - Privacy: These plugins are the primary integration point into this component + * - User: These plugins are intended to extend the user management system + * + * This is in addition to plugin groups which are imported before this method is triggered, generally this is the system group. + */ - PluginHelper::importPlugin('authentication'); - PluginHelper::importPlugin('captcha'); - PluginHelper::importPlugin('installer'); - PluginHelper::importPlugin('privacy'); - PluginHelper::importPlugin('user'); + PluginHelper::importPlugin('authentication'); + PluginHelper::importPlugin('captcha'); + PluginHelper::importPlugin('installer'); + PluginHelper::importPlugin('privacy'); + PluginHelper::importPlugin('user'); - $pluginResults = $app->triggerEvent('onPrivacyCollectAdminCapabilities'); + $pluginResults = $app->triggerEvent('onPrivacyCollectAdminCapabilities'); - // We are going to "cheat" here and include this component's capabilities without using a plugin - $extensionCapabilities = [ - Text::_('COM_PRIVACY') => [ - Text::_('COM_PRIVACY_EXTENSION_CAPABILITY_PERSONAL_INFO'), - ], - ]; + // We are going to "cheat" here and include this component's capabilities without using a plugin + $extensionCapabilities = [ + Text::_('COM_PRIVACY') => [ + Text::_('COM_PRIVACY_EXTENSION_CAPABILITY_PERSONAL_INFO'), + ], + ]; - foreach ($pluginResults as $pluginResult) - { - $extensionCapabilities += $pluginResult; - } + foreach ($pluginResults as $pluginResult) { + $extensionCapabilities += $pluginResult; + } - // Sort the extension list alphabetically - ksort($extensionCapabilities); + // Sort the extension list alphabetically + ksort($extensionCapabilities); - // Always prepend the core capabilities to the array - return $coreCapabilities + $extensionCapabilities; - } + // Always prepend the core capabilities to the array + return $coreCapabilities + $extensionCapabilities; + } - /** - * Method to auto-populate the model state. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - } + /** + * Method to auto-populate the model state. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + } } diff --git a/code/administrator/components/com_privacy/src/Model/ConsentsModel.php b/code/administrator/components/com_privacy/src/Model/ConsentsModel.php index ca255fed..61645662 100644 --- a/code/administrator/components/com_privacy/src/Model/ConsentsModel.php +++ b/code/administrator/components/com_privacy/src/Model/ConsentsModel.php @@ -1,4 +1,5 @@ getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select($this->getState('list.select', 'a.*')); - $query->from($db->quoteName('#__privacy_consents', 'a')); - - // Join over the users for the username and name. - $query->select($db->quoteName('u.username', 'username')) - ->select($db->quoteName('u.name', 'name')); - $query->join('LEFT', $db->quoteName('#__users', 'u') . ' ON u.id = a.user_id'); - - // Filter by search in email - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - elseif (stripos($search, 'uid:') === 0) - { - $uid = (int) substr($search, 4); - $query->where($db->quoteName('a.user_id') . ' = :uid') - ->bind(':uid', $uid, ParameterType::INTEGER); - } - elseif (stripos($search, 'name:') === 0) - { - $search = '%' . substr($search, 5) . '%'; - $query->where($db->quoteName('u.name') . ' LIKE :search') - ->bind(':search', $search); - } - else - { - $search = '%' . $search . '%'; - $query->where('(' . $db->quoteName('u.username') . ' LIKE :search)') - ->bind(':search', $search); - } - } - - $state = $this->getState('filter.state'); - - if ($state != '') - { - $state = (int) $state; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - - // Handle the list ordering. - $ordering = $this->getState('list.ordering'); - $direction = $this->getState('list.direction'); - - if (!empty($ordering)) - { - $query->order($db->escape($ordering) . ' ' . $db->escape($direction)); - } - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string - * - * @since 3.9.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState($ordering = 'a.id', $direction = 'desc') - { - // Load the filter state. - $this->setState( - 'filter.search', - $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search') - ); - - $this->setState( - 'filter.subject', - $this->getUserStateFromRequest($this->context . '.filter.subject', 'filter_subject') - ); - - $this->setState( - 'filter.state', - $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state') - ); - - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to invalidate specific consents. - * - * @param array $pks The ids of the consents to invalidate. - * - * @return boolean True on success. - */ - public function invalidate($pks) - { - // Sanitize the ids. - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - - try - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__privacy_consents')) - ->set($db->quoteName('state') . ' = -1') - ->whereIn($db->quoteName('id'), $pks) - ->where($db->quoteName('state') . ' = 1'); - $db->setQuery($query); - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return true; - } - - /** - * Method to invalidate a group of specific consents. - * - * @param array $subject The subject of the consents to invalidate. - * - * @return boolean True on success. - */ - public function invalidateAll($subject) - { - try - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__privacy_consents')) - ->set($db->quoteName('state') . ' = -1') - ->where($db->quoteName('subject') . ' = :subject') - ->where($db->quoteName('state') . ' = 1') - ->bind(':subject', $subject); - $db->setQuery($query); - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 3.9.0 + */ + public function __construct($config = []) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'id', 'a.id', + 'user_id', 'a.user_id', + 'subject', 'a.subject', + 'created', 'a.created', + 'username', 'u.username', + 'name', 'u.name', + 'state', 'a.state', + ]; + } + + parent::__construct($config); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return DatabaseQuery + * + * @since 3.9.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select($this->getState('list.select', 'a.*')); + $query->from($db->quoteName('#__privacy_consents', 'a')); + + // Join over the users for the username and name. + $query->select($db->quoteName('u.username', 'username')) + ->select($db->quoteName('u.name', 'name')); + $query->join('LEFT', $db->quoteName('#__users', 'u') . ' ON u.id = a.user_id'); + + // Filter by search in email + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } elseif (stripos($search, 'uid:') === 0) { + $uid = (int) substr($search, 4); + $query->where($db->quoteName('a.user_id') . ' = :uid') + ->bind(':uid', $uid, ParameterType::INTEGER); + } elseif (stripos($search, 'name:') === 0) { + $search = '%' . substr($search, 5) . '%'; + $query->where($db->quoteName('u.name') . ' LIKE :search') + ->bind(':search', $search); + } else { + $search = '%' . $search . '%'; + $query->where('(' . $db->quoteName('u.username') . ' LIKE :search)') + ->bind(':search', $search); + } + } + + $state = $this->getState('filter.state'); + + if ($state != '') { + $state = (int) $state; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } + + // Handle the list ordering. + $ordering = $this->getState('list.ordering'); + $direction = $this->getState('list.direction'); + + if (!empty($ordering)) { + $query->order($db->escape($ordering) . ' ' . $db->escape($direction)); + } + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string + * + * @since 3.9.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + // Load the filter state. + $this->setState( + 'filter.search', + $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search') + ); + + $this->setState( + 'filter.subject', + $this->getUserStateFromRequest($this->context . '.filter.subject', 'filter_subject') + ); + + $this->setState( + 'filter.state', + $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state') + ); + + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to invalidate specific consents. + * + * @param array $pks The ids of the consents to invalidate. + * + * @return boolean True on success. + */ + public function invalidate($pks) + { + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__privacy_consents')) + ->set($db->quoteName('state') . ' = -1') + ->whereIn($db->quoteName('id'), $pks) + ->where($db->quoteName('state') . ' = 1'); + $db->setQuery($query); + $db->execute(); + } catch (ExecutionFailureException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } + + /** + * Method to invalidate a group of specific consents. + * + * @param array $subject The subject of the consents to invalidate. + * + * @return boolean True on success. + */ + public function invalidateAll($subject) + { + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__privacy_consents')) + ->set($db->quoteName('state') . ' = -1') + ->where($db->quoteName('subject') . ' = :subject') + ->where($db->quoteName('state') . ' = 1') + ->bind(':subject', $subject); + $db->setQuery($query); + $db->execute(); + } catch (ExecutionFailureException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } } diff --git a/code/administrator/components/com_privacy/src/Model/ExportModel.php b/code/administrator/components/com_privacy/src/Model/ExportModel.php index acbbc335..2b6dd704 100644 --- a/code/administrator/components/com_privacy/src/Model/ExportModel.php +++ b/code/administrator/components/com_privacy/src/Model/ExportModel.php @@ -1,4 +1,5 @@ getState($this->getName() . '.request_id'); - - if (!$id) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_EXPORT')); - - return false; - } - - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); + /** + * Create the export document for an information request. + * + * @param integer $id The request ID to process + * + * @return Domain[]|boolean A SimpleXMLElement object for a successful export or boolean false on an error + * + * @since 3.9.0 + */ + public function collectDataForExportRequest($id = null) + { + $id = !empty($id) ? $id : (int) $this->getState($this->getName() . '.request_id'); + + if (!$id) { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_EXPORT')); + + return false; + } + + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + if ($table->request_type !== 'export') { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_EXPORT')); + + return false; + } + + if ($table->status != 1) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_EXPORT_UNCONFIRMED_REQUEST')); + + return false; + } + + // If there is a user account associated with the email address, load it here for use in the plugins + $db = $this->getDatabase(); + + $userId = (int) $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $table->email) + ->setLimit(1) + )->loadResult(); + + $user = $userId ? User::getInstance($userId) : null; + + // Log the export + $this->logExport($table); + + PluginHelper::importPlugin('privacy'); + + $pluginResults = Factory::getApplication()->triggerEvent('onPrivacyExportRequest', [$table, $user]); + + $domains = []; + + foreach ($pluginResults as $pluginDomains) { + $domains = array_merge($domains, $pluginDomains); + } + + return $domains; + } + + /** + * Email the data export to the user. + * + * @param integer $id The request ID to process + * + * @return boolean + * + * @since 3.9.0 + */ + public function emailDataExport($id = null) + { + $id = !empty($id) ? $id : (int) $this->getState($this->getName() . '.request_id'); + + if (!$id) { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_EXPORT')); + + return false; + } + + $exportData = $this->collectDataForExportRequest($id); + + if ($exportData === false) { + // Error is already set, we just need to bail + return false; + } + + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + if ($table->request_type !== 'export') { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_EXPORT')); - return false; - } + return false; + } - if ($table->request_type !== 'export') - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_EXPORT')); - - return false; - } - - if ($table->status != 1) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_EXPORT_UNCONFIRMED_REQUEST')); - - return false; - } - - // If there is a user account associated with the email address, load it here for use in the plugins - $db = $this->getDbo(); - - $userId = (int) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $table->email) - ->setLimit(1) - )->loadResult(); - - $user = $userId ? User::getInstance($userId) : null; - - // Log the export - $this->logExport($table); - - PluginHelper::importPlugin('privacy'); - - $pluginResults = Factory::getApplication()->triggerEvent('onPrivacyExportRequest', [$table, $user]); - - $domains = []; - - foreach ($pluginResults as $pluginDomains) - { - $domains = array_merge($domains, $pluginDomains); - } - - return $domains; - } - - /** - * Email the data export to the user. - * - * @param integer $id The request ID to process - * - * @return boolean - * - * @since 3.9.0 - */ - public function emailDataExport($id = null) - { - $id = !empty($id) ? $id : (int) $this->getState($this->getName() . '.request_id'); - - if (!$id) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_EXPORT')); - - return false; - } - - $exportData = $this->collectDataForExportRequest($id); - - if ($exportData === false) - { - // Error is already set, we just need to bail - return false; - } - - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - if ($table->request_type !== 'export') - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_EXPORT')); - - return false; - } - - if ($table->status != 1) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_EXPORT_UNCONFIRMED_REQUEST')); - - return false; - } - - // Log the email - $this->logExportEmailed($table); - - /* - * If there is an associated user account, we will attempt to send this email in the user's preferred language. - * Because of this, it is expected that Language::_() is directly called and that the Text class is NOT used - * for translating all messages. - * - * Error messages will still be displayed to the administrator, so those messages should continue to use the Text class. - */ - - $lang = Factory::getLanguage(); - - $db = $this->getDbo(); - - $userId = (int) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $table->email), - 0, - 1 - )->loadResult(); - - if ($userId) - { - $receiver = User::getInstance($userId); - - /* - * We don't know if the user has admin access, so we will check if they have an admin language in their parameters, - * falling back to the site language, falling back to the currently active language - */ - - $langCode = $receiver->getParam('admin_language', ''); - - if (!$langCode) - { - $langCode = $receiver->getParam('language', $lang->getTag()); - } - - $lang = Language::getInstance($langCode, $lang->getDebug()); - } - - // Ensure the right language files have been loaded - $lang->load('com_privacy', JPATH_ADMINISTRATOR) - || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); - - // The mailer can be set to either throw Exceptions or return boolean false, account for both - try - { - $app = Factory::getApplication(); - $mailer = new MailTemplate('com_privacy.userdataexport', $app->getLanguage()->getTag()); - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'url' => Uri::root(), - ]; - - $mailer->addRecipient($table->email); - $mailer->addTemplateData($templateData); - $mailer->addAttachment('user-data_' . Uri::getInstance()->toString(['host']) . '.xml', PrivacyHelper::renderDataAsXml($exportData)); - - if ($mailer->send() === false) - { - $this->setError($mailer->ErrorInfo); - - return false; - } - - return true; - } - catch (phpmailerException $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Log the data export to the action log system. - * - * @param RequestTable $request The request record being processed - * - * @return void - * - * @since 3.9.0 - */ - public function logExport(RequestTable $request) - { - $user = Factory::getUser(); - - $message = [ - 'action' => 'export', - 'id' => $request->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_EXPORT', 'com_privacy.request', $user->id); - } - - /** - * Log the data export email to the action log system. - * - * @param RequestTable $request The request record being processed - * - * @return void - * - * @since 3.9.0 - */ - public function logExportEmailed(RequestTable $request) - { - $user = Factory::getUser(); - - $message = [ - 'action' => 'export_emailed', - 'id' => $request->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_EXPORT_EMAILED', 'com_privacy.request', $user->id); - } - - /** - * Method to auto-populate the model state. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the pk of the record from the request. - $this->setState($this->getName() . '.request_id', Factory::getApplication()->input->getUint('id')); - - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + if ($table->status != 1) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_EXPORT_UNCONFIRMED_REQUEST')); + + return false; + } + + // Log the email + $this->logExportEmailed($table); + + /* + * If there is an associated user account, we will attempt to send this email in the user's preferred language. + * Because of this, it is expected that Language::_() is directly called and that the Text class is NOT used + * for translating all messages. + * + * Error messages will still be displayed to the administrator, so those messages should continue to use the Text class. + */ + + $lang = Factory::getLanguage(); + + $db = $this->getDatabase(); + + $userId = (int) $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $table->email), + 0, + 1 + )->loadResult(); + + if ($userId) { + $receiver = User::getInstance($userId); + + /* + * We don't know if the user has admin access, so we will check if they have an admin language in their parameters, + * falling back to the site language, falling back to the currently active language + */ + + $langCode = $receiver->getParam('admin_language', ''); + + if (!$langCode) { + $langCode = $receiver->getParam('language', $lang->getTag()); + } + + $lang = Language::getInstance($langCode, $lang->getDebug()); + } + + // Ensure the right language files have been loaded + $lang->load('com_privacy', JPATH_ADMINISTRATOR) + || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); + + // The mailer can be set to either throw Exceptions or return boolean false, account for both + try { + $app = Factory::getApplication(); + $mailer = new MailTemplate('com_privacy.userdataexport', $app->getLanguage()->getTag()); + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'url' => Uri::root(), + ]; + + $mailer->addRecipient($table->email); + $mailer->addTemplateData($templateData); + $mailer->addAttachment('user-data_' . Uri::getInstance()->toString(['host']) . '.xml', PrivacyHelper::renderDataAsXml($exportData)); + + if ($mailer->send() === false) { + $this->setError($mailer->ErrorInfo); + + return false; + } + + return true; + } catch (phpmailerException $exception) { + $this->setError($exception->getMessage()); + + return false; + } + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Log the data export to the action log system. + * + * @param RequestTable $request The request record being processed + * + * @return void + * + * @since 3.9.0 + */ + public function logExport(RequestTable $request) + { + $user = Factory::getUser(); + + $message = [ + 'action' => 'export', + 'id' => $request->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_EXPORT', 'com_privacy.request', $user->id); + } + + /** + * Log the data export email to the action log system. + * + * @param RequestTable $request The request record being processed + * + * @return void + * + * @since 3.9.0 + */ + public function logExportEmailed(RequestTable $request) + { + $user = Factory::getUser(); + + $message = [ + 'action' => 'export_emailed', + 'id' => $request->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_EXPORT_EMAILED', 'com_privacy.request', $user->id); + } + + /** + * Method to auto-populate the model state. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the pk of the record from the request. + $this->setState($this->getName() . '.request_id', Factory::getApplication()->input->getUint('id')); + + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/code/administrator/components/com_privacy/src/Model/RemoveModel.php b/code/administrator/components/com_privacy/src/Model/RemoveModel.php index d950ee14..068b1d99 100644 --- a/code/administrator/components/com_privacy/src/Model/RemoveModel.php +++ b/code/administrator/components/com_privacy/src/Model/RemoveModel.php @@ -1,4 +1,5 @@ getState($this->getName() . '.request_id'); - - if (!$id) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_REMOVE')); - - return false; - } - - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - if ($table->request_type !== 'remove') - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_REMOVE')); - - return false; - } - - if ($table->status != 1) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_REMOVE_UNCONFIRMED_REQUEST')); - - return false; - } - - // If there is a user account associated with the email address, load it here for use in the plugins - $db = $this->getDbo(); - - $userId = (int) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $table->email) - ->setLimit(1) - )->loadResult(); - - $user = $userId ? User::getInstance($userId) : null; - - $canRemove = true; - - PluginHelper::importPlugin('privacy'); - - /** @var Status[] $pluginResults */ - $pluginResults = Factory::getApplication()->triggerEvent('onPrivacyCanRemoveData', [$table, $user]); - - foreach ($pluginResults as $status) - { - if (!$status->canRemove) - { - $this->setError($status->reason ?: Text::_('COM_PRIVACY_ERROR_CANNOT_REMOVE_DATA')); - - $canRemove = false; - } - } - - if (!$canRemove) - { - $this->logRemoveBlocked($table, $this->getErrors()); - - return false; - } - - // Log the removal - $this->logRemove($table); - - Factory::getApplication()->triggerEvent('onPrivacyRemoveData', [$table, $user]); - - return true; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Log the data removal to the action log system. - * - * @param RequestTable $request The request record being processed - * - * @return void - * - * @since 3.9.0 - */ - public function logRemove(RequestTable $request) - { - $user = Factory::getUser(); - - $message = [ - 'action' => 'remove', - 'id' => $request->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_REMOVE', 'com_privacy.request', $user->id); - } - - /** - * Log the data removal being blocked to the action log system. - * - * @param RequestTable $request The request record being processed - * @param string[] $reasons The reasons given why the record could not be removed. - * - * @return void - * - * @since 3.9.0 - */ - public function logRemoveBlocked(RequestTable $request, array $reasons) - { - $user = Factory::getUser(); - - $message = [ - 'action' => 'remove-blocked', - 'id' => $request->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - 'reasons' => implode('; ', $reasons), - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_REMOVE_BLOCKED', 'com_privacy.request', $user->id); - } - - /** - * Method to auto-populate the model state. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the pk of the record from the request. - $this->setState($this->getName() . '.request_id', Factory::getApplication()->input->getUint('id')); - - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + /** + * Remove the user data. + * + * @param integer $id The request ID to process + * + * @return boolean + * + * @since 3.9.0 + */ + public function removeDataForRequest($id = null) + { + $id = !empty($id) ? $id : (int) $this->getState($this->getName() . '.request_id'); + + if (!$id) { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_REMOVE')); + + return false; + } + + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + if ($table->request_type !== 'remove') { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_REMOVE')); + + return false; + } + + if ($table->status != 1) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_REMOVE_UNCONFIRMED_REQUEST')); + + return false; + } + + // If there is a user account associated with the email address, load it here for use in the plugins + $db = $this->getDatabase(); + + $userId = (int) $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $table->email) + ->setLimit(1) + )->loadResult(); + + $user = $userId ? User::getInstance($userId) : null; + + $canRemove = true; + + PluginHelper::importPlugin('privacy'); + + /** @var Status[] $pluginResults */ + $pluginResults = Factory::getApplication()->triggerEvent('onPrivacyCanRemoveData', [$table, $user]); + + foreach ($pluginResults as $status) { + if (!$status->canRemove) { + $this->setError($status->reason ?: Text::_('COM_PRIVACY_ERROR_CANNOT_REMOVE_DATA')); + + $canRemove = false; + } + } + + if (!$canRemove) { + $this->logRemoveBlocked($table, $this->getErrors()); + + return false; + } + + // Log the removal + $this->logRemove($table); + + Factory::getApplication()->triggerEvent('onPrivacyRemoveData', [$table, $user]); + + return true; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Log the data removal to the action log system. + * + * @param RequestTable $request The request record being processed + * + * @return void + * + * @since 3.9.0 + */ + public function logRemove(RequestTable $request) + { + $user = Factory::getUser(); + + $message = [ + 'action' => 'remove', + 'id' => $request->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_REMOVE', 'com_privacy.request', $user->id); + } + + /** + * Log the data removal being blocked to the action log system. + * + * @param RequestTable $request The request record being processed + * @param string[] $reasons The reasons given why the record could not be removed. + * + * @return void + * + * @since 3.9.0 + */ + public function logRemoveBlocked(RequestTable $request, array $reasons) + { + $user = Factory::getUser(); + + $message = [ + 'action' => 'remove-blocked', + 'id' => $request->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + 'reasons' => implode('; ', $reasons), + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_REMOVE_BLOCKED', 'com_privacy.request', $user->id); + } + + /** + * Method to auto-populate the model state. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the pk of the record from the request. + $this->setState($this->getName() . '.request_id', Factory::getApplication()->input->getUint('id')); + + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/code/administrator/components/com_privacy/src/Model/RequestModel.php b/code/administrator/components/com_privacy/src/Model/RequestModel.php index 5ccb8721..779130e0 100644 --- a/code/administrator/components/com_privacy/src/Model/RequestModel.php +++ b/code/administrator/components/com_privacy/src/Model/RequestModel.php @@ -1,4 +1,5 @@ loadForm('com_privacy.request', 'request', ['control' => 'jform', 'load_data' => $loadData]); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 3.9.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_privacy.edit.request.data', []); - - if (empty($data)) - { - $data = $this->getItem(); - } - - return $data; - } - - /** - * Log the completion of a request to the action log system. - * - * @param integer $id The ID of the request to process. - * - * @return boolean - * - * @since 3.9.0 - */ - public function logRequestCompleted($id) - { - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - $user = Factory::getUser(); - - $message = [ - 'action' => 'request-completed', - 'requesttype' => $table->request_type, - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_COMPLETED_REQUEST', 'com_privacy.request', $user->id); - - return true; - } - - /** - * Log the creation of a request to the action log system. - * - * @param integer $id The ID of the request to process. - * - * @return boolean - * - * @since 3.9.0 - */ - public function logRequestCreated($id) - { - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - $user = Factory::getUser(); - - $message = [ - 'action' => 'request-created', - 'requesttype' => $table->request_type, - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_CREATED_REQUEST', 'com_privacy.request', $user->id); - - return true; - } - - /** - * Log the invalidation of a request to the action log system. - * - * @param integer $id The ID of the request to process. - * - * @return boolean - * - * @since 3.9.0 - */ - public function logRequestInvalidated($id) - { - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - $user = Factory::getUser(); - - $message = [ - 'action' => 'request-invalidated', - 'requesttype' => $table->request_type, - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_INVALIDATED_REQUEST', 'com_privacy.request', $user->id); - - return true; - } - - /** - * Notifies the user that an information request has been created by a site administrator. - * - * Because confirmation tokens are stored in the database as a hashed value, this method will generate a new confirmation token - * for the request. - * - * @param integer $id The ID of the request to process. - * - * @return boolean - * - * @since 3.9.0 - */ - public function notifyUserAdminCreatedRequest($id) - { - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - /* - * If there is an associated user account, we will attempt to send this email in the user's preferred language. - * Because of this, it is expected that Language::_() is directly called and that the Text class is NOT used - * for translating all messages. - * - * Error messages will still be displayed to the administrator, so those messages should continue to use the Text class. - */ - - $lang = Factory::getLanguage(); - - $db = $this->getDbo(); - - $userId = (int) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $table->email) - ->setLimit(1) - )->loadResult(); - - if ($userId) - { - $receiver = User::getInstance($userId); - - /* - * We don't know if the user has admin access, so we will check if they have an admin language in their parameters, - * falling back to the site language, falling back to the currently active language - */ - - $langCode = $receiver->getParam('admin_language', ''); - - if (!$langCode) - { - $langCode = $receiver->getParam('language', $lang->getTag()); - } - - $lang = Language::getInstance($langCode, $lang->getDebug()); - } - - // Ensure the right language files have been loaded - $lang->load('com_privacy', JPATH_ADMINISTRATOR) - || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); - - // Regenerate the confirmation token - $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $hashedToken = UserHelper::hashPassword($token); - - $table->confirm_token = $hashedToken; - $table->confirm_token_created_at = Factory::getDate()->toSql(); - - try - { - $table->store(); - } - catch (ExecutionFailureException $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - - // The mailer can be set to either throw Exceptions or return boolean false, account for both - try - { - $app = Factory::getApplication(); - - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'url' => Uri::root(), - 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm&confirm_token=' . $token, false, $linkMode, true), - 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm', false, $linkMode, true), - 'token' => $token, - ]; - - switch ($table->request_type) - { - case 'export': - $mailer = new MailTemplate('com_privacy.notification.admin.export', $app->getLanguage()->getTag()); - - break; - - case 'remove': - $mailer = new MailTemplate('com_privacy.notification.admin.remove', $app->getLanguage()->getTag()); - - break; - - default: - $this->setError(Text::_('COM_PRIVACY_ERROR_UNKNOWN_REQUEST_TYPE')); - - return false; - } - - $mailer->addTemplateData($templateData); - $mailer->addRecipient($table->email); - - $mailer->send(); - - return true; - } - catch (MailDisabledException | phpmailerException $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 3.9.0 - */ - public function save($data) - { - $table = $this->getTable(); - $key = $table->getKeyName(); - $pk = !empty($data[$key]) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); - - if (!$pk && !Factory::getApplication()->get('mailonline', 1)) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED')); - - return false; - } - - return parent::save($data); - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see \Joomla\CMS\Form\FormRule - * @see JFilterInput - * @since 3.9.0 - */ - public function validate($form, $data, $group = null) - { - $validatedData = parent::validate($form, $data, $group); - - // If parent validation failed there's no point in doing our extended validation - if ($validatedData === false) - { - return false; - } - - // Make sure the status is always 0 - $validatedData['status'] = 0; - - // The user cannot create a request for their own account - if (strtolower(Factory::getUser()->email) === strtolower($validatedData['email'])) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_FOR_SELF')); - - return false; - } - - // Check for an active request for this email address - $db = $this->getDbo(); - - $query = $db->getQuery(true) - ->select('COUNT(id)') - ->from($db->quoteName('#__privacy_requests')) - ->where($db->quoteName('email') . ' = :email') - ->where($db->quoteName('request_type') . ' = :requesttype') - ->whereIn($db->quoteName('status'), [0, 1]) - ->bind(':email', $validatedData['email']) - ->bind(':requesttype', $validatedData['request_type']); - - $activeRequestCount = (int) $db->setQuery($query)->loadResult(); - - if ($activeRequestCount > 0) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_ACTIVE_REQUEST_FOR_EMAIL')); - - return false; - } - - return $validatedData; - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + /** + * Clean the cache + * + * @param string $group The cache group + * + * @return void + * + * @since 3.9.0 + */ + protected function cleanCache($group = 'com_privacy') + { + parent::cleanCache('com_privacy'); + } + + /** + * Method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 3.9.0 + */ + public function getForm($data = [], $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_privacy.request', 'request', ['control' => 'jform', 'load_data' => $loadData]); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 3.9.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_privacy.edit.request.data', []); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + /** + * Log the completion of a request to the action log system. + * + * @param integer $id The ID of the request to process. + * + * @return boolean + * + * @since 3.9.0 + */ + public function logRequestCompleted($id) + { + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + $user = Factory::getUser(); + + $message = [ + 'action' => 'request-completed', + 'requesttype' => $table->request_type, + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_COMPLETED_REQUEST', 'com_privacy.request', $user->id); + + return true; + } + + /** + * Log the creation of a request to the action log system. + * + * @param integer $id The ID of the request to process. + * + * @return boolean + * + * @since 3.9.0 + */ + public function logRequestCreated($id) + { + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + $user = Factory::getUser(); + + $message = [ + 'action' => 'request-created', + 'requesttype' => $table->request_type, + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_CREATED_REQUEST', 'com_privacy.request', $user->id); + + return true; + } + + /** + * Log the invalidation of a request to the action log system. + * + * @param integer $id The ID of the request to process. + * + * @return boolean + * + * @since 3.9.0 + */ + public function logRequestInvalidated($id) + { + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + $user = Factory::getUser(); + + $message = [ + 'action' => 'request-invalidated', + 'requesttype' => $table->request_type, + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_INVALIDATED_REQUEST', 'com_privacy.request', $user->id); + + return true; + } + + /** + * Notifies the user that an information request has been created by a site administrator. + * + * Because confirmation tokens are stored in the database as a hashed value, this method will generate a new confirmation token + * for the request. + * + * @param integer $id The ID of the request to process. + * + * @return boolean + * + * @since 3.9.0 + */ + public function notifyUserAdminCreatedRequest($id) + { + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + /* + * If there is an associated user account, we will attempt to send this email in the user's preferred language. + * Because of this, it is expected that Language::_() is directly called and that the Text class is NOT used + * for translating all messages. + * + * Error messages will still be displayed to the administrator, so those messages should continue to use the Text class. + */ + + $lang = Factory::getLanguage(); + + $db = $this->getDatabase(); + + $userId = (int) $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $table->email) + ->setLimit(1) + )->loadResult(); + + if ($userId) { + $receiver = User::getInstance($userId); + + /* + * We don't know if the user has admin access, so we will check if they have an admin language in their parameters, + * falling back to the site language, falling back to the currently active language + */ + + $langCode = $receiver->getParam('admin_language', ''); + + if (!$langCode) { + $langCode = $receiver->getParam('language', $lang->getTag()); + } + + $lang = Language::getInstance($langCode, $lang->getDebug()); + } + + // Ensure the right language files have been loaded + $lang->load('com_privacy', JPATH_ADMINISTRATOR) + || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); + + // Regenerate the confirmation token + $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $hashedToken = UserHelper::hashPassword($token); + + $table->confirm_token = $hashedToken; + $table->confirm_token_created_at = Factory::getDate()->toSql(); + + try { + $table->store(); + } catch (ExecutionFailureException $exception) { + $this->setError($exception->getMessage()); + + return false; + } + + // The mailer can be set to either throw Exceptions or return boolean false, account for both + try { + $app = Factory::getApplication(); + + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'url' => Uri::root(), + 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm&confirm_token=' . $token, false, $linkMode, true), + 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm', false, $linkMode, true), + 'token' => $token, + ]; + + switch ($table->request_type) { + case 'export': + $mailer = new MailTemplate('com_privacy.notification.admin.export', $app->getLanguage()->getTag()); + + break; + + case 'remove': + $mailer = new MailTemplate('com_privacy.notification.admin.remove', $app->getLanguage()->getTag()); + + break; + + default: + $this->setError(Text::_('COM_PRIVACY_ERROR_UNKNOWN_REQUEST_TYPE')); + + return false; + } + + $mailer->addTemplateData($templateData); + $mailer->addRecipient($table->email); + + $mailer->send(); + + return true; + } catch (MailDisabledException | phpmailerException $exception) { + $this->setError($exception->getMessage()); + + return false; + } + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 3.9.0 + */ + public function save($data) + { + $table = $this->getTable(); + $key = $table->getKeyName(); + $pk = !empty($data[$key]) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); + + if (!$pk && !Factory::getApplication()->get('mailonline', 1)) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED')); + + return false; + } + + return parent::save($data); + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see \Joomla\CMS\Form\FormRule + * @see JFilterInput + * @since 3.9.0 + */ + public function validate($form, $data, $group = null) + { + $validatedData = parent::validate($form, $data, $group); + + // If parent validation failed there's no point in doing our extended validation + if ($validatedData === false) { + return false; + } + + // Make sure the status is always 0 + $validatedData['status'] = 0; + + // The user cannot create a request for their own account + if (strtolower(Factory::getUser()->email) === strtolower($validatedData['email'])) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_FOR_SELF')); + + return false; + } + + // Check for an active request for this email address + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('COUNT(id)') + ->from($db->quoteName('#__privacy_requests')) + ->where($db->quoteName('email') . ' = :email') + ->where($db->quoteName('request_type') . ' = :requesttype') + ->whereIn($db->quoteName('status'), [0, 1]) + ->bind(':email', $validatedData['email']) + ->bind(':requesttype', $validatedData['request_type']); + + $activeRequestCount = (int) $db->setQuery($query)->loadResult(); + + if ($activeRequestCount > 0) { + $this->setError(Text::_('COM_PRIVACY_ERROR_ACTIVE_REQUEST_FOR_EMAIL')); + + return false; + } + + return $validatedData; + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/code/administrator/components/com_privacy/src/Model/RequestsModel.php b/code/administrator/components/com_privacy/src/Model/RequestsModel.php index d8903172..25e5042a 100644 --- a/code/administrator/components/com_privacy/src/Model/RequestsModel.php +++ b/code/administrator/components/com_privacy/src/Model/RequestsModel.php @@ -1,4 +1,5 @@ getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select($this->getState('list.select', 'a.*')); - $query->from($db->quoteName('#__privacy_requests', 'a')); - - // Filter by status - $status = $this->getState('filter.status'); - - if (is_numeric($status)) - { - $status = (int) $status; - $query->where($db->quoteName('a.status') . ' = :status') - ->bind(':status', $status, ParameterType::INTEGER); - } - - // Filter by request type - $requestType = $this->getState('filter.request_type', ''); - - if ($requestType) - { - $query->where($db->quoteName('a.request_type') . ' = :requesttype') - ->bind(':requesttype', $requestType); - } - - // Filter by search in email - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . $search . '%'; - $query->where('(' . $db->quoteName('a.email') . ' LIKE :search)') - ->bind(':search', $search); - } - } - - // Handle the list ordering. - $ordering = $this->getState('list.ordering'); - $direction = $this->getState('list.direction'); - - if (!empty($ordering)) - { - $query->order($db->escape($ordering) . ' ' . $db->escape($direction)); - } - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string - * - * @since 3.9.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.status'); - $id .= ':' . $this->getState('filter.request_type'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState($ordering = 'a.id', $direction = 'desc') - { - // Load the filter state. - $this->setState( - 'filter.search', - $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search') - ); - - $this->setState( - 'filter.status', - $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'int') - ); - - $this->setState( - 'filter.request_type', - $this->getUserStateFromRequest($this->context . '.filter.request_type', 'filter_request_type', '', 'string') - ); - - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to return number privacy requests older than X days. - * - * @return integer - * - * @since 3.9.0 - */ - public function getNumberUrgentRequests() - { - // Load the parameters. - $params = ComponentHelper::getComponent('com_privacy')->getParams(); - $notify = (int) $params->get('notify', 14); - $now = Factory::getDate()->toSql(); - $period = '-' . $notify; - - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('COUNT(*)'); - $query->from($db->quoteName('#__privacy_requests')); - $query->where($db->quoteName('status') . ' = 1 '); - $query->where($query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('requested_at')); - $db->setQuery($query); - - return (int) $db->loadResult(); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 3.9.0 + */ + public function __construct($config = []) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'id', 'a.id', + 'email', 'a.email', + 'requested_at', 'a.requested_at', + 'request_type', 'a.request_type', + 'status', 'a.status', + ]; + } + + parent::__construct($config); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return DatabaseQuery + * + * @since 3.9.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select($this->getState('list.select', 'a.*')); + $query->from($db->quoteName('#__privacy_requests', 'a')); + + // Filter by status + $status = $this->getState('filter.status'); + + if (is_numeric($status)) { + $status = (int) $status; + $query->where($db->quoteName('a.status') . ' = :status') + ->bind(':status', $status, ParameterType::INTEGER); + } + + // Filter by request type + $requestType = $this->getState('filter.request_type', ''); + + if ($requestType) { + $query->where($db->quoteName('a.request_type') . ' = :requesttype') + ->bind(':requesttype', $requestType); + } + + // Filter by search in email + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . $search . '%'; + $query->where('(' . $db->quoteName('a.email') . ' LIKE :search)') + ->bind(':search', $search); + } + } + + // Handle the list ordering. + $ordering = $this->getState('list.ordering'); + $direction = $this->getState('list.direction'); + + if (!empty($ordering)) { + $query->order($db->escape($ordering) . ' ' . $db->escape($direction)); + } + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string + * + * @since 3.9.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.status'); + $id .= ':' . $this->getState('filter.request_type'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + // Load the filter state. + $this->setState( + 'filter.search', + $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search') + ); + + $this->setState( + 'filter.status', + $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'int') + ); + + $this->setState( + 'filter.request_type', + $this->getUserStateFromRequest($this->context . '.filter.request_type', 'filter_request_type', '', 'string') + ); + + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to return number privacy requests older than X days. + * + * @return integer + * + * @since 3.9.0 + */ + public function getNumberUrgentRequests() + { + // Load the parameters. + $params = ComponentHelper::getComponent('com_privacy')->getParams(); + $notify = (int) $params->get('notify', 14); + $now = Factory::getDate()->toSql(); + $period = '-' . $notify; + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(*)'); + $query->from($db->quoteName('#__privacy_requests')); + $query->where($db->quoteName('status') . ' = 1 '); + $query->where($query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('requested_at')); + $db->setQuery($query); + + return (int) $db->loadResult(); + } } diff --git a/code/administrator/components/com_privacy/src/Plugin/PrivacyPlugin.php b/code/administrator/components/com_privacy/src/Plugin/PrivacyPlugin.php index 0679a471..a1cc4d88 100644 --- a/code/administrator/components/com_privacy/src/Plugin/PrivacyPlugin.php +++ b/code/administrator/components/com_privacy/src/Plugin/PrivacyPlugin.php @@ -1,4 +1,5 @@ name = $name; - $domain->description = $description; - - return $domain; - } - - /** - * Create an item object for an array - * - * @param array $data The array data to convert - * @param integer|null $itemId The ID of this item - * - * @return Item - * - * @since 3.9.0 - */ - protected function createItemFromArray(array $data, $itemId = null) - { - $item = new Item; - $item->id = $itemId; - - foreach ($data as $key => $value) - { - if (is_object($value)) - { - $value = (array) $value; - } - - if (is_array($value)) - { - $value = print_r($value, true); - } - - $field = new Field; - $field->name = $key; - $field->value = $value; - - $item->addField($field); - } - - return $item; - } - - /** - * Create an item object for a Table object - * - * @param Table $table The Table object to convert - * - * @return Item - * - * @since 3.9.0 - */ - protected function createItemForTable($table) - { - $data = []; - - foreach (array_keys($table->getFields()) as $fieldName) - { - $data[$fieldName] = $table->$fieldName; - } - - return $this->createItemFromArray($data, $table->{$table->getKeyName(false)}); - } - - /** - * Helper function to create the domain for the items custom fields. - * - * @param string $context The context - * @param array $items The items - * - * @return Domain - * - * @since 3.9.0 - */ - protected function createCustomFieldsDomain($context, $items = array()) - { - if (!is_array($items)) - { - $items = [$items]; - } - - $parts = FieldsHelper::extract($context); - - if (!$parts) - { - return []; - } - - $type = str_replace('com_', '', $parts[0]); - - $domain = $this->createDomain($type . '_' . $parts[1] . '_custom_fields', 'joomla_' . $type . '_' . $parts[1] . '_custom_fields_data'); - - foreach ($items as $item) - { - // Get item's fields, also preparing their value property for manual display - $fields = FieldsHelper::getFields($parts[0] . '.' . $parts[1], $item); - - foreach ($fields as $field) - { - $fieldValue = is_array($field->value) ? implode(', ', $field->value) : $field->value; - - $data = [ - $type . '_id' => $item->id, - 'field_name' => $field->name, - 'field_title' => $field->title, - 'field_value' => $fieldValue, - ]; - - $domain->addItem($this->createItemFromArray($data)); - } - } - - return $domain; - } + /** + * Database object + * + * @var \Joomla\Database\DatabaseDriver + * @since 3.9.0 + */ + protected $db; + + /** + * Affects constructor behaviour. If true, language files will be loaded automatically. + * + * @var boolean + * @since 3.9.0 + */ + protected $autoloadLanguage = true; + + /** + * Create a new domain object + * + * @param string $name The domain's name + * @param string $description The domain's description + * + * @return Domain + * + * @since 3.9.0 + */ + protected function createDomain($name, $description = '') + { + $domain = new Domain(); + $domain->name = $name; + $domain->description = $description; + + return $domain; + } + + /** + * Create an item object for an array + * + * @param array $data The array data to convert + * @param integer|null $itemId The ID of this item + * + * @return Item + * + * @since 3.9.0 + */ + protected function createItemFromArray(array $data, $itemId = null) + { + $item = new Item(); + $item->id = $itemId; + + foreach ($data as $key => $value) { + if (is_object($value)) { + $value = (array) $value; + } + + if (is_array($value)) { + $value = print_r($value, true); + } + + $field = new Field(); + $field->name = $key; + $field->value = $value; + + $item->addField($field); + } + + return $item; + } + + /** + * Create an item object for a Table object + * + * @param Table $table The Table object to convert + * + * @return Item + * + * @since 3.9.0 + */ + protected function createItemForTable($table) + { + $data = []; + + foreach (array_keys($table->getFields()) as $fieldName) { + $data[$fieldName] = $table->$fieldName; + } + + return $this->createItemFromArray($data, $table->{$table->getKeyName(false)}); + } + + /** + * Helper function to create the domain for the items custom fields. + * + * @param string $context The context + * @param array $items The items + * + * @return Domain + * + * @since 3.9.0 + */ + protected function createCustomFieldsDomain($context, $items = array()) + { + if (!is_array($items)) { + $items = [$items]; + } + + $parts = FieldsHelper::extract($context); + + if (!$parts) { + return []; + } + + $type = str_replace('com_', '', $parts[0]); + + $domain = $this->createDomain($type . '_' . $parts[1] . '_custom_fields', 'joomla_' . $type . '_' . $parts[1] . '_custom_fields_data'); + + foreach ($items as $item) { + // Get item's fields, also preparing their value property for manual display + $fields = FieldsHelper::getFields($parts[0] . '.' . $parts[1], $item); + + foreach ($fields as $field) { + $fieldValue = is_array($field->value) ? implode(', ', $field->value) : $field->value; + + $data = [ + $type . '_id' => $item->id, + 'field_name' => $field->name, + 'field_title' => $field->title, + 'field_value' => $fieldValue, + ]; + + $domain->addItem($this->createItemFromArray($data)); + } + } + + return $domain; + } } diff --git a/code/administrator/components/com_privacy/src/Removal/Status.php b/code/administrator/components/com_privacy/src/Removal/Status.php index 03b698de..0ac0a5ea 100644 --- a/code/administrator/components/com_privacy/src/Removal/Status.php +++ b/code/administrator/components/com_privacy/src/Removal/Status.php @@ -1,4 +1,5 @@ ' . Text::_('COM_PRIVACY_STATUS_COMPLETED') . ''; - - case 1: - return '' . Text::_('COM_PRIVACY_STATUS_CONFIRMED') . ''; - - case -1: - return '' . Text::_('COM_PRIVACY_STATUS_INVALID') . ''; - - default: - case 0: - return '' . Text::_('COM_PRIVACY_STATUS_PENDING') . ''; - } - } + /** + * Render a status badge + * + * @param integer $status The item status + * + * @return string + * + * @since 3.9.0 + */ + public function statusLabel($status) + { + switch ($status) { + case 2: + return '' . Text::_('COM_PRIVACY_STATUS_COMPLETED') . ''; + + case 1: + return '' . Text::_('COM_PRIVACY_STATUS_CONFIRMED') . ''; + + case -1: + return '' . Text::_('COM_PRIVACY_STATUS_INVALID') . ''; + + default: + case 0: + return '' . Text::_('COM_PRIVACY_STATUS_PENDING') . ''; + } + } } diff --git a/code/administrator/components/com_privacy/src/Table/ConsentTable.php b/code/administrator/components/com_privacy/src/Table/ConsentTable.php index 56330ce8..541ad180 100644 --- a/code/administrator/components/com_privacy/src/Table/ConsentTable.php +++ b/code/administrator/components/com_privacy/src/Table/ConsentTable.php @@ -1,4 +1,5 @@ id) - { - if (!$this->remind) - { - $this->remind = '0'; - } + // Set default values for new records + if (!$this->id) { + if (!$this->remind) { + $this->remind = '0'; + } - if (!$this->created) - { - $this->created = $date->toSql(); - } - } + if (!$this->created) { + $this->created = $date->toSql(); + } + } - return parent::store($updateNulls); - } + return parent::store($updateNulls); + } } diff --git a/code/administrator/components/com_privacy/src/Table/RequestTable.php b/code/administrator/components/com_privacy/src/Table/RequestTable.php index 8028385b..ef2d32cf 100644 --- a/code/administrator/components/com_privacy/src/Table/RequestTable.php +++ b/code/administrator/components/com_privacy/src/Table/RequestTable.php @@ -1,4 +1,5 @@ id) - { - if (!$this->status) - { - $this->status = '0'; - } + // Set default values for new records + if (!$this->id) { + if (!$this->status) { + $this->status = '0'; + } - if (!$this->requested_at) - { - $this->requested_at = $date->toSql(); - } + if (!$this->requested_at) { + $this->requested_at = $date->toSql(); + } - if (!$this->confirm_token_created_at) - { - $this->confirm_token_created_at = null; - } - } + if (!$this->confirm_token_created_at) { + $this->confirm_token_created_at = null; + } + } - return parent::store($updateNulls); - } + return parent::store($updateNulls); + } } diff --git a/code/administrator/components/com_privacy/src/View/Capabilities/HtmlView.php b/code/administrator/components/com_privacy/src/View/Capabilities/HtmlView.php index ca86b2fc..7f31a868 100644 --- a/code/administrator/components/com_privacy/src/View/Capabilities/HtmlView.php +++ b/code/administrator/components/com_privacy/src/View/Capabilities/HtmlView.php @@ -1,4 +1,5 @@ capabilities = $this->get('Capabilities'); - $this->state = $this->get('State'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise variables + $this->capabilities = $this->get('Capabilities'); + $this->state = $this->get('State'); - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new Genericdataexception(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new Genericdataexception(implode("\n", $errors), 500); + } - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.9.0 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_CAPABILITIES'), 'lock'); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.9.0 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_CAPABILITIES'), 'lock'); - ToolbarHelper::preferences('com_privacy'); + ToolbarHelper::preferences('com_privacy'); - ToolbarHelper::help('Privacy:_Extension_Capabilities'); - } + ToolbarHelper::help('Privacy:_Extension_Capabilities'); + } } diff --git a/code/administrator/components/com_privacy/src/View/Consents/HtmlView.php b/code/administrator/components/com_privacy/src/View/Consents/HtmlView.php index 13be2edb..415895a0 100644 --- a/code/administrator/components/com_privacy/src/View/Consents/HtmlView.php +++ b/code/administrator/components/com_privacy/src/View/Consents/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new Genericdataexception(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.9.0 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_CONSENTS'), 'lock'); - - $bar = Toolbar::getInstance('toolbar'); - - // Add a button to invalidate a consent - if (!$this->isEmptyState) - { - $bar->appendButton( - 'Confirm', - 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_CONFIRM_MSG', - 'trash', - 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE', - 'consents.invalidate', - true - ); - } - - // If the filter is restricted to a specific subject, show the "Invalidate all" button - if ($this->state->get('filter.subject') != '') - { - $bar->appendButton( - 'Confirm', - 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_ALL_CONFIRM_MSG', - 'cancel', - 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_ALL', - 'consents.invalidateAll', - false - ); - } - - ToolbarHelper::preferences('com_privacy'); - - ToolbarHelper::help('Privacy:_Consents'); - } + /** + * The active search tools filters + * + * @var array + * @since 3.9.0 + * @note Must be public to be accessed from the search tools layout + */ + public $activeFilters; + + /** + * Form instance containing the search tools filter form + * + * @var Form + * @since 3.9.0 + * @note Must be public to be accessed from the search tools layout + */ + public $filterForm; + + /** + * The items to display + * + * @var array + * @since 3.9.0 + */ + protected $items; + + /** + * The pagination object + * + * @var Pagination + * @since 3.9.0 + */ + protected $pagination; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + /** @var ConsentsModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new Genericdataexception(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.9.0 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_CONSENTS'), 'lock'); + + $bar = Toolbar::getInstance('toolbar'); + + // Add a button to invalidate a consent + if (!$this->isEmptyState) { + $bar->appendButton( + 'Confirm', + 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_CONFIRM_MSG', + 'trash', + 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE', + 'consents.invalidate', + true + ); + } + + // If the filter is restricted to a specific subject, show the "Invalidate all" button + if ($this->state->get('filter.subject') != '') { + $bar->appendButton( + 'Confirm', + 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_ALL_CONFIRM_MSG', + 'cancel', + 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_ALL', + 'consents.invalidateAll', + false + ); + } + + ToolbarHelper::preferences('com_privacy'); + + ToolbarHelper::help('Privacy:_Consents'); + } } diff --git a/code/administrator/components/com_privacy/src/View/Export/XmlView.php b/code/administrator/components/com_privacy/src/View/Export/XmlView.php index 8fdee62f..ba7e8f4f 100644 --- a/code/administrator/components/com_privacy/src/View/Export/XmlView.php +++ b/code/administrator/components/com_privacy/src/View/Export/XmlView.php @@ -1,4 +1,5 @@ getModel(); - - $exportData = $model->collectDataForExportRequest(); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $requestId = $model->getState($model->getName() . '.request_id'); - - // This document should always be downloaded - $this->document->setDownload(true); - $this->document->setName('export-request-' . $requestId); - - echo PrivacyHelper::renderDataAsXml($exportData); - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + * + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + /** @var ExportModel $model */ + $model = $this->getModel(); + + $exportData = $model->collectDataForExportRequest(); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $requestId = $model->getState($model->getName() . '.request_id'); + + // This document should always be downloaded + $this->document->setDownload(true); + $this->document->setName('export-request-' . $requestId); + + echo PrivacyHelper::renderDataAsXml($exportData); + } } diff --git a/code/administrator/components/com_privacy/src/View/Request/HtmlView.php b/code/administrator/components/com_privacy/src/View/Request/HtmlView.php index 0ca8c3de..89a5f8c3 100644 --- a/code/administrator/components/com_privacy/src/View/Request/HtmlView.php +++ b/code/administrator/components/com_privacy/src/View/Request/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->item = $model->getItem(); - $this->state = $model->getState(); - - // Variables only required for the default layout - if ($this->getLayout() === 'default') - { - /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogsModel $logsModel */ - $logsModel = $this->getModel('actionlogs'); - - $this->actionlogs = $logsModel->getLogsForItem('com_privacy.request', $this->item->id); - - // Load the com_actionlogs language strings for use in the layout - $lang = Factory::getLanguage(); - $lang->load('com_actionlogs', JPATH_ADMINISTRATOR) - || $lang->load('com_actionlogs', JPATH_ADMINISTRATOR . '/components/com_actionlogs'); - } - - // Variables only required for the edit layout - if ($this->getLayout() === 'edit') - { - $this->form = $this->get('Form'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.9.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - // Set the title and toolbar based on the layout - if ($this->getLayout() === 'edit') - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUEST_ADD_REQUEST'), 'lock'); - - ToolbarHelper::save('request.save'); - ToolbarHelper::cancel('request.cancel'); - ToolbarHelper::help('Privacy:_New_Information_Request'); - } - else - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUEST_SHOW_REQUEST'), 'lock'); - - $bar = Toolbar::getInstance('toolbar'); - - // Add transition and action buttons based on item status - switch ($this->item->status) - { - case '0': - $bar->appendButton('Standard', 'cancel-circle', 'COM_PRIVACY_TOOLBAR_INVALIDATE', 'request.invalidate', false); - - break; - - case '1': - $return = '&return=' . base64_encode('index.php?option=com_privacy&view=request&id=' . (int) $this->item->id); - - $bar->appendButton('Standard', 'apply', 'COM_PRIVACY_TOOLBAR_COMPLETE', 'request.complete', false); - $bar->appendButton('Standard', 'cancel-circle', 'COM_PRIVACY_TOOLBAR_INVALIDATE', 'request.invalidate', false); - - if ($this->item->request_type === 'export') - { - ToolbarHelper::link( - Route::_('index.php?option=com_privacy&task=request.export&format=xml&id=' . (int) $this->item->id . $return), - 'COM_PRIVACY_ACTION_EXPORT_DATA', - 'download' - ); - - if (Factory::getApplication()->get('mailonline', 1)) - { - ToolbarHelper::link( - Route::_( - 'index.php?option=com_privacy&task=request.emailexport&id=' . (int) $this->item->id . $return - . '&' . Session::getFormToken() . '=1' - ), - 'COM_PRIVACY_ACTION_EMAIL_EXPORT_DATA', - 'mail' - ); - } - } - - if ($this->item->request_type === 'remove') - { - $bar->appendButton('Standard', 'delete', 'COM_PRIVACY_ACTION_DELETE_DATA', 'request.remove', false); - } - - break; - - // Item is in a "locked" state and cannot transition - default: - break; - } - - ToolbarHelper::cancel('request.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::help('Privacy:_Review_Information_Request'); - } - } + /** + * The action logs for the item + * + * @var array + * @since 3.9.0 + */ + protected $actionlogs; + + /** + * The form object + * + * @var Form + * @since 3.9.0 + */ + protected $form; + + /** + * The item record + * + * @var CMSObject + * @since 3.9.0 + */ + protected $item; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + /** @var RequestsModel $model */ + $model = $this->getModel(); + $this->item = $model->getItem(); + $this->state = $model->getState(); + + // Variables only required for the default layout + if ($this->getLayout() === 'default') { + /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogsModel $logsModel */ + $logsModel = $this->getModel('actionlogs'); + + $this->actionlogs = $logsModel->getLogsForItem('com_privacy.request', $this->item->id); + + // Load the com_actionlogs language strings for use in the layout + $lang = Factory::getLanguage(); + $lang->load('com_actionlogs', JPATH_ADMINISTRATOR) + || $lang->load('com_actionlogs', JPATH_ADMINISTRATOR . '/components/com_actionlogs'); + } + + // Variables only required for the edit layout + if ($this->getLayout() === 'edit') { + $this->form = $this->get('Form'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.9.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + // Set the title and toolbar based on the layout + if ($this->getLayout() === 'edit') { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUEST_ADD_REQUEST'), 'lock'); + + ToolbarHelper::save('request.save'); + ToolbarHelper::cancel('request.cancel'); + ToolbarHelper::help('Privacy:_New_Information_Request'); + } else { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUEST_SHOW_REQUEST'), 'lock'); + + $bar = Toolbar::getInstance('toolbar'); + + // Add transition and action buttons based on item status + switch ($this->item->status) { + case '0': + $bar->appendButton('Standard', 'cancel-circle', 'COM_PRIVACY_TOOLBAR_INVALIDATE', 'request.invalidate', false); + + break; + + case '1': + $return = '&return=' . base64_encode('index.php?option=com_privacy&view=request&id=' . (int) $this->item->id); + + $bar->appendButton('Standard', 'apply', 'COM_PRIVACY_TOOLBAR_COMPLETE', 'request.complete', false); + $bar->appendButton('Standard', 'cancel-circle', 'COM_PRIVACY_TOOLBAR_INVALIDATE', 'request.invalidate', false); + + if ($this->item->request_type === 'export') { + ToolbarHelper::link( + Route::_('index.php?option=com_privacy&task=request.export&format=xml&id=' . (int) $this->item->id . $return), + 'COM_PRIVACY_ACTION_EXPORT_DATA', + 'download' + ); + + if (Factory::getApplication()->get('mailonline', 1)) { + ToolbarHelper::link( + Route::_( + 'index.php?option=com_privacy&task=request.emailexport&id=' . (int) $this->item->id . $return + . '&' . Session::getFormToken() . '=1' + ), + 'COM_PRIVACY_ACTION_EMAIL_EXPORT_DATA', + 'mail' + ); + } + } + + if ($this->item->request_type === 'remove') { + $bar->appendButton('Standard', 'delete', 'COM_PRIVACY_ACTION_DELETE_DATA', 'request.remove', false); + } + + break; + + // Item is in a "locked" state and cannot transition + default: + break; + } + + ToolbarHelper::cancel('request.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::help('Privacy:_Review_Information_Request'); + } + } } diff --git a/code/administrator/components/com_privacy/src/View/Requests/HtmlView.php b/code/administrator/components/com_privacy/src/View/Requests/HtmlView.php index 6e23cc33..351cb4d3 100644 --- a/code/administrator/components/com_privacy/src/View/Requests/HtmlView.php +++ b/code/administrator/components/com_privacy/src/View/Requests/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - $this->urgentRequestAge = (int) ComponentHelper::getParams('com_privacy')->get('notify', 14); - $this->sendMailEnabled = (bool) Factory::getApplication()->get('mailonline', 1); - - if (!count($this->items) && $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new Genericdataexception(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.9.0 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUESTS'), 'lock'); - - // Requests can only be created if mail sending is enabled - if (Factory::getApplication()->get('mailonline', 1)) - { - ToolbarHelper::addNew('request.add'); - } - - ToolbarHelper::preferences('com_privacy'); - ToolbarHelper::help('Privacy:_Information_Requests'); - } + /** + * The active search tools filters + * + * @var array + * @since 3.9.0 + * @note Must be public to be accessed from the search tools layout + */ + public $activeFilters; + + /** + * Form instance containing the search tools filter form + * + * @var Form + * @since 3.9.0 + * @note Must be public to be accessed from the search tools layout + */ + public $filterForm; + + /** + * The items to display + * + * @var array + * @since 3.9.0 + */ + protected $items; + + /** + * The pagination object + * + * @var Pagination + * @since 3.9.0 + */ + protected $pagination; + + /** + * Flag indicating the site supports sending email + * + * @var boolean + * @since 3.9.0 + */ + protected $sendMailEnabled; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * The age of urgent requests + * + * @var integer + * @since 3.9.0 + */ + protected $urgentRequestAge; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + /** @var RequestsModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + $this->urgentRequestAge = (int) ComponentHelper::getParams('com_privacy')->get('notify', 14); + $this->sendMailEnabled = (bool) Factory::getApplication()->get('mailonline', 1); + + if (!count($this->items) && $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new Genericdataexception(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.9.0 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUESTS'), 'lock'); + + // Requests can only be created if mail sending is enabled + if (Factory::getApplication()->get('mailonline', 1)) { + ToolbarHelper::addNew('request.add'); + } + + ToolbarHelper::preferences('com_privacy'); + ToolbarHelper::help('Privacy:_Information_Requests'); + } } diff --git a/code/administrator/components/com_privacy/tmpl/capabilities/default.php b/code/administrator/components/com_privacy/tmpl/capabilities/default.php index fe0bfc66..137ed9db 100644 --- a/code/administrator/components/com_privacy/tmpl/capabilities/default.php +++ b/code/administrator/components/com_privacy/tmpl/capabilities/default.php @@ -1,4 +1,5 @@
-
-

- -
- capabilities)) : ?> -
- - -
- - capabilities as $extension => $capabilities) : ?> -
- - -
- - -
- -
    - -
  • - -
- -
- - +
+

+ +
+ capabilities)) : ?> +
+ + +
+ + capabilities as $extension => $capabilities) : ?> +
+ + +
+ + +
+ +
    + +
  • + +
+ +
+ +
diff --git a/code/administrator/components/com_privacy/tmpl/consents/default.php b/code/administrator/components/com_privacy/tmpl/consents/default.php index d004d7ad..0878d0b2 100644 --- a/code/administrator/components/com_privacy/tmpl/consents/default.php +++ b/code/administrator/components/com_privacy/tmpl/consents/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -28,100 +30,100 @@ $now = Factory::getDate(); $stateIcons = array(-1 => 'delete', 0 => 'archive', 1 => 'publish'); $stateMsgs = array( - -1 => Text::_('COM_PRIVACY_CONSENTS_STATE_INVALIDATED'), - 0 => Text::_('COM_PRIVACY_CONSENTS_STATE_OBSOLETE'), - 1 => Text::_('COM_PRIVACY_CONSENTS_STATE_VALID') + -1 => Text::_('COM_PRIVACY_CONSENTS_STATE_INVALIDATED'), + 0 => Text::_('COM_PRIVACY_CONSENTS_STATE_OBSOLETE'), + 1 => Text::_('COM_PRIVACY_CONSENTS_STATE_VALID') ); ?>
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->username); ?> - - - state]; ?>"> - - username; ?> - - name; ?> - - user_id; ?> - - subject); ?> - - body; ?> - - created), null, $now); ?> -
- created, Text::_('DATE_FORMAT_LC6')); ?> -
-
- id; ?> -
- +
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->username); ?> + + + state]; ?>"> + + username; ?> + + name; ?> + + user_id; ?> + + subject); ?> + + body; ?> + + created), null, $now); ?> +
+ created, Text::_('DATE_FORMAT_LC6')); ?> +
+
+ id; ?> +
+ - - - -
+ + + +
diff --git a/code/administrator/components/com_privacy/tmpl/consents/emptystate.php b/code/administrator/components/com_privacy/tmpl/consents/emptystate.php index 4527f0f2..a76086b5 100644 --- a/code/administrator/components/com_privacy/tmpl/consents/emptystate.php +++ b/code/administrator/components/com_privacy/tmpl/consents/emptystate.php @@ -1,4 +1,5 @@ 'COM_PRIVACY_CONSENTS', - 'formURL' => 'index.php?option=com_privacy&view=consents', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Privacy:_Consents', - 'icon' => 'icon-lock', + 'textPrefix' => 'COM_PRIVACY_CONSENTS', + 'formURL' => 'index.php?option=com_privacy&view=consents', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Privacy:_Consents', + 'icon' => 'icon-lock', ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_privacy/tmpl/request/default.php b/code/administrator/components/com_privacy/tmpl/request/default.php index 8fa51ae5..95ae325b 100644 --- a/code/administrator/components/com_privacy/tmpl/request/default.php +++ b/code/administrator/components/com_privacy/tmpl/request/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
-
-
-
-

-
-
-
:
-
item->email; ?>
+
+
+
+

+
+
+
:
+
item->email; ?>
-
:
-
item->status); ?>
+
:
+
item->status); ?>
-
:
-
item->request_type); ?>
+
:
+
item->request_type); ?>
-
:
-
item->requested_at, Text::_('DATE_FORMAT_LC6')); ?>
-
-
-
-
-
-
-

-
- actionlogs)) : ?> -
- - -
- - - - - - - - - actionlogs as $i => $item) : ?> - - - - - - - -
- - - - - -
- - - log_date, Text::_('DATE_FORMAT_LC6')); ?> - - name; ?> -
- -
-
-
-
+
:
+
item->requested_at, Text::_('DATE_FORMAT_LC6')); ?>
+
+
+
+
+
+
+

+
+ actionlogs)) : ?> +
+ + +
+ + + + + + + + + actionlogs as $i => $item) : ?> + + + + + + + +
+ + + + + +
+ + + log_date, Text::_('DATE_FORMAT_LC6')); ?> + + name; ?> +
+ +
+
+
+
- - + +
diff --git a/code/administrator/components/com_privacy/tmpl/request/edit.php b/code/administrator/components/com_privacy/tmpl/request/edit.php index d38658ac..682e7931 100644 --- a/code/administrator/components/com_privacy/tmpl/request/edit.php +++ b/code/administrator/components/com_privacy/tmpl/request/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
-
-
-
-
- form->renderField('email'); ?> - form->renderField('status'); ?> - form->renderField('request_type'); ?> -
-
-
- - - -
+
+
+
+
+ form->renderField('email'); ?> + form->renderField('status'); ?> + form->renderField('request_type'); ?> +
+
+
+ + + +
diff --git a/code/administrator/components/com_privacy/tmpl/requests/default.php b/code/administrator/components/com_privacy/tmpl/requests/default.php index 31a300e9..5f8c0c0a 100644 --- a/code/administrator/components/com_privacy/tmpl/requests/default.php +++ b/code/administrator/components/com_privacy/tmpl/requests/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -33,96 +35,96 @@ ?>
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - items as $i => $item) : ?> - requested_at); - ?> - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - -
-
- status == 1 && $item->request_type === 'export') : ?> - - sendMailEnabled) : ?> - - - - status == 1 && $item->request_type === 'remove') : ?> - - -
-
- status); ?> - - status == 1 && $urgentRequestDate >= $itemRequestedAt) : ?> - - - - escape($item->email)); ?> - - - request_type); ?> - - -
- requested_at, Text::_('DATE_FORMAT_LC6')); ?> -
-
- id; ?> -
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + items as $i => $item) : ?> + requested_at); + ?> + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+
+ status == 1 && $item->request_type === 'export') : ?> + + sendMailEnabled) : ?> + + + + status == 1 && $item->request_type === 'remove') : ?> + + +
+
+ status); ?> + + status == 1 && $urgentRequestDate >= $itemRequestedAt) : ?> + + + + escape($item->email)); ?> + + + request_type); ?> + + +
+ requested_at, Text::_('DATE_FORMAT_LC6')); ?> +
+
+ id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
+ + + +
diff --git a/code/administrator/components/com_privacy/tmpl/requests/emptystate.php b/code/administrator/components/com_privacy/tmpl/requests/emptystate.php index c345a7be..14eb521e 100644 --- a/code/administrator/components/com_privacy/tmpl/requests/emptystate.php +++ b/code/administrator/components/com_privacy/tmpl/requests/emptystate.php @@ -1,4 +1,5 @@ 'COM_PRIVACY_REQUESTS', - 'formURL' => 'index.php?option=com_privacy&view=requests', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Privacy:_Information_Requests', - 'icon' => 'icon-lock', + 'textPrefix' => 'COM_PRIVACY_REQUESTS', + 'formURL' => 'index.php?option=com_privacy&view=requests', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Privacy:_Information_Requests', + 'icon' => 'icon-lock', ]; -if (Factory::getApplication()->get('mailonline', 1)) -{ - $displayData['createURL'] = 'index.php?option=com_privacy&task=request.add'; +if (Factory::getApplication()->get('mailonline', 1)) { + $displayData['createURL'] = 'index.php?option=com_privacy&task=request.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_redirect/helpers/redirect.php b/code/administrator/components/com_redirect/helpers/redirect.php index 0f6c31e9..aec98e0e 100644 --- a/code/administrator/components/com_redirect/helpers/redirect.php +++ b/code/administrator/components/com_redirect/helpers/redirect.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Redirect component helper. diff --git a/code/administrator/components/com_redirect/layouts/toolbar/batch.php b/code/administrator/components/com_redirect/layouts/toolbar/batch.php index 98e9e6de..c2b2ead1 100644 --- a/code/administrator/components/com_redirect/layouts/toolbar/batch.php +++ b/code/administrator/components/com_redirect/layouts/toolbar/batch.php @@ -1,4 +1,5 @@ diff --git a/code/administrator/components/com_redirect/redirect.xml b/code/administrator/components/com_redirect/redirect.xml index 14849930..009fec50 100644 --- a/code/administrator/components/com_redirect/redirect.xml +++ b/code/administrator/components/com_redirect/redirect.xml @@ -2,7 +2,7 @@ com_redirect Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_redirect/services/provider.php b/code/administrator/components/com_redirect/services/provider.php index 54db6ae4..d52d7e82 100644 --- a/code/administrator/components/com_redirect/services/provider.php +++ b/code/administrator/components/com_redirect/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Redirect')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Redirect')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Redirect')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Redirect')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new RedirectComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new RedirectComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_redirect/src/Controller/DisplayController.php b/code/administrator/components/com_redirect/src/Controller/DisplayController.php index 541446f8..8b280f7b 100644 --- a/code/administrator/components/com_redirect/src/Controller/DisplayController.php +++ b/code/administrator/components/com_redirect/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'links'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param mixed $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'links'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - if ($view === 'links') - { - $pluginEnabled = PluginHelper::isEnabled('system', 'redirect'); - $collectUrlsEnabled = RedirectHelper::collectUrlsEnabled(); + if ($view === 'links') { + $pluginEnabled = PluginHelper::isEnabled('system', 'redirect'); + $collectUrlsEnabled = RedirectHelper::collectUrlsEnabled(); - // Show messages about the enabled plugin and if the plugin should collect URLs - if ($pluginEnabled && $collectUrlsEnabled) - { - $this->app->enqueueMessage(Text::sprintf('COM_REDIRECT_COLLECT_URLS_ENABLED', Text::_('COM_REDIRECT_PLUGIN_ENABLED')), 'notice'); - } - else - { - $redirectPluginId = RedirectHelper::getRedirectPluginId(); - $link = HTMLHelper::_( - 'link', - '#plugin' . $redirectPluginId . 'Modal', - Text::_('COM_REDIRECT_SYSTEM_PLUGIN'), - 'class="alert-link" data-bs-toggle="modal" id="title-' . $redirectPluginId . '"' - ); + // Show messages about the enabled plugin and if the plugin should collect URLs + if ($pluginEnabled && $collectUrlsEnabled) { + $this->app->enqueueMessage(Text::sprintf('COM_REDIRECT_COLLECT_URLS_ENABLED', Text::_('COM_REDIRECT_PLUGIN_ENABLED')), 'notice'); + } else { + $redirectPluginId = RedirectHelper::getRedirectPluginId(); + $link = HTMLHelper::_( + 'link', + '#plugin' . $redirectPluginId . 'Modal', + Text::_('COM_REDIRECT_SYSTEM_PLUGIN'), + 'class="alert-link" data-bs-toggle="modal" id="title-' . $redirectPluginId . '"' + ); - if ($pluginEnabled && !$collectUrlsEnabled) - { - $this->app->enqueueMessage( - Text::sprintf('COM_REDIRECT_COLLECT_MODAL_URLS_DISABLED', Text::_('COM_REDIRECT_PLUGIN_ENABLED'), $link), - 'notice' - ); - } - else - { - $this->app->enqueueMessage(Text::sprintf('COM_REDIRECT_PLUGIN_MODAL_DISABLED', $link), 'error'); - } - } - } + if ($pluginEnabled && !$collectUrlsEnabled) { + $this->app->enqueueMessage( + Text::sprintf('COM_REDIRECT_COLLECT_MODAL_URLS_DISABLED', Text::_('COM_REDIRECT_PLUGIN_ENABLED'), $link), + 'notice' + ); + } else { + $this->app->enqueueMessage(Text::sprintf('COM_REDIRECT_PLUGIN_MODAL_DISABLED', $link), 'error'); + } + } + } - // Check for edit form. - if ($view == 'link' && $layout == 'edit' && !$this->checkEditId('com_redirect.edit.link', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'link' && $layout == 'edit' && !$this->checkEditId('com_redirect.edit.link', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_redirect&view=links', false)); + $this->setRedirect(Route::_('index.php?option=com_redirect&view=links', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/code/administrator/components/com_redirect/src/Controller/LinkController.php b/code/administrator/components/com_redirect/src/Controller/LinkController.php index e6b38f58..ffc966ce 100644 --- a/code/administrator/components/com_redirect/src/Controller/LinkController.php +++ b/code/administrator/components/com_redirect/src/Controller/LinkController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Redirect\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Redirect\Administrator\Controller; use Joomla\CMS\MVC\Controller\FormController; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Redirect link controller class. * @@ -19,5 +23,5 @@ */ class LinkController extends FormController { - // Parent class access checks are sufficient for this controller. + // Parent class access checks are sufficient for this controller. } diff --git a/code/administrator/components/com_redirect/src/Controller/LinksController.php b/code/administrator/components/com_redirect/src/Controller/LinksController.php index f18202fe..932441c0 100644 --- a/code/administrator/components/com_redirect/src/Controller/LinksController.php +++ b/code/administrator/components/com_redirect/src/Controller/LinksController.php @@ -1,4 +1,5 @@ checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('COM_REDIRECT_NO_ITEM_SELECTED'), 'warning'); - } - else - { - $newUrl = $this->input->getString('new_url'); - $comment = $this->input->getString('comment'); - - // Get the model. - $model = $this->getModel(); - - // Remove the items. - if (!$model->activate($ids, $newUrl, $comment)) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - else - { - $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_UPDATED', count($ids))); - } - } - - $this->setRedirect('index.php?option=com_redirect&view=links'); - } - - /** - * Method to duplicate URLs in records. - * - * @return void - * - * @since 3.6.0 - */ - public function duplicateUrls() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('COM_REDIRECT_NO_ITEM_SELECTED'), 'warning'); - } - else - { - $newUrl = $this->input->getString('new_url'); - $comment = $this->input->getString('comment'); - - // Get the model. - $model = $this->getModel(); - - // Remove the items. - if (!$model->duplicateUrls($ids, $newUrl, $comment)) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - else - { - $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_UPDATED', count($ids))); - } - } - - $this->setRedirect('index.php?option=com_redirect&view=links'); - } - - /** - * Proxy for getModel. - * - * @param string $name The name of the model. - * @param string $prefix The prefix of the model. - * @param array $config An array of settings. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model instance - * - * @since 1.6 - */ - public function getModel($name = 'Link', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Executes the batch process to add URLs to the database - * - * @return void - */ - public function batch() - { - // Check for request forgeries. - $this->checkToken(); - - $batch_urls_request = $this->input->post->get('batch_urls', array(), 'array'); - $batch_urls_lines = array_map('trim', explode("\n", $batch_urls_request[0])); - - $batch_urls = array(); - - foreach ($batch_urls_lines as $batch_urls_line) - { - if (!empty($batch_urls_line)) - { - $params = ComponentHelper::getParams('com_redirect'); - $separator = $params->get('separator', '|'); - - // Basic check to make sure the correct separator is being used - if (!\Joomla\String\StringHelper::strpos($batch_urls_line, $separator)) - { - $this->setMessage(Text::sprintf('COM_REDIRECT_NO_SEPARATOR_FOUND', $separator), 'error'); - $this->setRedirect('index.php?option=com_redirect&view=links'); - - return; - } - - $batch_urls[] = array_map('trim', explode($separator, $batch_urls_line)); - } - } - - // Set default message on error - overwrite if successful - $this->setMessage(Text::_('COM_REDIRECT_NO_ITEM_ADDED'), 'error'); - - if (!empty($batch_urls)) - { - $model = $this->getModel('Links'); - - // Execute the batch process - if ($model->batchProcess($batch_urls)) - { - $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_ADDED', count($batch_urls))); - } - } - - $this->setRedirect('index.php?option=com_redirect&view=links'); - } - - /** - * Clean out the unpublished links. - * - * @return void - * - * @since 3.5 - */ - public function purge() - { - // Check for request forgeries. - $this->checkToken(); - - $model = $this->getModel('Links'); - - if ($model->purge()) - { - $message = Text::_('COM_REDIRECT_CLEAR_SUCCESS'); - } - else - { - $message = Text::_('COM_REDIRECT_CLEAR_FAIL'); - } - - $this->setRedirect('index.php?option=com_redirect&view=links', $message); - } + /** + * Method to update a record. + * + * @return void + * + * @since 1.6 + */ + public function activate() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('COM_REDIRECT_NO_ITEM_SELECTED'), 'warning'); + } else { + $newUrl = $this->input->getString('new_url'); + $comment = $this->input->getString('comment'); + + // Get the model. + $model = $this->getModel(); + + // Remove the items. + if (!$model->activate($ids, $newUrl, $comment)) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } else { + $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_UPDATED', count($ids))); + } + } + + $this->setRedirect('index.php?option=com_redirect&view=links'); + } + + /** + * Method to duplicate URLs in records. + * + * @return void + * + * @since 3.6.0 + */ + public function duplicateUrls() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('COM_REDIRECT_NO_ITEM_SELECTED'), 'warning'); + } else { + $newUrl = $this->input->getString('new_url'); + $comment = $this->input->getString('comment'); + + // Get the model. + $model = $this->getModel(); + + // Remove the items. + if (!$model->duplicateUrls($ids, $newUrl, $comment)) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } else { + $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_UPDATED', count($ids))); + } + } + + $this->setRedirect('index.php?option=com_redirect&view=links'); + } + + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix of the model. + * @param array $config An array of settings. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model instance + * + * @since 1.6 + */ + public function getModel($name = 'Link', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Executes the batch process to add URLs to the database + * + * @return void + */ + public function batch() + { + // Check for request forgeries. + $this->checkToken(); + + $batch_urls_request = $this->input->post->get('batch_urls', array(), 'array'); + $batch_urls_lines = array_map('trim', explode("\n", $batch_urls_request[0])); + + $batch_urls = array(); + + foreach ($batch_urls_lines as $batch_urls_line) { + if (!empty($batch_urls_line)) { + $params = ComponentHelper::getParams('com_redirect'); + $separator = $params->get('separator', '|'); + + // Basic check to make sure the correct separator is being used + if (!\Joomla\String\StringHelper::strpos($batch_urls_line, $separator)) { + $this->setMessage(Text::sprintf('COM_REDIRECT_NO_SEPARATOR_FOUND', $separator), 'error'); + $this->setRedirect('index.php?option=com_redirect&view=links'); + + return; + } + + $batch_urls[] = array_map('trim', explode($separator, $batch_urls_line)); + } + } + + // Set default message on error - overwrite if successful + $this->setMessage(Text::_('COM_REDIRECT_NO_ITEM_ADDED'), 'error'); + + if (!empty($batch_urls)) { + $model = $this->getModel('Links'); + + // Execute the batch process + if ($model->batchProcess($batch_urls)) { + $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_ADDED', count($batch_urls))); + } + } + + $this->setRedirect('index.php?option=com_redirect&view=links'); + } + + /** + * Clean out the unpublished links. + * + * @return void + * + * @since 3.5 + */ + public function purge() + { + // Check for request forgeries. + $this->checkToken(); + + $model = $this->getModel('Links'); + + if ($model->purge()) { + $message = Text::_('COM_REDIRECT_CLEAR_SUCCESS'); + } else { + $message = Text::_('COM_REDIRECT_CLEAR_FAIL'); + } + + $this->setRedirect('index.php?option=com_redirect&view=links', $message); + } } diff --git a/code/administrator/components/com_redirect/src/Extension/RedirectComponent.php b/code/administrator/components/com_redirect/src/Extension/RedirectComponent.php index 2f155999..ee71626e 100644 --- a/code/administrator/components/com_redirect/src/Extension/RedirectComponent.php +++ b/code/administrator/components/com_redirect/src/Extension/RedirectComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('redirect', new Redirect); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('redirect', new Redirect()); + } } diff --git a/code/administrator/components/com_redirect/src/Field/RedirectField.php b/code/administrator/components/com_redirect/src/Field/RedirectField.php index 719c7b3c..26e16093 100644 --- a/code/administrator/components/com_redirect/src/Field/RedirectField.php +++ b/code/administrator/components/com_redirect/src/Field/RedirectField.php @@ -1,4 +1,5 @@ 'HTTP/1.1 100 Continue', - 101 => 'HTTP/1.1 101 Switching Protocols', - 102 => 'HTTP/1.1 102 Processing', - 103 => 'HTTP/1.1 103 Early Hints', - 200 => 'HTTP/1.1 200 OK', - 201 => 'HTTP/1.1 201 Created', - 202 => 'HTTP/1.1 202 Accepted', - 203 => 'HTTP/1.1 203 Non-Authoritative Information', - 204 => 'HTTP/1.1 204 No Content', - 205 => 'HTTP/1.1 205 Reset Content', - 206 => 'HTTP/1.1 206 Partial Content', - 207 => 'HTTP/1.1 207 Multi-Status', - 208 => 'HTTP/1.1 208 Already Reported', - 226 => 'HTTP/1.1 226 IM Used', - 300 => 'HTTP/1.1 300 Multiple Choices', - 301 => 'HTTP/1.1 301 Moved Permanently', - 302 => 'HTTP/1.1 302 Found', - 303 => 'HTTP/1.1 303 See other', - 304 => 'HTTP/1.1 304 Not Modified', - 305 => 'HTTP/1.1 305 Use Proxy', - 306 => 'HTTP/1.1 306 (Unused)', - 307 => 'HTTP/1.1 307 Temporary Redirect', - 308 => 'HTTP/1.1 308 Permanent Redirect', - 400 => 'HTTP/1.1 400 Bad Request', - 401 => 'HTTP/1.1 401 Unauthorized', - 402 => 'HTTP/1.1 402 Payment Required', - 403 => 'HTTP/1.1 403 Forbidden', - 404 => 'HTTP/1.1 404 Not Found', - 405 => 'HTTP/1.1 405 Method Not Allowed', - 406 => 'HTTP/1.1 406 Not Acceptable', - 407 => 'HTTP/1.1 407 Proxy Authentication Required', - 408 => 'HTTP/1.1 408 Request Timeout', - 409 => 'HTTP/1.1 409 Conflict', - 410 => 'HTTP/1.1 410 Gone', - 411 => 'HTTP/1.1 411 Length Required', - 412 => 'HTTP/1.1 412 Precondition Failed', - 413 => 'HTTP/1.1 413 Payload Too Large', - 414 => 'HTTP/1.1 414 URI Too Long', - 415 => 'HTTP/1.1 415 Unsupported Media Type', - 416 => 'HTTP/1.1 416 Requested Range Not Satisfiable', - 417 => 'HTTP/1.1 417 Expectation Failed', - 418 => 'HTTP/1.1 418 I\'m a teapot', - 421 => 'HTTP/1.1 421 Misdirected Request', - 422 => 'HTTP/1.1 422 Unprocessable Entity', - 423 => 'HTTP/1.1 423 Locked', - 424 => 'HTTP/1.1 424 Failed Dependency', - 425 => 'HTTP/1.1 425 Reserved for WebDAV advanced collections expired proposal', - 426 => 'HTTP/1.1 426 Upgrade Required', - 428 => 'HTTP/1.1 428 Precondition Required', - 429 => 'HTTP/1.1 429 Too Many Requests', - 431 => 'HTTP/1.1 431 Request Header Fields Too Large', - 451 => 'HTTP/1.1 451 Unavailable For Legal Reasons', - 500 => 'HTTP/1.1 500 Internal Server Error', - 501 => 'HTTP/1.1 501 Not Implemented', - 502 => 'HTTP/1.1 502 Bad Gateway', - 503 => 'HTTP/1.1 503 Service Unavailable', - 504 => 'HTTP/1.1 504 Gateway Timeout', - 505 => 'HTTP/1.1 505 HTTP Version Not Supported', - 506 => 'HTTP/1.1 506 Variant Also Negotiates (Experimental)', - 507 => 'HTTP/1.1 507 Insufficient Storage', - 508 => 'HTTP/1.1 508 Loop Detected', - 510 => 'HTTP/1.1 510 Not Extended', - 511 => 'HTTP/1.1 511 Network Authentication Required', - ); + /** + * A map of integer HTTP 1.1 response codes to the full HTTP Status for the headers. + * + * @var object + * @since 3.4 + * @link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + */ + protected $responseMap = array( + 100 => 'HTTP/1.1 100 Continue', + 101 => 'HTTP/1.1 101 Switching Protocols', + 102 => 'HTTP/1.1 102 Processing', + 103 => 'HTTP/1.1 103 Early Hints', + 200 => 'HTTP/1.1 200 OK', + 201 => 'HTTP/1.1 201 Created', + 202 => 'HTTP/1.1 202 Accepted', + 203 => 'HTTP/1.1 203 Non-Authoritative Information', + 204 => 'HTTP/1.1 204 No Content', + 205 => 'HTTP/1.1 205 Reset Content', + 206 => 'HTTP/1.1 206 Partial Content', + 207 => 'HTTP/1.1 207 Multi-Status', + 208 => 'HTTP/1.1 208 Already Reported', + 226 => 'HTTP/1.1 226 IM Used', + 300 => 'HTTP/1.1 300 Multiple Choices', + 301 => 'HTTP/1.1 301 Moved Permanently', + 302 => 'HTTP/1.1 302 Found', + 303 => 'HTTP/1.1 303 See other', + 304 => 'HTTP/1.1 304 Not Modified', + 305 => 'HTTP/1.1 305 Use Proxy', + 306 => 'HTTP/1.1 306 (Unused)', + 307 => 'HTTP/1.1 307 Temporary Redirect', + 308 => 'HTTP/1.1 308 Permanent Redirect', + 400 => 'HTTP/1.1 400 Bad Request', + 401 => 'HTTP/1.1 401 Unauthorized', + 402 => 'HTTP/1.1 402 Payment Required', + 403 => 'HTTP/1.1 403 Forbidden', + 404 => 'HTTP/1.1 404 Not Found', + 405 => 'HTTP/1.1 405 Method Not Allowed', + 406 => 'HTTP/1.1 406 Not Acceptable', + 407 => 'HTTP/1.1 407 Proxy Authentication Required', + 408 => 'HTTP/1.1 408 Request Timeout', + 409 => 'HTTP/1.1 409 Conflict', + 410 => 'HTTP/1.1 410 Gone', + 411 => 'HTTP/1.1 411 Length Required', + 412 => 'HTTP/1.1 412 Precondition Failed', + 413 => 'HTTP/1.1 413 Payload Too Large', + 414 => 'HTTP/1.1 414 URI Too Long', + 415 => 'HTTP/1.1 415 Unsupported Media Type', + 416 => 'HTTP/1.1 416 Requested Range Not Satisfiable', + 417 => 'HTTP/1.1 417 Expectation Failed', + 418 => 'HTTP/1.1 418 I\'m a teapot', + 421 => 'HTTP/1.1 421 Misdirected Request', + 422 => 'HTTP/1.1 422 Unprocessable Entity', + 423 => 'HTTP/1.1 423 Locked', + 424 => 'HTTP/1.1 424 Failed Dependency', + 425 => 'HTTP/1.1 425 Reserved for WebDAV advanced collections expired proposal', + 426 => 'HTTP/1.1 426 Upgrade Required', + 428 => 'HTTP/1.1 428 Precondition Required', + 429 => 'HTTP/1.1 429 Too Many Requests', + 431 => 'HTTP/1.1 431 Request Header Fields Too Large', + 451 => 'HTTP/1.1 451 Unavailable For Legal Reasons', + 500 => 'HTTP/1.1 500 Internal Server Error', + 501 => 'HTTP/1.1 501 Not Implemented', + 502 => 'HTTP/1.1 502 Bad Gateway', + 503 => 'HTTP/1.1 503 Service Unavailable', + 504 => 'HTTP/1.1 504 Gateway Timeout', + 505 => 'HTTP/1.1 505 HTTP Version Not Supported', + 506 => 'HTTP/1.1 506 Variant Also Negotiates (Experimental)', + 507 => 'HTTP/1.1 507 Insufficient Storage', + 508 => 'HTTP/1.1 508 Loop Detected', + 510 => 'HTTP/1.1 510 Not Extended', + 511 => 'HTTP/1.1 511 Network Authentication Required', + ); - /** - * Method to get the field input markup. - * - * @return array The field input markup. - * - * @since 3.4 - */ - protected function getOptions() - { - $options = array(); + /** + * Method to get the field input markup. + * + * @return array The field input markup. + * + * @since 3.4 + */ + protected function getOptions() + { + $options = array(); - foreach ($this->responseMap as $key => $value) - { - $options[] = HTMLHelper::_('select.option', $key, $value); - } + foreach ($this->responseMap as $key => $value) { + $options[] = HTMLHelper::_('select.option', $key, $value); + } - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); - return $options; - } + return $options; + } } diff --git a/code/administrator/components/com_redirect/src/Helper/RedirectHelper.php b/code/administrator/components/com_redirect/src/Helper/RedirectHelper.php index f09310ab..9ce5adf5 100644 --- a/code/administrator/components/com_redirect/src/Helper/RedirectHelper.php +++ b/code/administrator/components/com_redirect/src/Helper/RedirectHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('redirect')); - $db->setQuery($query); + /** + * Gets the redirect system plugin extension id. + * + * @return integer The redirect system plugin extension id. + * + * @since 3.6.0 + */ + public static function getRedirectPluginId() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('redirect')); + $db->setQuery($query); - try - { - $result = (int) $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } + try { + $result = (int) $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } - return $result; - } + return $result; + } - /** - * Checks whether the option "Collect URLs" is enabled for the output message - * - * @return boolean - * - * @since 3.4 - */ - public static function collectUrlsEnabled() - { - $collect_urls = false; + /** + * Checks whether the option "Collect URLs" is enabled for the output message + * + * @return boolean + * + * @since 3.4 + */ + public static function collectUrlsEnabled() + { + $collect_urls = false; - if (PluginHelper::isEnabled('system', 'redirect')) - { - $params = new Registry(PluginHelper::getPlugin('system', 'redirect')->params); - $collect_urls = (bool) $params->get('collect_urls', 1); - } + if (PluginHelper::isEnabled('system', 'redirect')) { + $params = new Registry(PluginHelper::getPlugin('system', 'redirect')->params); + $collect_urls = (bool) $params->get('collect_urls', 1); + } - return $collect_urls; - } + return $collect_urls; + } } diff --git a/code/administrator/components/com_redirect/src/Model/LinkModel.php b/code/administrator/components/com_redirect/src/Model/LinkModel.php index 13e85b6e..849290fa 100644 --- a/code/administrator/components/com_redirect/src/Model/LinkModel.php +++ b/code/administrator/components/com_redirect/src/Model/LinkModel.php @@ -1,4 +1,5 @@ published != -2) - { - return false; - } - - return parent::canDelete($record); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form A JForm object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_redirect.link', 'link', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if ($this->canEditState((object) $data) != true) - { - // Disable fields for display. - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - // If in advanced mode then we make sure the new URL field is not compulsory and the header - // field compulsory in case people select non-3xx redirects - if (ComponentHelper::getParams('com_redirect')->get('mode', 0) == true) - { - $form->setFieldAttribute('new_url', 'required', 'false'); - $form->setFieldAttribute('header', 'required', 'true'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_redirect.edit.link.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_redirect.link', $data); - - return $data; - } - - /** - * Method to activate links. - * - * @param array &$pks An array of link ids. - * @param string $url The new URL to set for the redirect. - * @param string $comment A comment for the redirect links. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - */ - public function activate(&$pks, $url, $comment = null) - { - $user = Factory::getUser(); - $db = $this->getDbo(); - - // Sanitize the ids. - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - - // Populate default comment if necessary. - $comment = (!empty($comment)) ? $comment : Text::sprintf('COM_REDIRECT_REDIRECTED_ON', HTMLHelper::_('date', time())); - - // Access checks. - if (!$user->authorise('core.edit', 'com_redirect')) - { - $pks = array(); - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); - - return false; - } - - if (!empty($pks)) - { - // Update the link rows. - $query = $db->getQuery(true) - ->update($db->quoteName('#__redirect_links')) - ->set($db->quoteName('new_url') . ' = :url') - ->set($db->quoteName('published') . ' = 1') - ->set($db->quoteName('comment') . ' = :comment') - ->whereIn($db->quoteName('id'), $pks) - ->bind(':url', $url) - ->bind(':comment', $comment); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - return true; - } - - /** - * Method to batch update URLs to have new redirect urls and comments. Note will publish any unpublished URLs. - * - * @param array &$pks An array of link ids. - * @param string $url The new URL to set for the redirect. - * @param string $comment A comment for the redirect links. - * - * @return boolean Returns true on success, false on failure. - * - * @since 3.6.0 - */ - public function duplicateUrls(&$pks, $url, $comment = null) - { - $user = Factory::getUser(); - $db = $this->getDbo(); - - // Sanitize the ids. - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - - // Access checks. - if (!$user->authorise('core.edit', 'com_redirect')) - { - $pks = array(); - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); - - return false; - } - - if (!empty($pks)) - { - $date = Factory::getDate()->toSql(); - - // Update the link rows. - $query = $db->getQuery(true) - ->update($db->quoteName('#__redirect_links')) - ->set($db->quoteName('new_url') . ' = :url') - ->set($db->quoteName('modified_date') . ' = :date') - ->set($db->quoteName('published') . ' = 1') - ->whereIn($db->quoteName('id'), $pks) - ->bind(':url', $url) - ->bind(':date', $date); - - if (!empty($comment)) - { - $query->set($db->quoteName('comment') . ' = ' . $db->quote($comment)); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - return true; - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_REDIRECT'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if ($record->published != -2) { + return false; + } + + return parent::canDelete($record); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form A JForm object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_redirect.link', 'link', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if ($this->canEditState((object) $data) != true) { + // Disable fields for display. + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + // If in advanced mode then we make sure the new URL field is not compulsory and the header + // field compulsory in case people select non-3xx redirects + if (ComponentHelper::getParams('com_redirect')->get('mode', 0) == true) { + $form->setFieldAttribute('new_url', 'required', 'false'); + $form->setFieldAttribute('header', 'required', 'true'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_redirect.edit.link.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_redirect.link', $data); + + return $data; + } + + /** + * Method to activate links. + * + * @param array &$pks An array of link ids. + * @param string $url The new URL to set for the redirect. + * @param string $comment A comment for the redirect links. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + */ + public function activate(&$pks, $url, $comment = null) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + + // Populate default comment if necessary. + $comment = (!empty($comment)) ? $comment : Text::sprintf('COM_REDIRECT_REDIRECTED_ON', HTMLHelper::_('date', time())); + + // Access checks. + if (!$user->authorise('core.edit', 'com_redirect')) { + $pks = array(); + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); + + return false; + } + + if (!empty($pks)) { + // Update the link rows. + $query = $db->getQuery(true) + ->update($db->quoteName('#__redirect_links')) + ->set($db->quoteName('new_url') . ' = :url') + ->set($db->quoteName('published') . ' = 1') + ->set($db->quoteName('comment') . ' = :comment') + ->whereIn($db->quoteName('id'), $pks) + ->bind(':url', $url) + ->bind(':comment', $comment); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + return true; + } + + /** + * Method to batch update URLs to have new redirect urls and comments. Note will publish any unpublished URLs. + * + * @param array &$pks An array of link ids. + * @param string $url The new URL to set for the redirect. + * @param string $comment A comment for the redirect links. + * + * @return boolean Returns true on success, false on failure. + * + * @since 3.6.0 + */ + public function duplicateUrls(&$pks, $url, $comment = null) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + + // Access checks. + if (!$user->authorise('core.edit', 'com_redirect')) { + $pks = array(); + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); + + return false; + } + + if (!empty($pks)) { + $date = Factory::getDate()->toSql(); + + // Update the link rows. + $query = $db->getQuery(true) + ->update($db->quoteName('#__redirect_links')) + ->set($db->quoteName('new_url') . ' = :url') + ->set($db->quoteName('modified_date') . ' = :date') + ->set($db->quoteName('published') . ' = 1') + ->whereIn($db->quoteName('id'), $pks) + ->bind(':url', $url) + ->bind(':date', $date); + + if (!empty($comment)) { + $query->set($db->quoteName('comment') . ' = ' . $db->quote($comment)); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + return true; + } } diff --git a/code/administrator/components/com_redirect/src/Model/LinksModel.php b/code/administrator/components/com_redirect/src/Model/LinksModel.php index b2379a02..2817a5d9 100644 --- a/code/administrator/components/com_redirect/src/Model/LinksModel.php +++ b/code/administrator/components/com_redirect/src/Model/LinksModel.php @@ -1,4 +1,5 @@ getDbo(); - - $query = $db->getQuery(true); - - $query->delete('#__redirect_links')->where($db->quoteName('published') . '= 0'); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\Exception $e) - { - return false; - } - - return true; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.old_url', $direction = 'asc') - { - // Load the parameters. - $params = ComponentHelper::getParams('com_redirect'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.http_status'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - $query->from($db->quoteName('#__redirect_links', 'a')); - - // Filter by published state - $state = (string) $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.published') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif ($state === '') - { - $query->whereIn($db->quoteName('a.published'), [0,1]); - } - - // Filter the items over the HTTP status code header. - if ($httpStatusCode = $this->getState('filter.http_status')) - { - $httpStatusCode = (int) $httpStatusCode; - $query->where($db->quoteName('a.header') . ' = :header') - ->bind(':header', $httpStatusCode, ParameterType::INTEGER); - } - - // Filter the items over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%'); - $query->where( - '(' . $db->quoteName('old_url') . ' LIKE :oldurl' - . ' OR ' . $db->quoteName('new_url') . ' LIKE :newurl' - . ' OR ' . $db->quoteName('comment') . ' LIKE :comment' - . ' OR ' . $db->quoteName('referer') . ' LIKE :referer)' - ) - ->bind(':oldurl', $search) - ->bind(':newurl', $search) - ->bind(':comment', $search) - ->bind(':referer', $search); - } - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.old_url')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Add the entered URLs into the database - * - * @param array $batchUrls Array of URLs to enter into the database - * - * @return boolean - */ - public function batchProcess($batchUrls) - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $params = ComponentHelper::getParams('com_redirect'); - $state = (int) $params->get('defaultImportState', 0); - $created = Factory::getDate()->toSql(); - - $columns = [ - 'old_url', - 'new_url', - 'referer', - 'comment', - 'hits', - 'published', - 'created_date', - 'modified_date', - ]; - - $values = [ - ':oldurl', - ':newurl', - $db->quote(''), - $db->quote(''), - 0, - ':state', - ':created', - ':modified', - ]; - - $query - ->insert($db->quoteName('#__redirect_links'), false) - ->columns($db->quoteName($columns)) - ->values(implode(', ', $values)) - ->bind(':oldurl', $old_url) - ->bind(':newurl', $new_url) - ->bind(':state', $state, ParameterType::INTEGER) - ->bind(':created', $created) - ->bind(':modified', $created); - - $db->setQuery($query); - - foreach ($batchUrls as $batch_url) - { - $old_url = $batch_url[0]; - - // Destination URL can also be an external URL - if (!empty($batch_url[1])) - { - $new_url = $batch_url[1]; - } - else - { - $new_url = ''; - } - - $db->execute(); - } - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'state', 'a.state', + 'old_url', 'a.old_url', + 'new_url', 'a.new_url', + 'referer', 'a.referer', + 'hits', 'a.hits', + 'created_date', 'a.created_date', + 'published', 'a.published', + 'header', 'a.header', 'http_status', + ); + } + + parent::__construct($config, $factory); + } + /** + * Removes all of the unpublished redirects from the table. + * + * @return boolean result of operation + * + * @since 3.5 + */ + public function purge() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true); + + $query->delete('#__redirect_links')->where($db->quoteName('published') . '= 0'); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\Exception $e) { + return false; + } + + return true; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.old_url', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_redirect'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.http_status'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__redirect_links', 'a')); + + // Filter by published state + $state = (string) $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.published') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif ($state === '') { + $query->whereIn($db->quoteName('a.published'), [0,1]); + } + + // Filter the items over the HTTP status code header. + if ($httpStatusCode = $this->getState('filter.http_status')) { + $httpStatusCode = (int) $httpStatusCode; + $query->where($db->quoteName('a.header') . ' = :header') + ->bind(':header', $httpStatusCode, ParameterType::INTEGER); + } + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%'); + $query->where( + '(' . $db->quoteName('old_url') . ' LIKE :oldurl' + . ' OR ' . $db->quoteName('new_url') . ' LIKE :newurl' + . ' OR ' . $db->quoteName('comment') . ' LIKE :comment' + . ' OR ' . $db->quoteName('referer') . ' LIKE :referer)' + ) + ->bind(':oldurl', $search) + ->bind(':newurl', $search) + ->bind(':comment', $search) + ->bind(':referer', $search); + } + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.old_url')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Add the entered URLs into the database + * + * @param array $batchUrls Array of URLs to enter into the database + * + * @return boolean + */ + public function batchProcess($batchUrls) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $params = ComponentHelper::getParams('com_redirect'); + $state = (int) $params->get('defaultImportState', 0); + $created = Factory::getDate()->toSql(); + + $columns = [ + 'old_url', + 'new_url', + 'referer', + 'comment', + 'hits', + 'published', + 'created_date', + 'modified_date', + ]; + + $values = [ + ':oldurl', + ':newurl', + $db->quote(''), + $db->quote(''), + 0, + ':state', + ':created', + ':modified', + ]; + + $query + ->insert($db->quoteName('#__redirect_links'), false) + ->columns($db->quoteName($columns)) + ->values(implode(', ', $values)) + ->bind(':oldurl', $old_url) + ->bind(':newurl', $new_url) + ->bind(':state', $state, ParameterType::INTEGER) + ->bind(':created', $created) + ->bind(':modified', $created); + + $db->setQuery($query); + + foreach ($batchUrls as $batch_url) { + $old_url = $batch_url[0]; + + // Destination URL can also be an external URL + if (!empty($batch_url[1])) { + $new_url = $batch_url[1]; + } else { + $new_url = ''; + } + + $db->execute(); + } + + return true; + } } diff --git a/code/administrator/components/com_redirect/src/Service/HTML/Redirect.php b/code/administrator/components/com_redirect/src/Service/HTML/Redirect.php index c7dfcb0d..09a1300f 100644 --- a/code/administrator/components/com_redirect/src/Service/HTML/Redirect.php +++ b/code/administrator/components/com_redirect/src/Service/HTML/Redirect.php @@ -1,4 +1,5 @@ array('publish', 'links.unpublish', 'JENABLED', 'COM_REDIRECT_DISABLE_LINK'), - 0 => array('unpublish', 'links.publish', 'JDISABLED', 'COM_REDIRECT_ENABLE_LINK'), - 2 => array('archive', 'links.unpublish', 'JARCHIVED', 'JUNARCHIVE'), - -2 => array('trash', 'links.publish', 'JTRASHED', 'COM_REDIRECT_ENABLE_LINK'), - ); + // Array of image, task, title, action + $states = array( + 1 => array('publish', 'links.unpublish', 'JENABLED', 'COM_REDIRECT_DISABLE_LINK'), + 0 => array('unpublish', 'links.publish', 'JDISABLED', 'COM_REDIRECT_ENABLE_LINK'), + 2 => array('archive', 'links.unpublish', 'JARCHIVED', 'JUNARCHIVE'), + -2 => array('trash', 'links.publish', 'JTRASHED', 'COM_REDIRECT_ENABLE_LINK'), + ); - $state = ArrayHelper::getValue($states, (int) $value, $states[0]); - $icon = $state[0]; + $state = ArrayHelper::getValue($states, (int) $value, $states[0]); + $icon = $state[0]; - if ($canChange) - { - $html = '' - . ''; - } + if ($canChange) { + $html = '' + . ''; + } - return $html; - } + return $html; + } } diff --git a/code/administrator/components/com_redirect/src/Table/LinkTable.php b/code/administrator/components/com_redirect/src/Table/LinkTable.php index fdc4ced7..d7885d9d 100644 --- a/code/administrator/components/com_redirect/src/Table/LinkTable.php +++ b/code/administrator/components/com_redirect/src/Table/LinkTable.php @@ -1,4 +1,5 @@ setError($e->getMessage()); - - return false; - } - - $this->old_url = trim(rawurldecode($this->old_url)); - $this->new_url = trim(rawurldecode($this->new_url)); - - // Check for valid name. - if (empty($this->old_url)) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_SOURCE_URL_REQUIRED')); - - return false; - } - - // Check for NOT NULL. - if (empty($this->referer)) - { - $this->referer = ''; - } - - // Check for valid name if not in advanced mode. - if (empty($this->new_url) && ComponentHelper::getParams('com_redirect')->get('mode', 0) == false) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_DESTINATION_URL_REQUIRED')); - - return false; - } - elseif (empty($this->new_url) && ComponentHelper::getParams('com_redirect')->get('mode', 0) == true) - { - // Else if an empty URL and in redirect mode only throw the same error if the code is a 3xx status code - if ($this->header < 400 && $this->header >= 300) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_DESTINATION_URL_REQUIRED')); - - return false; - } - } - - // Check for duplicates - if ($this->old_url == $this->new_url) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_DUPLICATE_URLS')); - - return false; - } - - $db = $this->getDbo(); - - // Check for existing name - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->select($db->quoteName('old_url')) - ->from($db->quoteName('#__redirect_links')) - ->where($db->quoteName('old_url') . ' = :url') - ->bind(':url', $this->old_url); - $db->setQuery($query); - $urls = $db->loadAssocList(); - - foreach ($urls as $url) - { - if ($url['old_url'] === $this->old_url && (int) $url['id'] != (int) $this->id) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_DUPLICATE_OLD_URL')); - - return false; - } - } - - return true; - } - - /** - * Overridden store method to set dates. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function store($updateNulls = false) - { - $date = Factory::getDate()->toSql(); - - if (!$this->id) - { - // New record. - $this->created_date = $date; - $this->modified_date = $date; - } - - if (empty($this->modified_date)) - { - $this->modified_date = $this->created_date; - } - - return parent::store($updateNulls); - } + /** + * Constructor + * + * @param DatabaseDriver $db Database object. + * + * @since 1.6 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__redirect_links', 'id', $db); + } + + /** + * Overloaded check function + * + * @return boolean + * + * @since 1.6 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->old_url = trim(rawurldecode($this->old_url)); + $this->new_url = trim(rawurldecode($this->new_url)); + + // Check for valid name. + if (empty($this->old_url)) { + $this->setError(Text::_('COM_REDIRECT_ERROR_SOURCE_URL_REQUIRED')); + + return false; + } + + // Check for NOT NULL. + if (empty($this->referer)) { + $this->referer = ''; + } + + // Check for valid name if not in advanced mode. + if (empty($this->new_url) && ComponentHelper::getParams('com_redirect')->get('mode', 0) == false) { + $this->setError(Text::_('COM_REDIRECT_ERROR_DESTINATION_URL_REQUIRED')); + + return false; + } elseif (empty($this->new_url) && ComponentHelper::getParams('com_redirect')->get('mode', 0) == true) { + // Else if an empty URL and in redirect mode only throw the same error if the code is a 3xx status code + if ($this->header < 400 && $this->header >= 300) { + $this->setError(Text::_('COM_REDIRECT_ERROR_DESTINATION_URL_REQUIRED')); + + return false; + } + } + + // Check for duplicates + if ($this->old_url == $this->new_url) { + $this->setError(Text::_('COM_REDIRECT_ERROR_DUPLICATE_URLS')); + + return false; + } + + $db = $this->getDbo(); + + // Check for existing name + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->select($db->quoteName('old_url')) + ->from($db->quoteName('#__redirect_links')) + ->where($db->quoteName('old_url') . ' = :url') + ->bind(':url', $this->old_url); + $db->setQuery($query); + $urls = $db->loadAssocList(); + + foreach ($urls as $url) { + if ($url['old_url'] === $this->old_url && (int) $url['id'] != (int) $this->id) { + $this->setError(Text::_('COM_REDIRECT_ERROR_DUPLICATE_OLD_URL')); + + return false; + } + } + + return true; + } + + /** + * Overridden store method to set dates. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function store($updateNulls = false) + { + $date = Factory::getDate()->toSql(); + + if (!$this->id) { + // New record. + $this->created_date = $date; + $this->modified_date = $date; + } + + if (empty($this->modified_date)) { + $this->modified_date = $this->created_date; + } + + return parent::store($updateNulls); + } } diff --git a/code/administrator/components/com_redirect/src/View/Link/HtmlView.php b/code/administrator/components/com_redirect/src/View/Link/HtmlView.php index c20f83e0..4d253993 100644 --- a/code/administrator/components/com_redirect/src/View/Link/HtmlView.php +++ b/code/administrator/components/com_redirect/src/View/Link/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - $canDo = ContentHelper::getActions('com_redirect'); - - ToolbarHelper::title($isNew ? Text::_('COM_REDIRECT_MANAGER_LINK_NEW') : Text::_('COM_REDIRECT_MANAGER_LINK_EDIT'), 'map-signs redirect'); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('link.apply'); - $toolbarButtons[] = ['save', 'link.save']; - } - - /** - * This component does not support Save as Copy due to uniqueness checks. - * While it can be done, it causes too much confusion if the user does - * not change the Old URL. - */ - if ($canDo->get('core.edit') && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'link.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('link.cancel'); - } - else - { - ToolbarHelper::cancel('link.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::help('Redirects:_New_or_Edit'); - } + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed False if unsuccessful, otherwise void. + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + $canDo = ContentHelper::getActions('com_redirect'); + + ToolbarHelper::title($isNew ? Text::_('COM_REDIRECT_MANAGER_LINK_NEW') : Text::_('COM_REDIRECT_MANAGER_LINK_EDIT'), 'map-signs redirect'); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('link.apply'); + $toolbarButtons[] = ['save', 'link.save']; + } + + /** + * This component does not support Save as Copy due to uniqueness checks. + * While it can be done, it causes too much confusion if the user does + * not change the Old URL. + */ + if ($canDo->get('core.edit') && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'link.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('link.cancel'); + } else { + ToolbarHelper::cancel('link.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::help('Redirects:_New_or_Edit'); + } } diff --git a/code/administrator/components/com_redirect/src/View/Links/HtmlView.php b/code/administrator/components/com_redirect/src/View/Links/HtmlView.php index f04a0b8c..74b8585f 100644 --- a/code/administrator/components/com_redirect/src/View/Links/HtmlView.php +++ b/code/administrator/components/com_redirect/src/View/Links/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->params = ComponentHelper::getParams('com_redirect'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - if (!(PluginHelper::isEnabled('system', 'redirect') && RedirectHelper::collectUrlsEnabled())) - { - $this->redirectPluginId = RedirectHelper::getRedirectPluginId(); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $canDo = ContentHelper::getActions('com_redirect'); - - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_REDIRECT_MANAGER_LINKS'), 'map-signs redirect'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('link.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($state->get('filter.state') != 2) - { - $childBar->publish('links.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $childBar->unpublish('links.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - } - - if ($state->get('filter.state') != -1) - { - if ($state->get('filter.state') != 2) - { - $childBar->archive('links.archive')->listCheck(true); - } - elseif ($state->get('filter.state') == 2) - { - $childBar->unarchive('links.unarchive')->listCheck(true); - } - } - - if (!$state->get('filter.state') == -2) - { - $childBar->trash('links.trash')->listCheck(true); - } - } - - if ($state->get('filter.state') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('links.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if (!$this->isEmptyState && (!$state->get('filter.state') == -2 && $canDo->get('core.delete'))) - { - $toolbar->confirmButton('delete') - ->text('COM_REDIRECT_TOOLBAR_PURGE') - ->message('COM_REDIRECT_CONFIRM_PURGE') - ->task('links.purge'); - } - - if ($canDo->get('core.create')) - { - $toolbar->popupButton('batch') - ->text('JTOOLBAR_BULK_IMPORT') - ->selector('collapseModal') - ->listCheck(false); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_redirect'); - } - - $toolbar->help('Redirects:_Links'); - } + /** + * True if "System - Redirect Plugin" is enabled + * + * @var boolean + */ + protected $enabled; + + /** + * True if "Collect URLs" is enabled + * + * @var boolean + */ + protected $collect_urls_enabled; + + /** + * The id of the redirect plugin in mysql + * + * @var integer + * @since 3.8.0 + */ + protected $redirectPluginId = 0; + + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The model state + * + * @var \Joomla\Registry\Registry + */ + protected $params; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws GenericDataException + * @since 1.6 + */ + public function display($tpl = null) + { + // Set variables + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->params = ComponentHelper::getParams('com_redirect'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + if (!(PluginHelper::isEnabled('system', 'redirect') && RedirectHelper::collectUrlsEnabled())) { + $this->redirectPluginId = RedirectHelper::getRedirectPluginId(); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $canDo = ContentHelper::getActions('com_redirect'); + + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_REDIRECT_MANAGER_LINKS'), 'map-signs redirect'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('link.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($state->get('filter.state') != 2) { + $childBar->publish('links.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $childBar->unpublish('links.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + } + + if ($state->get('filter.state') != -1) { + if ($state->get('filter.state') != 2) { + $childBar->archive('links.archive')->listCheck(true); + } elseif ($state->get('filter.state') == 2) { + $childBar->unarchive('links.unpublish')->listCheck(true); + } + } + + if (!$state->get('filter.state') == -2) { + $childBar->trash('links.trash')->listCheck(true); + } + } + + if ($state->get('filter.state') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('links.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if (!$this->isEmptyState && (!$state->get('filter.state') == -2 && $canDo->get('core.delete'))) { + $toolbar->confirmButton('delete') + ->text('COM_REDIRECT_TOOLBAR_PURGE') + ->message('COM_REDIRECT_CONFIRM_PURGE') + ->task('links.purge'); + } + + if ($canDo->get('core.create')) { + $toolbar->popupButton('batch') + ->text('JTOOLBAR_BULK_IMPORT') + ->selector('collapseModal') + ->listCheck(false); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_redirect'); + } + + $toolbar->help('Redirects:_Links'); + } } diff --git a/code/administrator/components/com_redirect/tmpl/link/edit.php b/code/administrator/components/com_redirect/tmpl/link/edit.php index 9f3320aa..9117e79d 100644 --- a/code/administrator/components/com_redirect/tmpl/link/edit.php +++ b/code/administrator/components/com_redirect/tmpl/link/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?> diff --git a/code/administrator/components/com_redirect/tmpl/links/default.php b/code/administrator/components/com_redirect/tmpl/links/default.php index 70b150d6..6ba0ef45 100644 --- a/code/administrator/components/com_redirect/tmpl/links/default.php +++ b/code/administrator/components/com_redirect/tmpl/links/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
- $this)); ?> - redirectPluginId) : ?> - redirectPluginId . '&tmpl=component&layout=modal'); ?> - redirectPluginId . 'Modal', - array( - 'url' => $link, - 'title' => Text::_('COM_REDIRECT_EDIT_PLUGIN_SETTINGS'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'closeButton' => false, - 'backdrop' => 'static', - 'keyboard' => false, - 'footer' => '' - . '' - . '' - ) - ); ?> - +
+ $this)); ?> + redirectPluginId) : ?> + redirectPluginId . '&tmpl=component&layout=modal'); ?> + redirectPluginId . 'Modal', + array( + 'url' => $link, + 'title' => Text::_('COM_REDIRECT_EDIT_PLUGIN_SETTINGS'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'closeButton' => false, + 'backdrop' => 'static', + 'keyboard' => false, + 'footer' => '' + . '' + . '' + ) + ); ?> + - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - items as $i => $item) : - $canEdit = $user->authorise('core.edit', 'com_redirect'); - $canChange = $user->authorise('core.edit.state', 'com_redirect'); - ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->old_url); ?> - - published, $i); ?> - - - - escape(str_replace(Uri::root(), '', rawurldecode($item->old_url))); ?> - - - escape(str_replace(Uri::root(), '', rawurldecode($item->old_url))); ?> - - - escape(rawurldecode($item->new_url)); ?> - - escape($item->referer); ?> - - created_date, Text::_('DATE_FORMAT_LC4')); ?> - - hits; ?> - - header; ?> - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + items as $i => $item) : + $canEdit = $user->authorise('core.edit', 'com_redirect'); + $canChange = $user->authorise('core.edit.state', 'com_redirect'); + ?> + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->old_url); ?> + + published, $i); ?> + + + + escape(str_replace(Uri::root(), '', rawurldecode($item->old_url))); ?> + + + escape(str_replace(Uri::root(), '', rawurldecode($item->old_url))); ?> + + + escape(rawurldecode($item->new_url)); ?> + + escape($item->referer); ?> + + created_date, Text::_('DATE_FORMAT_LC4')); ?> + + hits; ?> + + header; ?> + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - items)) : ?> - loadTemplate('addform'); ?> - - - authorise('core.create', 'com_redirect') - && $user->authorise('core.edit', 'com_redirect') - && $user->authorise('core.edit.state', 'com_redirect')) : ?> - Text::_('COM_REDIRECT_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - + items)) : ?> + loadTemplate('addform'); ?> + + + authorise('core.create', 'com_redirect') + && $user->authorise('core.edit', 'com_redirect') + && $user->authorise('core.edit.state', 'com_redirect') + ) : ?> + Text::_('COM_REDIRECT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + - - - -
+ + + +
diff --git a/code/administrator/components/com_redirect/tmpl/links/default_addform.php b/code/administrator/components/com_redirect/tmpl/links/default_addform.php index 186b4658..6510ef50 100644 --- a/code/administrator/components/com_redirect/tmpl/links/default_addform.php +++ b/code/administrator/components/com_redirect/tmpl/links/default_addform.php @@ -1,4 +1,5 @@
-
- -
-
-
-
-
-
- -
-
- - - - -
-
-
-
- -
-
- -
-
- -
-
-
+
+ +
+
+
+
+
+
+ +
+
+ + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
diff --git a/code/administrator/components/com_redirect/tmpl/links/default_batch_body.php b/code/administrator/components/com_redirect/tmpl/links/default_batch_body.php index 58ff121d..052eef41 100644 --- a/code/administrator/components/com_redirect/tmpl/links/default_batch_body.php +++ b/code/administrator/components/com_redirect/tmpl/links/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -16,12 +18,12 @@ ?>
-
-
- -
- -
-
-
+
+
+ +
+ +
+
+
diff --git a/code/administrator/components/com_redirect/tmpl/links/default_batch_footer.php b/code/administrator/components/com_redirect/tmpl/links/default_batch_footer.php index 08b76e82..acd58265 100644 --- a/code/administrator/components/com_redirect/tmpl/links/default_batch_footer.php +++ b/code/administrator/components/com_redirect/tmpl/links/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/code/administrator/components/com_redirect/tmpl/links/emptystate.php b/code/administrator/components/com_redirect/tmpl/links/emptystate.php index 705387dd..752d956a 100644 --- a/code/administrator/components/com_redirect/tmpl/links/emptystate.php +++ b/code/administrator/components/com_redirect/tmpl/links/emptystate.php @@ -1,4 +1,5 @@ 'COM_REDIRECT', - 'formURL' => 'index.php?option=com_redirect&view=links', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Redirects:_Links', - 'icon' => 'icon-map-signs redirect', + 'textPrefix' => 'COM_REDIRECT', + 'formURL' => 'index.php?option=com_redirect&view=links', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Redirects:_Links', + 'icon' => 'icon-map-signs redirect', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_redirect')) -{ - $displayData['createURL'] = 'index.php?option=com_redirect&task=link.add'; +if ($user->authorise('core.create', 'com_redirect')) { + $displayData['createURL'] = 'index.php?option=com_redirect&task=link.add'; } -if ($user->authorise('core.create', 'com_redirect') - && $user->authorise('core.edit', 'com_redirect') - && $user->authorise('core.edit.state', 'com_redirect')) -{ - $displayData['formAppend'] = HTMLHelper::_( - 'bootstrap.renderModal', - 'collapseModal', - [ - 'title' => Text::_('COM_REDIRECT_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ], - $this->loadTemplate('batch_body') - ); +if ( + $user->authorise('core.create', 'com_redirect') + && $user->authorise('core.edit', 'com_redirect') + && $user->authorise('core.edit.state', 'com_redirect') +) { + $displayData['formAppend'] = HTMLHelper::_( + 'bootstrap.renderModal', + 'collapseModal', + [ + 'title' => Text::_('COM_REDIRECT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ], + $this->loadTemplate('batch_body') + ); } ?> redirectPluginId) : ?> - redirectPluginId . '&tmpl=component&layout=modal'); ?> - redirectPluginId . 'Modal', - array( - 'url' => $link, - 'title' => Text::_('COM_REDIRECT_EDIT_PLUGIN_SETTINGS'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'closeButton' => false, - 'backdrop' => 'static', - 'keyboard' => false, - 'footer' => '' - . '' - . '' - ) - ); ?> + redirectPluginId . '&tmpl=component&layout=modal'); ?> + redirectPluginId . 'Modal', + array( + 'url' => $link, + 'title' => Text::_('COM_REDIRECT_EDIT_PLUGIN_SETTINGS'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'closeButton' => false, + 'backdrop' => 'static', + 'keyboard' => false, + 'footer' => '' + . '' + . '' + ) + ); ?> +
- - + +
diff --git a/code/administrator/components/com_scheduler/scheduler.xml b/code/administrator/components/com_scheduler/scheduler.xml index 2b1671cf..f9701dd7 100644 --- a/code/administrator/components/com_scheduler/scheduler.xml +++ b/code/administrator/components/com_scheduler/scheduler.xml @@ -2,7 +2,7 @@ com_scheduler Joomla! Project - July 2021 + 2021-07 (C) 2021 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_scheduler/services/provider.php b/code/administrator/components/com_scheduler/services/provider.php index 4511f0be..ded02210 100644 --- a/code/administrator/components/com_scheduler/services/provider.php +++ b/code/administrator/components/com_scheduler/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Scheduler')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Scheduler')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.1.0 + */ + public function register(Container $container) + { + /** + * Register the MVCFactory and ComponentDispatcherFactory providers to map + * 'MVCFactoryInterface' and 'ComponentDispatcherFactoryInterface' to their + * initializers and register them with the component's DI container. + */ + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Scheduler')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Scheduler')); - $container->set( - ComponentInterface::class, - function (Container $container) { - $component = new SchedulerComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new SchedulerComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_scheduler/src/Controller/DisplayController.php b/code/administrator/components/com_scheduler/src/Controller/DisplayController.php index eb79b7f7..0c017203 100644 --- a/code/administrator/components/com_scheduler/src/Controller/DisplayController.php +++ b/code/administrator/components/com_scheduler/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('layout', 'default'); + /** + * @var string + * @since 4.1.0 + */ + protected $default_view = 'tasks'; - // Check for edit form. - if ($layout === 'edit') - { - if (!$this->validateEntry()) - { - $tasksViewUrl = Route::_('index.php?option=com_scheduler&view=tasks', false); - $this->setRedirect($tasksViewUrl); + /** + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link InputFilter::clean()}. + * + * @return BaseController|boolean Returns either a BaseController object to support chaining, or false on failure + * + * @since 4.1.0 + * @throws \Exception + */ + public function display($cachable = false, $urlparams = array()) + { + $layout = $this->input->get('layout', 'default'); - return false; - } - } + // Check for edit form. + if ($layout === 'edit') { + if (!$this->validateEntry()) { + $tasksViewUrl = Route::_('index.php?option=com_scheduler&view=tasks', false); + $this->setRedirect($tasksViewUrl); - // Let the parent method take over - return parent::display($cachable, $urlparams); - } + return false; + } + } - /** - * Validates entry to the view - * - * @param string $layout The layout to validate entry for (defaults to 'edit') - * - * @return boolean True is entry is valid - * - * @since 4.1.0 - */ - private function validateEntry(string $layout = 'edit'): bool - { - $context = 'com_scheduler'; - $id = $this->input->getInt('id'); - $isValid = true; + // Let the parent method take over + return parent::display($cachable, $urlparams); + } - switch ($layout) - { - case 'edit': + /** + * Validates entry to the view + * + * @param string $layout The layout to validate entry for (defaults to 'edit') + * + * @return boolean True is entry is valid + * + * @since 4.1.0 + */ + private function validateEntry(string $layout = 'edit'): bool + { + $context = 'com_scheduler'; + $id = $this->input->getInt('id'); + $isValid = true; - // True if controller was called and verified permissions - $inEditList = $this->checkEditId("$context.edit.task", $id); - $isNew = ($id == 0); + switch ($layout) { + case 'edit': + // True if controller was called and verified permissions + $inEditList = $this->checkEditId("$context.edit.task", $id); + $isNew = ($id == 0); - // For new item, entry is invalid if task type was not selected through SelectView - if ($isNew && !$this->app->getUserState("$context.add.task.task_type")) - { - $this->setMessage((Text::_('COM_SCHEDULER_ERROR_FORBIDDEN_JUMP_TO_ADD_VIEW')), 'error'); - $isValid = false; - } - // For existing item, entry is invalid if TaskController has not granted access - elseif (!$inEditList) - { - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // For new item, entry is invalid if task type was not selected through SelectView + if ($isNew && !$this->app->getUserState("$context.add.task.task_type")) { + $this->setMessage((Text::_('COM_SCHEDULER_ERROR_FORBIDDEN_JUMP_TO_ADD_VIEW')), 'error'); + $isValid = false; + } elseif (!$inEditList) { + // For existing item, entry is invalid if TaskController has not granted access + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $isValid = false; - } - break; - default: - break; - } + $isValid = false; + } + break; + default: + break; + } - return $isValid; - } + return $isValid; + } } diff --git a/code/administrator/components/com_scheduler/src/Controller/TaskController.php b/code/administrator/components/com_scheduler/src/Controller/TaskController.php index 50e3409a..bc90d976 100644 --- a/code/administrator/components/com_scheduler/src/Controller/TaskController.php +++ b/code/administrator/components/com_scheduler/src/Controller/TaskController.php @@ -1,4 +1,5 @@ app; - $input = $app->getInput(); - $validTaskOptions = SchedulerHelper::getTaskOptions(); - - $canAdd = parent::add(); - - if ($canAdd !== true) - { - return false; - } - - $taskType = $input->get('type'); - $taskOption = $validTaskOptions->findOption($taskType) ?: null; - - if (!$taskOption) - { - // ? : Is this the right redirect [review] - $redirectUrl = 'index.php?option=' . $this->option . '&view=select&layout=edit'; - $this->setRedirect(Route::_($redirectUrl, false)); - $app->enqueueMessage(Text::_('COM_SCHEDULER_ERROR_INVALID_TASK_TYPE'), 'warning'); - $canAdd = false; - } - - $app->setUserState('com_scheduler.add.task.task_type', $taskType); - $app->setUserState('com_scheduler.add.task.task_option', $taskOption); - - // @todo : Parameter array handling below? - - return $canAdd; - } - - /** - * Override parent cancel method to reset the add task state - * - * @param ?string $key Primary key from the URL param - * - * @return boolean True if access level checks pass - * - * @since 4.1.0 - */ - public function cancel($key = null): bool - { - $result = parent::cancel($key); - - $this->app->setUserState('com_scheduler.add.task.task_type', null); - $this->app->setUserState('com_scheduler.add.task.task_option', null); - - // ? Do we need to redirect based on URL's 'return' param? {@see ModuleController} - - return $result; - } - - /** - * Check if user has the authority to edit an asset - * - * @param array $data Array of input data - * @param string $key Name of key for primary key, defaults to 'id' - * - * @return boolean True if user is allowed to edit record - * - * @since 4.1.0 - */ - protected function allowEdit($data = array(), $key = 'id'): bool - { - // Extract the recordId from $data, will come in handy - $recordId = (int) $data[$key] ?? 0; - - /** - * Zero record (id:0), return component edit permission by calling parent controller method - * ?: Is this the right way to do this? - */ - if ($recordId === 0) - { - return parent::allowEdit($data, $key); - } - - // @todo : Check if this works as expected - return $this->app->getIdentity()->authorise('core.edit', 'com_scheduler.task.' . $recordId); - - } + /** + * Add a new record + * + * @return boolean + * @since 4.1.0 + * @throws \Exception + */ + public function add(): bool + { + /** @var AdministratorApplication $app */ + $app = $this->app; + $input = $app->getInput(); + $validTaskOptions = SchedulerHelper::getTaskOptions(); + + $canAdd = parent::add(); + + if ($canAdd !== true) { + return false; + } + + $taskType = $input->get('type'); + $taskOption = $validTaskOptions->findOption($taskType) ?: null; + + if (!$taskOption) { + // ? : Is this the right redirect [review] + $redirectUrl = 'index.php?option=' . $this->option . '&view=select&layout=edit'; + $this->setRedirect(Route::_($redirectUrl, false)); + $app->enqueueMessage(Text::_('COM_SCHEDULER_ERROR_INVALID_TASK_TYPE'), 'warning'); + $canAdd = false; + } + + $app->setUserState('com_scheduler.add.task.task_type', $taskType); + $app->setUserState('com_scheduler.add.task.task_option', $taskOption); + + // @todo : Parameter array handling below? + + return $canAdd; + } + + /** + * Override parent cancel method to reset the add task state + * + * @param ?string $key Primary key from the URL param + * + * @return boolean True if access level checks pass + * + * @since 4.1.0 + */ + public function cancel($key = null): bool + { + $result = parent::cancel($key); + + $this->app->setUserState('com_scheduler.add.task.task_type', null); + $this->app->setUserState('com_scheduler.add.task.task_option', null); + + // ? Do we need to redirect based on URL's 'return' param? {@see ModuleController} + + return $result; + } + + /** + * Check if user has the authority to edit an asset + * + * @param array $data Array of input data + * @param string $key Name of key for primary key, defaults to 'id' + * + * @return boolean True if user is allowed to edit record + * + * @since 4.1.0 + */ + protected function allowEdit($data = array(), $key = 'id'): bool + { + // Extract the recordId from $data, will come in handy + $recordId = (int) $data[$key] ?? 0; + + /** + * Zero record (id:0), return component edit permission by calling parent controller method + * ?: Is this the right way to do this? + */ + if ($recordId === 0) { + return parent::allowEdit($data, $key); + } + + // @todo : Check if this works as expected + return $this->app->getIdentity()->authorise('core.edit', 'com_scheduler.task.' . $recordId); + } } diff --git a/code/administrator/components/com_scheduler/src/Controller/TasksController.php b/code/administrator/components/com_scheduler/src/Controller/TasksController.php index 635e169d..2f1b470f 100644 --- a/code/administrator/components/com_scheduler/src/Controller/TasksController.php +++ b/code/administrator/components/com_scheduler/src/Controller/TasksController.php @@ -1,4 +1,5 @@ true]): BaseDatabaseModel - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for the parent method. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return BaseDatabaseModel + * + * @since 4.1.0 + */ + public function getModel($name = 'Task', $prefix = 'Administrator', $config = ['ignore_request' => true]): BaseDatabaseModel + { + return parent::getModel($name, $prefix, $config); + } - /** - * Unlock a locked task, i.e., a task that is presumably still running but might have crashed and got stuck in the - * "locked" state. - * - * @return void - * - * @since 4.1.0 - */ - public function unlock(): void - { - // Check for request forgeries - $this->checkToken(); + /** + * Unlock a locked task, i.e., a task that is presumably still running but might have crashed and got stuck in the + * "locked" state. + * + * @return void + * + * @since 4.1.0 + */ + public function unlock(): void + { + // Check for request forgeries + $this->checkToken(); - /** @var integer[] $cid Items to publish (from request parameters). */ - $cid = (array) $this->input->get('cid', [], 'int'); + /** @var integer[] $cid Items to publish (from request parameters). */ + $cid = (array) $this->input->get('cid', [], 'int'); - // Remove zero values resulting from input filter - $cid = array_filter($cid); + // Remove zero values resulting from input filter + $cid = array_filter($cid); - if (empty($cid)) - { - $this->app->getLogger() - ->warning(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), array('category' => 'jerror')); - } - else - { - /** @var TaskModel $model */ - $model = $this->getModel(); + if (empty($cid)) { + $this->app->getLogger() + ->warning(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), array('category' => 'jerror')); + } else { + /** @var TaskModel $model */ + $model = $this->getModel(); - // Make sure the item IDs are integers - $cid = ArrayHelper::toInteger($cid); + // Make sure the item IDs are integers + $cid = ArrayHelper::toInteger($cid); - // Unlock the items. - try - { - $model->unlock($cid); - $errors = $model->getErrors(); - $noticeText = null; + // Unlock the items. + try { + $model->unlock($cid); + $errors = $model->getErrors(); + $noticeText = null; - if ($errors) - { - Factory::getApplication() - ->enqueueMessage(Text::plural($this->text_prefix . '_N_ITEMS_FAILED_UNLOCKING', \count($cid)), 'error'); - } - else - { - $noticeText = $this->text_prefix . '_N_ITEMS_UNLOCKED'; - } + if ($errors) { + $this->app->enqueueMessage(Text::plural($this->text_prefix . '_N_ITEMS_FAILED_UNLOCKING', \count($cid)), 'error'); + } else { + $noticeText = $this->text_prefix . '_N_ITEMS_UNLOCKED'; + } - if (\count($cid)) - { - $this->setMessage(Text::plural($noticeText, \count($cid))); - } - } - catch (\Exception $e) - { - $this->setMessage($e->getMessage(), 'error'); - } - } + if (\count($cid)) { + $this->setMessage(Text::plural($noticeText, \count($cid))); + } + } catch (\Exception $e) { + $this->setMessage($e->getMessage(), 'error'); + } + } - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), - false - ) - ); - } + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + } } diff --git a/code/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php b/code/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php index 31fb6550..df61d950 100644 --- a/code/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php +++ b/code/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php @@ -1,4 +1,5 @@ arguments['resultSnapshot'] = $snapshot; + /** + * Sets the task result snapshot and stops event propagation. + * + * @param array $snapshot The task snapshot. + * + * @return void + * + * @since 4.1.0 + */ + public function setResult(array $snapshot = []): void + { + $this->arguments['resultSnapshot'] = $snapshot; - if (!empty($snapshot)) - { - $this->stopPropagation(); - } - } + if (!empty($snapshot)) { + $this->stopPropagation(); + } + } - /** - * @return integer The task's taskId. - * - * @since 4.1.0 - */ - public function getTaskId(): int - { - return $this->arguments['subject']->get('id'); - } + /** + * @return integer The task's taskId. + * + * @since 4.1.0 + */ + public function getTaskId(): int + { + return $this->arguments['subject']->get('id'); + } - /** - * @return string The task's 'type'. - * - * @since 4.1.0 - */ - public function getRoutineId(): string - { - return $this->arguments['subject']->get('type'); - } + /** + * @return string The task's 'type'. + * + * @since 4.1.0 + */ + public function getRoutineId(): string + { + return $this->arguments['subject']->get('type'); + } - /** - * Returns the snapshot of the triggered task if available, else an empty array - * - * @return array The task snapshot if available, else null - * - * @since 4.1.0 - */ - public function getResultSnapshot(): array - { - return $this->arguments['resultSnapshot'] ?? []; - } + /** + * Returns the snapshot of the triggered task if available, else an empty array + * + * @return array The task snapshot if available, else null + * + * @since 4.1.0 + */ + public function getResultSnapshot(): array + { + return $this->arguments['resultSnapshot'] ?? []; + } } diff --git a/code/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php b/code/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php index 9f0b5827..2961a7e0 100644 --- a/code/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php +++ b/code/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php @@ -1,4 +1,5 @@ [0, 59], - 'hours' => [0, 23], - 'days_week' => [1, 7], - 'days_month' => [1, 31], - 'months' => [1, 12], - ]; - - /** - * Response labels for the 'month' and 'days_week' subtypes. - * The labels are language constants translated when needed. - * - * @var string[][] - * @since 4.1.0 - */ - private const PREPARED_RESPONSE_LABELS = [ - 'months' => [ - 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', - 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', - ], - 'days_week' => [ - 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', - 'FRIDAY', 'SATURDAY', 'SUNDAY', - ], - ]; - - /** - * The form field type. - * - * @var string - * - * @since 4.1.0 - */ - protected $type = 'cronIntervals'; - - /** - * The subtype of the CronIntervals field - * - * @var string - * @since 4.1.0 - */ - private $subtype; - - /** - * If true, field options will include a wildcard - * - * @var boolean - * @since 4.1.0 - */ - private $wildcard; - - /** - * If true, field will only have numeric labels (for days_week and months) - * - * @var boolean - * @since 4.1.0 - */ - private $onlyNumericLabels; - - /** - * Override the parent method to set deal with subtypes. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form - * field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for - * the field. For example if the field has `name="foo"` and the group value is - * set to "bar" then the full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.1.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null): bool - { - $parentResult = parent::setup($element, $value, $group); - - $subtype = ((string) $element['subtype'] ?? '') ?: null; - $wildcard = ((string) $element['wildcard'] ?? '') === 'true'; - $onlyNumericLabels = ((string) $element['onlyNumericLabels']) === 'true'; - - if (!($subtype && \in_array($subtype, self::SUBTYPES))) - { - return false; - } - - $this->subtype = $subtype; - $this->wildcard = $wildcard; - $this->onlyNumericLabels = $onlyNumericLabels; - - return $parentResult; - } - - /** - * Method to get field options - * - * @return array Array of objects representing options in the options list - * - * @since 4.1.0 - */ - protected function getOptions(): array - { - $subtype = $this->subtype; - $options = parent::getOptions(); - - if (!\in_array($subtype, self::SUBTYPES)) - { - return $options; - } - - if ($this->wildcard) - { - try - { - $options[] = HTMLHelper::_('select.option', '*', '*'); - } - catch (\InvalidArgumentException $e) - { - } - } - - [$optionLower, $optionUpper] = self::OPTIONS_RANGE[$subtype]; - - // If we need text labels, we translate them first - if (\array_key_exists($subtype, self::PREPARED_RESPONSE_LABELS) && !$this->onlyNumericLabels) - { - $labels = array_map( - static function (string $string): string { - return Text::_($string); - }, - self::PREPARED_RESPONSE_LABELS[$subtype] - ); - } - else - { - $labels = range(...self::OPTIONS_RANGE[$subtype]); - } - - for ([$i, $l] = [$optionLower, 0]; $i <= $optionUpper; $i++, $l++) - { - try - { - $options[] = HTMLHelper::_('select.option', (string) ($i), $labels[$l]); - } - catch (\InvalidArgumentException $e) - { - } - } - - return $options; - } + /** + * The subtypes supported by this field type. + * + * @var string[] + * + * @since 4.1.0 + */ + private const SUBTYPES = [ + 'minutes', + 'hours', + 'days_month', + 'months', + 'days_week', + ]; + + /** + * Count of predefined options for each subtype + * + * @var int[][] + * + * @since 4.1.0 + */ + private const OPTIONS_RANGE = [ + 'minutes' => [0, 59], + 'hours' => [0, 23], + 'days_week' => [1, 7], + 'days_month' => [1, 31], + 'months' => [1, 12], + ]; + + /** + * Response labels for the 'month' and 'days_week' subtypes. + * The labels are language constants translated when needed. + * + * @var string[][] + * @since 4.1.0 + */ + private const PREPARED_RESPONSE_LABELS = [ + 'months' => [ + 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', + 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', + ], + 'days_week' => [ + 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', + 'FRIDAY', 'SATURDAY', 'SUNDAY', + ], + ]; + + /** + * The form field type. + * + * @var string + * + * @since 4.1.0 + */ + protected $type = 'cronIntervals'; + + /** + * The subtype of the CronIntervals field + * + * @var string + * @since 4.1.0 + */ + private $subtype; + + /** + * If true, field options will include a wildcard + * + * @var boolean + * @since 4.1.0 + */ + private $wildcard; + + /** + * If true, field will only have numeric labels (for days_week and months) + * + * @var boolean + * @since 4.1.0 + */ + private $onlyNumericLabels; + + /** + * Override the parent method to set deal with subtypes. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form + * field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for + * the field. For example if the field has `name="foo"` and the group value is + * set to "bar" then the full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.1.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null): bool + { + $parentResult = parent::setup($element, $value, $group); + + $subtype = ((string) $element['subtype'] ?? '') ?: null; + $wildcard = ((string) $element['wildcard'] ?? '') === 'true'; + $onlyNumericLabels = ((string) $element['onlyNumericLabels']) === 'true'; + + if (!($subtype && \in_array($subtype, self::SUBTYPES))) { + return false; + } + + $this->subtype = $subtype; + $this->wildcard = $wildcard; + $this->onlyNumericLabels = $onlyNumericLabels; + + return $parentResult; + } + + /** + * Method to get field options + * + * @return array Array of objects representing options in the options list + * + * @since 4.1.0 + */ + protected function getOptions(): array + { + $subtype = $this->subtype; + $options = parent::getOptions(); + + if (!\in_array($subtype, self::SUBTYPES)) { + return $options; + } + + if ($this->wildcard) { + try { + $options[] = HTMLHelper::_('select.option', '*', '*'); + } catch (\InvalidArgumentException $e) { + } + } + + [$optionLower, $optionUpper] = self::OPTIONS_RANGE[$subtype]; + + // If we need text labels, we translate them first + if (\array_key_exists($subtype, self::PREPARED_RESPONSE_LABELS) && !$this->onlyNumericLabels) { + $labels = array_map( + static function (string $string): string { + return Text::_($string); + }, + self::PREPARED_RESPONSE_LABELS[$subtype] + ); + } else { + $labels = range(...self::OPTIONS_RANGE[$subtype]); + } + + for ([$i, $l] = [$optionLower, 0]; $i <= $optionUpper; $i++, $l++) { + try { + $options[] = HTMLHelper::_('select.option', (string) ($i), $labels[$l]); + } catch (\InvalidArgumentException $e) { + } + } + + return $options; + } } diff --git a/code/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php b/code/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php index 6f877357..a87c3ed5 100644 --- a/code/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php +++ b/code/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php @@ -1,4 +1,5 @@ 'COM_SCHEDULER_EXECUTION_INTERVAL_MINUTES', - 'interval-hours' => 'COM_SCHEDULER_EXECUTION_INTERVAL_HOURS', - 'interval-days' => 'COM_SCHEDULER_EXECUTION_INTERVAL_DAYS', - 'interval-months' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MONTHS', - 'cron-expression' => 'COM_SCHEDULER_EXECUTION_CRON_EXPRESSION', - 'manual' => 'COM_SCHEDULER_OPTION_EXECUTION_MANUAL_LABEL', - ]; + /** + * Available execution rules. + * + * @var string[] + * @since 4.1.0 + */ + protected $predefinedOptions = [ + 'interval-minutes' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MINUTES', + 'interval-hours' => 'COM_SCHEDULER_EXECUTION_INTERVAL_HOURS', + 'interval-days' => 'COM_SCHEDULER_EXECUTION_INTERVAL_DAYS', + 'interval-months' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MONTHS', + 'cron-expression' => 'COM_SCHEDULER_EXECUTION_CRON_EXPRESSION', + 'manual' => 'COM_SCHEDULER_OPTION_EXECUTION_MANUAL_LABEL', + ]; } diff --git a/code/administrator/components/com_scheduler/src/Field/IntervalField.php b/code/administrator/components/com_scheduler/src/Field/IntervalField.php index 7a7202e6..eaf5e87a 100644 --- a/code/administrator/components/com_scheduler/src/Field/IntervalField.php +++ b/code/administrator/components/com_scheduler/src/Field/IntervalField.php @@ -1,4 +1,5 @@ [minVal, maxVal] - * - * @var string[] - * @since 4.1.0 - */ - private const SUBTYPES = [ - 'minutes' => [1, 59], - 'hours' => [1, 23], - 'days' => [1, 30], - 'months' => [1, 12], - ]; + /** + * The subtypes supported by this field type => [minVal, maxVal] + * + * @var string[] + * @since 4.1.0 + */ + private const SUBTYPES = [ + 'minutes' => [1, 59], + 'hours' => [1, 23], + 'days' => [1, 30], + 'months' => [1, 12], + ]; - /** - * The allowable maximum value of the field. - * - * @var float - * @since 4.1.0 - */ - protected $max; + /** + * The allowable maximum value of the field. + * + * @var float + * @since 4.1.0 + */ + protected $max; - /** - * The allowable minimum value of the field. - * - * @var float - * @since 4.1.0 - */ - protected $min; + /** + * The allowable minimum value of the field. + * + * @var float + * @since 4.1.0 + */ + protected $min; - /** - * The step by which value of the field increased or decreased. - * - * @var float - * @since 4.1.0 - */ - protected $step = 1; + /** + * The step by which value of the field increased or decreased. + * + * @var float + * @since 4.1.0 + */ + protected $step = 1; - /** - * Override the parent method to set deal with subtypes. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form - * field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for - * the field. For example if the field has `name="foo"` and the group value is - * set to "bar" then the full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.1.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null): bool - { - $parentResult = FormField::setup($element, $value, $group); - $subtype = ((string) $element['subtype'] ?? '') ?: null; + /** + * Override the parent method to set deal with subtypes. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form + * field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for + * the field. For example if the field has `name="foo"` and the group value is + * set to "bar" then the full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.1.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null): bool + { + $parentResult = FormField::setup($element, $value, $group); + $subtype = ((string) $element['subtype'] ?? '') ?: null; - if (empty($subtype) || !\array_key_exists($subtype, self::SUBTYPES)) - { - return false; - } + if (empty($subtype) || !\array_key_exists($subtype, self::SUBTYPES)) { + return false; + } - [$this->min, $this->max] = self::SUBTYPES[$subtype]; + [$this->min, $this->max] = self::SUBTYPES[$subtype]; - return $parentResult; - } + return $parentResult; + } } diff --git a/code/administrator/components/com_scheduler/src/Field/TaskStateField.php b/code/administrator/components/com_scheduler/src/Field/TaskStateField.php index 4ea8ba93..3c16ed85 100644 --- a/code/administrator/components/com_scheduler/src/Field/TaskStateField.php +++ b/code/administrator/components/com_scheduler/src/Field/TaskStateField.php @@ -1,4 +1,5 @@ 'JTRASHED', - 0 => 'JDISABLED', - 1 => 'JENABLED', - '*' => 'JALL', - ]; + /** + * Available states + * + * @var string[] + * @since 4.1.0 + */ + protected $predefinedOptions = [ + -2 => 'JTRASHED', + 0 => 'JDISABLED', + 1 => 'JENABLED', + '*' => 'JALL', + ]; } diff --git a/code/administrator/components/com_scheduler/src/Field/TaskTypeField.php b/code/administrator/components/com_scheduler/src/Field/TaskTypeField.php index 86d2e237..d83520fa 100644 --- a/code/administrator/components/com_scheduler/src/Field/TaskTypeField.php +++ b/code/administrator/components/com_scheduler/src/Field/TaskTypeField.php @@ -1,4 +1,5 @@ options, - 'title', - 1 - ); + // Get all available task types and sort by title + $types = ArrayHelper::sortObjects( + SchedulerHelper::getTaskOptions()->options, + 'title', + 1 + ); - // Closure to add a TaskOption as a option in $options: array + $addTypeAsOption = function (TaskOption $type) use (&$options) { + try { + $options[] = HTMLHelper::_('select.option', $type->id, $type->title); + } catch (\InvalidArgumentException $e) { + } + }; - // Call $addTypeAsOption on each type - array_map($addTypeAsOption, $types); + // Call $addTypeAsOption on each type + array_map($addTypeAsOption, $types); - return $options; - } + return $options; + } } diff --git a/code/administrator/components/com_scheduler/src/Field/WebcronLinkField.php b/code/administrator/components/com_scheduler/src/Field/WebcronLinkField.php index 597afee3..77ebbe77 100644 --- a/code/administrator/components/com_scheduler/src/Field/WebcronLinkField.php +++ b/code/administrator/components/com_scheduler/src/Field/WebcronLinkField.php @@ -1,4 +1,5 @@ task = \is_array($task) ? $task : ArrayHelper::fromObject($task); - $rule = $this->getFromTask('cron_rules'); - $this->rule = \is_string($rule) - ? (object) json_decode($rule) - : (\is_array($rule) ? (object) $rule : $rule); - $this->type = $this->rule->type; - } + /** + * @param array|object $task A task entry + * + * @since 4.1.0 + */ + public function __construct($task) + { + $this->task = \is_array($task) ? $task : ArrayHelper::fromObject($task); + $rule = $this->getFromTask('cron_rules'); + $this->rule = \is_string($rule) + ? (object) json_decode($rule) + : (\is_array($rule) ? (object) $rule : $rule); + $this->type = $this->rule->type; + } - /** - * Get a property from the task array - * - * @param string $property The property to get - * @param mixed $default The default value returned if property does not exist - * - * @return mixed - * - * @since 4.1.0 - */ - private function getFromTask(string $property, $default = null) - { - $property = ArrayHelper::getValue($this->task, $property); + /** + * Get a property from the task array + * + * @param string $property The property to get + * @param mixed $default The default value returned if property does not exist + * + * @return mixed + * + * @since 4.1.0 + */ + private function getFromTask(string $property, $default = null) + { + $property = ArrayHelper::getValue($this->task, $property); - return $property ?? $default; - } + return $property ?? $default; + } - /** - * @param boolean $string If true, an SQL formatted string is returned. - * @param boolean $basisNow If true, the current date-time is used as the basis for projecting the next - * execution. - * - * @return ?Date|string - * - * @since 4.1.0 - * @throws \Exception - */ - public function nextExec(bool $string = true, bool $basisNow = false) - { - // Exception handling here - switch ($this->type) - { - case 'interval': - $lastExec = Factory::getDate($basisNow ? 'now' : $this->getFromTask('last_execution'), 'UTC'); - $interval = new \DateInterval($this->rule->exp); - $nextExec = $lastExec->add($interval); - $nextExec = $string ? $nextExec->toSql() : $nextExec; - break; - case 'cron-expression': - // @todo: testing - $cExp = new CronExpression((string) $this->rule->exp); - $nextExec = $cExp->getNextRunDate('now', 0, false, 'UTC'); - $nextExec = $string ? $this->dateTimeToSql($nextExec) : $nextExec; - break; - default: - // 'manual' execution is handled here. - $nextExec = null; - } + /** + * @param boolean $string If true, an SQL formatted string is returned. + * @param boolean $basisNow If true, the current date-time is used as the basis for projecting the next + * execution. + * + * @return ?Date|string + * + * @since 4.1.0 + * @throws \Exception + */ + public function nextExec(bool $string = true, bool $basisNow = false) + { + // Exception handling here + switch ($this->type) { + case 'interval': + $lastExec = Factory::getDate($basisNow ? 'now' : $this->getFromTask('last_execution'), 'UTC'); + $interval = new \DateInterval($this->rule->exp); + $nextExec = $lastExec->add($interval); + $nextExec = $string ? $nextExec->toSql() : $nextExec; + break; + case 'cron-expression': + // @todo: testing + $cExp = new CronExpression((string) $this->rule->exp); + $nextExec = $cExp->getNextRunDate('now', 0, false, 'UTC'); + $nextExec = $string ? $this->dateTimeToSql($nextExec) : $nextExec; + break; + default: + // 'manual' execution is handled here. + $nextExec = null; + } - return $nextExec; - } + return $nextExec; + } - /** - * Returns a sql-formatted string for a DateTime object. - * Only needed for DateTime objects returned by CronExpression, JDate supports this as class method. - * - * @param \DateTime $dateTime A DateTime object to format - * - * @return string - * - * @since 4.1.0 - */ - private function dateTimeToSql(\DateTime $dateTime): string - { - static $db; - $db = $db ?? Factory::getContainer()->get(DatabaseDriver::class); + /** + * Returns a sql-formatted string for a DateTime object. + * Only needed for DateTime objects returned by CronExpression, JDate supports this as class method. + * + * @param \DateTime $dateTime A DateTime object to format + * + * @return string + * + * @since 4.1.0 + */ + private function dateTimeToSql(\DateTime $dateTime): string + { + static $db; + $db = $db ?? Factory::getContainer()->get(DatabaseDriver::class); - return $dateTime->format($db->getDateFormat()); - } + return $dateTime->format($db->getDateFormat()); + } } diff --git a/code/administrator/components/com_scheduler/src/Helper/SchedulerHelper.php b/code/administrator/components/com_scheduler/src/Helper/SchedulerHelper.php index 3e0f13a2..c3dcd616 100644 --- a/code/administrator/components/com_scheduler/src/Helper/SchedulerHelper.php +++ b/code/administrator/components/com_scheduler/src/Helper/SchedulerHelper.php @@ -1,4 +1,5 @@ $options, - ] - ); + /** @var AdministratorApplication $app */ + $app = Factory::getApplication(); + $options = new TaskOptions(); + $event = AbstractEvent::create( + 'onTaskOptionsList', + [ + 'subject' => $options, + ] + ); - PluginHelper::importPlugin('task'); - $app->getDispatcher()->dispatch('onTaskOptionsList', $event); + PluginHelper::importPlugin('task'); + $app->getDispatcher()->dispatch('onTaskOptionsList', $event); - self::$taskOptionsCache = $options; + self::$taskOptionsCache = $options; - return $options; - } + return $options; + } } diff --git a/code/administrator/components/com_scheduler/src/Model/SelectModel.php b/code/administrator/components/com_scheduler/src/Model/SelectModel.php index ea92d6c8..1f7715d8 100644 --- a/code/administrator/components/com_scheduler/src/Model/SelectModel.php +++ b/code/administrator/components/com_scheduler/src/Model/SelectModel.php @@ -1,4 +1,5 @@ app = Factory::getApplication(); - - parent::__construct($config, $factory); - } - - /** - * @return TaskOption[] An array of TaskOption objects - * - * @throws \Exception - * @since 4.1.0 - */ - public function getItems(): array - { - return SchedulerHelper::getTaskOptions()->options; - } + /** + * The Application object, due removal. + * + * @var AdministratorApplication + * @since 4.1.0 + */ + protected $app; + + /** + * SelectModel constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param ?MVCFactoryInterface $factory The factory. + * + * @throws \Exception + * @since 4.1.0 + */ + public function __construct($config = array(), ?MVCFactoryInterface $factory = null) + { + $this->app = Factory::getApplication(); + + parent::__construct($config, $factory); + } + + /** + * @return TaskOption[] An array of TaskOption objects + * + * @throws \Exception + * @since 4.1.0 + */ + public function getItems(): array + { + return SchedulerHelper::getTaskOptions()->options; + } } diff --git a/code/administrator/components/com_scheduler/src/Model/TaskModel.php b/code/administrator/components/com_scheduler/src/Model/TaskModel.php index d74b195c..46588f8b 100644 --- a/code/administrator/components/com_scheduler/src/Model/TaskModel.php +++ b/code/administrator/components/com_scheduler/src/Model/TaskModel.php @@ -1,4 +1,5 @@ 1, - 'disabled' => 0, - 'trashed' => -2, - ]; - - /** - * The name of the database table with task records. - * - * @var string - * @since 4.1.0 - */ - public const TASK_TABLE = '#__scheduler_tasks'; - - /** - * Prefix used with controller messages - * - * @var string - * @since 4.1.0 - */ - protected $text_prefix = 'COM_SCHEDULER'; - - /** - * Type alias for content type - * - * @var string - * @since 4.1.0 - */ - public $typeAlias = 'com_scheduler.task'; - - /** - * The Application object, for convenience - * - * @var AdministratorApplication $app - * @since 4.1.0 - */ - protected $app; - - /** - * The event to trigger before unlocking the data. - * - * @var string - * @since 4.1.0 - */ - protected $event_before_unlock = null; - - /** - * The event to trigger after unlocking the data. - * - * @var string - * @since 4.1.0 - */ - protected $event_unlock = null; - - /** - * TaskModel constructor. Needed just to set $app - * - * @param array $config An array of configuration options - * @param MVCFactoryInterface|null $factory The factory - * @param FormFactoryInterface|null $formFactory The form factory - * - * @since 4.1.0 - * @throws \Exception - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) - { - $config['events_map'] = $config['events_map'] ?? []; - - $config['events_map'] = array_merge( - [ - 'save' => 'task', - 'validate' => 'task', - 'unlock' => 'task', - ], - $config['events_map'] - ); - - if (isset($config['event_before_unlock'])) - { - $this->event_before_unlock = $config['event_before_unlock']; - } - elseif (empty($this->event_before_unlock)) - { - $this->event_before_unlock = 'onContentBeforeUnlock'; - } - - if (isset($config['event_unlock'])) - { - $this->event_unlock = $config['event_unlock']; - } - elseif (empty($this->event_unlock)) - { - $this->event_unlock = 'onContentUnlock'; - } - - $this->app = Factory::getApplication(); - - parent::__construct($config, $factory, $formFactory); - } - - /** - * Fetches the form object associated with this model. By default, - * loads the corresponding data from the DB and binds it with the form. - * - * @param array $data Data that needs to go into the form - * @param bool $loadData Should the form load its data from the DB? - * - * @return Form|boolean A JForm object on success, false on failure. - * - * @since 4.1.0 - * @throws \Exception - */ - public function getForm($data = array(), $loadData = true) - { - Form::addFieldPath(JPATH_ADMINISTRATOR . 'components/com_scheduler/src/Field'); - - /** - * loadForm() (defined by FormBehaviourTrait) also loads the form data by calling - * loadFormData() : $data [implemented here] and binds it to the form by calling - * $form->bind($data). - */ - $form = $this->loadForm('com_scheduler.task', 'task', ['control' => 'jform', 'load_data' => $loadData]); - - if (empty($form)) - { - return false; - } - - $user = $this->app->getIdentity(); - - // If new entry, set task type from state - if ($this->getState('task.id', 0) === 0 && $this->getState('task.type') !== null) - { - $form->setValue('type', null, $this->getState('task.type')); - } - - // @todo : Check if this is working as expected for new items (id == 0) - if (!$user->authorise('core.edit.state', 'com_scheduler.task.' . $this->getState('task.id'))) - { - // Disable fields - $form->setFieldAttribute('state', 'disabled', 'true'); - - // No "hacking" ._. - $form->setFieldAttribute('state', 'filter', 'unset'); - } - - return $form; - } - - /** - * Determine whether a record may be deleted taking into consideration - * the user's permissions over the record. - * - * @param object $record The database row/record in question - * - * @return boolean True if the record may be deleted - * - * @since 4.1.0 - * @throws \Exception - */ - protected function canDelete($record): bool - { - // Record doesn't exist, can't delete - if (empty($record->id)) - { - return false; - } - - return $this->app->getIdentity()->authorise('core.delete', 'com_scheduler.task.' . $record->id); - } - - /** - * Populate the model state, we use these instead of toying with input or the global state - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - protected function populateState(): void - { - $app = $this->app; - - $taskId = $app->getInput()->getInt('id'); - $taskType = $app->getUserState('com_scheduler.add.task.task_type'); - - // @todo: Remove this. Get the option through a helper call. - $taskOption = $app->getUserState('com_scheduler.add.task.task_option'); - - $this->setState('task.id', $taskId); - $this->setState('task.type', $taskType); - $this->setState('task.option', $taskOption); - - // Load component params, though com_scheduler does not (yet) have any params - $cParams = ComponentHelper::getParams($this->option); - $this->setState('params', $cParams); - } - - /** - * Don't need to define this method since the parent getTable() - * implicitly deduces $name and $prefix anyways. This makes the object - * more transparent though. - * - * @param string $name Name of the table - * @param string $prefix Class prefix - * @param array $options Model config array - * - * @return Table - * - * @since 4.1.0 - * @throws \Exception - */ - public function getTable($name = 'Task', $prefix = 'Table', $options = array()): Table - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Fetches the data to be injected into the form - * - * @return object Associative array of form data. - * - * @since 4.1.0 - * @throws \Exception - */ - protected function loadFormData() - { - $data = $this->app->getUserState('com_scheduler.edit.task.data', array()); - - // If the data from UserState is empty, we fetch it with getItem() - if (empty($data)) - { - /** @var CMSObject $data */ - $data = $this->getItem(); - - // @todo : further data processing goes here - - // For a fresh object, set exec-day and exec-time - if (!($data->id ?? 0)) - { - $data->execution_rules['exec-day'] = gmdate('d'); - $data->execution_rules['exec-time'] = gmdate('H:i'); - } - } - - // Let plugins manipulate the data - $this->preprocessData('com_scheduler.task', $data, 'task'); - - return $data; - } - - /** - * Overloads the parent getItem() method. - * - * @param integer $pk Primary key - * - * @return object|boolean Object on success, false on failure - * - * @since 4.1.0 - * @throws \Exception - */ - public function getItem($pk = null) - { - $item = parent::getItem($pk); - - if (!\is_object($item)) - { - return false; - } - - // Parent call leaves `execution_rules` and `cron_rules` JSON encoded - $item->set('execution_rules', json_decode($item->get('execution_rules', ''))); - $item->set('cron_rules', json_decode($item->get('cron_rules', ''))); - - $taskOption = SchedulerHelper::getTaskOptions()->findOption( - ($item->id ?? 0) ? ($item->type ?? 0) : $this->getState('task.type') - ); - - $item->set('taskOption', $taskOption); - - return $item; - } - - /** - * Get a task from the database, only if an exclusive "lock" on the task can be acquired. - * The method supports options to customise the limitations on the fetch. - * - * @param array $options Array with options to fetch the task: - * 1. `id`: Optional id of the task to fetch. - * 2. `allowDisabled`: If true, disabled tasks can also be fetched. - * (default: false) - * 3. `bypassScheduling`: If true, tasks that are not due can also be - * fetched. Should only be true if an `id` is targeted instead of the - * task queue. (default: false) - * 4. `allowConcurrent`: If true, fetches even when another task is - * running ('locked'). (default: false) - * 5. `includeCliExclusive`: If true, can also fetch CLI exclusive tasks. (default: true) - * - * @return ?\stdClass Task entry as in the database. - * - * @since 4.1.0 - * @throws UndefinedOptionsException|InvalidOptionsException - * @throws \RuntimeException - */ - public function getTask(array $options = []): ?\stdClass - { - $resolver = new OptionsResolver; - - try - { - $this->configureTaskGetterOptions($resolver); - } - catch (\Exception $e) - { - } - - try - { - $options = $resolver->resolve($options); - } - catch (\Exception $e) - { - if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) - { - throw $e; - } - } - - $db = $this->getDbo(); - $now = Factory::getDate()->toSql(); - - // Get lock on the table to help with concurrency issues - $db->lockTable(self::TASK_TABLE); - - // If concurrency is not allowed, we only get a task if another one does not have a "lock" - if (!$options['allowConcurrent']) - { - // Get count of locked (presumed running) tasks - $lockCountQuery = $db->getQuery(true) - ->from($db->quoteName(self::TASK_TABLE)) - ->select('COUNT(id)') - ->where($db->quoteName('locked') . ' IS NOT NULL'); - - try - { - $runningCount = $db->setQuery($lockCountQuery)->loadResult(); - } - catch (\RuntimeException $e) - { - $db->unlockTables(); - - return null; - } - - if ($runningCount !== 0) - { - $db->unlockTables(); - - return null; - } - } - - $lockQuery = $db->getQuery(true); - - $lockQuery->update($db->quoteName(self::TASK_TABLE)) - ->set($db->quoteName('locked') . ' = :now1') - ->bind(':now1', $now); - - // Array of all active routine ids - $activeRoutines = array_map( - static function (TaskOption $taskOption): string - { - return $taskOption->id; - }, - SchedulerHelper::getTaskOptions()->options - ); - - // "Orphaned" tasks are not a part of the task queue! - $lockQuery->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); - - // If directed, exclude CLI exclusive tasks - if (!$options['includeCliExclusive']) - { - $lockQuery->where($db->quoteName('cli_exclusive') . ' = 0'); - } - - if (!$options['bypassScheduling']) - { - $lockQuery->where($db->quoteName('next_execution') . ' <= :now2') - ->bind(':now2', $now); - } - - if ($options['allowDisabled']) - { - $lockQuery->whereIn($db->quoteName('state'), [0, 1]); - } - else - { - $lockQuery->where($db->quoteName('state') . ' = 1'); - } - - if ($options['id'] > 0) - { - $lockQuery->where($db->quoteName('id') . ' = :taskId') - ->bind(':taskId', $options['id'], ParameterType::INTEGER); - } - // Pick from the front of the task queue if no 'id' is specified - else - { - // Get the id of the next task in the task queue - $idQuery = $db->getQuery(true) - ->from($db->quoteName(self::TASK_TABLE)) - ->select($db->quoteName('id')) - ->where($db->quoteName('state') . ' = 1') - ->order($db->quoteName('priority') . ' DESC') - ->order($db->quoteName('next_execution') . ' ASC') - ->setLimit(1); - - try - { - $ids = $db->setQuery($idQuery)->loadColumn(); - } - catch (\RuntimeException $e) - { - $db->unlockTables(); - - return null; - } - - if (count($ids) === 0) - { - $db->unlockTables(); - - return null; - } - - $lockQuery->whereIn($db->quoteName('id'), $ids); - } - - try - { - $db->setQuery($lockQuery)->execute(); - } - catch (\RuntimeException $e) - { - } - finally - { - $affectedRows = $db->getAffectedRows(); - - $db->unlockTables(); - } - - if ($affectedRows != 1) - { - /* - // @todo - // ? Fatal failure handling here? - // ! Question is, how? If we check for tasks running beyond there time here, we have no way of - // ! what's already been notified (since we're not auto-unlocking/recovering tasks anymore). - // The solution __may__ be in a "last_successful_finish" (or something) column. - */ - - return null; - } - - $getQuery = $db->getQuery(true); - - $getQuery->select('*') - ->from($db->quoteName(self::TASK_TABLE)) - ->where($db->quoteName('locked') . ' = :now') - ->bind(':now', $now); - - $task = $db->setQuery($getQuery)->loadObject(); - - $task->execution_rules = json_decode($task->execution_rules); - $task->cron_rules = json_decode($task->cron_rules); - - $task->taskOption = SchedulerHelper::getTaskOptions()->findOption($task->type); - - return $task; - } - - /** - * Set up an {@see OptionsResolver} to resolve options compatible with the {@see GetTask()} method. - * - * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. - * - * @return OptionsResolver - * - * @since 4.1.0 - * @throws AccessException - */ - public static function configureTaskGetterOptions(OptionsResolver $resolver): OptionsResolver - { - $resolver->setDefaults( - [ - 'id' => 0, - 'allowDisabled' => false, - 'bypassScheduling' => false, - 'allowConcurrent' => false, - 'includeCliExclusive' => true, - ] - ) - ->setAllowedTypes('id', 'numeric') - ->setAllowedTypes('allowDisabled', 'bool') - ->setAllowedTypes('bypassScheduling', 'bool') - ->setAllowedTypes('allowConcurrent', 'bool') - ->setAllowedTypes('includeCliExclusive', 'bool'); - - return $resolver; - } - - /** - * @param array $data The form data - * - * @return boolean True on success, false on failure - * - * @since 4.1.0 - * @throws \Exception - */ - public function save($data): bool - { - $id = (int) ($data['id'] ?? $this->getState('task.id')); - $isNew = $id === 0; - - // Clean up execution rules - $data['execution_rules'] = $this->processExecutionRules($data['execution_rules']); - - // If a new entry, we'll have to put in place a pseudo-last_execution - if ($isNew) - { - $basisDayOfMonth = $data['execution_rules']['exec-day']; - [$basisHour, $basisMinute] = explode(':', $data['execution_rules']['exec-time']); - - $data['last_execution'] = Factory::getDate('now', 'GMT')->format('Y-m') - . "-$basisDayOfMonth $basisHour:$basisMinute:00"; - } - else - { - $data['last_execution'] = $this->getItem($id)->last_execution; - } - - // Build the `cron_rules` column from `execution_rules` - $data['cron_rules'] = $this->buildExecutionRules($data['execution_rules']); - - // `next_execution` would be null if scheduling is disabled with the "manual" rule! - $data['next_execution'] = (new ExecRuleHelper($data))->nextExec(); - - if ($isNew) - { - $data['last_execution'] = null; - } - - // If no params, we set as empty array. - // ? Is this the right place to do this - $data['params'] = $data['params'] ?? []; - - // Parent method takes care of saving to the table - return parent::save($data); - } - - /** - * Clean up and standardise execution rules - * - * @param array $unprocessedRules The form data [? can just replace with execution_interval] - * - * @return array Processed rules - * - * @since 4.1.0 - */ - private function processExecutionRules(array $unprocessedRules): array - { - $executionRules = $unprocessedRules; - - $ruleType = $executionRules['rule-type']; - $retainKeys = ['rule-type', $ruleType, 'exec-day', 'exec-time']; - $executionRules = array_intersect_key($executionRules, array_flip($retainKeys)); - - // Default to current date-time in UTC/GMT as the basis - $executionRules['exec-day'] = $executionRules['exec-day'] ?: (string) gmdate('d'); - $executionRules['exec-time'] = $executionRules['exec-time'] ?: (string) gmdate('H:i'); - - // If custom ruleset, sort it - // ? Is this necessary - if ($ruleType === 'cron-expression') - { - foreach ($executionRules['cron-expression'] as &$values) - { - sort($values); - } - } - - return $executionRules; - } - - /** - * Private method to build execution expression from input execution rules. - * This expression is used internally to determine execution times/conditions. - * - * @param array $executionRules Execution rules from the Task form, post-processing. - * - * @return array - * - * @since 4.1.0 - * @throws \Exception - */ - private function buildExecutionRules(array $executionRules): array - { - // Maps interval strings, use with sprintf($map[intType], $interval) - $intervalStringMap = [ - 'minutes' => 'PT%dM', - 'hours' => 'PT%dH', - 'days' => 'P%dD', - 'months' => 'P%dM', - 'years' => 'P%dY', - ]; - - $ruleType = $executionRules['rule-type']; - $ruleClass = strpos($ruleType, 'interval') === 0 ? 'interval' : $ruleType; - $buildExpression = ''; - - if ($ruleClass === 'interval') - { - // Rule type for intervals interval- - $intervalType = explode('-', $ruleType)[1]; - $interval = $executionRules["interval-$intervalType"]; - $buildExpression = sprintf($intervalStringMap[$intervalType], $interval); - } - - if ($ruleClass === 'cron-expression') - { - // ! custom matches are disabled in the form - $matches = $executionRules['cron-expression']; - $buildExpression .= $this->wildcardIfMatch($matches['minutes'], range(0, 59), true); - $buildExpression .= ' ' . $this->wildcardIfMatch($matches['hours'], range(0, 23), true); - $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_month'], range(1, 31), true); - $buildExpression .= ' ' . $this->wildcardIfMatch($matches['months'], range(1, 12), true); - $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_week'], range(0, 6), true); - } - - return [ - 'type' => $ruleClass, - 'exp' => $buildExpression, - ]; - } - - /** - * This method releases "locks" on a set of tasks from the database. - * These locks are pseudo-locks that are used to keep a track of running tasks. However, they require require manual - * intervention to release these locks in cases such as when a task process crashes, leaving the task "locked". - * - * @param array $pks A list of the primary keys to unlock. - * - * @return boolean True on success. - * - * @since 4.1.0 - * @throws \RuntimeException|\UnexpectedValueException|\BadMethodCallException - */ - public function unlock(array &$pks): bool - { - /** @var TaskTable $table */ - $table = $this->getTable(); - - $user = Factory::getApplication()->getIdentity(); - - $context = $this->option . '.' . $this->name; - - // Include the plugins for the change of state event. - PluginHelper::importPlugin($this->events_map['unlock']); - - // Access checks. - foreach ($pks as $i => $pk) - { - $table->reset(); - - if ($table->load($pk)) - { - if (!$this->canEditState($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - - return false; - } - - // Prune items that are already at the given state. - $lockedColumnName = $table->getColumnAlias('locked'); - - if (property_exists($table, $lockedColumnName) && \is_null($table->get($lockedColumnName))) - { - unset($pks[$i]); - } - } - } - - // Check if there are items to change. - if (!\count($pks)) - { - return true; - } - - $event = AbstractEvent::create( - $this->event_before_unlock, - [ - 'subject' => $this, - 'context' => $context, - 'pks' => $pks, - ] - ); - - try - { - Factory::getApplication()->getDispatcher()->dispatch($this->event_before_unlock, $event); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Attempt to unlock the records. - if (!$table->unlock($pks, $user->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after unlock event - $event = AbstractEvent::create( - $this->event_unlock, - [ - 'subject' => $this, - 'context' => $context, - 'pks' => $pks, - ] - ); - - try - { - Factory::getApplication()->getDispatcher()->dispatch($this->event_unlock, $event); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Determine if an array is populated by all its possible values by comparison to a reference array, if found a - * match a wildcard '*' is returned. - * - * @param array $target The target array - * @param array $reference The reference array, populated by the complete set of possible values in $target - * @param bool $targetToInt If true, converts $target array values to integers before comparing - * - * @return string A wildcard string if $target is fully populated, else $target itself. - * - * @since 4.1.0 - */ - private function wildcardIfMatch(array $target, array $reference, bool $targetToInt = false): string - { - if ($targetToInt) - { - $target = array_map( - static function (string $x): int { - return (int) $x; - }, - $target - ); - } - - $isMatch = array_diff($reference, $target) === []; - - return $isMatch ? "*" : implode(',', $target); - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 4.1.0 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content'): void - { - // Load the 'task' plugin group - PluginHelper::importPlugin('task'); - - // Let the parent method take over - parent::preprocessForm($form, $data, $group); - } + /** + * Maps logical states to their values in the DB + * ? Do we end up using this? + * + * @var array + * @since 4.1.0 + */ + protected const TASK_STATES = [ + 'enabled' => 1, + 'disabled' => 0, + 'trashed' => -2, + ]; + + /** + * The name of the database table with task records. + * + * @var string + * @since 4.1.0 + */ + public const TASK_TABLE = '#__scheduler_tasks'; + + /** + * Prefix used with controller messages + * + * @var string + * @since 4.1.0 + */ + protected $text_prefix = 'COM_SCHEDULER'; + + /** + * Type alias for content type + * + * @var string + * @since 4.1.0 + */ + public $typeAlias = 'com_scheduler.task'; + + /** + * The Application object, for convenience + * + * @var AdministratorApplication $app + * @since 4.1.0 + */ + protected $app; + + /** + * The event to trigger before unlocking the data. + * + * @var string + * @since 4.1.0 + */ + protected $event_before_unlock = null; + + /** + * The event to trigger after unlocking the data. + * + * @var string + * @since 4.1.0 + */ + protected $event_unlock = null; + + /** + * TaskModel constructor. Needed just to set $app + * + * @param array $config An array of configuration options + * @param MVCFactoryInterface|null $factory The factory + * @param FormFactoryInterface|null $formFactory The form factory + * + * @since 4.1.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config['events_map'] = $config['events_map'] ?? []; + + $config['events_map'] = array_merge( + [ + 'save' => 'task', + 'validate' => 'task', + 'unlock' => 'task', + ], + $config['events_map'] + ); + + if (isset($config['event_before_unlock'])) { + $this->event_before_unlock = $config['event_before_unlock']; + } elseif (empty($this->event_before_unlock)) { + $this->event_before_unlock = 'onContentBeforeUnlock'; + } + + if (isset($config['event_unlock'])) { + $this->event_unlock = $config['event_unlock']; + } elseif (empty($this->event_unlock)) { + $this->event_unlock = 'onContentUnlock'; + } + + $this->app = Factory::getApplication(); + + parent::__construct($config, $factory, $formFactory); + } + + /** + * Fetches the form object associated with this model. By default, + * loads the corresponding data from the DB and binds it with the form. + * + * @param array $data Data that needs to go into the form + * @param bool $loadData Should the form load its data from the DB? + * + * @return Form|boolean A JForm object on success, false on failure. + * + * @since 4.1.0 + * @throws \Exception + */ + public function getForm($data = array(), $loadData = true) + { + Form::addFieldPath(JPATH_ADMINISTRATOR . 'components/com_scheduler/src/Field'); + + /** + * loadForm() (defined by FormBehaviourTrait) also loads the form data by calling + * loadFormData() : $data [implemented here] and binds it to the form by calling + * $form->bind($data). + */ + $form = $this->loadForm('com_scheduler.task', 'task', ['control' => 'jform', 'load_data' => $loadData]); + + if (empty($form)) { + return false; + } + + $user = $this->app->getIdentity(); + + // If new entry, set task type from state + if ($this->getState('task.id', 0) === 0 && $this->getState('task.type') !== null) { + $form->setValue('type', null, $this->getState('task.type')); + } + + // @todo : Check if this is working as expected for new items (id == 0) + if (!$user->authorise('core.edit.state', 'com_scheduler.task.' . $this->getState('task.id'))) { + // Disable fields + $form->setFieldAttribute('state', 'disabled', 'true'); + + // No "hacking" ._. + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + return $form; + } + + /** + * Determine whether a record may be deleted taking into consideration + * the user's permissions over the record. + * + * @param object $record The database row/record in question + * + * @return boolean True if the record may be deleted + * + * @since 4.1.0 + * @throws \Exception + */ + protected function canDelete($record): bool + { + // Record doesn't exist, can't delete + if (empty($record->id)) { + return false; + } + + return $this->app->getIdentity()->authorise('core.delete', 'com_scheduler.task.' . $record->id); + } + + /** + * Populate the model state, we use these instead of toying with input or the global state + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + protected function populateState(): void + { + $app = $this->app; + + $taskId = $app->getInput()->getInt('id'); + $taskType = $app->getUserState('com_scheduler.add.task.task_type'); + + // @todo: Remove this. Get the option through a helper call. + $taskOption = $app->getUserState('com_scheduler.add.task.task_option'); + + $this->setState('task.id', $taskId); + $this->setState('task.type', $taskType); + $this->setState('task.option', $taskOption); + + // Load component params, though com_scheduler does not (yet) have any params + $cParams = ComponentHelper::getParams($this->option); + $this->setState('params', $cParams); + } + + /** + * Don't need to define this method since the parent getTable() + * implicitly deduces $name and $prefix anyways. This makes the object + * more transparent though. + * + * @param string $name Name of the table + * @param string $prefix Class prefix + * @param array $options Model config array + * + * @return Table + * + * @since 4.1.0 + * @throws \Exception + */ + public function getTable($name = 'Task', $prefix = 'Table', $options = array()): Table + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Fetches the data to be injected into the form + * + * @return object Associative array of form data. + * + * @since 4.1.0 + * @throws \Exception + */ + protected function loadFormData() + { + $data = $this->app->getUserState('com_scheduler.edit.task.data', array()); + + // If the data from UserState is empty, we fetch it with getItem() + if (empty($data)) { + /** @var CMSObject $data */ + $data = $this->getItem(); + + // @todo : further data processing goes here + + // For a fresh object, set exec-day and exec-time + if (!($data->id ?? 0)) { + $data->execution_rules['exec-day'] = gmdate('d'); + $data->execution_rules['exec-time'] = gmdate('H:i'); + } + } + + // Let plugins manipulate the data + $this->preprocessData('com_scheduler.task', $data, 'task'); + + return $data; + } + + /** + * Overloads the parent getItem() method. + * + * @param integer $pk Primary key + * + * @return object|boolean Object on success, false on failure + * + * @since 4.1.0 + * @throws \Exception + */ + public function getItem($pk = null) + { + $item = parent::getItem($pk); + + if (!\is_object($item)) { + return false; + } + + // Parent call leaves `execution_rules` and `cron_rules` JSON encoded + $item->set('execution_rules', json_decode($item->get('execution_rules', ''))); + $item->set('cron_rules', json_decode($item->get('cron_rules', ''))); + + $taskOption = SchedulerHelper::getTaskOptions()->findOption( + ($item->id ?? 0) ? ($item->type ?? 0) : $this->getState('task.type') + ); + + $item->set('taskOption', $taskOption); + + return $item; + } + + /** + * Get a task from the database, only if an exclusive "lock" on the task can be acquired. + * The method supports options to customise the limitations on the fetch. + * + * @param array $options Array with options to fetch the task: + * 1. `id`: Optional id of the task to fetch. + * 2. `allowDisabled`: If true, disabled tasks can also be fetched. + * (default: false) + * 3. `bypassScheduling`: If true, tasks that are not due can also be + * fetched. Should only be true if an `id` is targeted instead of the + * task queue. (default: false) + * 4. `allowConcurrent`: If true, fetches even when another task is + * running ('locked'). (default: false) + * 5. `includeCliExclusive`: If true, can also fetch CLI exclusive tasks. (default: true) + * + * @return ?\stdClass Task entry as in the database. + * + * @since 4.1.0 + * @throws UndefinedOptionsException|InvalidOptionsException + * @throws \RuntimeException + */ + public function getTask(array $options = []): ?\stdClass + { + $resolver = new OptionsResolver(); + + try { + $this->configureTaskGetterOptions($resolver); + } catch (\Exception $e) { + } + + try { + $options = $resolver->resolve($options); + } catch (\Exception $e) { + if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { + throw $e; + } + } + + $db = $this->getDatabase(); + $now = Factory::getDate()->toSql(); + + // Get lock on the table to help with concurrency issues + $db->lockTable(self::TASK_TABLE); + + // If concurrency is not allowed, we only get a task if another one does not have a "lock" + if (!$options['allowConcurrent']) { + // Get count of locked (presumed running) tasks + $lockCountQuery = $db->getQuery(true) + ->from($db->quoteName(self::TASK_TABLE)) + ->select('COUNT(id)') + ->where($db->quoteName('locked') . ' IS NOT NULL'); + + try { + $runningCount = $db->setQuery($lockCountQuery)->loadResult(); + } catch (\RuntimeException $e) { + $db->unlockTables(); + + return null; + } + + if ($runningCount !== 0) { + $db->unlockTables(); + + return null; + } + } + + $lockQuery = $db->getQuery(true); + + $lockQuery->update($db->quoteName(self::TASK_TABLE)) + ->set($db->quoteName('locked') . ' = :now1') + ->bind(':now1', $now); + + // Array of all active routine ids + $activeRoutines = array_map( + static function (TaskOption $taskOption): string { + return $taskOption->id; + }, + SchedulerHelper::getTaskOptions()->options + ); + + // "Orphaned" tasks are not a part of the task queue! + $lockQuery->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); + + // If directed, exclude CLI exclusive tasks + if (!$options['includeCliExclusive']) { + $lockQuery->where($db->quoteName('cli_exclusive') . ' = 0'); + } + + if (!$options['bypassScheduling']) { + $lockQuery->where($db->quoteName('next_execution') . ' <= :now2') + ->bind(':now2', $now); + } + + if ($options['allowDisabled']) { + $lockQuery->whereIn($db->quoteName('state'), [0, 1]); + } else { + $lockQuery->where($db->quoteName('state') . ' = 1'); + } + + if ($options['id'] > 0) { + $lockQuery->where($db->quoteName('id') . ' = :taskId') + ->bind(':taskId', $options['id'], ParameterType::INTEGER); + } else { + // Pick from the front of the task queue if no 'id' is specified + // Get the id of the next task in the task queue + $idQuery = $db->getQuery(true) + ->from($db->quoteName(self::TASK_TABLE)) + ->select($db->quoteName('id')) + ->where($db->quoteName('state') . ' = 1') + ->order($db->quoteName('priority') . ' DESC') + ->order($db->quoteName('next_execution') . ' ASC') + ->setLimit(1); + + try { + $ids = $db->setQuery($idQuery)->loadColumn(); + } catch (\RuntimeException $e) { + $db->unlockTables(); + + return null; + } + + if (count($ids) === 0) { + $db->unlockTables(); + + return null; + } + + $lockQuery->whereIn($db->quoteName('id'), $ids); + } + + try { + $db->setQuery($lockQuery)->execute(); + } catch (\RuntimeException $e) { + } finally { + $affectedRows = $db->getAffectedRows(); + + $db->unlockTables(); + } + + if ($affectedRows != 1) { + /* + // @todo + // ? Fatal failure handling here? + // ! Question is, how? If we check for tasks running beyond there time here, we have no way of + // ! what's already been notified (since we're not auto-unlocking/recovering tasks anymore). + // The solution __may__ be in a "last_successful_finish" (or something) column. + */ + + return null; + } + + $getQuery = $db->getQuery(true); + + $getQuery->select('*') + ->from($db->quoteName(self::TASK_TABLE)) + ->where($db->quoteName('locked') . ' = :now') + ->bind(':now', $now); + + $task = $db->setQuery($getQuery)->loadObject(); + + $task->execution_rules = json_decode($task->execution_rules); + $task->cron_rules = json_decode($task->cron_rules); + + $task->taskOption = SchedulerHelper::getTaskOptions()->findOption($task->type); + + return $task; + } + + /** + * Set up an {@see OptionsResolver} to resolve options compatible with the {@see GetTask()} method. + * + * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. + * + * @return OptionsResolver + * + * @since 4.1.0 + * @throws AccessException + */ + public static function configureTaskGetterOptions(OptionsResolver $resolver): OptionsResolver + { + $resolver->setDefaults( + [ + 'id' => 0, + 'allowDisabled' => false, + 'bypassScheduling' => false, + 'allowConcurrent' => false, + 'includeCliExclusive' => true, + ] + ) + ->setAllowedTypes('id', 'numeric') + ->setAllowedTypes('allowDisabled', 'bool') + ->setAllowedTypes('bypassScheduling', 'bool') + ->setAllowedTypes('allowConcurrent', 'bool') + ->setAllowedTypes('includeCliExclusive', 'bool'); + + return $resolver; + } + + /** + * @param array $data The form data + * + * @return boolean True on success, false on failure + * + * @since 4.1.0 + * @throws \Exception + */ + public function save($data): bool + { + $id = (int) ($data['id'] ?? $this->getState('task.id')); + $isNew = $id === 0; + + // Clean up execution rules + $data['execution_rules'] = $this->processExecutionRules($data['execution_rules']); + + // If a new entry, we'll have to put in place a pseudo-last_execution + if ($isNew) { + $basisDayOfMonth = $data['execution_rules']['exec-day']; + [$basisHour, $basisMinute] = explode(':', $data['execution_rules']['exec-time']); + + $data['last_execution'] = Factory::getDate('now', 'GMT')->format('Y-m') + . "-$basisDayOfMonth $basisHour:$basisMinute:00"; + } else { + $data['last_execution'] = $this->getItem($id)->last_execution; + } + + // Build the `cron_rules` column from `execution_rules` + $data['cron_rules'] = $this->buildExecutionRules($data['execution_rules']); + + // `next_execution` would be null if scheduling is disabled with the "manual" rule! + $data['next_execution'] = (new ExecRuleHelper($data))->nextExec(); + + if ($isNew) { + $data['last_execution'] = null; + } + + // If no params, we set as empty array. + // ? Is this the right place to do this + $data['params'] = $data['params'] ?? []; + + // Parent method takes care of saving to the table + return parent::save($data); + } + + /** + * Clean up and standardise execution rules + * + * @param array $unprocessedRules The form data [? can just replace with execution_interval] + * + * @return array Processed rules + * + * @since 4.1.0 + */ + private function processExecutionRules(array $unprocessedRules): array + { + $executionRules = $unprocessedRules; + + $ruleType = $executionRules['rule-type']; + $retainKeys = ['rule-type', $ruleType, 'exec-day', 'exec-time']; + $executionRules = array_intersect_key($executionRules, array_flip($retainKeys)); + + // Default to current date-time in UTC/GMT as the basis + $executionRules['exec-day'] = $executionRules['exec-day'] ?: (string) gmdate('d'); + $executionRules['exec-time'] = $executionRules['exec-time'] ?: (string) gmdate('H:i'); + + // If custom ruleset, sort it + // ? Is this necessary + if ($ruleType === 'cron-expression') { + foreach ($executionRules['cron-expression'] as &$values) { + sort($values); + } + } + + return $executionRules; + } + + /** + * Private method to build execution expression from input execution rules. + * This expression is used internally to determine execution times/conditions. + * + * @param array $executionRules Execution rules from the Task form, post-processing. + * + * @return array + * + * @since 4.1.0 + * @throws \Exception + */ + private function buildExecutionRules(array $executionRules): array + { + // Maps interval strings, use with sprintf($map[intType], $interval) + $intervalStringMap = [ + 'minutes' => 'PT%dM', + 'hours' => 'PT%dH', + 'days' => 'P%dD', + 'months' => 'P%dM', + 'years' => 'P%dY', + ]; + + $ruleType = $executionRules['rule-type']; + $ruleClass = strpos($ruleType, 'interval') === 0 ? 'interval' : $ruleType; + $buildExpression = ''; + + if ($ruleClass === 'interval') { + // Rule type for intervals interval- + $intervalType = explode('-', $ruleType)[1]; + $interval = $executionRules["interval-$intervalType"]; + $buildExpression = sprintf($intervalStringMap[$intervalType], $interval); + } + + if ($ruleClass === 'cron-expression') { + // ! custom matches are disabled in the form + $matches = $executionRules['cron-expression']; + $buildExpression .= $this->wildcardIfMatch($matches['minutes'], range(0, 59), true); + $buildExpression .= ' ' . $this->wildcardIfMatch($matches['hours'], range(0, 23), true); + $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_month'], range(1, 31), true); + $buildExpression .= ' ' . $this->wildcardIfMatch($matches['months'], range(1, 12), true); + $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_week'], range(0, 6), true); + } + + return [ + 'type' => $ruleClass, + 'exp' => $buildExpression, + ]; + } + + /** + * This method releases "locks" on a set of tasks from the database. + * These locks are pseudo-locks that are used to keep a track of running tasks. However, they require require manual + * intervention to release these locks in cases such as when a task process crashes, leaving the task "locked". + * + * @param array $pks A list of the primary keys to unlock. + * + * @return boolean True on success. + * + * @since 4.1.0 + * @throws \RuntimeException|\UnexpectedValueException|\BadMethodCallException + */ + public function unlock(array &$pks): bool + { + /** @var TaskTable $table */ + $table = $this->getTable(); + + $user = Factory::getApplication()->getIdentity(); + + $context = $this->option . '.' . $this->name; + + // Include the plugins for the change of state event. + PluginHelper::importPlugin($this->events_map['unlock']); + + // Access checks. + foreach ($pks as $i => $pk) { + $table->reset(); + + if ($table->load($pk)) { + if (!$this->canEditState($table)) { + // Prune items that you can't change. + unset($pks[$i]); + Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + + return false; + } + + // Prune items that are already at the given state. + $lockedColumnName = $table->getColumnAlias('locked'); + + if (property_exists($table, $lockedColumnName) && \is_null($table->get($lockedColumnName))) { + unset($pks[$i]); + } + } + } + + // Check if there are items to change. + if (!\count($pks)) { + return true; + } + + $event = AbstractEvent::create( + $this->event_before_unlock, + [ + 'subject' => $this, + 'context' => $context, + 'pks' => $pks, + ] + ); + + try { + Factory::getApplication()->getDispatcher()->dispatch($this->event_before_unlock, $event); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Attempt to unlock the records. + if (!$table->unlock($pks, $user->id)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after unlock event + $event = AbstractEvent::create( + $this->event_unlock, + [ + 'subject' => $this, + 'context' => $context, + 'pks' => $pks, + ] + ); + + try { + Factory::getApplication()->getDispatcher()->dispatch($this->event_unlock, $event); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Determine if an array is populated by all its possible values by comparison to a reference array, if found a + * match a wildcard '*' is returned. + * + * @param array $target The target array + * @param array $reference The reference array, populated by the complete set of possible values in $target + * @param bool $targetToInt If true, converts $target array values to integers before comparing + * + * @return string A wildcard string if $target is fully populated, else $target itself. + * + * @since 4.1.0 + */ + private function wildcardIfMatch(array $target, array $reference, bool $targetToInt = false): string + { + if ($targetToInt) { + $target = array_map( + static function (string $x): int { + return (int) $x; + }, + $target + ); + } + + $isMatch = array_diff($reference, $target) === []; + + return $isMatch ? "*" : implode(',', $target); + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 4.1.0 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content'): void + { + // Load the 'task' plugin group + PluginHelper::importPlugin('task'); + + // Let the parent method take over + parent::preprocessForm($form, $data, $group); + } } diff --git a/code/administrator/components/com_scheduler/src/Model/TasksModel.php b/code/administrator/components/com_scheduler/src/Model/TasksModel.php index ed37565e..d6af6b2c 100644 --- a/code/administrator/components/com_scheduler/src/Model/TasksModel.php +++ b/code/administrator/components/com_scheduler/src/Model/TasksModel.php @@ -1,4 +1,5 @@ getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.type'); - $id .= ':' . $this->getState('filter.orphaned'); - $id .= ':' . $this->getState('filter.due'); - $id .= ':' . $this->getState('filter.locked'); - $id .= ':' . $this->getState('filter.trigger'); - $id .= ':' . $this->getState('list.select'); - - return parent::getStoreId($id); - } - - /** - * Method to create a query for a list of items. - * - * @return QueryInterface - * - * @since 4.1.0 - * @throws \Exception - */ - protected function getListQuery(): QueryInterface - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - /** - * Select the required fields from the table. - * ? Do we need all these defaults ? - * ? Does 'list.select' exist ? - */ - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.asset_id'), - $db->quoteName('a.title'), - $db->quoteName('a.type'), - $db->quoteName('a.execution_rules'), - $db->quoteName('a.state'), - $db->quoteName('a.last_exit_code'), - $db->quoteName('a.locked'), - $db->quoteName('a.last_execution'), - $db->quoteName('a.next_execution'), - $db->quoteName('a.times_executed'), - $db->quoteName('a.times_failed'), - $db->quoteName('a.priority'), - $db->quoteName('a.ordering'), - $db->quoteName('a.note'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - ] - ) - ) - ->select( - [ - $db->quoteName('uc.name', 'editor'), - ] - ) - ->from($db->quoteName('#__scheduler_tasks', 'a')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Filters go below - $filterCount = 0; - - /** - * Extends query if already filtered. - * - * @param string $outerGlue - * @param array $conditions - * @param string $innerGlue - * - * @since 4.1.0 - */ - $extendWhereIfFiltered = static function ( - string $outerGlue, - array $conditions, - string $innerGlue - ) use ($query, &$filterCount) { - if ($filterCount++) - { - $query->extendWhere($outerGlue, $conditions, $innerGlue); - } - else - { - $query->where($conditions, $innerGlue); - } - - }; - - // Filter over ID, title (redundant to search, but) --- - if (is_numeric($id = $this->getState('filter.id'))) - { - $filterCount++; - $id = (int) $id; - $query->where($db->qn('a.id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - } - elseif ($title = $this->getState('filter.title')) - { - $filterCount++; - $match = "%$title%"; - $query->where($db->qn('a.title') . ' LIKE :match') - ->bind(':match', $match); - } - - // Filter orphaned (-1: exclude, 0: include, 1: only) ---- - $filterOrphaned = (int) $this->getState('filter.orphaned'); - - if ($filterOrphaned !== 0) - { - $filterCount++; - $taskOptions = SchedulerHelper::getTaskOptions(); - - // Array of all active routine ids - $activeRoutines = array_map( - static function (TaskOption $taskOption): string - { - return $taskOption->id; - }, - $taskOptions->options - ); - - if ($filterOrphaned === -1) - { - $query->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); - } - else - { - $query->whereNotIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); - } - } - - // Filter over state ---- - $state = $this->getState('filter.state'); - - if ($state !== '*') - { - $filterCount++; - - if (is_numeric($state)) - { - $state = (int) $state; - - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - else - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - } - } - - // Filter over type ---- - $typeFilter = $this->getState('filter.type'); - - if ($typeFilter) - { - $filterCount++; - $query->where($db->quotename('a.type') . '= :type') - ->bind(':type', $typeFilter); - } - - // Filter over exit code ---- - $exitCode = $this->getState('filter.last_exit_code'); - - if (is_numeric($exitCode)) - { - $filterCount++; - $exitCode = (int) $exitCode; - $query->where($db->quoteName('a.last_exit_code') . '= :last_exit_code') - ->bind(':last_exit_code', $exitCode, ParameterType::INTEGER); - } - - // Filter due (-1: exclude, 0: include, 1: only) ---- - $due = $this->getState('filter.due'); - - if (is_numeric($due) && $due != 0) - { - $now = Factory::getDate('now', 'GMT')->toSql(); - $operator = $due == 1 ? ' <= ' : ' > '; - $filterCount++; - $query->where($db->qn('a.next_execution') . $operator . ':now') - ->bind(':now', $now); - } - - /* - * Filter locked --- - * Locks can be either hard locks or soft locks. Locks that have expired (exceeded the task timeout) are soft - * locks. Hard-locked tasks are assumed to be running. Soft-locked tasks are assumed to have suffered a fatal - * failure. - * {-2: exclude-all, -1: exclude-hard-locked, 0: include, 1: include-only-locked, 2: include-only-soft-locked} - */ - $locked = $this->getState('filter.locked'); - - if (is_numeric($locked) && $locked != 0) - { - $now = Factory::getDate('now', 'GMT'); - $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); - $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); - $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); - $now = $now->toSql(); - - switch ($locked) - { - case -2: - $query->where($db->qn('a.locked') . 'IS NULL'); - break; - case -1: - $extendWhereIfFiltered( - 'AND', - [ - $db->qn('a.locked') . ' IS NULL', - $db->qn('a.locked') . ' < :threshold', - ], - 'OR' - ); - $query->bind(':threshold', $timeoutThreshold); - break; - case 1: - $query->where($db->qn('a.locked') . ' IS NOT NULL'); - break; - case 2: - $query->where($db->qn('a.locked') . ' < :threshold') - ->bind(':threshold', $timeoutThreshold); - } - } - - // Filter over search string if set (title, type title, note, id) ---- - $searchStr = $this->getState('filter.search'); - - if (!empty($searchStr)) - { - // Allow search by ID - if (stripos($searchStr, 'id:') === 0) - { - // Add array support [?] - $id = (int) substr($searchStr, 3); - $query->where($db->quoteName('a.id') . '= :id') - ->bind(':id', $id, ParameterType::INTEGER); - } - // Search by type is handled exceptionally in _getList() [@todo: remove refs] - elseif (stripos($searchStr, 'type:') !== 0) - { - $searchStr = "%$searchStr%"; - - // Bind keys to query - $query->bind(':title', $searchStr) - ->bind(':note', $searchStr); - $conditions = [ - $db->quoteName('a.title') . ' LIKE :title', - $db->quoteName('a.note') . ' LIKE :note', - ]; - $extendWhereIfFiltered('AND', $conditions, 'OR'); - } - } - - // Add list ordering clause. ---- - // @todo implement multi-column ordering someway - $multiOrdering = $this->state->get('list.multi_ordering'); - - if (!$multiOrdering || !\is_array($multiOrdering)) - { - $orderCol = $this->state->get('list.ordering', 'a.title'); - $orderDir = $this->state->get('list.direction', 'asc'); - - // Type title ordering is handled exceptionally in _getList() - if ($orderCol !== 'j.type_title') - { - $query->order($db->quoteName($orderCol) . ' ' . $orderDir); - - // If ordering by type or state, also order by title. - if (\in_array($orderCol, ['a.type', 'a.state', 'a.priority'])) - { - // @todo : Test if things are working as expected - $query->order($db->quoteName('a.title') . ' ' . $orderDir); - } - } - } - else - { - // @todo Should add quoting here - $query->order($multiOrdering); - } - - return $query; - } - - /** - * Overloads the parent _getList() method. - * Takes care of attaching TaskOption objects and sorting by type titles. - * - * @param DatabaseQuery $query The database query to get the list with - * @param int $limitstart The list offset - * @param int $limit Number of list items to fetch - * - * @return object[] - * - * @since 4.1.0 - * @throws \Exception - */ - protected function _getList($query, $limitstart = 0, $limit = 0): array - { - // Get stuff from the model state - $listOrder = $this->getState('list.ordering', 'a.title'); - $listDirectionN = strtolower($this->getState('list.direction', 'asc')) == 'desc' ? -1 : 1; - - // Set limit parameters and get object list - $query->setLimit($limit, $limitstart); - $this->getDbo()->setQuery($query); - - // Return optionally an extended class. - // @todo: Use something other than CMSObject.. - if ($this->getState('list.customClass')) - { - $responseList = array_map( - static function (array $arr) { - $o = new CMSObject; - - foreach ($arr as $k => $v) - { - $o->{$k} = $v; - } - - return $o; - }, - $this->getDbo()->loadAssocList() ?: [] - ); - } - else - { - $responseList = $this->getDbo()->loadObjectList(); - } - - // Attach TaskOptions objects and a safe type title - $this->attachTaskOptions($responseList); - - // If ordering by non-db fields, we need to sort here in code - if ($listOrder == 'j.type_title') - { - $responseList = ArrayHelper::sortObjects($responseList, 'safeTypeTitle', $listDirectionN, true, false); - } - - return $responseList; - } - - /** - * For an array of items, attaches TaskOption objects and (safe) type titles to each. - * - * @param array $items Array of items, passed by reference - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - private function attachTaskOptions(array $items): void - { - $taskOptions = SchedulerHelper::getTaskOptions(); - - foreach ($items as $item) - { - $item->taskOption = $taskOptions->findOption($item->type); - $item->safeTypeTitle = $item->taskOption->title ?? Text::_('JGLOBAL_NONAPPLICABLE'); - } - } - - /** - * Proxy for the parent method. - * Sets ordering defaults. - * - * @param string $ordering Field to order/sort list by - * @param string $direction Direction in which to sort list - * - * @return void - * @since 4.1.0 - */ - protected function populateState($ordering = 'a.title', $direction = 'ASC'): void - { - // Call the parent method - parent::populateState($ordering, $direction); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * + * @since 4.1.0 + * @throws \Exception + * @see \JControllerLegacy + */ + public function __construct($config = [], MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'id', 'a.id', + 'asset_id', 'a.asset_id', + 'title', 'a.title', + 'type', 'a.type', + 'type_title', 'j.type_title', + 'state', 'a.state', + 'last_exit_code', 'a.last_exit_code', + 'last_execution', 'a.last_execution', + 'next_execution', 'a.next_execution', + 'times_executed', 'a.times_executed', + 'times_failed', 'a.times_failed', + 'ordering', 'a.ordering', + 'priority', 'a.priority', + 'note', 'a.note', + 'created', 'a.created', + 'created_by', 'a.created_by', + ]; + } + + parent::__construct($config, $factory); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 4.1.0 + */ + protected function getStoreId($id = ''): string + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.type'); + $id .= ':' . $this->getState('filter.orphaned'); + $id .= ':' . $this->getState('filter.due'); + $id .= ':' . $this->getState('filter.locked'); + $id .= ':' . $this->getState('filter.trigger'); + $id .= ':' . $this->getState('list.select'); + + return parent::getStoreId($id); + } + + /** + * Method to create a query for a list of items. + * + * @return QueryInterface + * + * @since 4.1.0 + * @throws \Exception + */ + protected function getListQuery(): QueryInterface + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + /** + * Select the required fields from the table. + * ? Do we need all these defaults ? + * ? Does 'list.select' exist ? + */ + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.asset_id'), + $db->quoteName('a.title'), + $db->quoteName('a.type'), + $db->quoteName('a.execution_rules'), + $db->quoteName('a.state'), + $db->quoteName('a.last_exit_code'), + $db->quoteName('a.locked'), + $db->quoteName('a.last_execution'), + $db->quoteName('a.next_execution'), + $db->quoteName('a.times_executed'), + $db->quoteName('a.times_failed'), + $db->quoteName('a.priority'), + $db->quoteName('a.ordering'), + $db->quoteName('a.note'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + ] + ) + ) + ->select( + [ + $db->quoteName('uc.name', 'editor'), + ] + ) + ->from($db->quoteName('#__scheduler_tasks', 'a')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Filters go below + $filterCount = 0; + + /** + * Extends query if already filtered. + * + * @param string $outerGlue + * @param array $conditions + * @param string $innerGlue + * + * @since 4.1.0 + */ + $extendWhereIfFiltered = static function ( + string $outerGlue, + array $conditions, + string $innerGlue + ) use ( + $query, + &$filterCount +) { + if ($filterCount++) { + $query->extendWhere($outerGlue, $conditions, $innerGlue); + } else { + $query->where($conditions, $innerGlue); + } + }; + + // Filter over ID, title (redundant to search, but) --- + if (is_numeric($id = $this->getState('filter.id'))) { + $filterCount++; + $id = (int) $id; + $query->where($db->qn('a.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + } elseif ($title = $this->getState('filter.title')) { + $filterCount++; + $match = "%$title%"; + $query->where($db->qn('a.title') . ' LIKE :match') + ->bind(':match', $match); + } + + // Filter orphaned (-1: exclude, 0: include, 1: only) ---- + $filterOrphaned = (int) $this->getState('filter.orphaned'); + + if ($filterOrphaned !== 0) { + $filterCount++; + $taskOptions = SchedulerHelper::getTaskOptions(); + + // Array of all active routine ids + $activeRoutines = array_map( + static function (TaskOption $taskOption): string { + return $taskOption->id; + }, + $taskOptions->options + ); + + if ($filterOrphaned === -1) { + $query->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); + } else { + $query->whereNotIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); + } + } + + // Filter over state ---- + $state = $this->getState('filter.state'); + + if ($state !== '*') { + $filterCount++; + + if (is_numeric($state)) { + $state = (int) $state; + + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } else { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + } + } + + // Filter over type ---- + $typeFilter = $this->getState('filter.type'); + + if ($typeFilter) { + $filterCount++; + $query->where($db->quotename('a.type') . '= :type') + ->bind(':type', $typeFilter); + } + + // Filter over exit code ---- + $exitCode = $this->getState('filter.last_exit_code'); + + if (is_numeric($exitCode)) { + $filterCount++; + $exitCode = (int) $exitCode; + $query->where($db->quoteName('a.last_exit_code') . '= :last_exit_code') + ->bind(':last_exit_code', $exitCode, ParameterType::INTEGER); + } + + // Filter due (-1: exclude, 0: include, 1: only) ---- + $due = $this->getState('filter.due'); + + if (is_numeric($due) && $due != 0) { + $now = Factory::getDate('now', 'GMT')->toSql(); + $operator = $due == 1 ? ' <= ' : ' > '; + $filterCount++; + $query->where($db->qn('a.next_execution') . $operator . ':now') + ->bind(':now', $now); + } + + /* + * Filter locked --- + * Locks can be either hard locks or soft locks. Locks that have expired (exceeded the task timeout) are soft + * locks. Hard-locked tasks are assumed to be running. Soft-locked tasks are assumed to have suffered a fatal + * failure. + * {-2: exclude-all, -1: exclude-hard-locked, 0: include, 1: include-only-locked, 2: include-only-soft-locked} + */ + $locked = $this->getState('filter.locked'); + + if (is_numeric($locked) && $locked != 0) { + $now = Factory::getDate('now', 'GMT'); + $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); + $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); + $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); + $now = $now->toSql(); + + switch ($locked) { + case -2: + $query->where($db->qn('a.locked') . 'IS NULL'); + break; + case -1: + $extendWhereIfFiltered( + 'AND', + [ + $db->qn('a.locked') . ' IS NULL', + $db->qn('a.locked') . ' < :threshold', + ], + 'OR' + ); + $query->bind(':threshold', $timeoutThreshold); + break; + case 1: + $query->where($db->qn('a.locked') . ' IS NOT NULL'); + break; + case 2: + $query->where($db->qn('a.locked') . ' < :threshold') + ->bind(':threshold', $timeoutThreshold); + } + } + + // Filter over search string if set (title, type title, note, id) ---- + $searchStr = $this->getState('filter.search'); + + if (!empty($searchStr)) { + // Allow search by ID + if (stripos($searchStr, 'id:') === 0) { + // Add array support [?] + $id = (int) substr($searchStr, 3); + $query->where($db->quoteName('a.id') . '= :id') + ->bind(':id', $id, ParameterType::INTEGER); + } elseif (stripos($searchStr, 'type:') !== 0) { + // Search by type is handled exceptionally in _getList() [@todo: remove refs] + $searchStr = "%$searchStr%"; + + // Bind keys to query + $query->bind(':title', $searchStr) + ->bind(':note', $searchStr); + $conditions = [ + $db->quoteName('a.title') . ' LIKE :title', + $db->quoteName('a.note') . ' LIKE :note', + ]; + $extendWhereIfFiltered('AND', $conditions, 'OR'); + } + } + + // Add list ordering clause. ---- + // @todo implement multi-column ordering someway + $multiOrdering = $this->state->get('list.multi_ordering'); + + if (!$multiOrdering || !\is_array($multiOrdering)) { + $orderCol = $this->state->get('list.ordering', 'a.title'); + $orderDir = $this->state->get('list.direction', 'asc'); + + // Type title ordering is handled exceptionally in _getList() + if ($orderCol !== 'j.type_title') { + $query->order($db->quoteName($orderCol) . ' ' . $orderDir); + + // If ordering by type or state, also order by title. + if (\in_array($orderCol, ['a.type', 'a.state', 'a.priority'])) { + // @todo : Test if things are working as expected + $query->order($db->quoteName('a.title') . ' ' . $orderDir); + } + } + } else { + // @todo Should add quoting here + $query->order($multiOrdering); + } + + return $query; + } + + /** + * Overloads the parent _getList() method. + * Takes care of attaching TaskOption objects and sorting by type titles. + * + * @param DatabaseQuery $query The database query to get the list with + * @param int $limitstart The list offset + * @param int $limit Number of list items to fetch + * + * @return object[] + * + * @since 4.1.0 + * @throws \Exception + */ + protected function _getList($query, $limitstart = 0, $limit = 0): array + { + // Get stuff from the model state + $listOrder = $this->getState('list.ordering', 'a.title'); + $listDirectionN = strtolower($this->getState('list.direction', 'asc')) == 'desc' ? -1 : 1; + + // Set limit parameters and get object list + $query->setLimit($limit, $limitstart); + $this->getDatabase()->setQuery($query); + + // Return optionally an extended class. + // @todo: Use something other than CMSObject.. + if ($this->getState('list.customClass')) { + $responseList = array_map( + static function (array $arr) { + $o = new CMSObject(); + + foreach ($arr as $k => $v) { + $o->{$k} = $v; + } + + return $o; + }, + $this->getDatabase()->loadAssocList() ?: [] + ); + } else { + $responseList = $this->getDatabase()->loadObjectList(); + } + + // Attach TaskOptions objects and a safe type title + $this->attachTaskOptions($responseList); + + // If ordering by non-db fields, we need to sort here in code + if ($listOrder == 'j.type_title') { + $responseList = ArrayHelper::sortObjects($responseList, 'safeTypeTitle', $listDirectionN, true, false); + } + + return $responseList; + } + + /** + * For an array of items, attaches TaskOption objects and (safe) type titles to each. + * + * @param array $items Array of items, passed by reference + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + private function attachTaskOptions(array $items): void + { + $taskOptions = SchedulerHelper::getTaskOptions(); + + foreach ($items as $item) { + $item->taskOption = $taskOptions->findOption($item->type); + $item->safeTypeTitle = $item->taskOption->title ?? Text::_('JGLOBAL_NONAPPLICABLE'); + } + } + + /** + * Proxy for the parent method. + * Sets ordering defaults. + * + * @param string $ordering Field to order/sort list by + * @param string $direction Direction in which to sort list + * + * @return void + * @since 4.1.0 + */ + protected function populateState($ordering = 'a.title', $direction = 'ASC'): void + { + // Call the parent method + parent::populateState($ordering, $direction); + } } diff --git a/code/administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php b/code/administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php index 56f14999..bc833a03 100644 --- a/code/administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php +++ b/code/administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php @@ -1,4 +1,5 @@ ` tag for the form - * field object. - * @param mixed $value The form field value to validate. - * @param ?string $group The field name group control value. This acts as an array container for the - * field. For example if the field has `name="foo"` and the group value is set - * to "bar" then the full field name would end up being "bar[foo]". - * @param ?Registry $input An optional Registry object with the entire data set to validate against - * the entire form. - * @param ?Form $form The form object for which the field is being tested. - * - * @return boolean - * - * @since 4.1.0 - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null): bool - { - $fieldName = (string) $element['name']; - $ruleType = $input->get(self::RULE_TYPE_FIELD); + /** + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form + * field object. + * @param mixed $value The form field value to validate. + * @param ?string $group The field name group control value. This acts as an array container for the + * field. For example if the field has `name="foo"` and the group value is set + * to "bar" then the full field name would end up being "bar[foo]". + * @param ?Registry $input An optional Registry object with the entire data set to validate against + * the entire form. + * @param ?Form $form The form object for which the field is being tested. + * + * @return boolean + * + * @since 4.1.0 + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null): bool + { + $fieldName = (string) $element['name']; + $ruleType = $input->get(self::RULE_TYPE_FIELD); - if ($ruleType === $fieldName || ($ruleType === 'custom' && $group === self::CUSTOM_RULE_GROUP)) - { - return $this->validateField($element, $value, $group, $form); - } + if ($ruleType === $fieldName || ($ruleType === 'custom' && $group === self::CUSTOM_RULE_GROUP)) { + return $this->validateField($element, $value, $group, $form); + } - return true; - } + return true; + } - /** - * @param \SimpleXMLElement $element The SimpleXMLElement for the field. - * @param mixed $value The field value. - * @param ?string $group The form field group the element belongs to. - * @param Form|null $form The Form object against which the field is tested/ - * - * @return boolean True if field is valid - * - * @since 4.1.0 - */ - private function validateField(\SimpleXMLElement $element, $value, ?string $group = null, ?Form $form = null): bool - { - $elementType = (string) $element['type']; + /** + * @param \SimpleXMLElement $element The SimpleXMLElement for the field. + * @param mixed $value The field value. + * @param ?string $group The form field group the element belongs to. + * @param Form|null $form The Form object against which the field is tested/ + * + * @return boolean True if field is valid + * + * @since 4.1.0 + */ + private function validateField(\SimpleXMLElement $element, $value, ?string $group = null, ?Form $form = null): bool + { + $elementType = (string) $element['type']; - // If element is of cron type, we test against options and return - if ($elementType === 'cron') - { - return (new OptionsRule)->test($element, $value, $group, null, $form); - } + // If element is of cron type, we test against options and return + if ($elementType === 'cron') { + return (new OptionsRule())->test($element, $value, $group, null, $form); + } - // Test for a positive integer value and return - return filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); - } + // Test for a positive integer value and return + return filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); + } } diff --git a/code/administrator/components/com_scheduler/src/Scheduler/Scheduler.php b/code/administrator/components/com_scheduler/src/Scheduler/Scheduler.php index ea1c68f8..163be9f5 100644 --- a/code/administrator/components/com_scheduler/src/Scheduler/Scheduler.php +++ b/code/administrator/components/com_scheduler/src/Scheduler/Scheduler.php @@ -1,4 +1,5 @@ 'COM_SCHEDULER_SCHEDULER_TASK_COMPLETE', - Status::WILL_RESUME => 'COM_SCHEDULER_SCHEDULER_TASK_WILL_RESUME', - Status::NO_LOCK => 'COM_SCHEDULER_SCHEDULER_TASK_LOCKED', - Status::NO_RUN => 'COM_SCHEDULER_SCHEDULER_TASK_UNLOCKED', - Status::NO_ROUTINE => 'COM_SCHEDULER_SCHEDULER_TASK_ROUTINE_NA', - ]; - - /** - * Filters for the task queue. Can be used with fetchTaskRecords(). - * - * @since 4.1.0 - * @todo remove? - */ - public const TASK_QUEUE_FILTERS = [ - 'due' => 1, - 'locked' => -1, - ]; - - /** - * List config for the task queue. Can be used with fetchTaskRecords(). - * - * @since 4.1.0 - * @todo remove? - */ - public const TASK_QUEUE_LIST_CONFIG = [ - 'multi_ordering' => ['a.priority DESC ', 'a.next_execution ASC'], - ]; - - /** - * Run a scheduled task. - * Runs a single due task from the task queue by default if $id and $title are not passed. - * - * @param array $options Array with options to configure the method's behavior. Supports: - * 1. `id`: (Optional) ID of the task to run. - * 2. `allowDisabled`: Allow running disabled tasks. - * 3. `allowConcurrent`: Allow concurrent execution, i.e., running the task when another - * task may be running. - * - * @return ?Task The task executed or null if not exists - * - * @since 4.1.0 - * @throws \RuntimeException - */ - public function runTask(array $options): ?Task - { - $resolver = new OptionsResolver; - - try - { - $this->configureTaskRunnerOptions($resolver); - } - catch (\Exception $e) - { - } - - try - { - $options = $resolver->resolve($options); - } - catch (\Exception $e) - { - if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) - { - throw $e; - } - } - - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - - // ? Sure about inferring scheduling bypass? - $task = $this->getTask( - [ - 'id' => (int) $options['id'], - 'allowDisabled' => $options['allowDisabled'], - 'bypassScheduling' => (int) $options['id'] !== 0, - 'allowConcurrent' => $options['allowConcurrent'], - 'includeCliExclusive' => ($app->isClient('cli')), - ] - ); - - // ? Should this be logged? (probably, if an ID is passed?) - if (empty($task)) - { - return null; - } - - $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); - - $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; - $options['text_file'] = 'joomla_scheduler.php'; - Log::addLogger($options, Log::ALL, $task->logCategory); - - $taskId = $task->get('id'); - $taskTitle = $task->get('title'); - - $task->log(Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_START', $taskId, $taskTitle), 'info'); - - // Let's try to avoid time-outs - if (\function_exists('set_time_limit')) - { - set_time_limit(0); - } - - try - { - $task->run(); - } - catch (\Exception $e) - { - // We suppress the exception here, it's still accessible with `$task->getContent()['exception']`. - } - - $executionSnapshot = $task->getContent(); - $exitCode = $executionSnapshot['status'] ?? Status::NO_EXIT; - $netDuration = $executionSnapshot['netDuration'] ?? 0; - $duration = $executionSnapshot['duration'] ?? 0; - - if (\array_key_exists($exitCode, self::LOG_TEXT)) - { - $level = in_array($exitCode, [Status::OK, Status::WILL_RESUME]) ? 'info' : 'warning'; - $task->log(Text::sprintf(self::LOG_TEXT[$exitCode], $taskId, $duration, $netDuration), $level); - - return $task; - } - - $task->log( - Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_UNKNOWN_EXIT', $taskId, $duration, $netDuration, $exitCode), - 'warning' - ); - - return $task; - } - - /** - * Set up an {@see OptionsResolver} to resolve options compatible with {@see runTask}. - * - * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. - * - * @return void - * - * @since 4.1.0 - * @throws AccessException - */ - protected function configureTaskRunnerOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults( - [ - 'id' => 0, - 'allowDisabled' => false, - 'allowConcurrent' => false, - ] - ) - ->setAllowedTypes('id', 'numeric') - ->setAllowedTypes('allowDisabled', 'bool') - ->setAllowedTypes('allowConcurrent', 'bool'); - } - - /** - * Get the next task which is due to run, limit to a specific task when ID is given - * - * @param array $options Options for the getter, see {@see TaskModel::getTask()}. - * ! should probably also support a non-locking getter. - * - * @return Task $task The task to execute - * - * @since 4.1.0 - * @throws \RuntimeException - */ - public function getTask(array $options = []): ?Task - { - $resolver = new OptionsResolver; - - try - { - TaskModel::configureTaskGetterOptions($resolver); - } - catch (\Exception $e) - { - } - - try - { - $options = $resolver->resolve($options); - } - catch (\Exception $e) - { - if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) - { - throw $e; - } - } - - try - { - /** @var SchedulerComponent $component */ - $component = Factory::getApplication()->bootComponent('com_scheduler'); - - /** @var TaskModel $model */ - $model = $component->getMVCFactory()->createModel('Task', 'Administrator', ['ignore_request' => true]); - } - catch (\Exception $e) - { - } - - if (!isset($model)) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $task = $model->getTask($options); - - if (empty($task)) - { - return null; - } - - return new Task($task); - } - - /** - * Fetches a single scheduled task in a Task instance. - * If no id or title is specified, a due task is returned. - * - * @param int $id The task ID. - * @param bool $allowDisabled Allow disabled/trashed tasks? - * - * @return ?object A matching task record, if it exists - * - * @since 4.1.0 - * @throws \RuntimeException - */ - public function fetchTaskRecord(int $id = 0, bool $allowDisabled = false): ?object - { - $filters = []; - $listConfig = ['limit' => 1]; - - if ($id > 0) - { - $filters['id'] = $id; - } - else - { - // Filters and list config for scheduled task queue - $filters['due'] = 1; - $filters['locked'] = -1; - $listConfig['multi_ordering'] = [ - 'a.priority DESC', - 'a.next_execution ASC', - ]; - } - - if ($allowDisabled) - { - $filters['state'] = ''; - } - - return $this->fetchTaskRecords($filters, $listConfig)[0] ?? null; - } - - /** - * @param array $filters The filters to set to the model - * @param array $listConfig The list config (ordering, etc.) to set to the model - * - * @return array - * - * @since 4.1.0 - * @throws \RunTimeException - */ - public function fetchTaskRecords(array $filters, array $listConfig): array - { - $model = null; - - try - { - /** @var SchedulerComponent $component */ - $component = Factory::getApplication()->bootComponent('com_scheduler'); - - /** @var TasksModel $model */ - $model = $component->getMVCFactory() - ->createModel('Tasks', 'Administrator', ['ignore_request' => true]); - } - catch (\Exception $e) - { - } - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('list.select', 'a.*'); - - // Default to only enabled tasks - if (!isset($filters['state'])) - { - $model->setState('filter.state', 1); - } - - // Default to including orphaned tasks - $model->setState('filter.orphaned', 0); - - // Default to ordering by ID - $model->setState('list.ordering', 'a.id'); - $model->setState('list.direction', 'ASC'); - - // List options - foreach ($listConfig as $key => $value) - { - $model->setState('list.' . $key, $value); - } - - // Filter options - foreach ($filters as $type => $filter) - { - $model->setState('filter.' . $type, $filter); - } - - return $model->getItems() ?: []; - } + private const LOG_TEXT = [ + Status::OK => 'COM_SCHEDULER_SCHEDULER_TASK_COMPLETE', + Status::WILL_RESUME => 'COM_SCHEDULER_SCHEDULER_TASK_WILL_RESUME', + Status::NO_LOCK => 'COM_SCHEDULER_SCHEDULER_TASK_LOCKED', + Status::NO_RUN => 'COM_SCHEDULER_SCHEDULER_TASK_UNLOCKED', + Status::NO_ROUTINE => 'COM_SCHEDULER_SCHEDULER_TASK_ROUTINE_NA', + ]; + + /** + * Filters for the task queue. Can be used with fetchTaskRecords(). + * + * @since 4.1.0 + * @todo remove? + */ + public const TASK_QUEUE_FILTERS = [ + 'due' => 1, + 'locked' => -1, + ]; + + /** + * List config for the task queue. Can be used with fetchTaskRecords(). + * + * @since 4.1.0 + * @todo remove? + */ + public const TASK_QUEUE_LIST_CONFIG = [ + 'multi_ordering' => ['a.priority DESC ', 'a.next_execution ASC'], + ]; + + /** + * Run a scheduled task. + * Runs a single due task from the task queue by default if $id and $title are not passed. + * + * @param array $options Array with options to configure the method's behavior. Supports: + * 1. `id`: (Optional) ID of the task to run. + * 2. `allowDisabled`: Allow running disabled tasks. + * 3. `allowConcurrent`: Allow concurrent execution, i.e., running the task when another + * task may be running. + * + * @return ?Task The task executed or null if not exists + * + * @since 4.1.0 + * @throws \RuntimeException + */ + public function runTask(array $options): ?Task + { + $resolver = new OptionsResolver(); + + try { + $this->configureTaskRunnerOptions($resolver); + } catch (\Exception $e) { + } + + try { + $options = $resolver->resolve($options); + } catch (\Exception $e) { + if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { + throw $e; + } + } + + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + + // ? Sure about inferring scheduling bypass? + $task = $this->getTask( + [ + 'id' => (int) $options['id'], + 'allowDisabled' => $options['allowDisabled'], + 'bypassScheduling' => (int) $options['id'] !== 0, + 'allowConcurrent' => $options['allowConcurrent'], + 'includeCliExclusive' => ($app->isClient('cli')), + ] + ); + + // ? Should this be logged? (probably, if an ID is passed?) + if (empty($task)) { + return null; + } + + $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); + + $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; + $options['text_file'] = 'joomla_scheduler.php'; + Log::addLogger($options, Log::ALL, $task->logCategory); + + $taskId = $task->get('id'); + $taskTitle = $task->get('title'); + + $task->log(Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_START', $taskId, $taskTitle), 'info'); + + // Let's try to avoid time-outs + if (\function_exists('set_time_limit')) { + set_time_limit(0); + } + + try { + $task->run(); + } catch (\Exception $e) { + // We suppress the exception here, it's still accessible with `$task->getContent()['exception']`. + } + + $executionSnapshot = $task->getContent(); + $exitCode = $executionSnapshot['status'] ?? Status::NO_EXIT; + $netDuration = $executionSnapshot['netDuration'] ?? 0; + $duration = $executionSnapshot['duration'] ?? 0; + + if (\array_key_exists($exitCode, self::LOG_TEXT)) { + $level = in_array($exitCode, [Status::OK, Status::WILL_RESUME]) ? 'info' : 'warning'; + $task->log(Text::sprintf(self::LOG_TEXT[$exitCode], $taskId, $duration, $netDuration), $level); + + return $task; + } + + $task->log( + Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_UNKNOWN_EXIT', $taskId, $duration, $netDuration, $exitCode), + 'warning' + ); + + return $task; + } + + /** + * Set up an {@see OptionsResolver} to resolve options compatible with {@see runTask}. + * + * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. + * + * @return void + * + * @since 4.1.0 + * @throws AccessException + */ + protected function configureTaskRunnerOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults( + [ + 'id' => 0, + 'allowDisabled' => false, + 'allowConcurrent' => false, + ] + ) + ->setAllowedTypes('id', 'numeric') + ->setAllowedTypes('allowDisabled', 'bool') + ->setAllowedTypes('allowConcurrent', 'bool'); + } + + /** + * Get the next task which is due to run, limit to a specific task when ID is given + * + * @param array $options Options for the getter, see {@see TaskModel::getTask()}. + * ! should probably also support a non-locking getter. + * + * @return Task $task The task to execute + * + * @since 4.1.0 + * @throws \RuntimeException + */ + public function getTask(array $options = []): ?Task + { + $resolver = new OptionsResolver(); + + try { + TaskModel::configureTaskGetterOptions($resolver); + } catch (\Exception $e) { + } + + try { + $options = $resolver->resolve($options); + } catch (\Exception $e) { + if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { + throw $e; + } + } + + try { + /** @var SchedulerComponent $component */ + $component = Factory::getApplication()->bootComponent('com_scheduler'); + + /** @var TaskModel $model */ + $model = $component->getMVCFactory()->createModel('Task', 'Administrator', ['ignore_request' => true]); + } catch (\Exception $e) { + } + + if (!isset($model)) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $task = $model->getTask($options); + + if (empty($task)) { + return null; + } + + return new Task($task); + } + + /** + * Fetches a single scheduled task in a Task instance. + * If no id or title is specified, a due task is returned. + * + * @param int $id The task ID. + * @param bool $allowDisabled Allow disabled/trashed tasks? + * + * @return ?object A matching task record, if it exists + * + * @since 4.1.0 + * @throws \RuntimeException + */ + public function fetchTaskRecord(int $id = 0, bool $allowDisabled = false): ?object + { + $filters = []; + $listConfig = ['limit' => 1]; + + if ($id > 0) { + $filters['id'] = $id; + } else { + // Filters and list config for scheduled task queue + $filters['due'] = 1; + $filters['locked'] = -1; + $listConfig['multi_ordering'] = [ + 'a.priority DESC', + 'a.next_execution ASC', + ]; + } + + if ($allowDisabled) { + $filters['state'] = ''; + } + + return $this->fetchTaskRecords($filters, $listConfig)[0] ?? null; + } + + /** + * @param array $filters The filters to set to the model + * @param array $listConfig The list config (ordering, etc.) to set to the model + * + * @return array + * + * @since 4.1.0 + * @throws \RunTimeException + */ + public function fetchTaskRecords(array $filters, array $listConfig): array + { + $model = null; + + try { + /** @var SchedulerComponent $component */ + $component = Factory::getApplication()->bootComponent('com_scheduler'); + + /** @var TasksModel $model */ + $model = $component->getMVCFactory() + ->createModel('Tasks', 'Administrator', ['ignore_request' => true]); + } catch (\Exception $e) { + } + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('list.select', 'a.*'); + + // Default to only enabled tasks + if (!isset($filters['state'])) { + $model->setState('filter.state', 1); + } + + // Default to including orphaned tasks + $model->setState('filter.orphaned', 0); + + // Default to ordering by ID + $model->setState('list.ordering', 'a.id'); + $model->setState('list.direction', 'ASC'); + + // List options + foreach ($listConfig as $key => $value) { + $model->setState('list.' . $key, $value); + } + + // Filter options + foreach ($filters as $type => $filter) { + $model->setState('filter.' . $type, $filter); + } + + return $model->getItems() ?: []; + } } diff --git a/code/administrator/components/com_scheduler/src/Table/TaskTable.php b/code/administrator/components/com_scheduler/src/Table/TaskTable.php index d06b91ec..b5c3c862 100644 --- a/code/administrator/components/com_scheduler/src/Table/TaskTable.php +++ b/code/administrator/components/com_scheduler/src/Table/TaskTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - - parent::__construct('#__scheduler_tasks', 'id', $db); - } - - /** - * Overloads {@see Table::check()} to perform sanity checks on properties and make sure they're - * safe to store. - * - * @return boolean True if checks pass. - * - * @since 4.1.0 - * @throws \Exception - */ - public function check(): bool - { - try - { - parent::check(); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage()); - - return false; - } - - $this->title = htmlspecialchars_decode($this->title, ENT_QUOTES); - - // Set created date if not set. - // ? Might not need since the constructor already sets this - if (!(int) $this->created) - { - $this->created = Factory::getDate()->toSql(); - } - - // @todo : Add more checks if needed - - return true; - } - - /** - * Override {@see Table::store()} to update null fields as a default, which is needed when DATETIME - * fields need to be updated to NULL. This override is needed because {@see AdminModel::save()} does not - * expose an option to pass true to Table::store(). Also ensures the `created` and `created_by` fields are - * set. - * - * @param boolean $updateNulls True to update fields even if they're null. - * - * @return boolean True if successful. - * - * @since 4.1.0 - * @throws \Exception - */ - public function store($updateNulls = true): bool - { - $isNew = empty($this->getId()); - - // Set creation date if not set for a new item. - if ($isNew && empty($this->created)) - { - $this->created = Factory::getDate()->toSql(); - } - - // Set `created_by` if not set for a new item. - if ($isNew && empty($this->created_by)) - { - $this->created_by = Factory::getApplication()->getIdentity()->id; - } - - // @todo : Should we add modified, modified_by fields? [ ] - - return parent::store($updateNulls); - } - - /** - * Returns the asset name of the entry as it appears in the {@see Asset} table. - * - * @return string The asset name. - * - * @since 4.1.0 - */ - protected function _getAssetName(): string - { - $k = $this->_tbl_key; - - return 'com_scheduler.task.' . (int) $this->$k; - } - - /** - * Override {@see Table::bind()} to bind some fields even if they're null given they're present in $src. - * This override is needed specifically for DATETIME fields, of which the `next_execution` field is updated to - * null if a task is configured to execute only on manual trigger. - * - * @param array|object $src An associative array or object to bind to the Table instance. - * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean - * - * @since 4.1.0 - */ - public function bind($src, $ignore = array()): bool - { - $fields = ['next_execution']; - - foreach ($fields as $field) - { - if (\array_key_exists($field, $src) && \is_null($src[$field])) - { - $this->$field = $src[$field]; - } - } - - return parent::bind($src, $ignore); - } - - /** - * Release pseudo-locks on a set of task records. If an empty set is passed, this method releases lock on its - * instance primary key, if available. - * - * @param integer[] $pks An optional array of primary key values to update. If not set the instance property - * value is used. - * @param ?int $userId ID of the user unlocking the tasks. - * - * @return boolean True on success; false if $pks is empty. - * - * @since 4.1.0 - * @throws QueryTypeAlreadyDefinedException|\UnexpectedValueException|\BadMethodCallException - */ - public function unlock(array $pks = [], ?int $userId = null): bool - { - // Pre-processing by observers - $event = AbstractEvent::create( - 'onTaskBeforeUnlock', - [ - 'subject' => $this, - 'pks' => $pks, - 'userId' => $userId, - ] - ); - - $this->getDispatcher()->dispatch('onTaskBeforeUnlock', $event); - - // Some pre-processing before we can work with the keys. - if (!empty($pks)) - { - foreach ($pks as $key => $pk) - { - if (!\is_array($pk)) - { - $pks[$key] = array($this->_tbl_key => $pk); - } - } - } - - // If there are no primary keys set check to see if the instance key is set and use that. - if (empty($pks)) - { - $pk = []; - - foreach ($this->_tbl_keys as $key) - { - if ($this->$key) - { - $pk[$key] = $this->$key; - } - // We don't have a full primary key - return false. - else - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); - - return false; - } - } - - $pks = [$pk]; - } - - $lockedField = $this->getColumnAlias('locked'); - - foreach ($pks as $pk) - { - // Update the publishing state for rows with the given primary keys. - $query = $this->_db->getQuery(true) - ->update($this->_tbl) - ->set($this->_db->quoteName($lockedField) . ' = NULL'); - - // Build the WHERE clause for the primary keys. - $this->appendPrimaryKeys($query, $pk); - - $this->_db->setQuery($query); - - try - { - $this->_db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // If the Table instance value is in the list of primary keys that were set, set the instance. - $ours = true; - - foreach ($this->_tbl_keys as $key) - { - if ($this->$key != $pk[$key]) - { - $ours = false; - } - } - - if ($ours) - { - $this->$lockedField = null; - } - } - - // Pre-processing by observers - $event = AbstractEvent::create( - 'onTaskAfterUnlock', - [ - 'subject' => $this, - 'pks' => $pks, - 'userId' => $userId, - ] - ); - - $this->getDispatcher()->dispatch('onTaskAfterUnlock', $event); - - return true; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.1.1 + */ + protected $_supportNullValue = true; + + /** + * Ensure params are json encoded by the bind method. + * + * @var string[] + * @since 4.1.0 + */ + protected $_jsonEncode = ['params', 'execution_rules', 'cron_rules']; + + /** + * The 'created' column. + * + * @var string + * @since 4.1.0 + */ + public $created; + + /** + * The 'title' column. + * + * @var string + * @since 4.1.0 + */ + public $title; + + /** + * @var string + * @since 4.1.0 + */ + public $typeAlias = 'com_scheduler.task'; + + /** + * TaskTable constructor override, needed to pass the DB table name and primary key to {@see Table::__construct()}. + * + * @param DatabaseDriver $db A database connector object. + * + * @since 4.1.0 + */ + public function __construct(DatabaseDriver $db) + { + $this->setColumnAlias('published', 'state'); + + parent::__construct('#__scheduler_tasks', 'id', $db); + } + + /** + * Overloads {@see Table::check()} to perform sanity checks on properties and make sure they're + * safe to store. + * + * @return boolean True if checks pass. + * + * @since 4.1.0 + * @throws \Exception + */ + public function check(): bool + { + try { + parent::check(); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage()); + + return false; + } + + $this->title = htmlspecialchars_decode($this->title, ENT_QUOTES); + + // Set created date if not set. + // ? Might not need since the constructor already sets this + if (!(int) $this->created) { + $this->created = Factory::getDate()->toSql(); + } + + // @todo : Add more checks if needed + + return true; + } + + /** + * Override {@see Table::store()} to update null fields as a default, which is needed when DATETIME + * fields need to be updated to NULL. This override is needed because {@see AdminModel::save()} does not + * expose an option to pass true to Table::store(). Also ensures the `created` and `created_by` fields are + * set. + * + * @param boolean $updateNulls True to update fields even if they're null. + * + * @return boolean True if successful. + * + * @since 4.1.0 + * @throws \Exception + */ + public function store($updateNulls = true): bool + { + $isNew = empty($this->getId()); + + // Set creation date if not set for a new item. + if ($isNew && empty($this->created)) { + $this->created = Factory::getDate()->toSql(); + } + + // Set `created_by` if not set for a new item. + if ($isNew && empty($this->created_by)) { + $this->created_by = Factory::getApplication()->getIdentity()->id; + } + + // @todo : Should we add modified, modified_by fields? [ ] + + return parent::store($updateNulls); + } + + /** + * Returns the asset name of the entry as it appears in the {@see Asset} table. + * + * @return string The asset name. + * + * @since 4.1.0 + */ + protected function _getAssetName(): string + { + $k = $this->_tbl_key; + + return 'com_scheduler.task.' . (int) $this->$k; + } + + /** + * Override {@see Table::bind()} to bind some fields even if they're null given they're present in $src. + * This override is needed specifically for DATETIME fields, of which the `next_execution` field is updated to + * null if a task is configured to execute only on manual trigger. + * + * @param array|object $src An associative array or object to bind to the Table instance. + * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean + * + * @since 4.1.0 + */ + public function bind($src, $ignore = array()): bool + { + $fields = ['next_execution']; + + foreach ($fields as $field) { + if (\array_key_exists($field, $src) && \is_null($src[$field])) { + $this->$field = $src[$field]; + } + } + + return parent::bind($src, $ignore); + } + + /** + * Release pseudo-locks on a set of task records. If an empty set is passed, this method releases lock on its + * instance primary key, if available. + * + * @param integer[] $pks An optional array of primary key values to update. If not set the instance property + * value is used. + * @param ?int $userId ID of the user unlocking the tasks. + * + * @return boolean True on success; false if $pks is empty. + * + * @since 4.1.0 + * @throws QueryTypeAlreadyDefinedException|\UnexpectedValueException|\BadMethodCallException + */ + public function unlock(array $pks = [], ?int $userId = null): bool + { + // Pre-processing by observers + $event = AbstractEvent::create( + 'onTaskBeforeUnlock', + [ + 'subject' => $this, + 'pks' => $pks, + 'userId' => $userId, + ] + ); + + $this->getDispatcher()->dispatch('onTaskBeforeUnlock', $event); + + // Some pre-processing before we can work with the keys. + if (!empty($pks)) { + foreach ($pks as $key => $pk) { + if (!\is_array($pk)) { + $pks[$key] = array($this->_tbl_key => $pk); + } + } + } + + // If there are no primary keys set check to see if the instance key is set and use that. + if (empty($pks)) { + $pk = []; + + foreach ($this->_tbl_keys as $key) { + if ($this->$key) { + $pk[$key] = $this->$key; + } else { + // We don't have a full primary key - return false. + $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); + + return false; + } + } + + $pks = [$pk]; + } + + $lockedField = $this->getColumnAlias('locked'); + + foreach ($pks as $pk) { + // Update the publishing state for rows with the given primary keys. + $query = $this->_db->getQuery(true) + ->update($this->_tbl) + ->set($this->_db->quoteName($lockedField) . ' = NULL'); + + // Build the WHERE clause for the primary keys. + $this->appendPrimaryKeys($query, $pk); + + $this->_db->setQuery($query); + + try { + $this->_db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // If the Table instance value is in the list of primary keys that were set, set the instance. + $ours = true; + + foreach ($this->_tbl_keys as $key) { + if ($this->$key != $pk[$key]) { + $ours = false; + } + } + + if ($ours) { + $this->$lockedField = null; + } + } + + // Pre-processing by observers + $event = AbstractEvent::create( + 'onTaskAfterUnlock', + [ + 'subject' => $this, + 'pks' => $pks, + 'userId' => $userId, + ] + ); + + $this->getDispatcher()->dispatch('onTaskAfterUnlock', $event); + + return true; + } } diff --git a/code/administrator/components/com_scheduler/src/Task/Status.php b/code/administrator/components/com_scheduler/src/Task/Status.php index 50f46735..a7deeefd 100644 --- a/code/administrator/components/com_scheduler/src/Task/Status.php +++ b/code/administrator/components/com_scheduler/src/Task/Status.php @@ -1,4 +1,5 @@ 'trashed', - self::STATE_DISABLED => 'disabled', - self::STATE_ENABLED => 'enabled', - ]; - - /** - * The task snapshot - * - * @var array - * @since 4.1.0 - */ - protected $snapshot = []; - - /** - * @var Registry - * @since 4.1.0 - */ - protected $taskRegistry; - - /** - * @var string - * @since 4.1.0 - */ - public $logCategory; - - /** - * @var CMSApplication - * @since 4.1.0 - */ - protected $app; - - /** - * @var DatabaseInterface - * @since 4.1.0 - */ - protected $db; - - /** - * Maps task exit codes to events which should be dispatched when the task finishes. - * 'NA' maps to the event for general task failures. - * - * @var string[] - * @since 4.1.0 - */ - protected const EVENTS_MAP = [ - Status::OK => 'onTaskExecuteSuccess', - Status::NO_ROUTINE => 'onTaskRoutineNotFound', - Status::WILL_RESUME => 'onTaskRoutineWillResume', - 'NA' => 'onTaskExecuteFailure', - ]; - - /** - * Constructor for {@see Task}. - * - * @param object $record A task from {@see TaskTable}. - * - * @since 4.1.0 - * @throws \Exception - */ - public function __construct(object $record) - { - // Workaround because Registry dumps private properties otherwise. - $taskOption = $record->taskOption; - $record->params = json_decode($record->params, true); - - $this->taskRegistry = new Registry($record); - - $this->set('taskOption', $taskOption); - $this->app = Factory::getApplication(); - $this->db = Factory::getContainer()->get(DatabaseDriver::class); - $this->setLogger(Log::createDelegatedLogger()); - $this->logCategory = 'task' . $this->get('id'); - - if ($this->get('params.individual_log')) - { - $logFile = $this->get('params.log_file') ?? 'task_' . $this->get('id') . '.log.php'; - - $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; - $options['text_file'] = $logFile; - Log::addLogger($options, Log::ALL, [$this->logCategory]); - } - } - - /** - * Get the task as a data object that can be stored back in the database. - * ! This method should be removed or changed as part of a better API implementation for the driver. - * - * @return object - * - * @since 4.1.0 - */ - public function getRecord(): object - { - // ! Probably, an array instead - $recObject = $this->taskRegistry->toObject(); - - $recObject->cron_rules = (array) $recObject->cron_rules; - - return $recObject; - } - - /** - * Execute the task. - * - * @return boolean True if success - * - * @since 4.1.0 - * @throws \Exception - */ - public function run(): bool - { - /** - * We try to acquire the lock here, only if we don't already have one. - * We do this, so we can support two ways of running tasks: - * 1. Directly through {@see Scheduler}, which optimises acquiring a lock while fetching from the task queue. - * 2. Running a task without a pre-acquired lock. - * ! This needs some more thought, for whether it should be allowed or if the single-query optimisation - * should be used everywhere, although it doesn't make sense in the context of fetching - * a task when it doesn't need to be run. This might be solved if we force a re-fetch - * with the lock or do it here ourselves (using acquireLock as a proxy to the model's - * getter). - */ - if ($this->get('locked') === null) - { - $this->acquireLock(); - } - - // Exit early if task routine is not available - if (!SchedulerHelper::getTaskOptions()->findOption($this->get('type'))) - { - $this->snapshot['status'] = Status::NO_ROUTINE; - $this->skipExecution(); - $this->dispatchExitEvent(); - - return $this->isSuccess(); - } - - $this->snapshot['status'] = Status::RUNNING; - $this->snapshot['taskStart'] = $this->snapshot['taskStart'] ?? microtime(true); - $this->snapshot['netDuration'] = 0; - - /** @var ExecuteTaskEvent $event */ - $event = AbstractEvent::create( - 'onExecuteTask', - [ - 'eventClass' => ExecuteTaskEvent::class, - 'subject' => $this, - 'routineId' => $this->get('type'), - 'langConstPrefix' => $this->get('taskOption')->langConstPrefix, - 'params' => $this->get('params'), - ] - ); - - PluginHelper::importPlugin('task'); - - try - { - $this->app->getDispatcher()->dispatch('onExecuteTask', $event); - } - catch (\Exception $e) - { - // Suppress the exception for now, we'll throw it again once it's safe - $this->log(Text::sprintf('COM_SCHEDULER_TASK_ROUTINE_EXCEPTION', $e->getMessage()), 'error'); - $this->snapshot['exception'] = $e; - $this->snapshot['status'] = Status::KNOCKOUT; - } - - $resultSnapshot = $event->getResultSnapshot(); - - $this->snapshot['taskEnd'] = microtime(true); - $this->snapshot['netDuration'] = $this->snapshot['taskEnd'] - $this->snapshot['taskStart']; - $this->snapshot = array_merge($this->snapshot, $resultSnapshot); - - // @todo make the ExecRuleHelper usage less ugly, perhaps it should be composed into Task - // Update object state. - $this->set('last_execution', Factory::getDate('@' . (int) $this->snapshot['taskStart'])->toSql()); - $this->set('last_exit_code', $this->snapshot['status']); - - if ($this->snapshot['status'] !== Status::WILL_RESUME) - { - $this->set('next_execution', (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec()); - $this->set('times_executed', $this->get('times_executed') + 1); - } - else - { - /** - * Resumable tasks need special handling. - * - * They are rescheduled as soon as possible to let their next step to be executed without - * a very large temporal gap to the previous step. - * - * Moreover, the times executed does NOT increase for each step. It will increase once, - * after the last step, when they return Status::OK. - */ - $this->set('next_execution', Factory::getDate('now', 'UTC')->sub(new \DateInterval('PT1M'))->toSql()); - } - - // The only acceptable "successful" statuses are either clean exit or resuming execution. - if (!in_array($this->snapshot['status'], [Status::WILL_RESUME, Status::OK])) - { - $this->set('times_failed', $this->get('times_failed') + 1); - } - - if (!$this->releaseLock()) - { - $this->snapshot['status'] = Status::NO_RELEASE; - } - - $this->dispatchExitEvent(); - - if (!empty($this->snapshot['exception'])) - { - throw $this->snapshot['exception']; - } - - return $this->isSuccess(); - } - - /** - * Get the task execution snapshot. - * ! Access locations will need updates once a more robust Snapshot container is implemented. - * - * @return array - * - * @since 4.1.0 - */ - public function getContent(): array - { - return $this->snapshot; - } - - /** - * Acquire a pseudo-lock on the task record. - * ! At the moment, this method is not used anywhere as task locks are already - * acquired when they're fetched. As such this method is not functional and should - * not be reviewed until it is updated. - * - * @return boolean - * - * @since 4.1.0 - * @throws \Exception - */ - public function acquireLock(): bool - { - $db = $this->db; - $query = $db->getQuery(true); - $id = $this->get('id'); - $now = Factory::getDate('now', 'GMT'); - - $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); - $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); - $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); - $now = $now->toSql(); - - // @todo update or remove this method - $query->update($db->qn('#__scheduler_tasks')) - ->set('locked = :now') - ->where($db->qn('id') . ' = :taskId') - ->extendWhere( - 'AND', - [ - $db->qn('locked') . ' < :threshold', - $db->qn('locked') . 'IS NULL', - ], - 'OR' - ) - ->bind(':taskId', $id, ParameterType::INTEGER) - ->bind(':now', $now) - ->bind(':threshold', $timeoutThreshold); - - try - { - $db->lockTable('#__scheduler_tasks'); - $db->setQuery($query)->execute(); - } - catch (\RuntimeException $e) - { - return false; - } - finally - { - $db->unlockTables(); - } - - if ($db->getAffectedRows() === 0) - { - return false; - } - - $this->set('locked', $now); - - return true; - } - - /** - * Remove the pseudo-lock and optionally update the task record. - * - * @param bool $update If true, the record is updated with the snapshot - * - * @return boolean - * - * @since 4.1.0 - * @throws \Exception - */ - public function releaseLock(bool $update = true): bool - { - $db = $this->db; - $query = $db->getQuery(true); - $id = $this->get('id'); - - $query->update($db->qn('#__scheduler_tasks', 't')) - ->set('locked = NULL') - ->where($db->qn('id') . ' = :taskId') - ->where($db->qn('locked') . ' IS NOT NULL') - ->bind(':taskId', $id, ParameterType::INTEGER); - - if ($update) - { - $exitCode = $this->get('last_exit_code'); - $lastExec = $this->get('last_execution'); - $nextExec = $this->get('next_execution'); - $timesFailed = $this->get('times_failed'); - $timesExecuted = $this->get('times_executed'); - - $query->set( - [ - 'last_exit_code = :exitCode', - 'last_execution = :lastExec', - 'next_execution = :nextExec', - 'times_executed = :times_executed', - 'times_failed = :times_failed', - ] - ) - ->bind(':exitCode', $exitCode, ParameterType::INTEGER) - ->bind(':lastExec', $lastExec) - ->bind(':nextExec', $nextExec) - ->bind(':times_executed', $timesExecuted) - ->bind(':times_failed', $timesFailed); - } - - try - { - $db->setQuery($query)->execute(); - } - catch (\RuntimeException $e) - { - return false; - } - - if (!$db->getAffectedRows()) - { - return false; - } - - $this->set('locked', null); - - return true; - } - - /** - * @param string $message Log message - * @param string $priority Log level, defaults to 'info' - * - * @return void - * - * @since 4.1.0 - * @throws InvalidArgumentException - */ - public function log(string $message, string $priority = 'info'): void - { - $this->logger->log($priority, $message, ['category' => $this->logCategory]); - } - - /** - * Advance the task entry's next calculated execution, effectively skipping the current execution. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - public function skipExecution(): void - { - $db = $this->db; - $query = $db->getQuery(true); - - $id = $this->get('id'); - $nextExec = (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec(true, true); - - $query->update($db->qn('#__scheduler_tasks', 't')) - ->set('t.next_execution = :nextExec') - ->where('t.id = :id') - ->bind(':nextExec', $nextExec) - ->bind(':id', $id); - - try - { - $db->setQuery($query)->execute(); - } - catch (\RuntimeException $e) - { - } - - $this->set('next_execution', $nextExec); - } - - /** - * Handles task exit (dispatch event). - * - * @return void - * - * @since 4.1.0 - * - * @throws \UnexpectedValueException|\BadMethodCallException - */ - protected function dispatchExitEvent(): void - { - $exitCode = $this->snapshot['status'] ?? 'NA'; - $eventName = self::EVENTS_MAP[$exitCode] ?? self::EVENTS_MAP['NA']; - - $event = AbstractEvent::create( - $eventName, - [ - 'subject' => $this, - ] - ); - - $this->app->getDispatcher()->dispatch($eventName, $event); - } - - /** - * Was the task successful? - * - * @return boolean True if the task was successful. - * @since 4.1.0 - */ - public function isSuccess(): bool - { - return in_array(($this->snapshot['status'] ?? null), [Status::OK, Status::WILL_RESUME]); - } - - /** - * Set a task property. This method is a proxy to {@see Registry::set()}. - * - * @param string $path Registry path of the task property. - * @param mixed $value The value to set to the property. - * @param ?string $separator The key separator. - * - * @return mixed|null - * - * @since 4.1.0 - */ - protected function set(string $path, $value, string $separator = null) - { - return $this->taskRegistry->set($path, $value, $separator); - } - - /** - * Get a task property. This method is a proxy to {@see Registry::get()}. - * - * @param string $path Registry path of the task property. - * @param mixed $default Default property to return, if the actual value is null. - * - * @return mixed The task property. - * - * @since 4.1.0 - */ - public function get(string $path, $default = null) - { - return $this->taskRegistry->get($path, $default); - } - - /** - * Static method to determine whether an enumerated task state (as a string) is valid. - * - * @param string $state The task state (enumerated, as a string). - * - * @return boolean - * - * @since 4.1.0 - */ - public static function isValidState(string $state): bool - { - if (!is_numeric($state)) - { - return false; - } - - // Takes care of interpreting as float/int - $state = $state + 0; - - return ArrayHelper::getValue(self::STATE_MAP, $state) !== null; - } - - /** - * Static method to determine whether a task id is valid. Note that this does not - * validate ids against the database, but only verifies that an id may exist. - * - * @param string $id The task id (as a string). - * - * @return boolean - * - * @since 4.1.0 - */ - public static function isValidId(string $id): bool - { - $id = is_numeric($id) ? ($id + 0) : $id; - - if (!\is_int($id) || $id <= 0) - { - return false; - } - - return true; - } + use LoggerAwareTrait; + + /** + * Enumerated state for enabled tasks. + * + * @since 4.1.0 + */ + public const STATE_ENABLED = 1; + + /** + * Enumerated state for disabled tasks. + * + * @since 4.1.0 + */ + public const STATE_DISABLED = 0; + + /** + * Enumerated state for trashed tasks. + * + * @since 4.1.0 + */ + public const STATE_TRASHED = -2; + + /** + * Map state enumerations to logical language adjectives. + * + * @since 4.1.0 + */ + public const STATE_MAP = [ + self::STATE_TRASHED => 'trashed', + self::STATE_DISABLED => 'disabled', + self::STATE_ENABLED => 'enabled', + ]; + + /** + * The task snapshot + * + * @var array + * @since 4.1.0 + */ + protected $snapshot = []; + + /** + * @var Registry + * @since 4.1.0 + */ + protected $taskRegistry; + + /** + * @var string + * @since 4.1.0 + */ + public $logCategory; + + /** + * @var CMSApplication + * @since 4.1.0 + */ + protected $app; + + /** + * @var DatabaseInterface + * @since 4.1.0 + */ + protected $db; + + /** + * Maps task exit codes to events which should be dispatched when the task finishes. + * 'NA' maps to the event for general task failures. + * + * @var string[] + * @since 4.1.0 + */ + protected const EVENTS_MAP = [ + Status::OK => 'onTaskExecuteSuccess', + Status::NO_ROUTINE => 'onTaskRoutineNotFound', + Status::WILL_RESUME => 'onTaskRoutineWillResume', + 'NA' => 'onTaskExecuteFailure', + ]; + + /** + * Constructor for {@see Task}. + * + * @param object $record A task from {@see TaskTable}. + * + * @since 4.1.0 + * @throws \Exception + */ + public function __construct(object $record) + { + // Workaround because Registry dumps private properties otherwise. + $taskOption = $record->taskOption; + $record->params = json_decode($record->params, true); + + $this->taskRegistry = new Registry($record); + + $this->set('taskOption', $taskOption); + $this->app = Factory::getApplication(); + $this->db = Factory::getContainer()->get(DatabaseDriver::class); + $this->setLogger(Log::createDelegatedLogger()); + $this->logCategory = 'task' . $this->get('id'); + + if ($this->get('params.individual_log')) { + $logFile = $this->get('params.log_file') ?? 'task_' . $this->get('id') . '.log.php'; + + $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; + $options['text_file'] = $logFile; + Log::addLogger($options, Log::ALL, [$this->logCategory]); + } + } + + /** + * Get the task as a data object that can be stored back in the database. + * ! This method should be removed or changed as part of a better API implementation for the driver. + * + * @return object + * + * @since 4.1.0 + */ + public function getRecord(): object + { + // ! Probably, an array instead + $recObject = $this->taskRegistry->toObject(); + + $recObject->cron_rules = (array) $recObject->cron_rules; + + return $recObject; + } + + /** + * Execute the task. + * + * @return boolean True if success + * + * @since 4.1.0 + * @throws \Exception + */ + public function run(): bool + { + /** + * We try to acquire the lock here, only if we don't already have one. + * We do this, so we can support two ways of running tasks: + * 1. Directly through {@see Scheduler}, which optimises acquiring a lock while fetching from the task queue. + * 2. Running a task without a pre-acquired lock. + * ! This needs some more thought, for whether it should be allowed or if the single-query optimisation + * should be used everywhere, although it doesn't make sense in the context of fetching + * a task when it doesn't need to be run. This might be solved if we force a re-fetch + * with the lock or do it here ourselves (using acquireLock as a proxy to the model's + * getter). + */ + if ($this->get('locked') === null) { + $this->acquireLock(); + } + + // Exit early if task routine is not available + if (!SchedulerHelper::getTaskOptions()->findOption($this->get('type'))) { + $this->snapshot['status'] = Status::NO_ROUTINE; + $this->skipExecution(); + $this->dispatchExitEvent(); + + return $this->isSuccess(); + } + + $this->snapshot['status'] = Status::RUNNING; + $this->snapshot['taskStart'] = $this->snapshot['taskStart'] ?? microtime(true); + $this->snapshot['netDuration'] = 0; + + /** @var ExecuteTaskEvent $event */ + $event = AbstractEvent::create( + 'onExecuteTask', + [ + 'eventClass' => ExecuteTaskEvent::class, + 'subject' => $this, + 'routineId' => $this->get('type'), + 'langConstPrefix' => $this->get('taskOption')->langConstPrefix, + 'params' => $this->get('params'), + ] + ); + + PluginHelper::importPlugin('task'); + + try { + $this->app->getDispatcher()->dispatch('onExecuteTask', $event); + } catch (\Exception $e) { + // Suppress the exception for now, we'll throw it again once it's safe + $this->log(Text::sprintf('COM_SCHEDULER_TASK_ROUTINE_EXCEPTION', $e->getMessage()), 'error'); + $this->snapshot['exception'] = $e; + $this->snapshot['status'] = Status::KNOCKOUT; + } + + $resultSnapshot = $event->getResultSnapshot(); + + $this->snapshot['taskEnd'] = microtime(true); + $this->snapshot['netDuration'] = $this->snapshot['taskEnd'] - $this->snapshot['taskStart']; + $this->snapshot = array_merge($this->snapshot, $resultSnapshot); + + // @todo make the ExecRuleHelper usage less ugly, perhaps it should be composed into Task + // Update object state. + $this->set('last_execution', Factory::getDate('@' . (int) $this->snapshot['taskStart'])->toSql()); + $this->set('last_exit_code', $this->snapshot['status']); + + if ($this->snapshot['status'] !== Status::WILL_RESUME) { + $this->set('next_execution', (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec()); + $this->set('times_executed', $this->get('times_executed') + 1); + } else { + /** + * Resumable tasks need special handling. + * + * They are rescheduled as soon as possible to let their next step to be executed without + * a very large temporal gap to the previous step. + * + * Moreover, the times executed does NOT increase for each step. It will increase once, + * after the last step, when they return Status::OK. + */ + $this->set('next_execution', Factory::getDate('now', 'UTC')->sub(new \DateInterval('PT1M'))->toSql()); + } + + // The only acceptable "successful" statuses are either clean exit or resuming execution. + if (!in_array($this->snapshot['status'], [Status::WILL_RESUME, Status::OK])) { + $this->set('times_failed', $this->get('times_failed') + 1); + } + + if (!$this->releaseLock()) { + $this->snapshot['status'] = Status::NO_RELEASE; + } + + $this->dispatchExitEvent(); + + if (!empty($this->snapshot['exception'])) { + throw $this->snapshot['exception']; + } + + return $this->isSuccess(); + } + + /** + * Get the task execution snapshot. + * ! Access locations will need updates once a more robust Snapshot container is implemented. + * + * @return array + * + * @since 4.1.0 + */ + public function getContent(): array + { + return $this->snapshot; + } + + /** + * Acquire a pseudo-lock on the task record. + * ! At the moment, this method is not used anywhere as task locks are already + * acquired when they're fetched. As such this method is not functional and should + * not be reviewed until it is updated. + * + * @return boolean + * + * @since 4.1.0 + * @throws \Exception + */ + public function acquireLock(): bool + { + $db = $this->db; + $query = $db->getQuery(true); + $id = $this->get('id'); + $now = Factory::getDate('now', 'GMT'); + + $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); + $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); + $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); + $now = $now->toSql(); + + // @todo update or remove this method + $query->update($db->qn('#__scheduler_tasks')) + ->set('locked = :now') + ->where($db->qn('id') . ' = :taskId') + ->extendWhere( + 'AND', + [ + $db->qn('locked') . ' < :threshold', + $db->qn('locked') . 'IS NULL', + ], + 'OR' + ) + ->bind(':taskId', $id, ParameterType::INTEGER) + ->bind(':now', $now) + ->bind(':threshold', $timeoutThreshold); + + try { + $db->lockTable('#__scheduler_tasks'); + $db->setQuery($query)->execute(); + } catch (\RuntimeException $e) { + return false; + } finally { + $db->unlockTables(); + } + + if ($db->getAffectedRows() === 0) { + return false; + } + + $this->set('locked', $now); + + return true; + } + + /** + * Remove the pseudo-lock and optionally update the task record. + * + * @param bool $update If true, the record is updated with the snapshot + * + * @return boolean + * + * @since 4.1.0 + * @throws \Exception + */ + public function releaseLock(bool $update = true): bool + { + $db = $this->db; + $query = $db->getQuery(true); + $id = $this->get('id'); + + $query->update($db->qn('#__scheduler_tasks', 't')) + ->set('locked = NULL') + ->where($db->qn('id') . ' = :taskId') + ->where($db->qn('locked') . ' IS NOT NULL') + ->bind(':taskId', $id, ParameterType::INTEGER); + + if ($update) { + $exitCode = $this->get('last_exit_code'); + $lastExec = $this->get('last_execution'); + $nextExec = $this->get('next_execution'); + $timesFailed = $this->get('times_failed'); + $timesExecuted = $this->get('times_executed'); + + $query->set( + [ + 'last_exit_code = :exitCode', + 'last_execution = :lastExec', + 'next_execution = :nextExec', + 'times_executed = :times_executed', + 'times_failed = :times_failed', + ] + ) + ->bind(':exitCode', $exitCode, ParameterType::INTEGER) + ->bind(':lastExec', $lastExec) + ->bind(':nextExec', $nextExec) + ->bind(':times_executed', $timesExecuted) + ->bind(':times_failed', $timesFailed); + } + + try { + $db->setQuery($query)->execute(); + } catch (\RuntimeException $e) { + return false; + } + + if (!$db->getAffectedRows()) { + return false; + } + + $this->set('locked', null); + + return true; + } + + /** + * @param string $message Log message + * @param string $priority Log level, defaults to 'info' + * + * @return void + * + * @since 4.1.0 + * @throws InvalidArgumentException + */ + public function log(string $message, string $priority = 'info'): void + { + $this->logger->log($priority, $message, ['category' => $this->logCategory]); + } + + /** + * Advance the task entry's next calculated execution, effectively skipping the current execution. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function skipExecution(): void + { + $db = $this->db; + $query = $db->getQuery(true); + + $id = $this->get('id'); + $nextExec = (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec(true, true); + + $query->update($db->qn('#__scheduler_tasks', 't')) + ->set('t.next_execution = :nextExec') + ->where('t.id = :id') + ->bind(':nextExec', $nextExec) + ->bind(':id', $id); + + try { + $db->setQuery($query)->execute(); + } catch (\RuntimeException $e) { + } + + $this->set('next_execution', $nextExec); + } + + /** + * Handles task exit (dispatch event). + * + * @return void + * + * @since 4.1.0 + * + * @throws \UnexpectedValueException|\BadMethodCallException + */ + protected function dispatchExitEvent(): void + { + $exitCode = $this->snapshot['status'] ?? 'NA'; + $eventName = self::EVENTS_MAP[$exitCode] ?? self::EVENTS_MAP['NA']; + + $event = AbstractEvent::create( + $eventName, + [ + 'subject' => $this, + ] + ); + + $this->app->getDispatcher()->dispatch($eventName, $event); + } + + /** + * Was the task successful? + * + * @return boolean True if the task was successful. + * @since 4.1.0 + */ + public function isSuccess(): bool + { + return in_array(($this->snapshot['status'] ?? null), [Status::OK, Status::WILL_RESUME]); + } + + /** + * Set a task property. This method is a proxy to {@see Registry::set()}. + * + * @param string $path Registry path of the task property. + * @param mixed $value The value to set to the property. + * @param ?string $separator The key separator. + * + * @return mixed|null + * + * @since 4.1.0 + */ + protected function set(string $path, $value, string $separator = null) + { + return $this->taskRegistry->set($path, $value, $separator); + } + + /** + * Get a task property. This method is a proxy to {@see Registry::get()}. + * + * @param string $path Registry path of the task property. + * @param mixed $default Default property to return, if the actual value is null. + * + * @return mixed The task property. + * + * @since 4.1.0 + */ + public function get(string $path, $default = null) + { + return $this->taskRegistry->get($path, $default); + } + + /** + * Static method to determine whether an enumerated task state (as a string) is valid. + * + * @param string $state The task state (enumerated, as a string). + * + * @return boolean + * + * @since 4.1.0 + */ + public static function isValidState(string $state): bool + { + if (!is_numeric($state)) { + return false; + } + + // Takes care of interpreting as float/int + $state = $state + 0; + + return ArrayHelper::getValue(self::STATE_MAP, $state) !== null; + } + + /** + * Static method to determine whether a task id is valid. Note that this does not + * validate ids against the database, but only verifies that an id may exist. + * + * @param string $id The task id (as a string). + * + * @return boolean + * + * @since 4.1.0 + */ + public static function isValidId(string $id): bool + { + $id = is_numeric($id) ? ($id + 0) : $id; + + if (!\is_int($id) || $id <= 0) { + return false; + } + + return true; + } } diff --git a/code/administrator/components/com_scheduler/src/Task/TaskOption.php b/code/administrator/components/com_scheduler/src/Task/TaskOption.php index 05fc2ecd..02dbf899 100644 --- a/code/administrator/components/com_scheduler/src/Task/TaskOption.php +++ b/code/administrator/components/com_scheduler/src/Task/TaskOption.php @@ -1,4 +1,5 @@ id = $type; - $this->title = Text::_("${langConstPrefix}_TITLE"); - $this->desc = Text::_("${langConstPrefix}_DESC"); - $this->langConstPrefix = $langConstPrefix; - } + /** + * TaskOption constructor. + * + * @param string $type A unique ID string for a plugin task routine. + * @param string $langConstPrefix The Language constant prefix $p. Expects $p . _TITLE and $p . _DESC to exist. + * + * @since 4.1.0 + */ + public function __construct(string $type, string $langConstPrefix) + { + $this->id = $type; + $this->title = Text::_("${langConstPrefix}_TITLE"); + $this->desc = Text::_("${langConstPrefix}_DESC"); + $this->langConstPrefix = $langConstPrefix; + } - /** - * Magic method to allow read-only access to private properties. - * - * @param string $name The object property requested. - * - * @return ?string - * - * @since 4.1.0 - */ - public function __get(string $name) - { - if (property_exists($this, $name)) - { - return $this->$name; - } + /** + * Magic method to allow read-only access to private properties. + * + * @param string $name The object property requested. + * + * @return ?string + * + * @since 4.1.0 + */ + public function __get(string $name) + { + if (property_exists($this, $name)) { + return $this->$name; + } - // Trigger a deprecation for the 'type' property (replaced with {@see id}). - if ($name === 'type') - { - try - { - Log::add( - sprintf( - 'The %1$s property is deprecated. Use %2$s instead.', - $name, - 'id' - ), - Log::WARNING, - 'deprecated' - ); - } - catch (\RuntimeException $e) - { - // Pass - } + // Trigger a deprecation for the 'type' property (replaced with {@see id}). + if ($name === 'type') { + try { + Log::add( + sprintf( + 'The %1$s property is deprecated. Use %2$s instead.', + $name, + 'id' + ), + Log::WARNING, + 'deprecated' + ); + } catch (\RuntimeException $e) { + // Pass + } - return $this->id; - } + return $this->id; + } - return null; - } + return null; + } } diff --git a/code/administrator/components/com_scheduler/src/Task/TaskOptions.php b/code/administrator/components/com_scheduler/src/Task/TaskOptions.php index 4488aab4..a4336217 100644 --- a/code/administrator/components/com_scheduler/src/Task/TaskOptions.php +++ b/code/administrator/components/com_scheduler/src/Task/TaskOptions.php @@ -1,4 +1,5 @@ 'languageConstantPrefix', ... ] - * - * @return void - * - * @since 4.1.0 - */ - public function addOptions(array $taskRoutines): void - { - foreach ($taskRoutines as $routineId => $langConstPrefix) - { - $this->options[] = new TaskOption($routineId, $langConstPrefix); - } - } + /** + * A plugin can support several task routines + * This method is used by a plugin's onTaskOptionsList subscriber to advertise supported routines. + * + * @param array $taskRoutines An associative array of {@var TaskOption} constructor argument pairs: + * [ 'routineId' => 'languageConstantPrefix', ... ] + * + * @return void + * + * @since 4.1.0 + */ + public function addOptions(array $taskRoutines): void + { + foreach ($taskRoutines as $routineId => $langConstPrefix) { + $this->options[] = new TaskOption($routineId, $langConstPrefix); + } + } - /** - * @param ?string $routineId A unique identifier for a plugin task routine - * - * @return ?TaskOption A matching TaskOption if available, null otherwise - * - * @since 4.1.0 - */ - public function findOption(?string $routineId): ?TaskOption - { - if ($routineId === null) - { - return null; - } + /** + * @param ?string $routineId A unique identifier for a plugin task routine + * + * @return ?TaskOption A matching TaskOption if available, null otherwise + * + * @since 4.1.0 + */ + public function findOption(?string $routineId): ?TaskOption + { + if ($routineId === null) { + return null; + } - foreach ($this->options as $option) - { - if ($option->id === $routineId) - { - return $option; - } - } + foreach ($this->options as $option) { + if ($option->id === $routineId) { + return $option; + } + } - return null; - } + return null; + } } diff --git a/code/administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php b/code/administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php index 699ed6b1..4cae94e5 100644 --- a/code/administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php +++ b/code/administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php @@ -1,4 +1,5 @@ snapshot['logCategory'] = $event->getArgument('subject')->logCategory; - $this->snapshot['plugin'] = $this->_name; - $this->snapshot['startTime'] = microtime(true); - $this->snapshot['status'] = Status::RUNNING; - } - - /** - * Set information to {@see $snapshot} when ending a routine. This information includes the routine exit code and - * timing information. - * - * @param ExecuteTaskEvent $event The event - * @param ?int $exitCode The task exit code - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - protected function endRoutine(ExecuteTaskEvent $event, int $exitCode): void - { - if (!$this instanceof CMSPlugin) - { - return; - } - - $this->snapshot['endTime'] = $endTime = microtime(true); - $this->snapshot['duration'] = $endTime - $this->snapshot['startTime']; - $this->snapshot['status'] = $exitCode ?? Status::OK; - $event->setResult($this->snapshot); - } - - /** - * Enhance the task form with routine-specific fields from an XML file declared through the TASKS_MAP constant. - * If a plugin only supports the task form and does not need additional logic, this method can be mapped to the - * `onContentPrepareForm` event through {@see SubscriberInterface::getSubscribedEvents()} and will take care - * of injecting the fields without additional logic in the plugin class. - * - * @param EventInterface|Form $context The onContentPrepareForm event or the Form object. - * @param mixed $data The form data, required when $context is a {@see Form} instance. - * - * @return boolean True if the form was successfully enhanced or the context was not relevant. - * - * @since 4.1.0 - * @throws \Exception - */ - public function enhanceTaskItemForm($context, $data = null): bool - { - if ($context instanceof EventInterface) - { - /** @var Form $form */ - $form = $context->getArgument('0'); - $data = $context->getArgument('1'); - } - elseif ($context instanceof Form) - { - $form = $context; - } - else - { - throw new \InvalidArgumentException( - sprintf( - 'Argument 0 of %1$s must be an instance of %2$s or %3$s', - __METHOD__, - EventInterface::class, - Form::class - ) - ); - } - - if ($form->getName() !== 'com_scheduler.task') - { - return true; - } - - $routineId = $this->getRoutineId($form, $data); - $isSupported = \array_key_exists($routineId, self::TASKS_MAP); - $enhancementFormName = self::TASKS_MAP[$routineId]['form'] ?? ''; - - // Return if routine is not supported by the plugin or the routine does not have a form linked in TASKS_MAP. - if (!$isSupported || \strlen($enhancementFormName) === 0) - { - return true; - } - - // We expect the form XML in "{PLUGIN_PATH}/forms/{FORM_NAME}.xml" - $path = \dirname((new \ReflectionClass(static::class))->getFileName()); - $enhancementFormFile = $path . '/forms/' . $enhancementFormName . '.xml'; - - try - { - $enhancementFormFile = Path::check($enhancementFormFile); - } - catch (\Exception $e) - { - return false; - } - - if (is_file($enhancementFormFile)) - { - return $form->loadFile($enhancementFormFile); - } - - return false; - } - - /** - * Advertise the task routines supported by the plugin. This method should be mapped to the `onTaskOptionsList`, - * enabling the plugin to advertise its routines without any custom logic.
- * **Note:** This method expects the `TASKS_MAP` class constant to have relevant information. - * - * @param EventInterface $event onTaskOptionsList Event - * - * @return void - * - * @since 4.1.0 - */ - public function advertiseRoutines(EventInterface $event): void - { - $options = []; - - foreach (self::TASKS_MAP as $routineId => $details) - { - // Sanity check against non-compliant plugins - if (isset($details['langConstPrefix'])) - { - $options[$routineId] = $details['langConstPrefix']; - } - } - - $subject = $event->getArgument('subject'); - $subject->addOptions($options); - } - - /** - * Get the relevant task routine ID in the context of a form event, e.g., the `onContentPrepareForm` event. - * - * @param Form $form The form - * @param mixed $data The data - * - * @return string - * - * @since 4.1.0 - * @throws \Exception - */ - protected function getRoutineId(Form $form, $data): string - { - /* - * Depending on when the form is loaded, the ID may either be in $data or the data already bound to the form. - * $data can also either be an object or an array. - */ - $routineId = $data->taskOption->id ?? $data->type ?? $data['type'] ?? $form->getValue('type') ?? $data['taskOption']->id ?? ''; - - // If we're unable to find a routineId, it might be in the form input. - if (empty($routineId)) - { - $app = $this->app ?? Factory::getApplication(); - $form = $app->getInput()->get('jform', []); - $routineId = ArrayHelper::getValue($form, 'type', '', 'STRING'); - } - - return $routineId; - } - - /** - * Add a log message to the task log. - * - * @param string $message The log message - * @param string $priority The log message priority - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - * @todo : use dependency injection here (starting from the Task & Scheduler classes). - */ - protected function logTask(string $message, string $priority = 'info'): void - { - static $langLoaded; - static $priorityMap = [ - 'debug' => Log::DEBUG, - 'error' => Log::ERROR, - 'info' => Log::INFO, - 'notice' => Log::NOTICE, - 'warning' => Log::WARNING, - ]; - - if (!$langLoaded) - { - $app = $this->app ?? Factory::getApplication(); - $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); - $langLoaded = true; - } - - $category = $this->snapshot['logCategory']; - - Log::add(Text::_('COM_SCHEDULER_ROUTINE_LOG_PREFIX') . $message, $priorityMap[$priority] ?? Log::INFO, $category); - } - - /** - * Handler for *standard* task routines. Standard routines are mapped to valid class methods 'method' through - * `static::TASKS_MAP`. These methods are expected to take a single argument (the Event) and return an integer - * return status (see {@see Status}). For a plugin that maps each of its task routines to valid methods and does - * not need non-standard handling, this method can be mapped to the `onExecuteTask` event through - * {@see SubscriberInterface::getSubscribedEvents()}, which would allow it to then check if the event wants to - * execute a routine offered by the parent plugin, call the routine and do some other housework without any code - * in the parent classes.
- * **Compatible routine method signature:**   ({@see ExecuteTaskEvent::class}, ...): int - * - * @param ExecuteTaskEvent $event The `onExecuteTask` event. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - public function standardRoutineHandler(ExecuteTaskEvent $event): void - { - if (!\array_key_exists($event->getRoutineId(), self::TASKS_MAP)) - { - return; - } - - $this->startRoutine($event); - $routineId = $event->getRoutineId(); - $methodName = (string) self::TASKS_MAP[$routineId]['method'] ?? ''; - $exitCode = Status::NO_EXIT; - - // We call the mapped method if it exists and confirms to the ($event) -> int signature. - if (!empty($methodName) && ($staticReflection = new \ReflectionClass($this))->hasMethod($methodName)) - { - $method = $staticReflection->getMethod($methodName); - - // Might need adjustments here for PHP8 named parameters. - if (!($method->getNumberOfRequiredParameters() === 1) - || !$method->getParameters()[0]->hasType() - || $method->getParameters()[0]->getType()->getName() !== ExecuteTaskEvent::class - || !$method->hasReturnType() - || $method->getReturnType()->getName() !== 'int') - { - $this->logTask( - sprintf( - 'Incorrect routine method signature for %1$s(). See checks in %2$s()', - $method->getName(), - __METHOD__ - ), - 'error' - ); - - return; - } - - try - { - // Enable invocation of private/protected methods. - $method->setAccessible(true); - $exitCode = $method->invoke($this, $event); - } - catch (\ReflectionException $e) - { - // @todo replace with language string (?) - $this->logTask('Exception when calling routine: ' . $e->getMessage(), 'error'); - $exitCode = Status::NO_RUN; - } - } - else - { - $this->logTask( - sprintf( - 'Incorrectly configured TASKS_MAP in class %s. Missing valid method for `routine_id` %s', - static::class, - $routineId - ), - 'error' - ); - } - - /** - * Closure to validate a status against {@see Status} - * - * @since 4.1.0 - */ - $validateStatus = static function (int $statusCode): bool { - return \in_array( - $statusCode, - (new \ReflectionClass(Status::class))->getConstants() - ); - }; - - // Validate the exit code. - if (!\is_int($exitCode) || !$validateStatus($exitCode)) - { - $exitCode = Status::INVALID_EXIT; - } - - $this->endRoutine($event, $exitCode); - } + /** + * A snapshot of the routine state. + * + * @var array + * @since 4.1.0 + */ + protected $snapshot = []; + + /** + * Set information to {@see $snapshot} when initializing a routine. + * + * @param ExecuteTaskEvent $event The onExecuteTask event. + * + * @return void + * + * @since 4.1.0 + */ + protected function startRoutine(ExecuteTaskEvent $event): void + { + if (!$this instanceof CMSPlugin) { + return; + } + + $this->snapshot['logCategory'] = $event->getArgument('subject')->logCategory; + $this->snapshot['plugin'] = $this->_name; + $this->snapshot['startTime'] = microtime(true); + $this->snapshot['status'] = Status::RUNNING; + } + + /** + * Set information to {@see $snapshot} when ending a routine. This information includes the routine exit code and + * timing information. + * + * @param ExecuteTaskEvent $event The event + * @param ?int $exitCode The task exit code + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + protected function endRoutine(ExecuteTaskEvent $event, int $exitCode): void + { + if (!$this instanceof CMSPlugin) { + return; + } + + $this->snapshot['endTime'] = $endTime = microtime(true); + $this->snapshot['duration'] = $endTime - $this->snapshot['startTime']; + $this->snapshot['status'] = $exitCode ?? Status::OK; + $event->setResult($this->snapshot); + } + + /** + * Enhance the task form with routine-specific fields from an XML file declared through the TASKS_MAP constant. + * If a plugin only supports the task form and does not need additional logic, this method can be mapped to the + * `onContentPrepareForm` event through {@see SubscriberInterface::getSubscribedEvents()} and will take care + * of injecting the fields without additional logic in the plugin class. + * + * @param EventInterface|Form $context The onContentPrepareForm event or the Form object. + * @param mixed $data The form data, required when $context is a {@see Form} instance. + * + * @return boolean True if the form was successfully enhanced or the context was not relevant. + * + * @since 4.1.0 + * @throws \Exception + */ + public function enhanceTaskItemForm($context, $data = null): bool + { + if ($context instanceof EventInterface) { + /** @var Form $form */ + $form = $context->getArgument('0'); + $data = $context->getArgument('1'); + } elseif ($context instanceof Form) { + $form = $context; + } else { + throw new \InvalidArgumentException( + sprintf( + 'Argument 0 of %1$s must be an instance of %2$s or %3$s', + __METHOD__, + EventInterface::class, + Form::class + ) + ); + } + + if ($form->getName() !== 'com_scheduler.task') { + return true; + } + + $routineId = $this->getRoutineId($form, $data); + $isSupported = \array_key_exists($routineId, self::TASKS_MAP); + $enhancementFormName = self::TASKS_MAP[$routineId]['form'] ?? ''; + + // Return if routine is not supported by the plugin or the routine does not have a form linked in TASKS_MAP. + if (!$isSupported || \strlen($enhancementFormName) === 0) { + return true; + } + + // We expect the form XML in "{PLUGIN_PATH}/forms/{FORM_NAME}.xml" + $path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name; + $enhancementFormFile = $path . '/forms/' . $enhancementFormName . '.xml'; + + try { + $enhancementFormFile = Path::check($enhancementFormFile); + } catch (\Exception $e) { + return false; + } + + if (is_file($enhancementFormFile)) { + return $form->loadFile($enhancementFormFile); + } + + return false; + } + + /** + * Advertise the task routines supported by the plugin. This method should be mapped to the `onTaskOptionsList`, + * enabling the plugin to advertise its routines without any custom logic.
+ * **Note:** This method expects the `TASKS_MAP` class constant to have relevant information. + * + * @param EventInterface $event onTaskOptionsList Event + * + * @return void + * + * @since 4.1.0 + */ + public function advertiseRoutines(EventInterface $event): void + { + $options = []; + + foreach (self::TASKS_MAP as $routineId => $details) { + // Sanity check against non-compliant plugins + if (isset($details['langConstPrefix'])) { + $options[$routineId] = $details['langConstPrefix']; + } + } + + $subject = $event->getArgument('subject'); + $subject->addOptions($options); + } + + /** + * Get the relevant task routine ID in the context of a form event, e.g., the `onContentPrepareForm` event. + * + * @param Form $form The form + * @param mixed $data The data + * + * @return string + * + * @since 4.1.0 + * @throws \Exception + */ + protected function getRoutineId(Form $form, $data): string + { + /* + * Depending on when the form is loaded, the ID may either be in $data or the data already bound to the form. + * $data can also either be an object or an array. + */ + $routineId = $data->taskOption->id ?? $data->type ?? $data['type'] ?? $form->getValue('type') ?? $data['taskOption']->id ?? ''; + + // If we're unable to find a routineId, it might be in the form input. + if (empty($routineId)) { + $app = $this->getApplication() ?? ($this->app ?? Factory::getApplication()); + $form = $app->getInput()->get('jform', []); + $routineId = ArrayHelper::getValue($form, 'type', '', 'STRING'); + } + + return $routineId; + } + + /** + * Add a log message to the task log. + * + * @param string $message The log message + * @param string $priority The log message priority + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + * @todo : use dependency injection here (starting from the Task & Scheduler classes). + */ + protected function logTask(string $message, string $priority = 'info'): void + { + static $langLoaded; + static $priorityMap = [ + 'debug' => Log::DEBUG, + 'error' => Log::ERROR, + 'info' => Log::INFO, + 'notice' => Log::NOTICE, + 'warning' => Log::WARNING, + ]; + + if (!$langLoaded) { + $app = $this->getApplication() ?? ($this->app ?? Factory::getApplication()); + $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); + $langLoaded = true; + } + + $category = $this->snapshot['logCategory']; + + Log::add(Text::_('COM_SCHEDULER_ROUTINE_LOG_PREFIX') . $message, $priorityMap[$priority] ?? Log::INFO, $category); + } + + /** + * Handler for *standard* task routines. Standard routines are mapped to valid class methods 'method' through + * `static::TASKS_MAP`. These methods are expected to take a single argument (the Event) and return an integer + * return status (see {@see Status}). For a plugin that maps each of its task routines to valid methods and does + * not need non-standard handling, this method can be mapped to the `onExecuteTask` event through + * {@see SubscriberInterface::getSubscribedEvents()}, which would allow it to then check if the event wants to + * execute a routine offered by the parent plugin, call the routine and do some other housework without any code + * in the parent classes.
+ * **Compatible routine method signature:**   ({@see ExecuteTaskEvent::class}, ...): int + * + * @param ExecuteTaskEvent $event The `onExecuteTask` event. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function standardRoutineHandler(ExecuteTaskEvent $event): void + { + if (!\array_key_exists($event->getRoutineId(), self::TASKS_MAP)) { + return; + } + + $this->startRoutine($event); + $routineId = $event->getRoutineId(); + $methodName = (string) self::TASKS_MAP[$routineId]['method'] ?? ''; + $exitCode = Status::NO_EXIT; + + // We call the mapped method if it exists and confirms to the ($event) -> int signature. + if (!empty($methodName) && ($staticReflection = new \ReflectionClass($this))->hasMethod($methodName)) { + $method = $staticReflection->getMethod($methodName); + + // Might need adjustments here for PHP8 named parameters. + if ( + !($method->getNumberOfRequiredParameters() === 1) + || !$method->getParameters()[0]->hasType() + || $method->getParameters()[0]->getType()->getName() !== ExecuteTaskEvent::class + || !$method->hasReturnType() + || $method->getReturnType()->getName() !== 'int' + ) { + $this->logTask( + sprintf( + 'Incorrect routine method signature for %1$s(). See checks in %2$s()', + $method->getName(), + __METHOD__ + ), + 'error' + ); + + return; + } + + try { + // Enable invocation of private/protected methods. + $method->setAccessible(true); + $exitCode = $method->invoke($this, $event); + } catch (\ReflectionException $e) { + // @todo replace with language string (?) + $this->logTask('Exception when calling routine: ' . $e->getMessage(), 'error'); + $exitCode = Status::NO_RUN; + } + } else { + $this->logTask( + sprintf( + 'Incorrectly configured TASKS_MAP in class %s. Missing valid method for `routine_id` %s', + static::class, + $routineId + ), + 'error' + ); + } + + /** + * Closure to validate a status against {@see Status} + * + * @since 4.1.0 + */ + $validateStatus = static function (int $statusCode): bool { + return \in_array( + $statusCode, + (new \ReflectionClass(Status::class))->getConstants() + ); + }; + + // Validate the exit code. + if (!\is_int($exitCode) || !$validateStatus($exitCode)) { + $exitCode = Status::INVALID_EXIT; + } + + $this->endRoutine($event, $exitCode); + } } diff --git a/code/administrator/components/com_scheduler/src/View/Select/HtmlView.php b/code/administrator/components/com_scheduler/src/View/Select/HtmlView.php index c2d35d3a..2e9061c0 100644 --- a/code/administrator/components/com_scheduler/src/View/Select/HtmlView.php +++ b/code/administrator/components/com_scheduler/src/View/Select/HtmlView.php @@ -1,4 +1,5 @@ app = Factory::getApplication(); - - parent::__construct($config); - } - - /** - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - public function display($tpl = null): void - { - $this->state = $this->get('State'); - $this->items = $this->get('Items'); - $this->modalLink = ''; - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.1.0 - */ - protected function addToolbar(): void - { - /* - * Get the global Toolbar instance - * @todo : Replace usage with ToolbarFactoryInterface. but how? - * Probably some changes in the core, since mod_menu calls and renders the getInstance() toolbar - */ - $toolbar = Toolbar::getInstance(); - - // Add page title - ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); - - $toolbar->linkButton('cancel') - ->url('index.php?option=com_scheduler') - ->buttonClass('btn btn-danger') - ->icon('icon-times') - ->text(Text::_('JCANCEL')); - } + /** + * @var AdministratorApplication + * @since 4.1.0 + */ + protected $app; + + /** + * The model state + * + * @var CMSObject + * @since 4.1.0 + */ + protected $state; + + /** + * An array of items + * + * @var TaskOption[] + * @since 4.1.0 + */ + protected $items; + + /** + * A suffix for links for modal use [?] + * + * @var string + * @since 4.1.0 + */ + protected $modalLink; + + /** + * HtmlView constructor. + * + * @param array $config A named configuration array for object construction. + * name: the name (optional) of the view (defaults to the view class name suffix). + * charset: the character set to use for display + * escape: the name (optional) of the function to use for escaping strings + * base_path: the parent path (optional) of the `views` directory (defaults to the component + * folder) template_plath: the path (optional) of the layout directory (defaults to + * base_path + /views/ + view name helper_path: the path (optional) of the helper files + * (defaults to base_path + /helpers/) layout: the layout (optional) to use to display the + * view + * + * @since 4.1.0 + * @throws \Exception + */ + public function __construct($config = []) + { + $this->app = Factory::getApplication(); + + parent::__construct($config); + } + + /** + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function display($tpl = null): void + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->modalLink = ''; + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.1.0 + */ + protected function addToolbar(): void + { + /* + * Get the global Toolbar instance + * @todo : Replace usage with ToolbarFactoryInterface. but how? + * Probably some changes in the core, since mod_menu calls and renders the getInstance() toolbar + */ + $toolbar = Toolbar::getInstance(); + + // Add page title + ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); + + $toolbar->linkButton('cancel') + ->url('index.php?option=com_scheduler') + ->buttonClass('btn btn-danger') + ->icon('icon-times') + ->text(Text::_('JCANCEL')); + } } diff --git a/code/administrator/components/com_scheduler/src/View/Task/HtmlView.php b/code/administrator/components/com_scheduler/src/View/Task/HtmlView.php index 6023fc97..da180e8d 100644 --- a/code/administrator/components/com_scheduler/src/View/Task/HtmlView.php +++ b/code/administrator/components/com_scheduler/src/View/Task/HtmlView.php @@ -1,4 +1,5 @@ app = Factory::getApplication(); - parent::__construct($config); - } - - /** - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - public function display($tpl = null): void - { - /* - * Will call the getForm() method of TaskModel - */ - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - $this->canDo = ContentHelper::getActions('com_scheduler', 'task', $this->item->id); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Adds the page title and toolbar - * - * @return void - * - * @since 4.1.0 - */ - protected function addToolbar(): void - { - $app = $this->app; - - $app->getInput()->set('hidemainmenu', true); - $isNew = ($this->item->id == 0); - $canDo = $this->canDo; - - /* - * Get the toolbar object instance - * !! @todo : Replace usage with ToolbarFactoryInterface - */ - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title($isNew ? Text::_('COM_SCHEDULER_MANAGER_TASK_NEW') : Text::_('COM_SCHEDULER_MANAGER_TASK_EDIT'), 'clock'); - - if (($isNew && $canDo->get('core.create')) || (!$isNew && $canDo->get('core.edit'))) - { - $toolbar->apply('task.apply'); - $toolbar->save('task.save'); - } - - // @todo | ? : Do we need save2new, save2copy? - - $toolbar->cancel('task.cancel', $isNew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE'); - $toolbar->help('Scheduled_Tasks:_Edit'); - } + /** + * @var AdministratorApplication $app + * @since 4.1.0 + */ + protected $app; + + /** + * The Form object + * + * @var Form + * @since 4.1.0 + */ + protected $form; + + /** + * The active item + * + * @var object + * @since 4.1.0 + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + * @since 4.1.0 + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * @since 4.1.0 + */ + protected $canDo; + + /** + * Overloads the parent constructor. + * Just needed to fetch the Application object. + * + * @param array $config A named configuration array for object construction. + * name: the name (optional) of the view (defaults to the view class name suffix). + * charset: the character set to use for display + * escape: the name (optional) of the function to use for escaping strings + * base_path: the parent path (optional) of the `views` directory (defaults to the + * component folder) template_plath: the path (optional) of the layout directory (defaults + * to base_path + /views/ + view name helper_path: the path (optional) of the helper files + * (defaults to base_path + /helpers/) layout: the layout (optional) to use to display the + * view + * + * @since 4.1.0 + * @throws \Exception + */ + public function __construct($config = array()) + { + $this->app = Factory::getApplication(); + parent::__construct($config); + } + + /** + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function display($tpl = null): void + { + /* + * Will call the getForm() method of TaskModel + */ + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->canDo = ContentHelper::getActions('com_scheduler', 'task', $this->item->id); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Adds the page title and toolbar + * + * @return void + * + * @since 4.1.0 + */ + protected function addToolbar(): void + { + $app = $this->app; + + $app->getInput()->set('hidemainmenu', true); + $isNew = ($this->item->id == 0); + $canDo = $this->canDo; + + /* + * Get the toolbar object instance + * !! @todo : Replace usage with ToolbarFactoryInterface + */ + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title($isNew ? Text::_('COM_SCHEDULER_MANAGER_TASK_NEW') : Text::_('COM_SCHEDULER_MANAGER_TASK_EDIT'), 'clock'); + + if (($isNew && $canDo->get('core.create')) || (!$isNew && $canDo->get('core.edit'))) { + $toolbar->apply('task.apply'); + $toolbar->save('task.save'); + } + + // @todo | ? : Do we need save2new, save2copy? + + $toolbar->cancel('task.cancel', $isNew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE'); + $toolbar->help('Scheduled_Tasks:_Edit'); + } } diff --git a/code/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php b/code/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php index a7fd01bd..3dceb534 100644 --- a/code/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php +++ b/code/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('empty_state'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_scheduler'); - $user = Factory::getApplication()->getIdentity(); - - /* - * Get the toolbar object instance - * !! @todo : Replace usage with ToolbarFactoryInterface - */ - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); - - if ($canDo->get('core.create')) - { - $toolbar->linkButton('new', 'JTOOLBAR_NEW') - ->url('index.php?option=com_scheduler&view=select&layout=default') - ->buttonClass('btn btn-success') - ->icon('icon-new'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) - { - /** @var DropdownButton $dropdown */ - $dropdown = $toolbar->dropdownButton('status-group') - ->toggleSplit(false) - ->text('JTOOLBAR_CHANGE_STATUS') - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - // Add the batch Enable, Disable and Trash buttons if privileged - if ($canDo->get('core.edit.state')) - { - $childBar->publish('tasks.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $childBar->unpublish('tasks.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('tasks.checkin')->listCheck(true); - } - - $childBar->checkin('tasks.unlock', 'COM_SCHEDULER_TOOLBAR_UNLOCK')->listCheck(true)->icon('icon-unlock'); - - // We don't want the batch Trash button if displayed entries are all trashed - if ($this->state->get('filter.state') != -2) - { - $childBar->trash('tasks.trash')->listCheck(true); - } - } - } - - // Add "Empty Trash" button if filtering by trashed. - if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('tasks.delete') - ->message('JGLOBAL_CONFIRM_DELETE') - ->text('JTOOLBAR_EMPTY_TRASH') - ->listCheck(true); - } - - // Link to component preferences if user has admin privileges - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_scheduler'); - } - - $toolbar->help('Scheduled_Tasks'); - } + /** + * Array of task items. + * + * @var array + * @since 4.1.0 + */ + protected $items; + + /** + * The pagination object. + * + * @var Pagination + * @since 4.1.0 + * @todo Test pagination. + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 4.1.0 + */ + protected $state; + + /** + * A Form object for search filters. + * + * @var Form + * @since 4.1.0 + */ + public $filterForm; + + /** + * The active search filters. + * + * @var array + * @since 4.1.0 + */ + public $activeFilters; + + /** + * Is this view in an empty state? + * + * @var boolean + * @since 4.1.0 + */ + private $isEmptyState = false; + + /** + * @inheritDoc + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function display($tpl = null): void + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('empty_state'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_scheduler'); + $user = Factory::getApplication()->getIdentity(); + + /* + * Get the toolbar object instance + * !! @todo : Replace usage with ToolbarFactoryInterface + */ + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); + + if ($canDo->get('core.create')) { + $toolbar->linkButton('new', 'JTOOLBAR_NEW') + ->url('index.php?option=com_scheduler&view=select&layout=default') + ->buttonClass('btn btn-success') + ->icon('icon-new'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { + /** @var DropdownButton $dropdown */ + $dropdown = $toolbar->dropdownButton('status-group') + ->toggleSplit(false) + ->text('JTOOLBAR_CHANGE_STATUS') + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + // Add the batch Enable, Disable and Trash buttons if privileged + if ($canDo->get('core.edit.state')) { + $childBar->publish('tasks.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $childBar->unpublish('tasks.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + + if ($canDo->get('core.admin')) { + $childBar->checkin('tasks.checkin')->listCheck(true); + } + + $childBar->checkin('tasks.unlock', 'COM_SCHEDULER_TOOLBAR_UNLOCK')->listCheck(true)->icon('icon-unlock'); + + // We don't want the batch Trash button if displayed entries are all trashed + if ($this->state->get('filter.state') != -2) { + $childBar->trash('tasks.trash')->listCheck(true); + } + } + } + + // Add "Empty Trash" button if filtering by trashed. + if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('tasks.delete') + ->message('JGLOBAL_CONFIRM_DELETE') + ->text('JTOOLBAR_EMPTY_TRASH') + ->listCheck(true); + } + + // Link to component preferences if user has admin privileges + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_scheduler'); + } + + $toolbar->help('Scheduled_Tasks'); + } } diff --git a/code/administrator/components/com_scheduler/tmpl/select/default.php b/code/administrator/components/com_scheduler/tmpl/select/default.php index efffafd4..e3e1c5ff 100644 --- a/code/administrator/components/com_scheduler/tmpl/select/default.php +++ b/code/administrator/components/com_scheduler/tmpl/select/default.php @@ -1,4 +1,5 @@
-
-
- -
- -
- -
-
-
-
+
+
+ +
+ +
+ +
+
+
+
-
- -
- - -
-

- -

+
+ +
+ + +
+

+ +

- -
+ +
- - items as $item) : ?> - - id; ?> - escape($item->title); ?> - escape(strip_tags($item->desc)), 200); ?> - - -
-

-

- -

-
- - - -
- - -
-
+ + items as $item) : ?> + + id; ?> + escape($item->title); ?> + escape(strip_tags($item->desc)), 200); ?> + + +
+

+

+ +

+
+ + + +
+ + +
+
diff --git a/code/administrator/components/com_scheduler/tmpl/select/modal.php b/code/administrator/components/com_scheduler/tmpl/select/modal.php index 745adeee..5c2d1005 100644 --- a/code/administrator/components/com_scheduler/tmpl/select/modal.php +++ b/code/administrator/components/com_scheduler/tmpl/select/modal.php @@ -1,4 +1,5 @@
- setLayout('default'); ?> - - loadTemplate(); - } - catch (Exception $e) - { - die('Exception while loading template..'); - } - ?> + setLayout('default'); ?> + + loadTemplate(); + } catch (Exception $e) { + die('Exception while loading template..'); + } + ?>
diff --git a/code/administrator/components/com_scheduler/tmpl/task/edit.php b/code/administrator/components/com_scheduler/tmpl/task/edit.php index 184fcb1d..7d91554d 100644 --- a/code/administrator/components/com_scheduler/tmpl/task/edit.php +++ b/code/administrator/components/com_scheduler/tmpl/task/edit.php @@ -1,4 +1,5 @@ $fieldset) : - if ($name === 'task_params') : - unset($advancedFieldsets[$name]); - continue; - endif; + if ($name === 'task_params') : + unset($advancedFieldsets[$name]); + continue; + endif; - $this->ignore_fieldsets[] = $fieldset->name; + $this->ignore_fieldsets[] = $fieldset->name; endforeach; ?>
- - - - - -
- 'general')); ?> - - - item->id) ? Text::_('COM_SCHEDULER_NEW_TASK') : Text::_('COM_SCHEDULER_EDIT_TASK') - ); - ?> -
-
- - item->taskOption): - /** @var TaskOption $taskOption */ - $taskOption = $this->item->taskOption; ?> -
-

- title ?> -

- fieldset = 'description'; - $short_description = Text::_($taskOption->desc); - $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); - - if (!$long_description) - { - $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); - - if (strlen($truncated) > 500) - { - $long_description = $short_description; - $short_description = HTMLHelper::_('string.truncate', $truncated, 250); - - if ($short_description == $long_description) - { - $long_description = ''; - } - } - } - ?> -

- -

- - - -

- -
- - enqueueMessage(Text::_('COM_SCHEDULER_WARNING_EXISTING_TASK_TYPE_NOT_FOUND'), 'warning'); - ?> - -
- - form->renderFieldset('basic'); ?> -
- -
- - form->renderFieldset('custom-cron-rules'); ?> -
- -
- -
- form->renderFieldset('aside'); ?> -
-
- - - -
-
- -
-
- - - - -
-
-
- - form->renderFieldset('priority') ?> -
- -
- label ?: 'COM_SCHEDULER_FIELDSET_' . $fieldset->name) ?> - form->renderFieldset($fieldset->name) ?> -
- -
-
- - - - -
-
-
- - form->renderFieldset('exec_hist'); ?> -
-
-
- - - - -
-
-
- - form->renderFieldset('details'); ?> -
-
-
- - - - canDo->get('core.admin')) : ?> - -
- -
- form->getInput('rules'); ?> -
-
- - - - form->getInput('context'); ?> - - -
+ method="post" name="adminForm" id="task-form" + aria-label="item->id === 0 ? 'NEW' : 'EDIT'), true); ?>" + class="form-validate"> + + + + + +
+ 'general')); ?> + + + item->id) ? Text::_('COM_SCHEDULER_NEW_TASK') : Text::_('COM_SCHEDULER_EDIT_TASK') + ); + ?> +
+
+ + item->taskOption) : + /** @var TaskOption $taskOption */ + $taskOption = $this->item->taskOption; ?> +
+

+ title ?> +

+ fieldset = 'description'; + $short_description = Text::_($taskOption->desc); + $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); + + if (!$long_description) { + $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); + + if (strlen($truncated) > 500) { + $long_description = $short_description; + $short_description = HTMLHelper::_('string.truncate', $truncated, 250); + + if ($short_description == $long_description) { + $long_description = ''; + } + } + } + ?> +

+ +

+ + + +

+ +
+ + enqueueMessage(Text::_('COM_SCHEDULER_WARNING_EXISTING_TASK_TYPE_NOT_FOUND'), 'warning'); + ?> + +
+ + form->renderFieldset('basic'); ?> +
+ +
+ + form->renderFieldset('custom-cron-rules'); ?> +
+ +
+ +
+ form->renderFieldset('aside'); ?> +
+
+ + + +
+
+ +
+
+ + + + +
+
+
+ + form->renderFieldset('priority') ?> +
+ +
+ label ?: 'COM_SCHEDULER_FIELDSET_' . $fieldset->name) ?> + form->renderFieldset($fieldset->name) ?> +
+ +
+
+ + + + +
+
+
+ + form->renderFieldset('exec_hist'); ?> +
+
+
+ + + + +
+
+
+ + form->renderFieldset('details'); ?> +
+
+
+ + + + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + form->getInput('context'); ?> + + +
diff --git a/code/administrator/components/com_scheduler/tmpl/tasks/default.php b/code/administrator/components/com_scheduler/tmpl/tasks/default.php index 927d4883..c3399060 100644 --- a/code/administrator/components/com_scheduler/tmpl/tasks/default.php +++ b/code/administrator/components/com_scheduler/tmpl/tasks/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); +$wa->useScript('table.columns') + ->useScript('multiselect') + ->useScript('com_scheduler.test-task') + ->useStyle('com_scheduler.admin-view-tasks-css'); + Text::script('COM_SCHEDULER_TEST_RUN_TITLE'); Text::script('COM_SCHEDULER_TEST_RUN_TASK'); Text::script('COM_SCHEDULER_TEST_RUN_DURATION'); @@ -35,14 +43,11 @@ Text::script('JLIB_JS_AJAX_ERROR_NO_CONTENT'); Text::script('JLIB_JS_AJAX_ERROR_PARSE'); -try -{ - /** @var CMSWebApplicationInterface $app */ - $app = Factory::getApplication(); -} -catch (Exception $e) -{ - die('Failed to get app'); +try { + /** @var CMSWebApplicationInterface $app */ + $app = Factory::getApplication(); +} catch (Exception $e) { + die('Failed to get app'); } $user = $app->getIdentity(); @@ -53,243 +58,235 @@ $section = null; $mode = false; -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_scheduler&task=tasks.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_scheduler&task=tasks.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $this->document->addScriptOptions('com_scheduler.test-task.token', Session::getFormToken()); - -/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ -$wa = $this->document->getWebAssetManager(); -$wa->useScript('multiselect') - ->useScript('com_scheduler.test-task') - ->useStyle('com_scheduler.admin-view-tasks-css'); ?>
-
- $this)); - ?> - - - items)): ?> - -
- - -
- - - - items)): ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true" > - items as $i => $item): - $canCreate = $user->authorise('core.create', 'com_scheduler'); - $canEdit = $user->authorise('core.edit', 'com_scheduler'); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_scheduler') && $canCheckin; - ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - state, $i, 'tasks.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'tasks.', $canCheckin); ?> - - locked) : ?> - $canChange, 'prefix' => 'tasks.', - 'active_class' => 'none fa fa-running border-dark text-body', - 'inactive_class' => 'none fa fa-running', 'tip' => true, 'translate' => false, - 'active_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), - 'inactive_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), - ]); ?> - - - - escape($item->title); ?> - - - escape($item->title); ?> - - last_exit_code, [Status::OK, Status::WILL_RESUME])): ?> - -
- last_exit_code); ?> -
- -
- - note): ?> - - escape($item->note)); ?> - - -
- escape($item->safeTypeTitle); ?> - - last_execution ? HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5') : '-'; ?> - - - - priority === -1) : ?> - - priority === 0) : ?> - - priority === 1) : ?> - - - - id; ?> -
- - pagination->getListFooter(); - - // Modal for test runs - $modalparams = [ - 'title' => '', - ]; - - $modalbody = '
'; - - echo HTMLHelper::_('bootstrap.renderModal', 'scheduler-test-modal', $modalparams, $modalbody); - - ?> - - - - - - -
+ id="adminForm"> +
+ $this)); + ?> + + + items)) : ?> + +
+ + +
+ + + + items)) : ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true" > + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_scheduler'); + $canEdit = $user->authorise('core.edit', 'com_scheduler'); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_scheduler') && $canCheckin; + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + state, $i, 'tasks.', $canChange); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'tasks.', $canCheckin); ?> + + locked) : ?> + $canChange, 'prefix' => 'tasks.', + 'active_class' => 'none fa fa-running border-dark text-body', + 'inactive_class' => 'none fa fa-running', 'tip' => true, 'translate' => false, + 'active_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), + 'inactive_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), + ]); ?> + + + + escape($item->title); ?> + + + escape($item->title); ?> + + last_exit_code, [Status::OK, Status::WILL_RESUME])) : ?> + +
+ last_exit_code); ?> +
+ +
+ + note) : ?> + + escape($item->note)); ?> + + +
+ escape($item->safeTypeTitle); ?> + + last_execution ? HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5') : '-'; ?> + + + + priority === -1) : ?> + + priority === 0) : ?> + + priority === 1) : ?> + + + + id; ?> +
+ + pagination->getListFooter(); + + // Modal for test runs + $modalparams = [ + 'title' => '', + ]; + + $modalbody = '
'; + + echo HTMLHelper::_('bootstrap.renderModal', 'scheduler-test-modal', $modalparams, $modalbody); + + ?> + + + + + + +
diff --git a/code/administrator/components/com_scheduler/tmpl/tasks/empty_state.php b/code/administrator/components/com_scheduler/tmpl/tasks/empty_state.php index adbcc388..96e2eb09 100644 --- a/code/administrator/components/com_scheduler/tmpl/tasks/empty_state.php +++ b/code/administrator/components/com_scheduler/tmpl/tasks/empty_state.php @@ -1,4 +1,5 @@ 'COM_SCHEDULER', - 'formURL' => 'index.php?option=com_scheduler&task=task.add', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J4.x:Task_Scheduler', - 'icon' => 'icon-clock clock', + 'textPrefix' => 'COM_SCHEDULER', + 'formURL' => 'index.php?option=com_scheduler&task=task.add', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J4.x:Task_Scheduler', + 'icon' => 'icon-clock clock', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_scheduler')) -{ - $displayData['createURL'] = 'index.php?option=com_scheduler&view=select&layout=default'; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_scheduler')) { + $displayData['createURL'] = 'index.php?option=com_scheduler&view=select&layout=default'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_tags/config.xml b/code/administrator/components/com_tags/config.xml index edb4be41..244026d9 100644 --- a/code/administrator/components/com_tags/config.xml +++ b/code/administrator/components/com_tags/config.xml @@ -358,6 +358,7 @@ name="tag_field_ajax_mode" type="radio" label="COM_TAGS_TAG_FIELD_MODE_LABEL" + description="COM_TAGS_TAG_FIELD_MODE_DESC" layout="joomla.form.field.radio.switcher" default="1" > diff --git a/code/administrator/components/com_tags/services/provider.php b/code/administrator/components/com_tags/services/provider.php index 669a7013..6352e87a 100644 --- a/code/administrator/components/com_tags/services/provider.php +++ b/code/administrator/components/com_tags/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Tags')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Tags')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Tags')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new TagsComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Tags')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Tags')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Tags')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new TagsComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_tags/src/Controller/DisplayController.php b/code/administrator/components/com_tags/src/Controller/DisplayController.php index 366c926e..43eb72d6 100644 --- a/code/administrator/components/com_tags/src/Controller/DisplayController.php +++ b/code/administrator/components/com_tags/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'tags'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 3.1 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'tags'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'tag' && $layout == 'edit' && !$this->checkEditId('com_tags.edit.tag', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'tag' && $layout == 'edit' && !$this->checkEditId('com_tags.edit.tag', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_tags&view=tags', false)); + $this->setRedirect(Route::_('index.php?option=com_tags&view=tags', false)); - return false; - } + return false; + } - parent::display(); + parent::display(); - return $this; - } + return $this; + } } diff --git a/code/administrator/components/com_tags/src/Controller/TagController.php b/code/administrator/components/com_tags/src/Controller/TagController.php index 1abe0e5f..763ca549 100644 --- a/code/administrator/components/com_tags/src/Controller/TagController.php +++ b/code/administrator/components/com_tags/src/Controller/TagController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Tags\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Tags\Administrator\Controller; use Joomla\CMS\MVC\Controller\FormController; use Joomla\CMS\Versioning\VersionableControllerTrait; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * The Tag Controller * @@ -20,57 +24,57 @@ */ class TagController extends FormController { - use VersionableControllerTrait; + use VersionableControllerTrait; - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 3.1 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', 'com_tags'); - } + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 3.1 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', 'com_tags'); + } - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.1 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Since there is no asset tracking and no categories, revert to the component permissions. - return parent::allowEdit($data, $key); - } + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.1 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Since there is no asset tracking and no categories, revert to the component permissions. + return parent::allowEdit($data, $key); + } - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 3.1 - */ - public function batch($model = null) - { - $this->checkToken(); + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 3.1 + */ + public function batch($model = null) + { + $this->checkToken(); - // Set the model - $model = $this->getModel('Tag'); + // Set the model + $model = $this->getModel('Tag'); - // Preset the redirect - $this->setRedirect('index.php?option=com_tags&view=tags'); + // Preset the redirect + $this->setRedirect('index.php?option=com_tags&view=tags'); - return parent::batch($model); - } + return parent::batch($model); + } } diff --git a/code/administrator/components/com_tags/src/Controller/TagsController.php b/code/administrator/components/com_tags/src/Controller/TagsController.php index 78c4fefd..a0ccbadf 100644 --- a/code/administrator/components/com_tags/src/Controller/TagsController.php +++ b/code/administrator/components/com_tags/src/Controller/TagsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Rebuild the nested set tree. - * - * @return boolean False on failure or error, true on success. - * - * @since 3.1 - */ - public function rebuild() - { - $this->checkToken(); - - $this->setRedirect(Route::_('index.php?option=com_tags&view=tags', false)); - - /** @var \Joomla\Component\Tags\Administrator\Model\TagModel $model */ - $model = $this->getModel(); - - if ($model->rebuild()) - { - // Rebuild succeeded. - $this->setMessage(Text::_('COM_TAGS_REBUILD_SUCCESS')); - - return true; - } - else - { - // Rebuild failed. - $this->setMessage(Text::_('COM_TAGS_REBUILD_FAILURE')); - - return false; - } - } - - /** - * Method to get the JSON-encoded amount of published tags for quickicons - * - * @return void - * - * @since 4.1.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('tags'); - - $model->setState('filter.published', 1); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_TAGS_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_TAGS_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config An optional associative array of configuration settings. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 3.1 + */ + public function getModel($name = 'Tag', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Rebuild the nested set tree. + * + * @return boolean False on failure or error, true on success. + * + * @since 3.1 + */ + public function rebuild() + { + $this->checkToken(); + + $this->setRedirect(Route::_('index.php?option=com_tags&view=tags', false)); + + /** @var \Joomla\Component\Tags\Administrator\Model\TagModel $model */ + $model = $this->getModel(); + + if ($model->rebuild()) { + // Rebuild succeeded. + $this->setMessage(Text::_('COM_TAGS_REBUILD_SUCCESS')); + + return true; + } else { + // Rebuild failed. + $this->setMessage(Text::_('COM_TAGS_REBUILD_FAILURE')); + + return false; + } + } + + /** + * Method to get the JSON-encoded amount of published tags for quickicons + * + * @return void + * + * @since 4.1.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('tags'); + + $model->setState('filter.published', 1); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_TAGS_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_TAGS_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } } diff --git a/code/administrator/components/com_tags/src/Extension/TagsComponent.php b/code/administrator/components/com_tags/src/Extension/TagsComponent.php index 4ca170b8..50e2551f 100644 --- a/code/administrator/components/com_tags/src/Extension/TagsComponent.php +++ b/code/administrator/components/com_tags/src/Extension/TagsComponent.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage', - ); - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 3.1 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - return parent::canDelete($record); - } - - /** - * Auto-populate the model state. - * - * @note Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.1 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - $parentId = $app->input->getInt('parent_id'); - $this->setState('tag.parent_id', $parentId); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState($this->getName() . '.id', $pk); - - // Load the parameters. - $params = ComponentHelper::getParams('com_tags'); - $this->setState('params', $params); - } - - /** - * Method to get a tag. - * - * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. - * - * @return mixed Tag data object on success, false on failure. - * - * @since 3.1 - */ - public function getItem($pk = null) - { - if ($result = parent::getItem($pk)) - { - // Prime required properties. - if (empty($result->id)) - { - $result->parent_id = $this->getState('tag.parent_id'); - } - - // Convert the metadata field to an array. - $registry = new Registry($result->metadata); - $result->metadata = $registry->toArray(); - - // Convert the images field to an array. - $registry = new Registry($result->images); - $result->images = $registry->toArray(); - - // Convert the urls field to an array. - $registry = new Registry($result->urls); - $result->urls = $registry->toArray(); - - // Convert the modified date to local user time for display in the form. - $tz = new \DateTimeZone(Factory::getApplication()->get('offset')); - - if ((int) $result->modified_time) - { - $date = new Date($result->modified_time); - $date->setTimezone($tz); - $result->modified_time = $date->toSql(true); - } - else - { - $result->modified_time = null; - } - } - - return $result; - } - - /** - * Method to get the row form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return bool|\Joomla\CMS\Form\Form A Form object on success, false on failure - * - * @since 3.1 - */ - public function getForm($data = array(), $loadData = true) - { - $jinput = Factory::getApplication()->input; - - // Get the form. - $form = $this->loadForm('com_tags.tag', 'tag', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - $user = Factory::getUser(); - - if (!$user->authorise('core.edit.state', 'com_tags' . $jinput->get('id'))) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 3.1 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_tags.edit.tag.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_tags.tag', $data); - - return $data; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function save($data) - { - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - $table = $this->getTable(); - $input = Factory::getApplication()->input; - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); - $isNew = true; - $context = $this->option . '.' . $this->name; - - // Include the plugins for the save events. - PluginHelper::importPlugin($this->events_map['save']); - - try - { - // Load the row if saving an existing tag. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - // Set the new parent id if parent id not matched OR while New/Save as Copy . - if ($table->parent_id != $data['parent_id'] || $data['id'] == 0) - { - $table->setLocation($data['parent_id'], 'last-child'); - } - - // Alter the title for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['title'] == $origTable->title) - { - list($title, $alias) = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); - $data['title'] = $title; - $data['alias'] = $alias; - } - elseif ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - - $data['published'] = 0; - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Prepare the row for saving - $this->prepareTable($table); - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, $table, $isNew, $data)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, $table, $isNew)); - - // Rebuild the path for the tag: - if (!$table->rebuildPath($table->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Rebuild the paths of the tag's children: - if (!$table->rebuild($table->id, $table->lft, $table->level, $table->path)) - { - $this->setError($table->getError()); - - return false; - } - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $this->setState($this->getName() . '.id', $table->id); - $this->setState($this->getName() . '.new', $isNew); - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Prepare and sanitise the table data prior to saving. - * - * @param \Joomla\CMS\Table\Table $table A Table object. - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - // Increment the content version number. - $table->version++; - } - - /** - * Method rebuild the entire nested set tree. - * - * @return boolean False on failure or error, true otherwise. - * - * @since 3.1 - */ - public function rebuild() - { - // Get an instance of the table object. - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - - $table = $this->getTable(); - - if (!$table->rebuild()) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to save the reordered nested set tree. - * First we save the new order values in the lft values of the changed ids. - * Then we invoke the table rebuild to implement the new ordering. - * - * @param array $idArray An array of primary key ids. - * @param integer $lftArray The lft value - * - * @return boolean False on failure or error, True otherwise - * - * @since 3.1 - */ - public function saveorder($idArray = null, $lftArray = null) - { - // Get an instance of the table object. - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - - $table = $this->getTable(); - - if (!$table->saveorder($idArray, $lftArray)) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the title & alias. - * - * @param integer $parentId The id of the parent. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 3.1 - */ - protected function generateNewTitle($parentId, $alias, $title) - { - // Alter the title & alias - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - - $table = $this->getTable(); - - while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) - { - $title = ($table->title != $title) ? $title : StringHelper::increment($title); - $alias = StringHelper::increment($alias, 'dash'); - } - - return array($title, $alias); - } + use VersionableModelTrait; + + /** + * @var string The prefix to use with controller messages. + * @since 3.1 + */ + protected $text_prefix = 'COM_TAGS'; + + /** + * @var string The type alias for this content type. + * @since 3.2 + */ + public $typeAlias = 'com_tags.tag'; + + /** + * Allowed batch commands + * + * @var array + * @since 3.7.0 + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage', + ); + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 3.1 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + return parent::canDelete($record); + } + + /** + * Auto-populate the model state. + * + * @note Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.1 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + $parentId = $app->input->getInt('parent_id'); + $this->setState('tag.parent_id', $parentId); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState($this->getName() . '.id', $pk); + + // Load the parameters. + $params = ComponentHelper::getParams('com_tags'); + $this->setState('params', $params); + } + + /** + * Method to get a tag. + * + * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. + * + * @return mixed Tag data object on success, false on failure. + * + * @since 3.1 + */ + public function getItem($pk = null) + { + if ($result = parent::getItem($pk)) { + // Prime required properties. + if (empty($result->id)) { + $result->parent_id = $this->getState('tag.parent_id'); + } + + // Convert the metadata field to an array. + $registry = new Registry($result->metadata); + $result->metadata = $registry->toArray(); + + // Convert the images field to an array. + $registry = new Registry($result->images); + $result->images = $registry->toArray(); + + // Convert the urls field to an array. + $registry = new Registry($result->urls); + $result->urls = $registry->toArray(); + + // Convert the modified date to local user time for display in the form. + $tz = new \DateTimeZone(Factory::getApplication()->get('offset')); + + if ((int) $result->modified_time) { + $date = new Date($result->modified_time); + $date->setTimezone($tz); + $result->modified_time = $date->toSql(true); + } else { + $result->modified_time = null; + } + } + + return $result; + } + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return bool|\Joomla\CMS\Form\Form A Form object on success, false on failure + * + * @since 3.1 + */ + public function getForm($data = array(), $loadData = true) + { + $jinput = Factory::getApplication()->input; + + // Get the form. + $form = $this->loadForm('com_tags.tag', 'tag', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + $user = Factory::getUser(); + + if (!$user->authorise('core.edit.state', 'com_tags' . $jinput->get('id'))) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 3.1 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_tags.edit.tag.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_tags.tag', $data); + + return $data; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function save($data) + { + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + $table = $this->getTable(); + $input = Factory::getApplication()->input; + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); + $isNew = true; + $context = $this->option . '.' . $this->name; + + // Include the plugins for the save events. + PluginHelper::importPlugin($this->events_map['save']); + + try { + // Load the row if saving an existing tag. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + // Set the new parent id if parent id not matched OR while New/Save as Copy . + if ($table->parent_id != $data['parent_id'] || $data['id'] == 0) { + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Alter the title for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['title'] == $origTable->title) { + list($title, $alias) = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); + $data['title'] = $title; + $data['alias'] = $alias; + } elseif ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + + $data['published'] = 0; + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, $table, $isNew, $data)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, $table, $isNew)); + + // Rebuild the path for the tag: + if (!$table->rebuildPath($table->id)) { + $this->setError($table->getError()); + + return false; + } + + // Rebuild the paths of the tag's children: + if (!$table->rebuild($table->id, $table->lft, $table->level, $table->path)) { + $this->setError($table->getError()); + + return false; + } + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->setState($this->getName() . '.id', $table->id); + $this->setState($this->getName() . '.new', $isNew); + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Prepare and sanitise the table data prior to saving. + * + * @param \Joomla\CMS\Table\Table $table A Table object. + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + // Increment the content version number. + $table->version++; + } + + /** + * Method rebuild the entire nested set tree. + * + * @return boolean False on failure or error, true otherwise. + * + * @since 3.1 + */ + public function rebuild() + { + // Get an instance of the table object. + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + + $table = $this->getTable(); + + if (!$table->rebuild()) { + $this->setError($table->getError()); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to save the reordered nested set tree. + * First we save the new order values in the lft values of the changed ids. + * Then we invoke the table rebuild to implement the new ordering. + * + * @param array $idArray An array of primary key ids. + * @param integer $lftArray The lft value + * + * @return boolean False on failure or error, True otherwise + * + * @since 3.1 + */ + public function saveorder($idArray = null, $lftArray = null) + { + // Get an instance of the table object. + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + + $table = $this->getTable(); + + if (!$table->saveorder($idArray, $lftArray)) { + $this->setError($table->getError()); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the title & alias. + * + * @param integer $parentId The id of the parent. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 3.1 + */ + protected function generateNewTitle($parentId, $alias, $title) + { + // Alter the title & alias + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + + $table = $this->getTable(); + + while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) { + $title = ($table->title != $title) ? $title : StringHelper::increment($title); + $alias = StringHelper::increment($alias, 'dash'); + } + + return array($title, $alias); + } } diff --git a/code/administrator/components/com_tags/src/Model/TagsModel.php b/code/administrator/components/com_tags/src/Model/TagsModel.php index 1b08eaeb..782d3b08 100644 --- a/code/administrator/components/com_tags/src/Model/TagsModel.php +++ b/code/administrator/components/com_tags/src/Model/TagsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', 'cmd'); - - $this->setState('filter.extension', $extension); - $parts = explode('.', $extension); - - // Extract the component name - $this->setState('filter.component', $parts[0]); - - // Extract the optional section name - $this->setState('filter.section', (count($parts) > 1) ? $parts[1] : null); - - // Load the parameters. - $params = ComponentHelper::getParams('com_tags'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 3.1 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.level'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.language'); - - return parent::getStoreId($id); - } - - /** - * Method to create a query for a list of items. - * - * @return string - * - * @since 3.1 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.title, a.alias, a.note, a.published, a.access, a.description' . - ', a.checked_out, a.checked_out_time, a.created_user_id' . - ', a.path, a.parent_id, a.level, a.lft, a.rgt' . - ', a.language' - ) - ); - $query->from($db->quoteName('#__tags', 'a')) - ->where($db->quoteName('a.alias') . ' <> ' . $db->quote('root')); - - // Join over the language - $query->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - ] - ) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Join over the users for the author. - $query->select($db->quoteName('ua.name', 'author_name')) - ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_user_id')) - ->select($db->quoteName('ug.title', 'access_title')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ug'), $db->quoteName('ug.id') . ' = ' . $db->quoteName('a.access')); - - // Count Items - $subQueryCountTaggedItems = $db->getQuery(true); - $subQueryCountTaggedItems - ->select('COUNT(' . $db->quoteName('tag_map.content_item_id') . ')') - ->from($db->quoteName('#__contentitem_tag_map', 'tag_map')) - ->where($db->quoteName('tag_map.tag_id') . ' = ' . $db->quoteName('a.id')); - $query->select('(' . (string) $subQueryCountTaggedItems . ') AS ' . $db->quoteName('countTaggedItems')); - - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('a.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - // Filter by published state - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->whereIn($db->quoteName('a.published'), [0, 1]); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.title') . ' LIKE :title', - $db->quoteName('a.alias') . ' LIKE :alias', - $db->quoteName('a.note') . ' LIKE :note', - - ], - 'OR' - ); - $query->bind(':title', $search) - ->bind(':alias', $search) - ->bind(':note', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Add the list ordering clause - $listOrdering = $this->getState('list.ordering', 'a.lft'); - $listDirn = $db->escape($this->getState('list.direction', 'ASC')); - - if ($listOrdering == 'a.access') - { - $query->order('a.access ' . $listDirn . ', a.lft ' . $listDirn); - } - else - { - $query->order($db->escape($listOrdering) . ' ' . $listDirn); - } - - return $query; - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.0.1 - */ - public function getItems() - { - $items = parent::getItems(); - - if ($items != false) - { - $extension = $this->getState('filter.extension'); - - $this->countItems($items, $extension); - } - - return $items; - } - - /** - * Method to load the countItems method from the extensions - * - * @param \stdClass[] &$items The category items - * @param string $extension The category extension - * - * @return void - * - * @since 3.5 - */ - public function countItems(&$items, $extension) - { - $parts = explode('.', $extension); - - if (count($parts) < 2) - { - return; - } - - $component = Factory::getApplication()->bootComponent($parts[0]); - - if ($component instanceof TagServiceInterface) - { - $component->countTagItems($items, $extension); - } - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $query->where($this->_db->quoteName('alias') . ' != ' . $this->_db->quote('root')); - - return $query; - } + /** + * Constructor. + * + * @param MVCFactoryInterface $factory The factory. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', + 'a.id', + 'title', + 'a.title', + 'alias', + 'a.alias', + 'published', + 'a.published', + 'access', + 'a.access', + 'access_level', + 'language', + 'a.language', + 'checked_out', + 'a.checked_out', + 'checked_out_time', + 'a.checked_out_time', + 'created_time', + 'a.created_time', + 'created_user_id', + 'a.created_user_id', + 'lft', + 'a.lft', + 'rgt', + 'a.rgt', + 'level', + 'a.level', + 'path', + 'a.path', + 'countTaggedItems', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.1 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $extension = $this->getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', 'cmd'); + + $this->setState('filter.extension', $extension); + $parts = explode('.', $extension); + + // Extract the component name + $this->setState('filter.component', $parts[0]); + + // Extract the optional section name + $this->setState('filter.section', (count($parts) > 1) ? $parts[1] : null); + + // Load the parameters. + $params = ComponentHelper::getParams('com_tags'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 3.1 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.level'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.language'); + + return parent::getStoreId($id); + } + + /** + * Method to create a query for a list of items. + * + * @return string + * + * @since 3.1 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.title, a.alias, a.note, a.published, a.access, a.description' . + ', a.checked_out, a.checked_out_time, a.created_user_id' . + ', a.path, a.parent_id, a.level, a.lft, a.rgt' . + ', a.language' + ) + ); + $query->from($db->quoteName('#__tags', 'a')) + ->where($db->quoteName('a.alias') . ' <> ' . $db->quote('root')); + + // Join over the language + $query->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + ] + ) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Join over the users for the author. + $query->select($db->quoteName('ua.name', 'author_name')) + ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_user_id')) + ->select($db->quoteName('ug.title', 'access_title')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ug'), $db->quoteName('ug.id') . ' = ' . $db->quoteName('a.access')); + + // Count Items + $subQueryCountTaggedItems = $db->getQuery(true); + $subQueryCountTaggedItems + ->select('COUNT(' . $db->quoteName('tag_map.content_item_id') . ')') + ->from($db->quoteName('#__contentitem_tag_map', 'tag_map')) + ->where($db->quoteName('tag_map.tag_id') . ' = ' . $db->quoteName('a.id')); + $query->select('(' . (string) $subQueryCountTaggedItems . ') AS ' . $db->quoteName('countTaggedItems')); + + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('a.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + // Filter by published state + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->whereIn($db->quoteName('a.published'), [0, 1]); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.title') . ' LIKE :title', + $db->quoteName('a.alias') . ' LIKE :alias', + $db->quoteName('a.note') . ' LIKE :note', + + ], + 'OR' + ); + $query->bind(':title', $search) + ->bind(':alias', $search) + ->bind(':note', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Add the list ordering clause + $listOrdering = $this->getState('list.ordering', 'a.lft'); + $listDirn = $db->escape($this->getState('list.direction', 'ASC')); + + if ($listOrdering == 'a.access') { + $query->order('a.access ' . $listDirn . ', a.lft ' . $listDirn); + } else { + $query->order($db->escape($listOrdering) . ' ' . $listDirn); + } + + return $query; + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.0.1 + */ + public function getItems() + { + $items = parent::getItems(); + + if ($items != false) { + $extension = $this->getState('filter.extension'); + + $this->countItems($items, $extension); + } + + return $items; + } + + /** + * Method to load the countItems method from the extensions + * + * @param \stdClass[] &$items The category items + * @param string $extension The category extension + * + * @return void + * + * @since 3.5 + */ + public function countItems(&$items, $extension) + { + $parts = explode('.', $extension); + + if (count($parts) < 2) { + return; + } + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof TagServiceInterface) { + $component->countTagItems($items, $extension); + } + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $db = $this->getDatabase(); + + $query->where($db->quoteName('alias') . ' != ' . $db->quote('root')); + + return $query; + } } diff --git a/code/administrator/components/com_tags/src/Table/TagTable.php b/code/administrator/components/com_tags/src/Table/TagTable.php index 5e65a182..ec039287 100644 --- a/code/administrator/components/com_tags/src/Table/TagTable.php +++ b/code/administrator/components/com_tags/src/Table/TagTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_tags.tag'; - - parent::__construct('#__tags', 'id', $db); - } - - /** - * Overloaded check method to ensure data integrity. - * - * @return boolean True on success. - * - * @since 3.1 - * @throws \UnexpectedValueException - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Check for valid name. - if (trim($this->title) == '') - { - throw new \UnexpectedValueException('The title is empty'); - } - - if (empty($this->alias)) - { - $this->alias = $this->title; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); - - if (trim(str_replace('-', '', $this->alias)) == '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - // Check the publish down date is not earlier than publish up. - if (!empty($this->publish_down) && !empty($this->publish_up) && $this->publish_down < $this->publish_up) - { - throw new \UnexpectedValueException('End publish date is before start publish date.'); - } - - // Clean up description -- eliminate quotes and <> brackets - if (!empty($this->metadesc)) - { - // Only process if not empty - $bad_characters = array("\"", '<', '>'); - $this->metadesc = StringHelper::str_ireplace($bad_characters, '', $this->metadesc); - } - - if (empty($this->path)) - { - $this->path = ''; - } - - if (empty($this->hits)) - { - $this->hits = 0; - } - - if (empty($this->params)) - { - $this->params = '{}'; - } - - if (empty($this->metadesc)) - { - $this->metadesc = ''; - } - - if (empty($this->metakey)) - { - $this->metakey = ''; - } - - if (empty($this->metadata)) - { - $this->metadata = '{}'; - } - - if (empty($this->urls)) - { - $this->urls = '{}'; - } - - if (empty($this->images)) - { - $this->images = '{}'; - } - - if (!(int) $this->checked_out_time) - { - $this->checked_out_time = null; - } - - if (!(int) $this->publish_up) - { - $this->publish_up = null; - } - - if (!(int) $this->publish_down) - { - $this->publish_down = null; - } - - return true; - } - - /** - * Overridden \JTable::store to set modified data and user id. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - if ($this->id) - { - // Existing item - $this->modified_user_id = $user->get('id'); - $this->modified_time = $date->toSql(); - } - else - { - // New tag. A tag created and created_by field can be set by the user, - // so we don't touch either of these if they are set. - if (!(int) $this->created_time) - { - $this->created_time = $date->toSql(); - } - - if (empty($this->created_user_id)) - { - $this->created_user_id = $user->get('id'); - } - - if (!(int) $this->modified_time) - { - $this->modified_time = $this->created_time; - } - - if (empty($this->modified_user_id)) - { - $this->modified_user_id = $this->created_user_id; - } - } - - // Verify that the alias is unique - $table = new static($this->getDbo()); - - if ($table->load(array('alias' => $this->alias)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_TAGS_ERROR_UNIQUE_ALIAS')); - - return false; - } - - return parent::store($updateNulls); - } - - /** - * Method to delete a node and, optionally, its child nodes from the table. - * - * @param integer $pk The primary key of the node to delete. - * @param boolean $children True to delete child nodes, false to move them up a level. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function delete($pk = null, $children = false) - { - $return = parent::delete($pk, $children); - - if ($return) - { - $helper = new TagsHelper; - $helper->tagDeleteInstances($pk); - } - - return $return; - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + /** + * An array of key names to be json encoded in the bind function + * + * @var array + * @since 4.0.0 + */ + protected $_jsonEncode = ['params', 'metadata', 'urls', 'images']; + + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Constructor + * + * @param DatabaseDriver $db A database connector object + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_tags.tag'; + + parent::__construct('#__tags', 'id', $db); + } + + /** + * Overloaded check method to ensure data integrity. + * + * @return boolean True on success. + * + * @since 3.1 + * @throws \UnexpectedValueException + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Check for valid name. + if (trim($this->title) == '') { + throw new \UnexpectedValueException('The title is empty'); + } + + if (empty($this->alias)) { + $this->alias = $this->title; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); + + if (trim(str_replace('-', '', $this->alias)) == '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + // Check the publish down date is not earlier than publish up. + if (!empty($this->publish_down) && !empty($this->publish_up) && $this->publish_down < $this->publish_up) { + throw new \UnexpectedValueException('End publish date is before start publish date.'); + } + + // Clean up description -- eliminate quotes and <> brackets + if (!empty($this->metadesc)) { + // Only process if not empty + $bad_characters = array("\"", '<', '>'); + $this->metadesc = StringHelper::str_ireplace($bad_characters, '', $this->metadesc); + } + + if (empty($this->path)) { + $this->path = ''; + } + + if (empty($this->hits)) { + $this->hits = 0; + } + + if (empty($this->params)) { + $this->params = '{}'; + } + + if (empty($this->metadesc)) { + $this->metadesc = ''; + } + + if (empty($this->metakey)) { + $this->metakey = ''; + } + + if (empty($this->metadata)) { + $this->metadata = '{}'; + } + + if (empty($this->urls)) { + $this->urls = '{}'; + } + + if (empty($this->images)) { + $this->images = '{}'; + } + + if (!(int) $this->checked_out_time) { + $this->checked_out_time = null; + } + + if (!(int) $this->publish_up) { + $this->publish_up = null; + } + + if (!(int) $this->publish_down) { + $this->publish_down = null; + } + + return true; + } + + /** + * Overridden \JTable::store to set modified data and user id. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + if ($this->id) { + // Existing item + $this->modified_user_id = $user->get('id'); + $this->modified_time = $date->toSql(); + } else { + // New tag. A tag created and created_by field can be set by the user, + // so we don't touch either of these if they are set. + if (!(int) $this->created_time) { + $this->created_time = $date->toSql(); + } + + if (empty($this->created_user_id)) { + $this->created_user_id = $user->get('id'); + } + + if (!(int) $this->modified_time) { + $this->modified_time = $this->created_time; + } + + if (empty($this->modified_user_id)) { + $this->modified_user_id = $this->created_user_id; + } + } + + // Verify that the alias is unique + $table = new static($this->getDbo()); + + if ($table->load(array('alias' => $this->alias)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_TAGS_ERROR_UNIQUE_ALIAS')); + + return false; + } + + return parent::store($updateNulls); + } + + /** + * Method to delete a node and, optionally, its child nodes from the table. + * + * @param integer $pk The primary key of the node to delete. + * @param boolean $children True to delete child nodes, false to move them up a level. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function delete($pk = null, $children = false) + { + $return = parent::delete($pk, $children); + + if ($return) { + $helper = new TagsHelper(); + $helper->tagDeleteInstances($pk); + } + + return $return; + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/code/administrator/components/com_tags/src/View/Tag/HtmlView.php b/code/administrator/components/com_tags/src/View/Tag/HtmlView.php index d7a903ed..55f925b2 100644 --- a/code/administrator/components/com_tags/src/View/Tag/HtmlView.php +++ b/code/administrator/components/com_tags/src/View/Tag/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @since 3.1 - * - * @return void - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = Factory::getUser(); - $userId = $user->get('id'); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - $canDo = ContentHelper::getActions('com_tags'); - - ToolbarHelper::title($isNew ? Text::_('COM_TAGS_MANAGER_TAG_NEW') : Text::_('COM_TAGS_MANAGER_TAG_EDIT'), 'tag'); - - // Build the actions for new and existing records. - if ($isNew) - { - ToolbarHelper::apply('tag.apply'); - ToolbarHelper::saveGroup( - [ - ['save', 'tag.save'], - ['save2new', 'tag.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('tag.cancel'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_user_id == $userId); - - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - ToolbarHelper::apply('tag.apply'); - $toolbarButtons[] = ['save', 'tag.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'tag.save2new']; - } - } - - // If checked out, we can still save - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'tag.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('tag.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) - { - ToolbarHelper::versions('com_tags.tag', $this->item->id); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Tags:_New_or_Edit'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * Flag if an association exists + * + * @var boolean + */ + protected $assoc; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @since 3.1 + * + * @return void + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->get('id'); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + $canDo = ContentHelper::getActions('com_tags'); + + ToolbarHelper::title($isNew ? Text::_('COM_TAGS_MANAGER_TAG_NEW') : Text::_('COM_TAGS_MANAGER_TAG_EDIT'), 'tag'); + + // Build the actions for new and existing records. + if ($isNew) { + ToolbarHelper::apply('tag.apply'); + ToolbarHelper::saveGroup( + [ + ['save', 'tag.save'], + ['save2new', 'tag.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('tag.cancel'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_user_id == $userId); + + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + ToolbarHelper::apply('tag.apply'); + $toolbarButtons[] = ['save', 'tag.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'tag.save2new']; + } + } + + // If checked out, we can still save + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'tag.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('tag.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) { + ToolbarHelper::versions('com_tags.tag', $this->item->id); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Tags:_New_or_Edit'); + } } diff --git a/code/administrator/components/com_tags/src/View/Tags/HtmlView.php b/code/administrator/components/com_tags/src/View/Tags/HtmlView.php index 3616b1fb..57dd6903 100644 --- a/code/administrator/components/com_tags/src/View/Tags/HtmlView.php +++ b/code/administrator/components/com_tags/src/View/Tags/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Preprocess the list of items to find ordering divisions. - foreach ($this->items as &$item) - { - $this->ordering[$item->parent_id][] = $item->id; - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.1 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_tags'); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_TAGS_MANAGER_TAGS'), 'tags'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('tag.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('tags.publish')->listCheck(true); - $childBar->unpublish('tags.unpublish')->listCheck(true); - $childBar->archive('tags.archive')->listCheck(true); - } - - if ($user->authorise('core.admin')) - { - $childBar->checkin('tags.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - $childBar->trash('tags.trash')->listCheck(true); - } - - // Add a batch button - if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('tags.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_tags'); - } - - $toolbar->help('Tags'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Preprocess the list of items to find ordering divisions. + foreach ($this->items as &$item) { + $this->ordering[$item->parent_id][] = $item->id; + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.1 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_tags'); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_TAGS_MANAGER_TAGS'), 'tags'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('tag.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('tags.publish')->listCheck(true); + $childBar->unpublish('tags.unpublish')->listCheck(true); + $childBar->archive('tags.archive')->listCheck(true); + } + + if ($user->authorise('core.admin')) { + $childBar->checkin('tags.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + $childBar->trash('tags.trash')->listCheck(true); + } + + // Add a batch button + if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('tags.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_tags'); + } + + $toolbar->help('Tags'); + } } diff --git a/code/administrator/components/com_tags/tags.xml b/code/administrator/components/com_tags/tags.xml index e768dfb3..456eae23 100644 --- a/code/administrator/components/com_tags/tags.xml +++ b/code/administrator/components/com_tags/tags.xml @@ -2,7 +2,7 @@ com_tags Joomla! Project - December 2013 + 2013-12 (C) 2013 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_tags/tmpl/tag/edit.php b/code/administrator/components/com_tags/tmpl/tag/edit.php index d4cd2270..f451c3cb 100644 --- a/code/administrator/components/com_tags/tmpl/tag/edit.php +++ b/code/administrator/components/com_tags/tmpl/tag/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); // Fieldsets to not automatically render by /layouts/joomla/edit/params.php $this->ignore_fieldsets = ['jmetadata']; @@ -27,50 +28,50 @@
- + -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
-
- form->getLabel('description'); ?> - form->getInput('description'); ?> -
-
-
- -
-
- + +
+
+
+ form->getLabel('description'); ?> + form->getInput('description'); ?> +
+
+
+ +
+
+ - + - -
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ - -
- - + +
+ +
diff --git a/code/administrator/components/com_tags/tmpl/tags/default.php b/code/administrator/components/com_tags/tmpl/tags/default.php index 1ac9d077..c5cc7f25 100644 --- a/code/administrator/components/com_tags/tmpl/tags/default.php +++ b/code/administrator/components/com_tags/tmpl/tags/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -34,249 +36,241 @@ $section = null; $mode = false; -if (count($parts) > 1) -{ - $section = $parts[1]; - $inflector = Inflector::getInstance(); +if (count($parts) > 1) { + $section = $parts[1]; + $inflector = Inflector::getInstance(); - if (!$inflector->isPlural($section)) - { - $section = $inflector->toPlural($section); - } + if (!$inflector->isPlural($section)) { + $section = $inflector->toPlural($section); + } } -if ($section === 'categories') -{ - $mode = true; - $section = $component; - $component = 'com_categories'; +if ($section === 'categories') { + $mode = true; + $section = $component; + $component = 'com_categories'; } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_tags&task=tags.saveOrderAjax&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_tags&task=tags.saveOrderAjax&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
- $this)); - ?> - items)) : ?> -
- - -
- - - - - - - - - +
+ $this)); + ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - -
+ + + + + + + - items[0]) && property_exists($this->items[0], 'count_published')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_archived')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> - - + items[0]) && property_exists($this->items[0], 'count_published')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_archived')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> + + - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $orderkey = array_search($item->id, $this->ordering[$item->parent_id]); - $canCreate = $user->authorise('core.create', 'com_tags'); - $canEdit = $user->authorise('core.edit', 'com_tags'); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_tags') && $canCheckin; + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $orderkey = array_search($item->id, $this->ordering[$item->parent_id]); + $canCreate = $user->authorise('core.create', 'com_tags'); + $canEdit = $user->authorise('core.edit', 'com_tags'); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_tags') && $canCheckin; - // Get the parents of item for sorting - if ($item->level > 1) - { - $parentsStr = ''; - $_currentParentId = $item->parent_id; - $parentsStr = ' ' . $_currentParentId; - for ($j = 0; $j < $item->level; $j++) - { - foreach ($this->ordering as $k => $v) - { - $v = implode('-', $v); - $v = '-' . $v . '-'; - if (strpos($v, '-' . $_currentParentId . '-') !== false) - { - $parentsStr .= ' ' . $k; - $_currentParentId = $k; - break; - } - } - } - } - else - { - $parentsStr = ''; - } - ?> - - - - - + // Get the parents of item for sorting + if ($item->level > 1) { + $parentsStr = ''; + $_currentParentId = $item->parent_id; + $parentsStr = ' ' . $_currentParentId; + for ($j = 0; $j < $item->level; $j++) { + foreach ($this->ordering as $k => $v) { + $v = implode('-', $v); + $v = '-' . $v . '-'; + if (strpos($v, '-' . $_currentParentId . '-') !== false) { + $parentsStr .= ' ' . $k; + $_currentParentId = $k; + break; + } + } + } + } else { + $parentsStr = ''; + } + ?> + + + + + - items[0]) && property_exists($this->items[0], 'count_published')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_archived')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + - - - - - - - - + + + + + + + + - - - state->get('list.direction'), $this->state->get('list.ordering')); ?> - - - - -
+ + + state->get('list.direction'), $this->state->get('list.ordering')); ?> + + + + +
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - published, $i, 'tags.', $canChange); ?> - - $item->level)); ?> - checked_out) : ?> - editor, $item->checked_out_time, 'tags.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
-
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + published, $i, 'tags.', $canChange); ?> + + $item->level)); ?> + checked_out) : ?> + editor, $item->checked_out_time, 'tags.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
+
- - count_published; ?> - - - count_unpublished; ?> - - - count_archived; ?> - - - count_trashed; ?> - - escape($item->access_title); ?> - - - - - countTaggedItems; ?> - - - id; ?> -
+ items[0]) && property_exists($this->items[0], 'count_published')) : ?> + + + count_published; ?> + + + items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> + + + count_unpublished; ?> + + + items[0]) && property_exists($this->items[0], 'count_archived')) : ?> + + + count_archived; ?> + + + items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> + + + count_trashed; ?> + + + + escape($item->access_title); ?> + + + + + + + + + countTaggedItems; ?> + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_tags') - && $user->authorise('core.edit', 'com_tags') - && $user->authorise('core.edit.state', 'com_tags')) : ?> - Text::_('COM_TAGS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_tags') + && $user->authorise('core.edit', 'com_tags') + && $user->authorise('core.edit.state', 'com_tags') + ) : ?> + Text::_('COM_TAGS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - -
+ + + +
diff --git a/code/administrator/components/com_tags/tmpl/tags/default_batch_body.php b/code/administrator/components/com_tags/tmpl/tags/default_batch_body.php index bc82733e..cffa2753 100644 --- a/code/administrator/components/com_tags/tmpl/tags/default_batch_body.php +++ b/code/administrator/components/com_tags/tmpl/tags/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Multilanguage; @@ -15,18 +17,18 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
+
+ +
+
+ +
+
+ +
+
+ +
+
+
diff --git a/code/administrator/components/com_tags/tmpl/tags/default_batch_footer.php b/code/administrator/components/com_tags/tmpl/tags/default_batch_footer.php index a30292fe..c448ec1b 100644 --- a/code/administrator/components/com_tags/tmpl/tags/default_batch_footer.php +++ b/code/administrator/components/com_tags/tmpl/tags/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/code/administrator/components/com_tags/tmpl/tags/emptystate.php b/code/administrator/components/com_tags/tmpl/tags/emptystate.php index 04eb96ca..2b49d1b6 100644 --- a/code/administrator/components/com_tags/tmpl/tags/emptystate.php +++ b/code/administrator/components/com_tags/tmpl/tags/emptystate.php @@ -1,4 +1,5 @@ 'COM_TAGS', - 'formURL' => 'index.php?option=com_tags&task=tag.add', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J3.x:How_To_Use_Content_Tags_in_Joomla!', - 'icon' => 'icon-tags tags', + 'textPrefix' => 'COM_TAGS', + 'formURL' => 'index.php?option=com_tags&task=tag.add', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J3.x:How_To_Use_Content_Tags_in_Joomla!', + 'icon' => 'icon-tags tags', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_tags')) -{ - $displayData['createURL'] = 'index.php?option=com_tags&task=tag.add'; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_tags')) { + $displayData['createURL'] = 'index.php?option=com_tags&task=tag.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_templates/forms/source.xml b/code/administrator/components/com_templates/forms/source.xml index e2bd3e08..a2a79b39 100644 --- a/code/administrator/components/com_templates/forms/source.xml +++ b/code/administrator/components/com_templates/forms/source.xml @@ -43,8 +43,8 @@ layout="joomla.form.field.radio.switcher" default="0" > - - + +
- - + +
diff --git a/code/administrator/components/com_templates/helpers/template.php b/code/administrator/components/com_templates/helpers/template.php index 38115180..c928e289 100644 --- a/code/administrator/components/com_templates/helpers/template.php +++ b/code/administrator/components/com_templates/helpers/template.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Template Helper class. diff --git a/code/administrator/components/com_templates/helpers/templates.php b/code/administrator/components/com_templates/helpers/templates.php index 6ff30532..ea7c67d4 100644 --- a/code/administrator/components/com_templates/helpers/templates.php +++ b/code/administrator/components/com_templates/helpers/templates.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Templates component helper. diff --git a/code/administrator/components/com_templates/services/provider.php b/code/administrator/components/com_templates/services/provider.php index 737e8e5f..af2dff27 100644 --- a/code/administrator/components/com_templates/services/provider.php +++ b/code/administrator/components/com_templates/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Templates')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Templates')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Templates')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Templates')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new TemplatesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new TemplatesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_templates/src/Controller/DisplayController.php b/code/administrator/components/com_templates/src/Controller/DisplayController.php index d7eec744..27494044 100644 --- a/code/administrator/components/com_templates/src/Controller/DisplayController.php +++ b/code/administrator/components/com_templates/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'styles'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'styles'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // For JSON requests - if ($this->app->getDocument()->getType() == 'json') - { - return parent::display(); - } + // For JSON requests + if ($this->app->getDocument()->getType() == 'json') { + return parent::display(); + } - // Check for edit form. - if ($view == 'style' && $layout == 'edit' && !$this->checkEditId('com_templates.edit.style', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'style' && $layout == 'edit' && !$this->checkEditId('com_templates.edit.style', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_templates&view=styles', false)); + $this->setRedirect(Route::_('index.php?option=com_templates&view=styles', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/code/administrator/components/com_templates/src/Controller/StyleController.php b/code/administrator/components/com_templates/src/Controller/StyleController.php index 748af6bc..004899b2 100644 --- a/code/administrator/components/com_templates/src/Controller/StyleController.php +++ b/code/administrator/components/com_templates/src/Controller/StyleController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Templates\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Templates\Administrator\Controller; use Joomla\CMS\Form\Form; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\FormController; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Template style controller class. * @@ -21,135 +25,124 @@ */ class StyleController extends FormController { - /** - * The prefix to use with controller messages. - * - * @var string - * @since 1.6 - */ - protected $text_prefix = 'COM_TEMPLATES_STYLE'; - - /** - * Method to save a template style. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 1.6 - */ - public function save($key = null, $urlVar = null) - { - $this->checkToken(); - - if ($this->app->getDocument()->getType() === 'json') - { - $model = $this->getModel('Style', 'Administrator'); - $table = $model->getTable(); - $data = $this->input->post->get('params', array(), 'array'); - $checkin = $table->hasField('checked_out'); - $context = $this->option . '.edit.' . $this->context; - - $item = $model->getItem($this->app->getTemplate(true)->id); - - // Setting received params - $item->set('params', $data); - - $data = $item->getProperties(); - unset($data['xml']); - - $key = $table->getKeyName(); - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return false; - } - - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_templates/forms'); - - // Validate the posted data. - // Sometimes the form needs some posted data, such as for plugins and modules. - $form = $model->getForm($data, false); - - if (!$form) - { - $this->app->enqueueMessage($model->getError(), 'error'); - - return false; - } - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - return false; - } - - if (!isset($validData['tags'])) - { - $validData['tags'] = null; - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - $this->app->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - - return false; - } - - // Save succeeded, so check-in the record. - if ($checkin && $model->checkin($validData[$key]) === false) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - // Check-in failed, so go back to the record and display a notice. - $this->app->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); - - return false; - } - - // Redirect the user and adjust session state - // Set the record data in the session. - $recordId = $model->getState($this->context . '.id'); - $this->holdEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - $model->checkout($recordId); - - // Invoke the postSave method to allow for the child class to access the model. - $this->postSaveHook($model, $validData); + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_TEMPLATES_STYLE'; + + /** + * Method to save a template style. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 1.6 + */ + public function save($key = null, $urlVar = null) + { + $this->checkToken(); + + if ($this->app->getDocument()->getType() === 'json') { + $model = $this->getModel('Style', 'Administrator'); + $table = $model->getTable(); + $data = $this->input->post->get('params', array(), 'array'); + $checkin = $table->hasField('checked_out'); + $context = $this->option . '.edit.' . $this->context; + + $item = $model->getItem($this->app->getTemplate(true)->id); + + // Setting received params + $item->set('params', $data); + + $data = $item->getProperties(); + unset($data['xml']); + + $key = $table->getKeyName(); + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return false; + } + + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_templates/forms'); + + // Validate the posted data. + // Sometimes the form needs some posted data, such as for plugins and modules. + $form = $model->getForm($data, false); + + if (!$form) { + $this->app->enqueueMessage($model->getError(), 'error'); + + return false; + } + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + return false; + } + + if (!isset($validData['tags'])) { + $validData['tags'] = null; + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + $this->app->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + + return false; + } + + // Save succeeded, so check-in the record. + if ($checkin && $model->checkin($validData[$key]) === false) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + // Check-in failed, so go back to the record and display a notice. + $this->app->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); + + return false; + } + + // Redirect the user and adjust session state + // Set the record data in the session. + $recordId = $model->getState($this->context . '.id'); + $this->holdEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + $model->checkout($recordId); + + // Invoke the postSave method to allow for the child class to access the model. + $this->postSaveHook($model, $validData); - return true; - } + return true; + } - return parent::save($key, $urlVar); - } + return parent::save($key, $urlVar); + } } diff --git a/code/administrator/components/com_templates/src/Controller/StylesController.php b/code/administrator/components/com_templates/src/Controller/StylesController.php index 2a3afaa5..50d78c80 100644 --- a/code/administrator/components/com_templates/src/Controller/StylesController.php +++ b/code/administrator/components/com_templates/src/Controller/StylesController.php @@ -1,4 +1,5 @@ checkToken(); - - $pks = (array) $this->input->post->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $pks = array_filter($pks); - - try - { - if (empty($pks)) - { - throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); - } - - $model = $this->getModel(); - $model->duplicate($pks); - $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_DUPLICATED')); - } - catch (\Exception $e) - { - $this->app->enqueueMessage($e->getMessage(), 'error'); - } - - $this->setRedirect('index.php?option=com_templates&view=styles'); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return BaseDatabaseModel - * - * @since 1.6 - */ - public function getModel($name = 'Style', $prefix = 'Administrator', $config = array()) - { - return parent::getModel($name, $prefix, array('ignore_request' => true)); - } - - /** - * Method to set the home template for a client. - * - * @return void - * - * @since 1.6 - */ - public function setDefault() - { - // Check for request forgeries - $this->checkToken(); - - $pks = (array) $this->input->post->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $pks = array_filter($pks); - - try - { - if (empty($pks)) - { - throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); - } - - // Pop off the first element. - $id = array_shift($pks); - - /** @var \Joomla\Component\Templates\Administrator\Model\StyleModel $model */ - $model = $this->getModel(); - $model->setHome($id); - $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_HOME_SET')); - } - catch (\Exception $e) - { - $this->setMessage($e->getMessage(), 'warning'); - } - - $this->setRedirect('index.php?option=com_templates&view=styles'); - } - - /** - * Method to unset the default template for a client and for a language - * - * @return void - * - * @since 1.6 - */ - public function unsetDefault() - { - // Check for request forgeries - $this->checkToken('request'); - - $pks = (array) $this->input->get->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $pks = array_filter($pks); - - try - { - if (empty($pks)) - { - throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); - } - - // Pop off the first element. - $id = array_shift($pks); - - /** @var \Joomla\Component\Templates\Administrator\Model\StyleModel $model */ - $model = $this->getModel(); - $model->unsetHome($id); - $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_HOME_UNSET')); - } - catch (\Exception $e) - { - $this->setMessage($e->getMessage(), 'warning'); - } - - $this->setRedirect('index.php?option=com_templates&view=styles'); - } + /** + * Method to clone and existing template style. + * + * @return void + */ + public function duplicate() + { + // Check for request forgeries + $this->checkToken(); + + $pks = (array) $this->input->post->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $pks = array_filter($pks); + + try { + if (empty($pks)) { + throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); + } + + $model = $this->getModel(); + $model->duplicate($pks); + $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_DUPLICATED')); + } catch (\Exception $e) { + $this->app->enqueueMessage($e->getMessage(), 'error'); + } + + $this->setRedirect('index.php?option=com_templates&view=styles'); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return BaseDatabaseModel + * + * @since 1.6 + */ + public function getModel($name = 'Style', $prefix = 'Administrator', $config = array()) + { + return parent::getModel($name, $prefix, array('ignore_request' => true)); + } + + /** + * Method to set the home template for a client. + * + * @return void + * + * @since 1.6 + */ + public function setDefault() + { + // Check for request forgeries + $this->checkToken(); + + $pks = (array) $this->input->post->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $pks = array_filter($pks); + + try { + if (empty($pks)) { + throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); + } + + // Pop off the first element. + $id = array_shift($pks); + + /** @var \Joomla\Component\Templates\Administrator\Model\StyleModel $model */ + $model = $this->getModel(); + $model->setHome($id); + $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_HOME_SET')); + } catch (\Exception $e) { + $this->setMessage($e->getMessage(), 'warning'); + } + + $this->setRedirect('index.php?option=com_templates&view=styles'); + } + + /** + * Method to unset the default template for a client and for a language + * + * @return void + * + * @since 1.6 + */ + public function unsetDefault() + { + // Check for request forgeries + $this->checkToken('request'); + + $pks = (array) $this->input->get->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $pks = array_filter($pks); + + try { + if (empty($pks)) { + throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); + } + + // Pop off the first element. + $id = array_shift($pks); + + /** @var \Joomla\Component\Templates\Administrator\Model\StyleModel $model */ + $model = $this->getModel(); + $model->unsetHome($id); + $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_HOME_UNSET')); + } catch (\Exception $e) { + $this->setMessage($e->getMessage(), 'warning'); + } + + $this->setRedirect('index.php?option=com_templates&view=styles'); + } } diff --git a/code/administrator/components/com_templates/src/Controller/TemplateController.php b/code/administrator/components/com_templates/src/Controller/TemplateController.php index 15643a5c..a99ae5ae 100644 --- a/code/administrator/components/com_templates/src/Controller/TemplateController.php +++ b/code/administrator/components/com_templates/src/Controller/TemplateController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - $this->registerTask('unpublish', 'publish'); - $this->registerTask('publish', 'publish'); - $this->registerTask('deleteOverrideHistory', 'publish'); - } - - /** - * Method for closing the template. - * - * @return void - * - * @since 3.2 - */ - public function cancel() - { - $this->setRedirect(Route::_('index.php?option=com_templates&view=templates', false)); - } - - /** - * Method for closing a file. - * - * @return void - * - * @since 3.2 - */ - public function close() - { - $file = base64_encode('home'); - $id = (int) $this->input->get('id', 0, 'int'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . - $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - - /** - * Marked as Checked/Unchecked of override history. - * - * @return void - * - * @since 4.0.0 - */ - public function publish() - { - // Check for request forgeries. - $this->checkToken(); - - $file = $this->input->get('file'); - $id = $this->input->get('id'); - - $ids = (array) $this->input->get('cid', array(), 'string'); - $values = array('publish' => 1, 'unpublish' => 0, 'deleteOverrideHistory' => -3); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - if (empty($ids)) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_NO_FILE_SELECTED'), 'warning'); - } - else - { - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Change the state of the records. - if (!$model->publish($ids, $value, $id)) - { - $this->setMessage(implode('
', $model->getErrors()), 'warning'); - } - else - { - if ($value === 1) - { - $ntext = 'COM_TEMPLATES_N_OVERRIDE_CHECKED'; - } - elseif ($value === 0) - { - $ntext = 'COM_TEMPLATES_N_OVERRIDE_UNCHECKED'; - } - elseif ($value === -3) - { - $ntext = 'COM_TEMPLATES_N_OVERRIDE_DELETED'; - } - - $this->setMessage(Text::plural($ntext, count($ids))); - } - } - - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . - $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - - /** - * Method for copying the template. - * - * @return boolean true on success, false otherwise - * - * @since 3.2 - */ - public function copy() - { - // Check for request forgeries - $this->checkToken(); - - $app = $this->app; - $this->input->set('installtype', 'folder'); - $newNameRaw = $this->input->get('new_name', null, 'string'); - // Only accept letters, numbers and underscore for template name - $newName = preg_replace('/[^a-zA-Z0-9_]/', '', $newNameRaw); - $templateID = (int) $this->input->getInt('id', 0); - $file = (string) $this->input->get('file', '', 'cmd'); - - // Access check. - if (!$this->allowEdit()) - { - $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return false; - } - - $this->setRedirect('index.php?option=com_templates&view=template&id=' . $templateID . '&file=' . $file); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel('Template', 'Administrator'); - $model->setState('new_name', $newName); - $model->setState('tmp_prefix', uniqid('template_copy_')); - $model->setState('to_path', $app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); - - // Process only if we have a new name entered - if (strlen($newName) > 0) - { - if (!$this->app->getIdentity()->authorise('core.create', 'com_templates')) - { - // User is not authorised to delete - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_CREATE_NOT_PERMITTED'), 'error'); - - return false; - } - - // Check that new name is valid - if (($newNameRaw !== null) && ($newName !== $newNameRaw)) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); - - return false; - } - - // Check that new name doesn't already exist - if (!$model->checkNewName()) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_DUPLICATE_TEMPLATE_NAME'), 'error'); - - return false; - } - - // Check that from name does exist and get the folder name - $fromName = $model->getFromName(); - - if (!$fromName) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); - - return false; - } - - // Call model's copy method - if (!$model->copy()) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_COPY'), 'error'); - - return false; - } - - // Call installation model - $this->input->set('install_directory', $app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); - - /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $installModel */ - $installModel = $this->app->bootComponent('com_installer') - ->getMVCFactory()->createModel('Install', 'Administrator'); - $this->app->getLanguage()->load('com_installer'); - - if (!$installModel->install()) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_INSTALL'), 'error'); - - return false; - } - - $this->setMessage(Text::sprintf('COM_TEMPLATES_COPY_SUCCESS', $newName)); - $model->cleanup(); - - return true; - } - - $this->setMessage(Text::sprintf('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); - - return false; - } - - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional (note, the empty array is atypical compared to other models). - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 3.2 - */ - public function getModel($name = 'Template', $prefix = 'Administrator', $config = array()) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to check if you can add a new record. - * - * @return boolean - * - * @since 3.2 - */ - protected function allowEdit() - { - return $this->app->getIdentity()->authorise('core.admin'); - } - - /** - * Saves a template source file. - * - * @return void - * - * @since 3.2 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - $data = $this->input->post->get('jform', array(), 'array'); - $task = $this->getTask(); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $fileName = (string) $this->input->getCmd('file', ''); - $explodeArray = explode(':', str_replace('//', '/', base64_decode($fileName))); - - // Access check. - if (!$this->allowEdit()) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - // Match the stored id's with the submitted. - if (empty($data['extension_id']) || empty($data['filename'])) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); - - return; - } - elseif ($data['extension_id'] != $model->getState('extension.id')) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); - - return; - } - elseif (str_ends_with(end($explodeArray), Path::clean($data['filename'], '/'))) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); - - return; - } - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - $this->setMessage($model->getError(), 'error'); - - return; - } - - $data = $model->validate($form, $data); - - // Check for validation errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Redirect back to the edit screen. - $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - - return; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED', $model->getError()), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - - return; - } - - $this->setMessage(Text::_('COM_TEMPLATES_FILE_SAVE_SUCCESS')); - - // Redirect the user based on the chosen task. - switch ($task) - { - case 'apply': - // Redirect back to the edit screen. - $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - break; - - default: - // Redirect to the list screen. - $file = base64_encode('home'); - $id = (int) $this->input->get('id', 0, 'int'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - break; - } - } - - /** - * Method for creating override. - * - * @return void - * - * @since 3.2 - */ - public function overrides() - { - // Check for request forgeries. - $this->checkToken('get'); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $file = (string) $this->input->getCmd('file', ''); - $override = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('folder', '')), 'path'); - $id = (int) $this->input->get('id', 0, 'int'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - $model->createOverride($override); - - // Redirect back to the edit screen. - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - - /** - * Method for deleting a file. - * - * @return void - * - * @since 3.2 - */ - public function delete() - { - // Check for request forgeries - $this->checkToken(); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (base64_decode(urldecode($file)) == '/index.php') - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INDEX_DELETE'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif (base64_decode(urldecode($file)) == '/joomla.asset.json') - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_DELETE'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->deleteFile($file)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_DELETE_SUCCESS')); - $file = base64_encode('home'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_DELETE'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for creating a new file. - * - * @return void - * - * @since 3.2 - */ - public function createFile() - { - // Check for request forgeries - $this->checkToken(); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->get('file', '', 'cmd'); - $name = (string) $this->input->get('name', '', 'cmd'); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - $type = (string) $this->input->get('type', '', 'cmd'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if ($type == 'null') - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_TYPE'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $name)) - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->createFile($name, $type, $location)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_CREATE_SUCCESS')); - $file = urlencode(base64_encode($location . '/' . $name . '.' . $type)); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_CREATE'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for uploading a file. - * - * @return void - * - * @since 3.2 - */ - public function uploadFile() - { - // Check for request forgeries - $this->checkToken(); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $upload = $this->input->files->get('files'); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if ($return = $model->uploadFile($upload, $location)) - { - $this->setMessage(Text::sprintf('COM_TEMPLATES_FILE_UPLOAD_SUCCESS', $upload['name'])); - $redirect = base64_encode($return); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $redirect . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_UPLOAD'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for creating a new folder. - * - * @return void - * - * @since 3.2 - */ - public function createFolder() - { - // Check for request forgeries - $this->checkToken(); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $name = $this->input->get('name'); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (!preg_match('/^[a-zA-Z0-9-_.]+$/', $name)) - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FOLDER_NAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->createFolder($name, $location)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FOLDER_CREATE'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for deleting a folder. - * - * @return void - * - * @since 3.2 - */ - public function deleteFolder() - { - // Check for request forgeries - $this->checkToken(); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $isMedia = (int) $this->input->get('isMedia', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (empty($location)) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_ROOT_DELETE'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->deleteFolder($location)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_SUCCESS')); - - if (stristr(base64_decode($file), $location) != false) - { - $file = base64_encode('home'); - } - - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for renaming a file. - * - * @return void - * - * @since 3.2 - */ - public function renameFile() - { - // Check for request forgeries - $this->checkToken(); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $isMedia = (int) $this->input->get('isMedia', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $newName = $this->input->get('new_name'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (base64_decode(urldecode($file)) == '/index.php') - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_RENAME_INDEX'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - elseif (base64_decode(urldecode($file)) == '/joomla.asset.json') - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_RENAME_ASSET_FILE'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $newName)) - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - elseif ($rename = $model->renameFile($file, $newName)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_RENAME_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $rename . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_RENAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for cropping an image. - * - * @return void - * - * @since 3.2 - */ - public function cropImage() - { - // Check for request forgeries - $this->checkToken(); - - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->get('file', '', 'cmd'); - $x = $this->input->get('x'); - $y = $this->input->get('y'); - $w = $this->input->get('w'); - $h = $this->input->get('h'); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (empty($w) && empty($h) && empty($x) && empty($y)) - { - $this->setMessage(Text::_('COM_TEMPLATES_CROP_AREA_ERROR'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->cropImage($file, $w, $h, $x, $y)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_CROP_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_CROP_ERROR'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for resizing an image. - * - * @return void - * - * @since 3.2 - */ - public function resizeImage() - { - // Check for request forgeries - $this->checkToken(); - - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $width = $this->input->get('width'); - $height = $this->input->get('height'); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if ($model->resizeImage($file, $width, $height)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_RESIZE_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_RESIZE_ERROR'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for copying a file. - * - * @return void - * - * @since 3.2 - */ - public function copyFile() - { - // Check for request forgeries - $this->checkToken(); - - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $newName = $this->input->get('new_name'); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (!preg_match('/^[a-zA-Z0-9-_]+$/', $newName)) - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->copyFile($newName, $location, $file)) - { - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_COPY_FAIL'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for extracting an archive file. - * - * @return void - * - * @since 3.2 - */ - public function extractArchive() - { - // Check for request forgeries - $this->checkToken(); - - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if ($model->extractArchive($file)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_FAIL'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Fetch and report updates in \JSON format, for AJAX requests - * - * @return void - * - * @since 4.0.0 - */ - public function ajax() - { - $app = $this->app; - - if (!Session::checkToken('get')) - { - $app->setHeader('status', 403, true); - $app->sendHeaders(); - echo Text::_('JINVALID_TOKEN_NOTICE'); - $app->close(); - } - - // Checks status of installer override plugin. - if (!PluginHelper::isEnabled('installer', 'override')) - { - $error = array('installerOverride' => 'disabled'); - - echo json_encode($error); - - $app->close(); - } - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - $result = $model->getUpdatedList(true, true); - - echo json_encode($result); - - $app->close(); - } - - - /** - * Method for creating a child template. - * - * @return boolean true on success, false otherwise - * - * @since 4.1.0 - */ - public function child() - { - // Check for request forgeries - $this->checkToken(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return false; - } - - $this->input->set('installtype', 'folder'); - $newNameRaw = $this->input->get('new_name', null, 'string'); - - // Only accept letters, numbers and underscore for template name - $newName = preg_replace('/[^a-zA-Z0-9_]/', '', $newNameRaw); - $templateID = (int) $this->input->getInt('id', 0); - $file = (string) $this->input->get('file', '', 'cmd'); - $extraStyles = (array) $this->input->get('style_ids', [], 'array'); - - $this->setRedirect('index.php?option=com_templates&view=template&id=' . $templateID . '&file=' . $file); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel('Template', 'Administrator'); - $model->setState('new_name', $newName); - $model->setState('tmp_prefix', uniqid('template_child_')); - $model->setState('to_path', $this->app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); - - // Process only if we have a new name entered - if (!strlen($newName)) { - $this->setMessage(Text::sprintf('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); - - return false; - } - - // Process only if user is allowed to create child template - if (!$this->app->getIdentity()->authorise('core.create', 'com_templates')) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_CREATE_NOT_PERMITTED'), 'error'); - - return false; - } - - // Check that new name is valid - if (($newNameRaw !== null) && ($newName !== $newNameRaw)) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); - - return false; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 1.6 + * @see BaseController + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('apply', 'save'); + $this->registerTask('unpublish', 'publish'); + $this->registerTask('publish', 'publish'); + $this->registerTask('deleteOverrideHistory', 'publish'); + } + + /** + * Method for closing the template. + * + * @return void + * + * @since 3.2 + */ + public function cancel() + { + $this->setRedirect(Route::_('index.php?option=com_templates&view=templates', false)); + } + + /** + * Method for closing a file. + * + * @return void + * + * @since 3.2 + */ + public function close() + { + $file = base64_encode('home'); + $id = (int) $this->input->get('id', 0, 'int'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . + $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + + /** + * Marked as Checked/Unchecked of override history. + * + * @return void + * + * @since 4.0.0 + */ + public function publish() + { + // Check for request forgeries. + $this->checkToken(); + + $file = $this->input->get('file'); + $id = $this->input->get('id'); + + $ids = (array) $this->input->get('cid', array(), 'string'); + $values = array('publish' => 1, 'unpublish' => 0, 'deleteOverrideHistory' => -3); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_NO_FILE_SELECTED'), 'warning'); + } else { + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Change the state of the records. + if (!$model->publish($ids, $value, $id)) { + $this->setMessage(implode('
', $model->getErrors()), 'warning'); + } else { + if ($value === 1) { + $ntext = 'COM_TEMPLATES_N_OVERRIDE_CHECKED'; + } elseif ($value === 0) { + $ntext = 'COM_TEMPLATES_N_OVERRIDE_UNCHECKED'; + } elseif ($value === -3) { + $ntext = 'COM_TEMPLATES_N_OVERRIDE_DELETED'; + } + + $this->setMessage(Text::plural($ntext, count($ids))); + } + } + + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . + $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + + /** + * Method for copying the template. + * + * @return boolean true on success, false otherwise + * + * @since 3.2 + */ + public function copy() + { + // Check for request forgeries + $this->checkToken(); + + $app = $this->app; + $this->input->set('installtype', 'folder'); + $newNameRaw = $this->input->get('new_name', null, 'string'); + // Only accept letters, numbers and underscore for template name + $newName = preg_replace('/[^a-zA-Z0-9_]/', '', $newNameRaw); + $templateID = (int) $this->input->getInt('id', 0); + $file = (string) $this->input->get('file', '', 'cmd'); + + // Access check. + if (!$this->allowEdit()) { + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return false; + } + + $this->setRedirect('index.php?option=com_templates&view=template&id=' . $templateID . '&file=' . $file); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel('Template', 'Administrator'); + $model->setState('new_name', $newName); + $model->setState('tmp_prefix', uniqid('template_copy_')); + $model->setState('to_path', $app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); + + // Process only if we have a new name entered + if (strlen($newName) > 0) { + if (!$this->app->getIdentity()->authorise('core.create', 'com_templates')) { + // User is not authorised to delete + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_CREATE_NOT_PERMITTED'), 'error'); + + return false; + } + + // Check that new name is valid + if (($newNameRaw !== null) && ($newName !== $newNameRaw)) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); + + return false; + } + + // Check that new name doesn't already exist + if (!$model->checkNewName()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_DUPLICATE_TEMPLATE_NAME'), 'error'); + + return false; + } + + // Check that from name does exist and get the folder name + $fromName = $model->getFromName(); + + if (!$fromName) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); + + return false; + } + + // Call model's copy method + if (!$model->copy()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_COPY'), 'error'); + + return false; + } + + // Call installation model + $this->input->set('install_directory', $app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); + + /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $installModel */ + $installModel = $this->app->bootComponent('com_installer') + ->getMVCFactory()->createModel('Install', 'Administrator'); + $this->app->getLanguage()->load('com_installer'); + + if (!$installModel->install()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_INSTALL'), 'error'); + + return false; + } + + $this->setMessage(Text::sprintf('COM_TEMPLATES_COPY_SUCCESS', $newName)); + $model->cleanup(); + + return true; + } + + $this->setMessage(Text::sprintf('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); + + return false; + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional (note, the empty array is atypical compared to other models). + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 3.2 + */ + public function getModel($name = 'Template', $prefix = 'Administrator', $config = array()) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to check if you can add a new record. + * + * @return boolean + * + * @since 3.2 + */ + protected function allowEdit() + { + return $this->app->getIdentity()->authorise('core.admin'); + } + + /** + * Saves a template source file. + * + * @return void + * + * @since 3.2 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + $data = $this->input->post->get('jform', array(), 'array'); + $task = $this->getTask(); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $fileName = (string) $this->input->getCmd('file', ''); + $explodeArray = explode(':', str_replace('//', '/', base64_decode($fileName))); + + // Access check. + if (!$this->allowEdit()) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + // Match the stored id's with the submitted. + if (empty($data['extension_id']) || empty($data['filename'])) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); + + return; + } elseif ($data['extension_id'] != $model->getState('extension.id')) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); + + return; + } elseif (str_ends_with(end($explodeArray), Path::clean($data['filename'], '/'))) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); + + return; + } + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + $this->setMessage($model->getError(), 'error'); + + return; + } + + $data = $model->validate($form, $data); + + // Check for validation errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Redirect back to the edit screen. + $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + + return; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED', $model->getError()), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + + return; + } + + $this->setMessage(Text::_('COM_TEMPLATES_FILE_SAVE_SUCCESS')); + + // Redirect the user based on the chosen task. + switch ($task) { + case 'apply': + // Redirect back to the edit screen. + $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + break; + + default: + // Redirect to the list screen. + $file = base64_encode('home'); + $id = (int) $this->input->get('id', 0, 'int'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + break; + } + } + + /** + * Method for creating override. + * + * @return void + * + * @since 3.2 + */ + public function overrides() + { + // Check for request forgeries. + $this->checkToken('get'); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $file = (string) $this->input->getCmd('file', ''); + $override = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('folder', '')), 'path'); + $id = (int) $this->input->get('id', 0, 'int'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + $model->createOverride($override); + + // Redirect back to the edit screen. + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + + /** + * Method for deleting a file. + * + * @return void + * + * @since 3.2 + */ + public function delete() + { + // Check for request forgeries + $this->checkToken(); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (base64_decode(urldecode($file)) == '/index.php') { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INDEX_DELETE'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif (base64_decode(urldecode($file)) == '/joomla.asset.json') { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_DELETE'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->deleteFile($file)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_DELETE_SUCCESS')); + $file = base64_encode('home'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_DELETE'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for creating a new file. + * + * @return void + * + * @since 3.2 + */ + public function createFile() + { + // Check for request forgeries + $this->checkToken(); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->get('file', '', 'cmd'); + $name = (string) $this->input->get('name', '', 'cmd'); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + $type = (string) $this->input->get('type', '', 'cmd'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if ($type == 'null') { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_TYPE'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $name)) { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->createFile($name, $type, $location)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_CREATE_SUCCESS')); + $file = urlencode(base64_encode($location . '/' . $name . '.' . $type)); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_CREATE'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for uploading a file. + * + * @return void + * + * @since 3.2 + */ + public function uploadFile() + { + // Check for request forgeries + $this->checkToken(); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $upload = $this->input->files->get('files'); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if ($return = $model->uploadFile($upload, $location)) { + $this->setMessage(Text::sprintf('COM_TEMPLATES_FILE_UPLOAD_SUCCESS', $upload['name'])); + $redirect = base64_encode($return); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $redirect . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_UPLOAD'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for creating a new folder. + * + * @return void + * + * @since 3.2 + */ + public function createFolder() + { + // Check for request forgeries + $this->checkToken(); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $name = $this->input->get('name'); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (!preg_match('/^[a-zA-Z0-9-_.]+$/', $name)) { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FOLDER_NAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->createFolder($name, $location)) { + $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FOLDER_CREATE'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for deleting a folder. + * + * @return void + * + * @since 3.2 + */ + public function deleteFolder() + { + // Check for request forgeries + $this->checkToken(); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $isMedia = (int) $this->input->get('isMedia', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (empty($location)) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_ROOT_DELETE'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } elseif ($model->deleteFolder($location)) { + $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_SUCCESS')); + + if (stristr(base64_decode($file), $location) != false) { + $file = base64_encode('home'); + } + + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for renaming a file. + * + * @return void + * + * @since 3.2 + */ + public function renameFile() + { + // Check for request forgeries + $this->checkToken(); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $isMedia = (int) $this->input->get('isMedia', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $newName = $this->input->get('new_name'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (base64_decode(urldecode($file)) == '/index.php') { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_RENAME_INDEX'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } elseif (base64_decode(urldecode($file)) == '/joomla.asset.json') { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_RENAME_ASSET_FILE'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $newName)) { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } elseif ($rename = $model->renameFile($file, $newName)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_RENAME_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $rename . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_RENAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for cropping an image. + * + * @return void + * + * @since 3.2 + */ + public function cropImage() + { + // Check for request forgeries + $this->checkToken(); + + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->get('file', '', 'cmd'); + $x = $this->input->get('x'); + $y = $this->input->get('y'); + $w = $this->input->get('w'); + $h = $this->input->get('h'); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (empty($w) && empty($h) && empty($x) && empty($y)) { + $this->setMessage(Text::_('COM_TEMPLATES_CROP_AREA_ERROR'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->cropImage($file, $w, $h, $x, $y)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_CROP_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_CROP_ERROR'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for resizing an image. + * + * @return void + * + * @since 3.2 + */ + public function resizeImage() + { + // Check for request forgeries + $this->checkToken(); + + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $width = $this->input->get('width'); + $height = $this->input->get('height'); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if ($model->resizeImage($file, $width, $height)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_RESIZE_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_RESIZE_ERROR'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for copying a file. + * + * @return void + * + * @since 3.2 + */ + public function copyFile() + { + // Check for request forgeries + $this->checkToken(); + + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $newName = $this->input->get('new_name'); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (!preg_match('/^[a-zA-Z0-9-_]+$/', $newName)) { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->copyFile($newName, $location, $file)) { + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_COPY_FAIL'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for extracting an archive file. + * + * @return void + * + * @since 3.2 + */ + public function extractArchive() + { + // Check for request forgeries + $this->checkToken(); + + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if ($model->extractArchive($file)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_FAIL'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Fetch and report updates in \JSON format, for AJAX requests + * + * @return void + * + * @since 4.0.0 + */ + public function ajax() + { + $app = $this->app; + + if (!Session::checkToken('get')) { + $app->setHeader('status', 403, true); + $app->sendHeaders(); + echo Text::_('JINVALID_TOKEN_NOTICE'); + $app->close(); + } + + // Checks status of installer override plugin. + if (!PluginHelper::isEnabled('installer', 'override')) { + $error = array('installerOverride' => 'disabled'); + + echo json_encode($error); + + $app->close(); + } + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + $result = $model->getUpdatedList(true, true); + + echo json_encode($result); + + $app->close(); + } + + + /** + * Method for creating a child template. + * + * @return boolean true on success, false otherwise + * + * @since 4.1.0 + */ + public function child() + { + // Check for request forgeries + $this->checkToken(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return false; + } + + $this->input->set('installtype', 'folder'); + $newNameRaw = $this->input->get('new_name', null, 'string'); + + // Only accept letters, numbers and underscore for template name + $newName = preg_replace('/[^a-zA-Z0-9_]/', '', $newNameRaw); + $templateID = (int) $this->input->getInt('id', 0); + $file = (string) $this->input->get('file', '', 'cmd'); + $extraStyles = (array) $this->input->get('style_ids', [], 'array'); + + $this->setRedirect('index.php?option=com_templates&view=template&id=' . $templateID . '&file=' . $file); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel('Template', 'Administrator'); + $model->setState('new_name', $newName); + $model->setState('tmp_prefix', uniqid('template_child_')); + $model->setState('to_path', $this->app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); + + // Process only if we have a new name entered + if (!strlen($newName)) { + $this->setMessage(Text::sprintf('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); + + return false; + } + + // Process only if user is allowed to create child template + if (!$this->app->getIdentity()->authorise('core.create', 'com_templates')) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_CREATE_NOT_PERMITTED'), 'error'); + + return false; + } + + // Check that new name is valid + if (($newNameRaw !== null) && ($newName !== $newNameRaw)) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); + + return false; + } - // Check that new name doesn't already exist - if (!$model->checkNewName()) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_DUPLICATE_TEMPLATE_NAME'), 'error'); + // Check that new name doesn't already exist + if (!$model->checkNewName()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_DUPLICATE_TEMPLATE_NAME'), 'error'); - return false; - } + return false; + } - // Check that from name does exist and get the folder name - $fromName = $model->getFromName(); + // Check that from name does exist and get the folder name + $fromName = $model->getFromName(); - if (!$fromName) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); + if (!$fromName) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); - return false; - } + return false; + } - // Call model's copy method - if (!$model->child()) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_COPY'), 'error'); + // Call model's copy method + if (!$model->child()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_COPY'), 'error'); - return false; - } + return false; + } - // Call installation model - $this->input->set('install_directory', $this->app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); + // Call installation model + $this->input->set('install_directory', $this->app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); - /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $installModel */ - $installModel = $this->app->bootComponent('com_installer') - ->getMVCFactory()->createModel('Install', 'Administrator'); - $this->app->getLanguage()->load('com_installer'); + /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $installModel */ + $installModel = $this->app->bootComponent('com_installer') + ->getMVCFactory()->createModel('Install', 'Administrator'); + $this->app->getLanguage()->load('com_installer'); - if (!$installModel->install()) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_INSTALL'), 'error'); + if (!$installModel->install()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_INSTALL'), 'error'); - return false; - } + return false; + } - $this->setMessage(Text::sprintf('COM_TEMPLATES_CHILD_SUCCESS', $newName)); - $model->cleanup(); + $this->setMessage(Text::sprintf('COM_TEMPLATES_CHILD_SUCCESS', $newName)); + $model->cleanup(); - if (\count($extraStyles) > 0) - { - $model->setState('stylesToCopy', $extraStyles); - $model->copyStyles(); - } + if (\count($extraStyles) > 0) { + $model->setState('stylesToCopy', $extraStyles); + $model->copyStyles(); + } - return true; - } + return true; + } } diff --git a/code/administrator/components/com_templates/src/Extension/TemplatesComponent.php b/code/administrator/components/com_templates/src/Extension/TemplatesComponent.php index e0736517..dccb7590 100644 --- a/code/administrator/components/com_templates/src/Extension/TemplatesComponent.php +++ b/code/administrator/components/com_templates/src/Extension/TemplatesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('templates', new Templates); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('templates', new Templates()); + } } diff --git a/code/administrator/components/com_templates/src/Field/TemplatelocationField.php b/code/administrator/components/com_templates/src/Field/TemplatelocationField.php index cf4793eb..d766f349 100644 --- a/code/administrator/components/com_templates/src/Field/TemplatelocationField.php +++ b/code/administrator/components/com_templates/src/Field/TemplatelocationField.php @@ -1,4 +1,5 @@ getUserStateFromRequest('com_templates.styles.client_id', 'client_id', '0', 'string'); - - // Get the templates for the selected client_id. - $options = TemplatesHelper::getTemplateOptions($clientId); - - // Merge into the parent options. - return array_merge(parent::getOptions(), $options); - } + /** + * The form field type. + * + * @var string + * @since 3.5 + */ + protected $type = 'TemplateName'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 1.6 + */ + public function getOptions() + { + // Get the client_id filter from the user state. + $clientId = Factory::getApplication()->getUserStateFromRequest('com_templates.styles.client_id', 'client_id', '0', 'string'); + + // Get the templates for the selected client_id. + $options = TemplatesHelper::getTemplateOptions($clientId); + + // Merge into the parent options. + return array_merge(parent::getOptions(), $options); + } } diff --git a/code/administrator/components/com_templates/src/Helper/TemplateHelper.php b/code/administrator/components/com_templates/src/Helper/TemplateHelper.php index dbbfd061..29c51fa7 100644 --- a/code/administrator/components/com_templates/src/Helper/TemplateHelper.php +++ b/code/administrator/components/com_templates/src/Helper/TemplateHelper.php @@ -1,4 +1,5 @@ enqueueMessage(Text::_('COM_TEMPLATES_ERROR_UPLOAD_INPUT'), 'error'); - - return false; - } - - // Media file names should never have executable extensions buried in them. - $executable = array( - 'exe', 'phtml','java', 'perl', 'py', 'asp','dll', 'go', 'jar', - 'ade', 'adp', 'bat', 'chm', 'cmd', 'com', 'cpl', 'hta', 'ins', 'isp', - 'jse', 'lib', 'mde', 'msc', 'msp', 'mst', 'pif', 'scr', 'sct', 'shb', - 'sys', 'vb', 'vbe', 'vbs', 'vxd', 'wsc', 'wsf', 'wsh' - ); - $explodedFileName = explode('.', $file['name']); - - if (count($explodedFileName) > 2) - { - foreach ($executable as $extensionName) - { - if (in_array($extensionName, $explodedFileName)) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXECUTABLE'), 'error'); - - return false; - } - } - } - - if ($file['name'] !== File::makeSafe($file['name']) || preg_match('/\s/', File::makeSafe($file['name']))) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILENAME'), 'error'); - - return false; - } - - $format = strtolower(File::getExt($file['name'])); - - $imageTypes = explode(',', $params->get('image_formats')); - $sourceTypes = explode(',', $params->get('source_formats')); - $fontTypes = explode(',', $params->get('font_formats')); - $archiveTypes = explode(',', $params->get('compressed_formats')); - - $allowable = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes); - - if ($format == '' || $format == false || (!in_array($format, $allowable))) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILETYPE'), 'error'); - - return false; - } - - if (in_array($format, $archiveTypes)) - { - $zip = new \ZipArchive; - - if ($zip->open($file['tmp_name']) === true) - { - for ($i = 0; $i < $zip->numFiles; $i++) - { - $entry = $zip->getNameIndex($i); - $endString = substr($entry, -1); - - if ($endString != DIRECTORY_SEPARATOR) - { - $explodeArray = explode('.', $entry); - $ext = end($explodeArray); - - if (!in_array($ext, $allowable)) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UNSUPPORTED_ARCHIVE'), 'error'); - - return false; - } - } - } - } - else - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); - - return false; - } - } - - // Max upload size set to 2 MB for Template Manager - $maxSize = (int) ($params->get('upload_limit') * 1024 * 1024); - - if ($maxSize > 0 && (int) $file['size'] > $maxSize) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILETOOLARGE'), 'error'); - - return false; - } - - $xss_check = file_get_contents($file['tmp_name'], false, null, -1, 256); - $html_tags = array( - 'abbr', 'acronym', 'address', 'applet', 'area', 'audioscope', 'base', 'basefont', 'bdo', 'bgsound', 'big', 'blackface', 'blink', 'blockquote', - 'body', 'bq', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'comment', 'custom', 'dd', 'del', 'dfn', 'dir', 'div', - 'dl', 'dt', 'em', 'embed', 'fieldset', 'fn', 'font', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', - 'iframe', 'ilayer', 'img', 'input', 'ins', 'isindex', 'keygen', 'kbd', 'label', 'layer', 'legend', 'li', 'limittext', 'link', 'listing', - 'map', 'marquee', 'menu', 'meta', 'multicol', 'nobr', 'noembed', 'noframes', 'noscript', 'nosmartquotes', 'object', 'ol', 'optgroup', 'option', - 'param', 'plaintext', 'pre', 'rt', 'ruby', 's', 'samp', 'script', 'select', 'server', 'shadow', 'sidebar', 'small', 'spacer', 'span', 'strike', - 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'tt', 'ul', 'var', 'wbr', 'xml', - 'xmp', '!DOCTYPE', '!--' - ); - - foreach ($html_tags as $tag) - { - // A tag is '' - if (stristr($xss_check, '<' . $tag . ' ') || stristr($xss_check, '<' . $tag . '>')) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNIEXSS'), 'error'); - - return false; - } - } - - return true; - } + /** + * Checks if the file is an image + * + * @param string $fileName The filename + * + * @return boolean + * + * @since 3.2 + */ + public static function getTypeIcon($fileName) + { + // Get file extension + return strtolower(substr($fileName, strrpos($fileName, '.') + 1)); + } + + /** + * Checks if the file can be uploaded + * + * @param array $file File information + * @param string $err An error message to be returned + * + * @return boolean + * + * @since 3.2 + */ + public static function canUpload($file, $err = '') + { + $params = ComponentHelper::getParams('com_templates'); + + if (empty($file['name'])) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_UPLOAD_INPUT'), 'error'); + + return false; + } + + // Media file names should never have executable extensions buried in them. + $executable = array( + 'exe', 'phtml','java', 'perl', 'py', 'asp','dll', 'go', 'jar', + 'ade', 'adp', 'bat', 'chm', 'cmd', 'com', 'cpl', 'hta', 'ins', 'isp', + 'jse', 'lib', 'mde', 'msc', 'msp', 'mst', 'pif', 'scr', 'sct', 'shb', + 'sys', 'vb', 'vbe', 'vbs', 'vxd', 'wsc', 'wsf', 'wsh' + ); + $explodedFileName = explode('.', $file['name']); + + if (count($explodedFileName) > 2) { + foreach ($executable as $extensionName) { + if (in_array($extensionName, $explodedFileName)) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXECUTABLE'), 'error'); + + return false; + } + } + } + + if ($file['name'] !== File::makeSafe($file['name']) || preg_match('/\s/', File::makeSafe($file['name']))) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILENAME'), 'error'); + + return false; + } + + $format = strtolower(File::getExt($file['name'])); + + $imageTypes = explode(',', $params->get('image_formats')); + $sourceTypes = explode(',', $params->get('source_formats')); + $fontTypes = explode(',', $params->get('font_formats')); + $archiveTypes = explode(',', $params->get('compressed_formats')); + + $allowable = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes); + + if ($format == '' || $format == false || (!in_array($format, $allowable))) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILETYPE'), 'error'); + + return false; + } + + if (in_array($format, $archiveTypes)) { + $zip = new \ZipArchive(); + + if ($zip->open($file['tmp_name']) === true) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $entry = $zip->getNameIndex($i); + $endString = substr($entry, -1); + + if ($endString != DIRECTORY_SEPARATOR) { + $explodeArray = explode('.', $entry); + $ext = end($explodeArray); + + if (!in_array($ext, $allowable)) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UNSUPPORTED_ARCHIVE'), 'error'); + + return false; + } + } + } + } else { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); + + return false; + } + } + + // Max upload size set to 10 MB for Template Manager + $maxSize = (int) ($params->get('upload_limit') * 1024 * 1024); + + if ($maxSize > 0 && (int) $file['size'] > $maxSize) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILETOOLARGE'), 'error'); + + return false; + } + + $xss_check = file_get_contents($file['tmp_name'], false, null, -1, 256); + $html_tags = array( + 'abbr', 'acronym', 'address', 'applet', 'area', 'audioscope', 'base', 'basefont', 'bdo', 'bgsound', 'big', 'blackface', 'blink', 'blockquote', + 'body', 'bq', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'comment', 'custom', 'dd', 'del', 'dfn', 'dir', 'div', + 'dl', 'dt', 'em', 'embed', 'fieldset', 'fn', 'font', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', + 'iframe', 'ilayer', 'img', 'input', 'ins', 'isindex', 'keygen', 'kbd', 'label', 'layer', 'legend', 'li', 'limittext', 'link', 'listing', + 'map', 'marquee', 'menu', 'meta', 'multicol', 'nobr', 'noembed', 'noframes', 'noscript', 'nosmartquotes', 'object', 'ol', 'optgroup', 'option', + 'param', 'plaintext', 'pre', 'rt', 'ruby', 's', 'samp', 'script', 'select', 'server', 'shadow', 'sidebar', 'small', 'spacer', 'span', 'strike', + 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'tt', 'ul', 'var', 'wbr', 'xml', + 'xmp', '!DOCTYPE', '!--' + ); + + foreach ($html_tags as $tag) { + // A tag is '' + if (stristr($xss_check, '<' . $tag . ' ') || stristr($xss_check, '<' . $tag . '>')) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNIEXSS'), 'error'); + + return false; + } + } + + return true; + } } diff --git a/code/administrator/components/com_templates/src/Helper/TemplatesHelper.php b/code/administrator/components/com_templates/src/Helper/TemplatesHelper.php index 53a50bdb..bef95829 100644 --- a/code/administrator/components/com_templates/src/Helper/TemplatesHelper.php +++ b/code/administrator/components/com_templates/src/Helper/TemplatesHelper.php @@ -1,4 +1,5 @@ getQuery(true); - - $query->select($db->quoteName('element', 'value')) - ->select($db->quoteName('name', 'text')) - ->select($db->quoteName('extension_id', 'e_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('template')) - ->where($db->quoteName('enabled') . ' = 1') - ->order($db->quoteName('client_id') . ' ASC') - ->order($db->quoteName('name') . ' ASC'); - - if ($clientId != '*') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - $db->setQuery($query); - $options = $db->loadObjectList(); - - return $options; - } - - /** - * @param string $templateBaseDir - * @param string $templateDir - * - * @return boolean|CMSObject - */ - public static function parseXMLTemplateFile($templateBaseDir, $templateDir) - { - $data = new CMSObject; - - // Check of the xml file exists - $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); - - if (is_file($filePath)) - { - $xml = Installer::parseXMLInstallFile($filePath); - - if ($xml['type'] != 'template') - { - return false; - } - - foreach ($xml as $key => $value) - { - $data->set($key, $value); - } - } - - return $data; - } - - /** - * @param integer $clientId - * @param string $templateDir - * - * @return boolean|array - * - * @since 3.0 - */ - public static function getPositions($clientId, $templateDir) - { - $positions = array(); - - $templateBaseDir = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); - - if (is_file($filePath)) - { - // Read the file to see if it's a valid component XML file - $xml = simplexml_load_file($filePath); - - if (!$xml) - { - return false; - } - - // Check for a valid XML root tag. - - // Extensions use 'extension' as the root tag. Languages use 'metafile' instead - - if ($xml->getName() != 'extension' && $xml->getName() != 'metafile') - { - unset($xml); - - return false; - } - - $positions = (array) $xml->positions; - - if (isset($positions['position'])) - { - $positions = (array) $positions['position']; - } - else - { - $positions = array(); - } - } - - return $positions; - } + /** + * Get a list of filter options for the application clients. + * + * @return array An array of HtmlOption elements. + */ + public static function getClientOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR')); + + return $options; + } + + /** + * Get a list of filter options for the templates with styles. + * + * @param mixed $clientId The CMS client id (0:site | 1:administrator) or '*' for all. + * + * @return array + */ + public static function getTemplateOptions($clientId = '*') + { + // Build the filter options. + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('element', 'value')) + ->select($db->quoteName('name', 'text')) + ->select($db->quoteName('extension_id', 'e_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('template')) + ->where($db->quoteName('enabled') . ' = 1') + ->order($db->quoteName('client_id') . ' ASC') + ->order($db->quoteName('name') . ' ASC'); + + if ($clientId != '*') { + $clientId = (int) $clientId; + $query->where($db->quoteName('client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + $db->setQuery($query); + $options = $db->loadObjectList(); + + return $options; + } + + /** + * @param string $templateBaseDir + * @param string $templateDir + * + * @return boolean|CMSObject + */ + public static function parseXMLTemplateFile($templateBaseDir, $templateDir) + { + $data = new CMSObject(); + + // Check of the xml file exists + $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); + + if (is_file($filePath)) { + $xml = Installer::parseXMLInstallFile($filePath); + + if ($xml['type'] != 'template') { + return false; + } + + foreach ($xml as $key => $value) { + $data->set($key, $value); + } + } + + return $data; + } + + /** + * @param integer $clientId + * @param string $templateDir + * + * @return boolean|array + * + * @since 3.0 + */ + public static function getPositions($clientId, $templateDir) + { + $positions = array(); + + $templateBaseDir = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); + + if (is_file($filePath)) { + // Read the file to see if it's a valid component XML file + $xml = simplexml_load_file($filePath); + + if (!$xml) { + return false; + } + + // Check for a valid XML root tag. + + // Extensions use 'extension' as the root tag. Languages use 'metafile' instead + + if ($xml->getName() != 'extension' && $xml->getName() != 'metafile') { + unset($xml); + + return false; + } + + $positions = (array) $xml->positions; + + if (isset($positions['position'])) { + $positions = (array) $positions['position']; + } else { + $positions = array(); + } + } + + return $positions; + } } diff --git a/code/administrator/components/com_templates/src/Model/StyleModel.php b/code/administrator/components/com_templates/src/Model/StyleModel.php index d63bff00..07e95a2d 100644 --- a/code/administrator/components/com_templates/src/Model/StyleModel.php +++ b/code/administrator/components/com_templates/src/Model/StyleModel.php @@ -1,4 +1,5 @@ 'onExtensionBeforeDelete', - 'event_after_delete' => 'onExtensionAfterDelete', - 'event_before_save' => 'onExtensionBeforeSave', - 'event_after_save' => 'onExtensionAfterSave', - 'events_map' => array('delete' => 'extension', 'save' => 'extension') - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Method to auto-populate the model state. - * - * @note Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState('style.id', $pk); - - // Load the parameters. - $params = ComponentHelper::getParams('com_templates'); - $this->setState('params', $params); - } - - /** - * Method to delete rows. - * - * @param array &$pks An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $user = Factory::getUser(); - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - PluginHelper::importPlugin($this->events_map['delete']); - - // Iterate the items to delete each one. - foreach ($pks as $pk) - { - if ($table->load($pk)) - { - // Access checks. - if (!$user->authorise('core.delete', 'com_templates')) - { - throw new \Exception(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED')); - } - - // You should not delete a default style - if ($table->home != '0') - { - Factory::getApplication()->enqueueMessage(Text::_('COM_TEMPLATES_STYLE_CANNOT_DELETE_DEFAULT_STYLE'), 'error'); - - return false; - } - - // Trigger the before delete event. - $result = Factory::getApplication()->triggerEvent($this->event_before_delete, array($context, $table)); - - if (in_array(false, $result, true) || !$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after delete event. - Factory::getApplication()->triggerEvent($this->event_after_delete, array($context, $table)); - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - // Clean cache - $this->cleanCache(); - - return true; - } - - /** - * Method to duplicate styles. - * - * @param array &$pks An array of primary key IDs. - * - * @return boolean True if successful. - * - * @throws \Exception - */ - public function duplicate(&$pks) - { - $user = Factory::getUser(); - - // Access checks. - if (!$user->authorise('core.create', 'com_templates')) - { - throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED')); - } - - $context = $this->option . '.' . $this->name; - - // Include the plugins for the save events. - PluginHelper::importPlugin($this->events_map['save']); - - $table = $this->getTable(); - - foreach ($pks as $pk) - { - if ($table->load($pk, true)) - { - // Reset the id to create a new record. - $table->id = 0; - - // Reset the home (don't want dupes of that field). - $table->home = 0; - - // Alter the title. - $m = null; - $table->title = $this->generateNewTitle(null, null, $table->title); - - if (!$table->check()) - { - throw new \Exception($table->getError()); - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, true)); - - if (in_array(false, $result, true) || !$table->store()) - { - throw new \Exception($table->getError()); - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, true)); - } - else - { - throw new \Exception($table->getError()); - } - } - - // Clean cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the title. - * - * @param integer $categoryId The id of the category. - * @param string $alias The alias. - * @param string $title The title. - * - * @return string New title. - * - * @since 1.7.1 - */ - protected function generateNewTitle($categoryId, $alias, $title) - { - // Alter the title - $table = $this->getTable(); - - while ($table->load(array('title' => $title))) - { - $title = StringHelper::increment($title); - } - - return $title; - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // The folder and element vars are passed when saving the form. - if (empty($data)) - { - $item = $this->getItem(); - $clientId = $item->client_id; - $template = $item->template; - } - else - { - $clientId = ArrayHelper::getValue($data, 'client_id'); - $template = ArrayHelper::getValue($data, 'template'); - } - - // Add the default fields directory - $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - Form::addFieldPath($baseFolder . '/templates/' . $template . '/field'); - - // These variables are used to add data from the plugin XML files. - $this->setState('item.client_id', $clientId); - $this->setState('item.template', $template); - - // Get the form. - $form = $this->loadForm('com_templates.style', 'style', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('home', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('home', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_templates.edit.style.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_templates.style', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('style.id'); - - if (!isset($this->_cache[$pk])) - { - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($pk); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - - // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. - $properties = $table->getProperties(1); - $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); - - // Convert the params field to an array. - $registry = new Registry($table->params); - $this->_cache[$pk]->params = $registry->toArray(); - - // Get the template XML. - $client = ApplicationHelper::getClientInfo($table->client_id); - $path = Path::clean($client->path . '/templates/' . $table->template . '/templateDetails.xml'); - - if (file_exists($path)) - { - $this->_cache[$pk]->xml = simplexml_load_file($path); - } - else - { - $this->_cache[$pk]->xml = null; - } - } - - return $this->_cache[$pk]; - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $clientId = $this->getState('item.client_id'); - $template = $this->getState('item.template'); - $lang = Factory::getLanguage(); - $client = ApplicationHelper::getClientInfo($clientId); - - if (!$form->loadFile('style_' . $client->name, true)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - $formFile = Path::clean($client->path . '/templates/' . $template . '/templateDetails.xml'); - - // Load the core and/or local language file(s). - $lang->load('tpl_' . $template, $client->path) - || (!empty($data->parent) && $lang->load('tpl_' . $data->parent, $client->path)) - || (!empty($data->parent) && $lang->load('tpl_' . $data->parent, $client->path . '/templates/' . $data->parent)) - || $lang->load('tpl_' . $template, $client->path . '/templates/' . $template); - - if (file_exists($formFile)) - { - // Get the template form. - if (!$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - // Disable home field if it is default style - - if ((is_array($data) && array_key_exists('home', $data) && $data['home'] == '1') - || (is_object($data) && isset($data->home) && $data->home == '1')) - { - $form->setFieldAttribute('home', 'readonly', 'true'); - } - - if ($client->name === 'site' && !Multilanguage::isEnabled()) - { - $form->setFieldAttribute('home', 'type', 'radio'); - $form->setFieldAttribute('home', 'layout', 'joomla.form.field.radio.switcher'); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/extension/help'); - - if (!empty($help)) - { - $helpKey = trim((string) $help[0]['key']); - $helpURL = trim((string) $help[0]['url']); - - $this->helpKey = $helpKey ?: $this->helpKey; - $this->helpURL = $helpURL ?: $this->helpURL; - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - */ - public function save($data) - { - // Detect disabled extension - $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\'); - - if ($extension->load(array('enabled' => 0, 'type' => 'template', 'element' => $data['template'], 'client_id' => $data['client_id']))) - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE')); - - return false; - } - - $app = Factory::getApplication(); - $table = $this->getTable(); - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('style.id'); - $isNew = true; - - // Include the extension plugins for the save events. - PluginHelper::importPlugin($this->events_map['save']); - - // Load the row if saving an existing record. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - if ($app->input->get('task') == 'save2copy') - { - $data['title'] = $this->generateNewTitle(null, null, $data['title']); - $data['home'] = 0; - $data['assigned'] = ''; - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Prepare the row for saving - $this->prepareTable($table); - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array('com_templates.style', &$table, $isNew)); - - // Store the data. - if (in_array(false, $result, true) || !$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - $user = Factory::getUser(); - - if ($user->authorise('core.edit', 'com_menus') && $table->client_id == 0) - { - $n = 0; - $db = $this->getDbo(); - $user = Factory::getUser(); - $tableId = (int) $table->id; - $userId = (int) $user->id; - - if (!empty($data['assigned']) && is_array($data['assigned'])) - { - $data['assigned'] = ArrayHelper::toInteger($data['assigned']); - - // Update the mapping for menu items that this style IS assigned to. - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('template_style_id') . ' = :newtsid') - ->whereIn($db->quoteName('id'), $data['assigned']) - ->where($db->quoteName('template_style_id') . ' != :tsid') - ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)') - ->bind(':userid', $userId, ParameterType::INTEGER) - ->bind(':newtsid', $tableId, ParameterType::INTEGER) - ->bind(':tsid', $tableId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - $n += $db->getAffectedRows(); - } - - // Remove style mappings for menu items this style is NOT assigned to. - // If unassigned then all existing maps will be removed. - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('template_style_id') . ' = 0'); - - if (!empty($data['assigned'])) - { - $query->whereNotIn($db->quoteName('id'), $data['assigned']); - } - - $query->where($db->quoteName('template_style_id') . ' = :templatestyleid') - ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)') - ->bind(':userid', $userId, ParameterType::INTEGER) - ->bind(':templatestyleid', $tableId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - $n += $db->getAffectedRows(); - - if ($n > 0) - { - $app->enqueueMessage(Text::plural('COM_TEMPLATES_MENU_CHANGED', $n)); - } - } - - // Clean the cache. - $this->cleanCache(); - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array('com_templates.style', &$table, $isNew)); - - $this->setState('style.id', $table->id); - - return true; - } - - /** - * Method to set a template style as home. - * - * @param integer $id The primary key ID for the style. - * - * @return boolean True if successful. - * - * @throws \Exception - */ - public function setHome($id = 0) - { - $user = Factory::getUser(); - $db = $this->getDbo(); - - // Access checks. - if (!$user->authorise('core.edit.state', 'com_templates')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - } - - $style = $this->getTable(); - - if (!$style->load((int) $id)) - { - throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND')); - } - - // Detect disabled extension - $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\'); - - if ($extension->load(array('enabled' => 0, 'type' => 'template', 'element' => $style->template, 'client_id' => $style->client_id))) - { - throw new \Exception(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE')); - } - - $clientId = (int) $style->client_id; - $id = (int) $id; - - // Reset the home fields for the client_id. - $query = $db->getQuery(true) - ->update($db->quoteName('#__template_styles')) - ->set($db->quoteName('home') . ' = ' . $db->quote('0')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->where($db->quoteName('home') . ' = ' . $db->quote('1')) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Set the new home style. - $query = $db->getQuery(true) - ->update($db->quoteName('#__template_styles')) - ->set($db->quoteName('home') . ' = ' . $db->quote('1')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Clean the cache. - $this->cleanCache(); - - return true; - } - - /** - * Method to unset a template style as default for a language. - * - * @param integer $id The primary key ID for the style. - * - * @return boolean True if successful. - * - * @throws \Exception - */ - public function unsetHome($id = 0) - { - $user = Factory::getUser(); - $db = $this->getDbo(); - - // Access checks. - if (!$user->authorise('core.edit.state', 'com_templates')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - } - - $id = (int) $id; - - // Lookup the client_id. - $query = $db->getQuery(true) - ->select($db->quoteName(['client_id', 'home'])) - ->from($db->quoteName('#__template_styles')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $style = $db->loadObject(); - - if (!is_numeric($style->client_id)) - { - throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND')); - } - elseif ($style->home == '1') - { - throw new \Exception(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE')); - } - - // Set the new home style. - $query = $db->getQuery(true) - ->update($db->quoteName('#__template_styles')) - ->set($db->quoteName('home') . ' = ' . $db->quote('0')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Clean the cache. - $this->cleanCache(); - - return true; - } - - /** - * Get the necessary data to load an item help screen. - * - * @return object An object with key, url, and local properties for loading the item help screen. - * - * @since 1.6 - */ - public function getHelp() - { - return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); - } - - /** - * Custom clean cache method - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_templates'); - parent::cleanCache('_system'); - } + /** + * The help screen key for the module. + * + * @var string + * @since 1.6 + */ + protected $helpKey = 'Templates:_Edit_Style'; + + /** + * The help screen base URL for the module. + * + * @var string + * @since 1.6 + */ + protected $helpURL; + + /** + * Item cache. + * + * @var array + * @since 1.6 + */ + private $_cache = array(); + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_before_delete' => 'onExtensionBeforeDelete', + 'event_after_delete' => 'onExtensionAfterDelete', + 'event_before_save' => 'onExtensionBeforeSave', + 'event_after_save' => 'onExtensionAfterSave', + 'events_map' => array('delete' => 'extension', 'save' => 'extension') + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * @note Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState('style.id', $pk); + + // Load the parameters. + $params = ComponentHelper::getParams('com_templates'); + $this->setState('params', $params); + } + + /** + * Method to delete rows. + * + * @param array &$pks An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $user = Factory::getUser(); + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + PluginHelper::importPlugin($this->events_map['delete']); + + // Iterate the items to delete each one. + foreach ($pks as $pk) { + if ($table->load($pk)) { + // Access checks. + if (!$user->authorise('core.delete', 'com_templates')) { + throw new \Exception(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED')); + } + + // You should not delete a default style + if ($table->home != '0') { + Factory::getApplication()->enqueueMessage(Text::_('COM_TEMPLATES_STYLE_CANNOT_DELETE_DEFAULT_STYLE'), 'error'); + + return false; + } + + // Trigger the before delete event. + $result = Factory::getApplication()->triggerEvent($this->event_before_delete, array($context, $table)); + + if (in_array(false, $result, true) || !$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after delete event. + Factory::getApplication()->triggerEvent($this->event_after_delete, array($context, $table)); + } else { + $this->setError($table->getError()); + + return false; + } + } + + // Clean cache + $this->cleanCache(); + + return true; + } + + /** + * Method to duplicate styles. + * + * @param array &$pks An array of primary key IDs. + * + * @return boolean True if successful. + * + * @throws \Exception + */ + public function duplicate(&$pks) + { + $user = Factory::getUser(); + + // Access checks. + if (!$user->authorise('core.create', 'com_templates')) { + throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED')); + } + + $context = $this->option . '.' . $this->name; + + // Include the plugins for the save events. + PluginHelper::importPlugin($this->events_map['save']); + + $table = $this->getTable(); + + foreach ($pks as $pk) { + if ($table->load($pk, true)) { + // Reset the id to create a new record. + $table->id = 0; + + // Reset the home (don't want dupes of that field). + $table->home = 0; + + // Alter the title. + $m = null; + $table->title = $this->generateNewTitle(null, null, $table->title); + + if (!$table->check()) { + throw new \Exception($table->getError()); + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, true)); + + if (in_array(false, $result, true) || !$table->store()) { + throw new \Exception($table->getError()); + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, true)); + } else { + throw new \Exception($table->getError()); + } + } + + // Clean cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the title. + * + * @param integer $categoryId The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return string New title. + * + * @since 1.7.1 + */ + protected function generateNewTitle($categoryId, $alias, $title) + { + // Alter the title + $table = $this->getTable(); + + while ($table->load(array('title' => $title))) { + $title = StringHelper::increment($title); + } + + return $title; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // The folder and element vars are passed when saving the form. + if (empty($data)) { + $item = $this->getItem(); + $clientId = $item->client_id; + $template = $item->template; + } else { + $clientId = ArrayHelper::getValue($data, 'client_id'); + $template = ArrayHelper::getValue($data, 'template'); + } + + // Add the default fields directory + $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + Form::addFieldPath($baseFolder . '/templates/' . $template . '/field'); + + // These variables are used to add data from the plugin XML files. + $this->setState('item.client_id', $clientId); + $this->setState('item.template', $template); + + // Get the form. + $form = $this->loadForm('com_templates.style', 'style', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('home', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('home', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_templates.edit.style.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_templates.style', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('style.id'); + + if (!isset($this->_cache[$pk])) { + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($pk); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + + // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. + $properties = $table->getProperties(1); + $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); + + // Convert the params field to an array. + $registry = new Registry($table->params); + $this->_cache[$pk]->params = $registry->toArray(); + + // Get the template XML. + $client = ApplicationHelper::getClientInfo($table->client_id); + $path = Path::clean($client->path . '/templates/' . $table->template . '/templateDetails.xml'); + + if (file_exists($path)) { + $this->_cache[$pk]->xml = simplexml_load_file($path); + } else { + $this->_cache[$pk]->xml = null; + } + } + + return $this->_cache[$pk]; + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $clientId = $this->getState('item.client_id'); + $template = $this->getState('item.template'); + $lang = Factory::getLanguage(); + $client = ApplicationHelper::getClientInfo($clientId); + + if (!$form->loadFile('style_' . $client->name, true)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + $formFile = Path::clean($client->path . '/templates/' . $template . '/templateDetails.xml'); + + // Load the core and/or local language file(s). + $lang->load('tpl_' . $template, $client->path) + || (!empty($data->parent) && $lang->load('tpl_' . $data->parent, $client->path)) + || (!empty($data->parent) && $lang->load('tpl_' . $data->parent, $client->path . '/templates/' . $data->parent)) + || $lang->load('tpl_' . $template, $client->path . '/templates/' . $template); + + if (file_exists($formFile)) { + // Get the template form. + if (!$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + // Disable home field if it is default style + + if ( + (is_array($data) && array_key_exists('home', $data) && $data['home'] == '1') + || (is_object($data) && isset($data->home) && $data->home == '1') + ) { + $form->setFieldAttribute('home', 'readonly', 'true'); + } + + if ($client->name === 'site' && !Multilanguage::isEnabled()) { + $form->setFieldAttribute('home', 'type', 'radio'); + $form->setFieldAttribute('home', 'layout', 'joomla.form.field.radio.switcher'); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/extension/help'); + + if (!empty($help)) { + $helpKey = trim((string) $help[0]['key']); + $helpURL = trim((string) $help[0]['url']); + + $this->helpKey = $helpKey ?: $this->helpKey; + $this->helpURL = $helpURL ?: $this->helpURL; + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + */ + public function save($data) + { + // Detect disabled extension + $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\'); + + if ($extension->load(array('enabled' => 0, 'type' => 'template', 'element' => $data['template'], 'client_id' => $data['client_id']))) { + $this->setError(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE')); + + return false; + } + + $app = Factory::getApplication(); + $table = $this->getTable(); + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('style.id'); + $isNew = true; + + // Include the extension plugins for the save events. + PluginHelper::importPlugin($this->events_map['save']); + + // Load the row if saving an existing record. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + if ($app->input->get('task') == 'save2copy') { + $data['title'] = $this->generateNewTitle(null, null, $data['title']); + $data['home'] = 0; + $data['assigned'] = ''; + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array('com_templates.style', &$table, $isNew)); + + // Store the data. + if (in_array(false, $result, true) || !$table->store()) { + $this->setError($table->getError()); + + return false; + } + + $user = Factory::getUser(); + + if ($user->authorise('core.edit', 'com_menus') && $table->client_id == 0) { + $n = 0; + $db = $this->getDatabase(); + $user = Factory::getUser(); + $tableId = (int) $table->id; + $userId = (int) $user->id; + + if (!empty($data['assigned']) && is_array($data['assigned'])) { + $data['assigned'] = ArrayHelper::toInteger($data['assigned']); + + // Update the mapping for menu items that this style IS assigned to. + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('template_style_id') . ' = :newtsid') + ->whereIn($db->quoteName('id'), $data['assigned']) + ->where($db->quoteName('template_style_id') . ' != :tsid') + ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)') + ->bind(':userid', $userId, ParameterType::INTEGER) + ->bind(':newtsid', $tableId, ParameterType::INTEGER) + ->bind(':tsid', $tableId, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + $n += $db->getAffectedRows(); + } + + // Remove style mappings for menu items this style is NOT assigned to. + // If unassigned then all existing maps will be removed. + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('template_style_id') . ' = 0'); + + if (!empty($data['assigned'])) { + $query->whereNotIn($db->quoteName('id'), $data['assigned']); + } + + $query->where($db->quoteName('template_style_id') . ' = :templatestyleid') + ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)') + ->bind(':userid', $userId, ParameterType::INTEGER) + ->bind(':templatestyleid', $tableId, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + $n += $db->getAffectedRows(); + + if ($n > 0) { + $app->enqueueMessage(Text::plural('COM_TEMPLATES_MENU_CHANGED', $n)); + } + } + + // Clean the cache. + $this->cleanCache(); + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array('com_templates.style', &$table, $isNew)); + + $this->setState('style.id', $table->id); + + return true; + } + + /** + * Method to set a template style as home. + * + * @param integer $id The primary key ID for the style. + * + * @return boolean True if successful. + * + * @throws \Exception + */ + public function setHome($id = 0) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Access checks. + if (!$user->authorise('core.edit.state', 'com_templates')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + } + + $style = $this->getTable(); + + if (!$style->load((int) $id)) { + throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND')); + } + + // Detect disabled extension + $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\'); + + if ($extension->load(array('enabled' => 0, 'type' => 'template', 'element' => $style->template, 'client_id' => $style->client_id))) { + throw new \Exception(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE')); + } + + $clientId = (int) $style->client_id; + $id = (int) $id; + + // Reset the home fields for the client_id. + $query = $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('home') . ' = ' . $db->quote('0')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->where($db->quoteName('home') . ' = ' . $db->quote('1')) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Set the new home style. + $query = $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('home') . ' = ' . $db->quote('1')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Clean the cache. + $this->cleanCache(); + + return true; + } + + /** + * Method to unset a template style as default for a language. + * + * @param integer $id The primary key ID for the style. + * + * @return boolean True if successful. + * + * @throws \Exception + */ + public function unsetHome($id = 0) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Access checks. + if (!$user->authorise('core.edit.state', 'com_templates')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + } + + $id = (int) $id; + + // Lookup the client_id. + $query = $db->getQuery(true) + ->select($db->quoteName(['client_id', 'home'])) + ->from($db->quoteName('#__template_styles')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $style = $db->loadObject(); + + if (!is_numeric($style->client_id)) { + throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND')); + } elseif ($style->home == '1') { + throw new \Exception(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE')); + } + + // Set the new home style. + $query = $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('home') . ' = ' . $db->quote('0')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Clean the cache. + $this->cleanCache(); + + return true; + } + + /** + * Get the necessary data to load an item help screen. + * + * @return object An object with key, url, and local properties for loading the item help screen. + * + * @since 1.6 + */ + public function getHelp() + { + return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); + } + + /** + * Returns the back end template for the given style. + * + * @param int $styleId The style id + * + * @return stdClass + * + * @since 4.2.0 + */ + public function getAdminTemplate(int $styleId): stdClass + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['s.template', 's.params', 's.inheritable', 's.parent'])) + ->from($db->quoteName('#__template_styles', 's')) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.type') . ' = ' . $db->quote('template') + . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template') + . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id') + ) + ->where( + [ + $db->quoteName('s.client_id') . ' = 1', + $db->quoteName('s.home') . ' = ' . $db->quote('1'), + ] + ); + + if ($styleId) { + $query->extendWhere( + 'OR', + [ + $db->quoteName('s.client_id') . ' = 1', + $db->quoteName('s.id') . ' = :style', + $db->quoteName('e.enabled') . ' = 1', + ] + ) + ->bind(':style', $styleId, ParameterType::INTEGER); + } + + $query->order($db->quoteName('s.home')); + $db->setQuery($query); + + return $db->loadObject(); + } + + /** + * Returns the front end templates. + * + * @return array + * + * @since 4.2.0 + */ + public function getSiteTemplates(): array + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'home', 'template', 's.params', 'inheritable', 'parent'])) + ->from($db->quoteName('#__template_styles', 's')) + ->where( + [ + $db->quoteName('s.client_id') . ' = 0', + $db->quoteName('e.enabled') . ' = 1', + ] + ) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template') + . ' AND ' . $db->quoteName('e.type') . ' = ' . $db->quote('template') + . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id') + ); + + $db->setQuery($query); + + return $db->loadObjectList('id'); + } + + /** + * Custom clean cache method + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_templates'); + parent::cleanCache('_system'); + } } diff --git a/code/administrator/components/com_templates/src/Model/StylesModel.php b/code/administrator/components/com_templates/src/Model/StylesModel.php index b900bcd6..cf06d587 100644 --- a/code/administrator/components/com_templates/src/Model/StylesModel.php +++ b/code/administrator/components/com_templates/src/Model/StylesModel.php @@ -1,4 +1,5 @@ isClient('api')) - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.template', $this->getUserStateFromRequest($this->context . '.filter.template', 'filter_template', '', 'string')); - $this->setState('filter.menuitem', $this->getUserStateFromRequest($this->context . '.filter.menuitem', 'filter_menuitem', '', 'cmd')); + if (!$app->isClient('api')) { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.template', $this->getUserStateFromRequest($this->context . '.filter.template', 'filter_template', '', 'string')); + $this->setState('filter.menuitem', $this->getUserStateFromRequest($this->context . '.filter.menuitem', 'filter_menuitem', '', 'cmd')); - // Special case for the client id. - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = !in_array($clientId, [0, 1]) ? 0 : $clientId; - $this->setState('client_id', $clientId); - } + // Special case for the client id. + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = !in_array($clientId, [0, 1]) ? 0 : $clientId; + $this->setState('client_id', $clientId); + } - // Load the parameters. - $params = ComponentHelper::getParams('com_templates'); - $this->setState('params', $params); + // Load the parameters. + $params = ComponentHelper::getParams('com_templates'); + $this->setState('params', $params); - // List state information. - parent::populateState($ordering, $direction); - } + // List state information. + parent::populateState($ordering, $direction); + } - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.template'); - $id .= ':' . $this->getState('filter.menuitem'); + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.template'); + $id .= ':' . $this->getState('filter.menuitem'); - return parent::getStoreId($id); - } + return parent::getStoreId($id); + } - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - $clientId = (int) $this->getState('client_id'); + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + $clientId = (int) $this->getState('client_id'); - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.template'), - $db->quoteName('a.title'), - $db->quoteName('a.home'), - $db->quoteName('a.client_id'), - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image'), - $db->quoteName('l.sef', 'language_sef'), - ] - ) - ) - ->select( - [ - 'COUNT(' . $db->quoteName('m.template_style_id') . ') AS assigned', - $db->quoteName('extension_id', 'e_id'), - ] - ) - ->from($db->quoteName('#__template_styles', 'a')) - ->where($db->quoteName('a.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.template'), + $db->quoteName('a.title'), + $db->quoteName('a.home'), + $db->quoteName('a.client_id'), + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image'), + $db->quoteName('l.sef', 'language_sef'), + ] + ) + ) + ->select( + [ + 'COUNT(' . $db->quoteName('m.template_style_id') . ') AS assigned', + $db->quoteName('extension_id', 'e_id'), + ] + ) + ->from($db->quoteName('#__template_styles', 'a')) + ->where($db->quoteName('a.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); - // Join on menus. - $query->join('LEFT', $db->quoteName('#__menu', 'm'), $db->quoteName('m.template_style_id') . ' = ' . $db->quoteName('a.id')) - ->group( - [ - $db->quoteName('a.id'), - $db->quoteName('a.template'), - $db->quoteName('a.title'), - $db->quoteName('a.home'), - $db->quoteName('a.client_id'), - $db->quoteName('l.title'), - $db->quoteName('l.image'), - $db->quoteName('l.sef'), - $db->quoteName('e.extension_id'), - ] - ); + // Join on menus. + $query->join('LEFT', $db->quoteName('#__menu', 'm'), $db->quoteName('m.template_style_id') . ' = ' . $db->quoteName('a.id')) + ->group( + [ + $db->quoteName('a.id'), + $db->quoteName('a.template'), + $db->quoteName('a.title'), + $db->quoteName('a.home'), + $db->quoteName('a.client_id'), + $db->quoteName('l.title'), + $db->quoteName('l.image'), + $db->quoteName('l.sef'), + $db->quoteName('e.extension_id'), + ] + ); - // Join over the language. - $query->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.home')); + // Join over the language. + $query->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.home')); - // Filter by extension enabled. - $query->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.element') . ' = ' . $db->quoteName('a.template') - . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('a.client_id') - ) - ->where( - [ - $db->quoteName('e.enabled') . ' = 1', - $db->quoteName('e.type') . ' = ' . $db->quote('template'), - ] - ); + // Filter by extension enabled. + $query->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.element') . ' = ' . $db->quoteName('a.template') + . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('a.client_id') + ) + ->where( + [ + $db->quoteName('e.enabled') . ' = 1', + $db->quoteName('e.type') . ' = ' . $db->quote('template'), + ] + ); - // Filter by template. - if ($template = $this->getState('filter.template')) - { - $query->where($db->quoteName('a.template') . ' = :template') - ->bind(':template', $template); - } + // Filter by template. + if ($template = $this->getState('filter.template')) { + $query->where($db->quoteName('a.template') . ' = :template') + ->bind(':template', $template); + } - // Filter by menuitem. - $menuItemId = $this->getState('filter.menuitem'); + // Filter by menuitem. + $menuItemId = $this->getState('filter.menuitem'); - if ($clientId === 0 && is_numeric($menuItemId)) - { - // If user selected the templates styles that are not assigned to any page. - if ((int) $menuItemId === -1) - { - // Only custom template styles overrides not assigned to any menu item. - $query->where( - [ - $db->quoteName('a.home') . ' = ' . $db->quote('0'), - $db->quoteName('m.id') . ' IS NULL', - ] - ); - } - // If user selected the templates styles assigned to particular pages. - else - { - // Subquery to get the language of the selected menu item. - $menuItemId = (int) $menuItemId; - $menuItemLanguageSubQuery = $db->getQuery(true); - $menuItemLanguageSubQuery->select($db->quoteName('language')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('id') . ' = :menuitemid'); - $query->bind(':menuitemid', $menuItemId, ParameterType::INTEGER); + if ($clientId === 0 && is_numeric($menuItemId)) { + // If user selected the templates styles that are not assigned to any page. + if ((int) $menuItemId === -1) { + // Only custom template styles overrides not assigned to any menu item. + $query->where( + [ + $db->quoteName('a.home') . ' = ' . $db->quote('0'), + $db->quoteName('m.id') . ' IS NULL', + ] + ); + } else { + // If user selected the templates styles assigned to particular pages. + // Subquery to get the language of the selected menu item. + $menuItemId = (int) $menuItemId; + $menuItemLanguageSubQuery = $db->getQuery(true); + $menuItemLanguageSubQuery->select($db->quoteName('language')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('id') . ' = :menuitemid'); + $query->bind(':menuitemid', $menuItemId, ParameterType::INTEGER); - // Subquery to get the language of the selected menu item. - $templateStylesMenuItemsSubQuery = $db->getQuery(true); - $templateStylesMenuItemsSubQuery->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('template_style_id') . ' = ' . $db->quoteName('a.id')); + // Subquery to get the language of the selected menu item. + $templateStylesMenuItemsSubQuery = $db->getQuery(true); + $templateStylesMenuItemsSubQuery->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('template_style_id') . ' = ' . $db->quoteName('a.id')); - // Main query where clause. - $query->where('(' . - // Default template style (fallback template style to all menu items). - $db->quoteName('a.home') . ' = ' . $db->quote('1') . ' OR ' . - // Default template style for specific language (fallback template style to the selected menu item language). - $db->quoteName('a.home') . ' IN (' . $menuItemLanguageSubQuery . ') OR ' . - // Custom template styles override (only if assigned to the selected menu item). - '(' . $db->quoteName('a.home') . ' = ' . $db->quote('0') . ' AND ' . $menuItemId . ' IN (' . $templateStylesMenuItemsSubQuery . '))' . - ')' - ); - } - } + // Main query where clause. + $query->where('(' . + // Default template style (fallback template style to all menu items). + $db->quoteName('a.home') . ' = ' . $db->quote('1') . ' OR ' . + // Default template style for specific language (fallback template style to the selected menu item language). + $db->quoteName('a.home') . ' IN (' . $menuItemLanguageSubQuery . ') OR ' . + // Custom template styles override (only if assigned to the selected menu item). + '(' . $db->quoteName('a.home') . ' = ' . $db->quote('0') . ' AND ' . $menuItemId . ' IN (' . $templateStylesMenuItemsSubQuery . '))' . + ')'); + } + } - // Filter by search in title. - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . StringHelper::strtolower($search) . '%'; - $query->extendWhere( - 'AND', - [ - 'LOWER(' . $db->quoteName('a.template') . ') LIKE :template', - 'LOWER(' . $db->quoteName('a.title') . ') LIKE :title', - ], - 'OR' - ) - ->bind(':template', $search) - ->bind(':title', $search); - } - } + // Filter by search in title. + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . StringHelper::strtolower($search) . '%'; + $query->extendWhere( + 'AND', + [ + 'LOWER(' . $db->quoteName('a.template') . ') LIKE :template', + 'LOWER(' . $db->quoteName('a.title') . ') LIKE :title', + ], + 'OR' + ) + ->bind(':template', $search) + ->bind(':title', $search); + } + } - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.template')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.template')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - return $query; - } + return $query; + } } diff --git a/code/administrator/components/com_templates/src/Model/TemplateModel.php b/code/administrator/components/com_templates/src/Model/TemplateModel.php index 82281f8e..f22dfce1 100644 --- a/code/administrator/components/com_templates/src/Model/TemplateModel.php +++ b/code/administrator/components/com_templates/src/Model/TemplateModel.php @@ -1,4 +1,5 @@ getTemplate()) - { - $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $path); - $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $path); - $temp->name = $name; - $temp->id = urlencode(base64_encode(str_replace('\\', '//', $path))); - - return $temp; - } - } - - /** - * Method to store file information. - * - * @param string $path The base path. - * @param string $name The file name. - * @param stdClass $template The std class object of template. - * - * @return object stdClass object. - * - * @since 4.0.0 - */ - protected function storeFileInfo($path, $name, $template) - { - $temp = new \stdClass; - $temp->id = base64_encode($path . $name); - $temp->client = $template->client_id; - $temp->template = $template->element; - $temp->extension_id = $template->extension_id; - - if ($coreFile = $this->getCoreFile($path . $name, $template->client_id)) - { - $temp->coreFile = md5_file($coreFile); - } - else - { - $temp->coreFile = null; - } - - return $temp; - } - - /** - * Method to get all template list. - * - * @return object stdClass object - * - * @since 4.0.0 - */ - public function getTemplateList() - { - // Get a db connection. - $db = $this->getDbo(); - - // Create a new query object. - $query = $db->getQuery(true); - - // Select the required fields from the table - $query->select( - $this->getState( - 'list.select', - 'a.extension_id, a.name, a.element, a.client_id' - ) - ); - - $query->from($db->quoteName('#__extensions', 'a')) - ->where($db->quoteName('a.enabled') . ' = 1') - ->where($db->quoteName('a.type') . ' = ' . $db->quote('template')); - - // Reset the query. - $db->setQuery($query); - - // Load the results as a list of stdClass objects. - $results = $db->loadObjectList(); - - return $results; - } - - /** - * Method to get all updated file list. - * - * @param boolean $state The optional parameter if you want unchecked list. - * @param boolean $all The optional parameter if you want all list. - * @param boolean $cleanup The optional parameter if you want to clean record which is no more required. - * - * @return object stdClass object - * - * @since 4.0.0 - */ - public function getUpdatedList($state = false, $all = false, $cleanup = false) - { - // Get a db connection. - $db = $this->getDbo(); - - // Create a new query object. - $query = $db->getQuery(true); - - // Select the required fields from the table - $query->select( - $this->getState( - 'list.select', - 'a.template, a.hash_id, a.extension_id, a.state, a.action, a.client_id, a.created_date, a.modified_date' - ) - ); - - $template = $this->getTemplate(); - - $query->from($db->quoteName('#__template_overrides', 'a')); - - if (!$all) - { - $teid = (int) $template->extension_id; - $query->where($db->quoteName('extension_id') . ' = :teid') - ->bind(':teid', $teid, ParameterType::INTEGER); - } - - if ($state) - { - $query->where($db->quoteName('state') . ' = 0'); - } - - $query->order($db->quoteName('a.modified_date') . ' DESC'); - - // Reset the query. - $db->setQuery($query); - - // Load the results as a list of stdClass objects. - $pks = $db->loadObjectList(); - - if ($state) - { - return $pks; - } - - $results = array(); - - foreach ($pks as $pk) - { - $client = ApplicationHelper::getClientInfo($pk->client_id); - $path = Path::clean($client->path . '/templates/' . $pk->template . base64_decode($pk->hash_id)); - - if (file_exists($path)) - { - $results[] = $pk; - } - elseif ($cleanup) - { - $cleanupIds = array(); - $cleanupIds[] = $pk->hash_id; - $this->publish($cleanupIds, -3, $pk->extension_id); - } - } - - return $results; - } - - /** - * Method to get a list of all the core files of override files. - * - * @return array An array of all core files. - * - * @since 4.0.0 - */ - public function getCoreList() - { - // Get list of all templates - $templates = $this->getTemplateList(); - - // Initialize the array variable to store core file list. - $this->coreFileList = array(); - - $app = Factory::getApplication(); - - foreach ($templates as $template) - { - $client = ApplicationHelper::getClientInfo($template->client_id); - $element = Path::clean($client->path . '/templates/' . $template->element . '/'); - $path = Path::clean($element . 'html/'); - - if (is_dir($path)) - { - $this->prepareCoreFiles($path, $element, $template); - } - } - - // Sort list of stdClass array. - usort( - $this->coreFileList, - function ($a, $b) - { - return strcmp($a->id, $b->id); - } - ); - - return $this->coreFileList; - } - - /** - * Prepare core files. - * - * @param string $dir The path of the directory to scan. - * @param string $element The path of the template element. - * @param \stdClass $template The stdClass object of template. - * - * @return array - * - * @since 4.0.0 - */ - public function prepareCoreFiles($dir, $element, $template) - { - $dirFiles = scandir($dir); - - foreach ($dirFiles as $key => $value) - { - if (in_array($value, array('.', '..', 'node_modules'))) - { - continue; - } - - if (is_dir($dir . $value)) - { - $relativePath = str_replace($element, '', $dir . $value); - $this->prepareCoreFiles($dir . $value . '/', $element, $template); - } - else - { - $ext = pathinfo($dir . $value, PATHINFO_EXTENSION); - $allowedFormat = $this->checkFormat($ext); - - if ($allowedFormat === true) - { - $relativePath = str_replace($element, '', $dir); - $info = $this->storeFileInfo('/' . $relativePath, $value, $template); - - if ($info) - { - $this->coreFileList[] = $info; - } - } - } - } - } - - /** - * Method to update status of list. - * - * @param array $ids The base path. - * @param array $value The file name. - * @param integer $exid The template extension id. - * - * @return integer Number of files changed. - * - * @since 4.0.0 - */ - public function publish($ids, $value, $exid) - { - $db = $this->getDbo(); - - foreach ($ids as $id) - { - if ($value === -3) - { - $deleteQuery = $db->getQuery(true) - ->delete($db->quoteName('#__template_overrides')) - ->where($db->quoteName('hash_id') . ' = :hashid') - ->where($db->quoteName('extension_id') . ' = :exid') - ->bind(':hashid', $id) - ->bind(':exid', $exid, ParameterType::INTEGER); - - try - { - // Set the query using our newly populated query object and execute it. - $db->setQuery($deleteQuery); - $result = $db->execute(); - } - catch (\RuntimeException $e) - { - return $e; - } - } - elseif ($value === 1 || $value === 0) - { - $updateQuery = $db->getQuery(true) - ->update($db->quoteName('#__template_overrides')) - ->set($db->quoteName('state') . ' = :state') - ->where($db->quoteName('hash_id') . ' = :hashid') - ->where($db->quoteName('extension_id') . ' = :exid') - ->bind(':state', $value, ParameterType::INTEGER) - ->bind(':hashid', $id) - ->bind(':exid', $exid, ParameterType::INTEGER); - - try - { - // Set the query using our newly populated query object and execute it. - $db->setQuery($updateQuery); - $result = $db->execute(); - } - catch (\RuntimeException $e) - { - return $e; - } - } - } - - return $result; - } - - /** - * Method to get a list of all the files to edit in a template. - * - * @return array A nested array of relevant files. - * - * @since 1.6 - */ - public function getFiles() - { - $result = array(); - - if ($template = $this->getTemplate()) - { - $app = Factory::getApplication(); - $client = ApplicationHelper::getClientInfo($template->client_id); - $path = Path::clean($client->path . '/templates/' . $template->element . '/'); - $lang = Factory::getLanguage(); - - // Load the core and/or local language file(s). - $lang->load('tpl_' . $template->element, $client->path) - || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path)) - || $lang->load('tpl_' . $template->element, $client->path . '/templates/' . $template->element) - || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path . '/templates/' . $template->xmldata->parent)); - $this->element = $path; - - if (!is_writable($path)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error'); - } - - if (is_dir($path)) - { - $result = $this->getDirectoryTree($path); - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_TEMPLATE_FOLDER_NOT_FOUND'), 'error'); - - return false; - } - - // Clean up override history - $this->getUpdatedList(false, true, true); - } - - return $result; - } - - /** - * Get the directory tree. - * - * @param string $dir The path of the directory to scan - * - * @return array - * - * @since 3.2 - */ - public function getDirectoryTree($dir) - { - $result = array(); - - $dirFiles = scandir($dir); - - foreach ($dirFiles as $key => $value) - { - if (!in_array($value, array('.', '..', 'node_modules'))) - { - if (is_dir($dir . $value)) - { - $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value); - $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) .'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath); - $result[str_replace('\\', '//', $relativePath)] = $this->getDirectoryTree($dir . $value . '/'); - } - else - { - $ext = pathinfo($dir . $value, PATHINFO_EXTENSION); - $allowedFormat = $this->checkFormat($ext); - - if ($allowedFormat == true) - { - $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media'. DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value); - $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath); - $result[] = $this->getFile($relativePath, $value); - } - } - } - } - - return $result; - } - - /** - * Method to get the core file of override file - * - * @param string $file Override file - * @param integer $client_id Client Id - * - * @return string $corefile The full path and file name for the target file, or boolean false if the file is not found in any of the paths. - * - * @since 4.0.0 - */ - public function getCoreFile($file, $client_id) - { - $app = Factory::getApplication(); - $filePath = Path::clean($file); - $explodeArray = explode(DIRECTORY_SEPARATOR, $filePath); - - // Only allow html/ folder - if ($explodeArray['1'] !== 'html') - { - return false; - } - - $fileName = basename($filePath); - $type = $explodeArray['2']; - $client = ApplicationHelper::getClientInfo($client_id); - - $componentPath = Path::clean($client->path . '/components/'); - $modulePath = Path::clean($client->path . '/modules/'); - $layoutPath = Path::clean(JPATH_ROOT . '/layouts/'); - - // For modules - if (stristr($type, 'mod_') !== false) - { - $folder = $explodeArray['2']; - $htmlPath = Path::clean($modulePath . $folder . '/tmpl/'); - $fileName = $this->getSafeName($fileName); - $coreFile = Path::find($htmlPath, $fileName); - - return $coreFile; - } - elseif (stristr($type, 'com_') !== false) - { - // For components - $folder = $explodeArray['2']; - $subFolder = $explodeArray['3']; - $fileName = $this->getSafeName($fileName); - - // The new scheme, if a view has a tmpl folder - $newHtmlPath = Path::clean($componentPath . $folder . '/tmpl/' . $subFolder . '/'); - - if (!$coreFile = Path::find($newHtmlPath, $fileName)) - { - // The old scheme, the views are directly in the component/tmpl folder - $oldHtmlPath = Path::clean($componentPath . $folder . '/views/' . $subFolder . '/tmpl/'); - $coreFile = Path::find($oldHtmlPath, $fileName); - - return $coreFile; - } - - return $coreFile; - } - elseif (stristr($type, 'layouts') !== false) - { - // For Layouts - $subtype = $explodeArray['3']; - - if (stristr($subtype, 'com_')) - { - $folder = $explodeArray['3']; - $subFolder = array_slice($explodeArray, 4, -1); - $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder); - $htmlPath = Path::clean($componentPath . $folder . '/layouts/' . $subFolder); - $fileName = $this->getSafeName($fileName); - $coreFile = Path::find($htmlPath, $fileName); - - return $coreFile; - } - elseif (stristr($subtype, 'joomla') || stristr($subtype, 'libraries') || stristr($subtype, 'plugins')) - { - $subFolder = array_slice($explodeArray, 3, -1); - $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder); - $htmlPath = Path::clean($layoutPath . $subFolder); - $fileName = $this->getSafeName($fileName); - $coreFile = Path::find($htmlPath, $fileName); - - return $coreFile; - } - } - - return false; - } - - /** - * Creates a safe file name for the given name. - * - * @param string $name The filename - * - * @return string $fileName The filtered name without Date - * - * @since 4.0.0 - */ - private function getSafeName($name) - { - if (strpos($name, '-') !== false && preg_match('/[0-9]/', $name)) - { - // Get the extension - $extension = File::getExt($name); - - // Remove ( Date ) from file - $explodeArray = explode('-', $name); - $size = count($explodeArray); - $date = $explodeArray[$size - 2] . '-' . str_replace('.' . $extension, '', $explodeArray[$size - 1]); - - if ($this->validateDate($date)) - { - $nameWithoutExtension = implode('-', array_slice($explodeArray, 0, -2)); - - // Filtered name - $name = $nameWithoutExtension . '.' . $extension; - } - } - - return $name; - } - - /** - * Validate Date in file name. - * - * @param string $date Date to validate. - * - * @return boolean Return true if date is valid and false if not. - * - * @since 4.0.0 - */ - private function validateDate($date) - { - $format = 'Ymd-His'; - $valid = Date::createFromFormat($format, $date); - - return $valid && $valid->format($format) === $date; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState('extension.id', $pk); - - // Load the parameters. - $params = ComponentHelper::getParams('com_templates'); - $this->setState('params', $params); - } - - /** - * Method to get the template information. - * - * @return mixed Object if successful, false if not and internal error is set. - * - * @since 1.6 - */ - public function &getTemplate() - { - if (empty($this->template)) - { - $pk = (int) $this->getState('extension.id'); - $db = $this->getDbo(); - $app = Factory::getApplication(); - - // Get the template information. - $query = $db->getQuery(true) - ->select($db->quoteName(['extension_id', 'client_id', 'element', 'name', 'manifest_cache'])) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('extension_id') . ' = :pk') - ->where($db->quoteName('type') . ' = ' . $db->quote('template')) - ->bind(':pk', $pk, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $result = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'warning'); - $this->template = false; - - return false; - } - - if (empty($result)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'error'); - $this->template = false; - } - else - { - $this->template = $result; - - // Client ID is not always an integer, so enforce here - $this->template->client_id = (int) $this->template->client_id; - - if (!isset($this->template->xmldata)) - { - $this->template->xmldata = TemplatesHelper::parseXMLTemplateFile($this->template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $this->template->name); - } - } - } - - return $this->template; - } - - /** - * Method to check if new template name already exists - * - * @return boolean true if name is not used, false otherwise - * - * @since 2.5 - */ - public function checkNewName() - { - $db = $this->getDbo(); - $name = $this->getState('new_name'); - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('name') . ' = :name') - ->bind(':name', $name); - $db->setQuery($query); - - return ($db->loadResult() == 0); - } - - /** - * Method to check if new template name already exists - * - * @return string name of current template - * - * @since 2.5 - */ - public function getFromName() - { - return $this->getTemplate()->element; - } - - /** - * Method to check if new template name already exists - * - * @return boolean true if name is not used, false otherwise - * - * @since 2.5 - */ - public function copy() - { - $app = Factory::getApplication(); - - if ($template = $this->getTemplate()) - { - $client = ApplicationHelper::getClientInfo($template->client_id); - $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/'); - - // Delete new folder if it exists - $toPath = $this->getState('to_path'); - - if (Folder::exists($toPath)) - { - if (!Folder::delete($toPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - } - - // Copy all files from $fromName template to $newName folder - if (!Folder::copy($fromPath, $toPath)) - { - return false; - } - - // Check manifest for additional files - $manifest = simplexml_load_file($toPath . '/templateDetails.xml'); - - // Copy language files from global folder - if ($languages = $manifest->languages) - { - $folder = (string) $languages->attributes()->folder; - $languageFiles = $languages->language; - - Folder::create($toPath . '/' . $folder . '/' . $languageFiles->attributes()->tag); - - foreach ($languageFiles as $languageFile) - { - $src = Path::clean($client->path . '/language/' . $languageFile); - $dst = Path::clean($toPath . '/' . $folder . '/' . $languageFile); - - if (File::exists($src)) - { - File::copy($src, $dst); - } - } - } - - // Copy media files - if ($media = $manifest->media) - { - $folder = (string) $media->attributes()->folder; - $destination = (string) $media->attributes()->destination; - - Folder::copy(JPATH_SITE . '/media/' . $destination, $toPath . '/' . $folder); - } - - // Adjust to new template name - if (!$this->fixTemplateName()) - { - return false; - } - - return true; - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); - - return false; - } - } - - /** - * Method to delete tmp folder - * - * @return boolean true if delete successful, false otherwise - * - * @since 2.5 - */ - public function cleanup() - { - // Clear installation messages - $app = Factory::getApplication(); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - // Delete temporary directory - return Folder::delete($this->getState('to_path')); - } - - /** - * Method to rename the template in the XML files and rename the language files - * - * @return boolean true if successful, false otherwise - * - * @since 2.5 - */ - protected function fixTemplateName() - { - // Rename Language files - // Get list of language files - $result = true; - $files = Folder::files($this->getState('to_path'), '\.ini$', true, true); - $newName = strtolower($this->getState('new_name')); - $template = $this->getTemplate(); - $oldName = $template->element; - $manifest = json_decode($template->manifest_cache); - - foreach ($files as $file) - { - $newFile = '/' . str_replace($oldName, $newName, basename($file)); - $result = File::move($file, dirname($file) . $newFile) && $result; - } - - // Edit XML file - $xmlFile = $this->getState('to_path') . '/templateDetails.xml'; - - if (File::exists($xmlFile)) - { - $contents = file_get_contents($xmlFile); - $pattern[] = '#\s*' . $manifest->name . '\s*#i'; - $replace[] = '' . $newName . ''; - $pattern[] = '##'; - $replace[] = ''; - $pattern[] = '##'; - $replace[] = ''; - $contents = preg_replace($pattern, $replace, $contents); - $result = File::write($xmlFile, $contents) && $result; - } - - return $result; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - $app = Factory::getApplication(); - - // Codemirror or Editor None should be enabled - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from('#__extensions as a') - ->where( - '(a.name =' . $db->quote('plg_editors_codemirror') . - ' AND a.enabled = 1) OR (a.name =' . - $db->quote('plg_editors_none') . - ' AND a.enabled = 1)' - ); - $db->setQuery($query); - $state = $db->loadResult(); - - if ((int) $state < 1) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EDITOR_DISABLED'), 'warning'); - } - - // Get the form. - $form = $this->loadForm('com_templates.source', 'source', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $data = $this->getSource(); - - $this->preprocessData('com_templates.source', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function &getSource() - { - $app = Factory::getApplication(); - $item = new \stdClass; - - if (!$this->template) - { - $this->getTemplate(); - } - - if ($this->template) - { - $input = Factory::getApplication()->input; - $fileName = base64_decode($input->get('file')); - $fileName = str_replace('//', '/', $fileName); - $isMedia = $input->getInt('isMedia', 0); - - $fileName = $isMedia ? Path::clean(JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName) - : Path::clean(JPATH_ROOT . ($this->template->client_id === 0 ? '' : '/administrator') . '/templates/' . $this->template->element . $fileName); - - try - { - $filePath = Path::check($fileName); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error'); - - return; - } - - if (file_exists($filePath)) - { - $item->extension_id = $this->getState('extension.id'); - $item->filename = Path::clean($fileName); - $item->source = file_get_contents($filePath); - $item->filePath = Path::clean($filePath); - $ds = DIRECTORY_SEPARATOR; - $cleanFileName = str_replace(JPATH_ROOT . ($this->template->client_id === 1 ? $ds . 'administrator' . $ds : $ds) . 'templates' . $ds . $this->template->element, '', $fileName); - - if ($coreFile = $this->getCoreFile($cleanFileName, $this->template->client_id)) - { - $item->coreFile = $coreFile; - $item->core = file_get_contents($coreFile); - } - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error'); - } - } - - return $item; - } - - /** - * Method to store the source file contents. - * - * @param array $data The source data to save. - * - * @return boolean True on success, false otherwise and internal error set. - * - * @since 1.6 - */ - public function save($data) - { - // Get the template. - $template = $this->getTemplate(); - - if (empty($template)) - { - return false; - } - - $app = Factory::getApplication(); - $fileName = base64_decode($app->input->get('file')); - $isMedia = $app->input->getInt('isMedia', 0); - $fileName = $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName : - JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . $fileName; - - $filePath = Path::clean($fileName); - - // Include the extension plugins for the save events. - PluginHelper::importPlugin('extension'); - - $user = get_current_user(); - chown($filePath, $user); - Path::setPermissions($filePath, '0644'); - - // Try to make the template file writable. - if (!is_writable($filePath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_WRITABLE'), 'warning'); - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_PERMISSIONS', Path::getPermissions($filePath)), 'warning'); - - if (!Path::isOwner($filePath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_CHECK_FILE_OWNERSHIP'), 'warning'); - } - - return false; - } - - // Make sure EOL is Unix - $data['source'] = str_replace(array("\r\n", "\r"), "\n", $data['source']); - - // If the asset file for the template ensure we have valid template so we don't instantly destroy it - if ($fileName === '/joomla.asset.json' && json_decode($data['source']) === null) - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_INVALID_JSON')); - - return false; - } - - $return = File::write($filePath, $data['source']); - - if (!$return) - { - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_ERROR_FAILED_TO_SAVE_FILENAME', $fileName), 'error'); - - return false; - } - - // Get the extension of the changed file. - $explodeArray = explode('.', $fileName); - $ext = end($explodeArray); - - if ($ext == 'less') - { - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_COMPILE_LESS', $fileName)); - } - - return true; - } - - /** - * Get overrides folder. - * - * @param string $name The name of override. - * @param string $path Location of override. - * - * @return object containing override name and path. - * - * @since 3.2 - */ - public function getOverridesFolder($name,$path) - { - $folder = new \stdClass; - $folder->name = $name; - $folder->path = base64_encode($path . $name); - - return $folder; - } - - /** - * Get a list of overrides. - * - * @return array containing overrides. - * - * @since 3.2 - */ - public function getOverridesList() - { - if ($template = $this->getTemplate()) - { - $client = ApplicationHelper::getClientInfo($template->client_id); - $componentPath = Path::clean($client->path . '/components/'); - $modulePath = Path::clean($client->path . '/modules/'); - $pluginPath = Path::clean(JPATH_ROOT . '/plugins/'); - $layoutPath = Path::clean(JPATH_ROOT . '/layouts/'); - $components = Folder::folders($componentPath); - - foreach ($components as $component) - { - // Collect the folders with views - $folders = Folder::folders($componentPath . '/' . $component, '^view[s]?$', false, true); - $folders = array_merge($folders, Folder::folders($componentPath . '/' . $component, '^tmpl?$', false, true)); - - if (!$folders) - { - continue; - } - - foreach ($folders as $folder) - { - // The subfolders are views - $views = Folder::folders($folder); - - foreach ($views as $view) - { - // The old scheme, if a view has a tmpl folder - $path = $folder . '/' . $view . '/tmpl'; - - // The new scheme, the views are directly in the component/tmpl folder - if (!is_dir($path) && substr($folder, -4) == 'tmpl') - { - $path = $folder . '/' . $view; - } - - // Check if the folder exists - if (!is_dir($path)) - { - continue; - } - - $result['components'][$component][] = $this->getOverridesFolder($view, Path::clean($folder . '/')); - } - } - } - - foreach (Folder::folders($pluginPath) as $pluginGroup) - { - foreach (Folder::folders($pluginPath . '/' . $pluginGroup) as $plugin) - { - if (file_exists($pluginPath . '/' . $pluginGroup . '/' . $plugin . '/tmpl/')) - { - $pluginLayoutPath = Path::clean($pluginPath . '/' . $pluginGroup . '/'); - $result['plugins'][$pluginGroup][] = $this->getOverridesFolder($plugin, $pluginLayoutPath); - } - } - } - - $modules = Folder::folders($modulePath); - - foreach ($modules as $module) - { - $result['modules'][] = $this->getOverridesFolder($module, $modulePath); - } - - $layoutFolders = Folder::folders($layoutPath); - - foreach ($layoutFolders as $layoutFolder) - { - $layoutFolderPath = Path::clean($layoutPath . '/' . $layoutFolder . '/'); - $layouts = Folder::folders($layoutFolderPath); - - foreach ($layouts as $layout) - { - $result['layouts'][$layoutFolder][] = $this->getOverridesFolder($layout, $layoutFolderPath); - } - } - - // Check for layouts in component folders - foreach ($components as $component) - { - if (file_exists($componentPath . '/' . $component . '/layouts/')) - { - $componentLayoutPath = Path::clean($componentPath . '/' . $component . '/layouts/'); - - if ($componentLayoutPath) - { - $layouts = Folder::folders($componentLayoutPath); - - foreach ($layouts as $layout) - { - $result['layouts'][$component][] = $this->getOverridesFolder($layout, $componentLayoutPath); - } - } - } - } - } - - if (!empty($result)) - { - return $result; - } - } - - /** - * Create overrides. - * - * @param string $override The override location. - * - * @return boolean true if override creation is successful, false otherwise - * - * @since 3.2 - */ - public function createOverride($override) - { - if ($template = $this->getTemplate()) - { - $app = Factory::getApplication(); - $explodeArray = explode(DIRECTORY_SEPARATOR, $override); - $name = end($explodeArray); - $client = ApplicationHelper::getClientInfo($template->client_id); - - if (stristr($name, 'mod_') != false) - { - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $name); - } - elseif (stristr($override, 'com_') != false) - { - $size = count($explodeArray); - - $url = Path::clean($explodeArray[$size - 3] . '/' . $explodeArray[$size - 1]); - - if ($explodeArray[$size - 2] == 'layouts') - { - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $url); - } - else - { - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $url); - } - } - elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) - { - $size = count($explodeArray); - $layoutPath = Path::clean('plg_' . $explodeArray[$size - 2] . '_' . $explodeArray[$size - 1]); - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $layoutPath); - } - else - { - $layoutPath = implode('/', array_slice($explodeArray, -2)); - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $layoutPath); - } - - // Check Html folder, create if not exist - if (!Folder::exists($htmlPath)) - { - if (!Folder::create($htmlPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_ERROR'), 'error'); - - return false; - } - } - - if (stristr($name, 'mod_') != false) - { - $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath); - } - elseif (stristr($override, 'com_') != false && stristr($override, 'layouts') == false) - { - $path = $override . '/tmpl'; - - // View can also be in the top level folder - if (!is_dir($path)) - { - $path = $override; - } - - $return = $this->createTemplateOverride(Path::clean($path), $htmlPath); - } - elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) - { - $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath); - } - else - { - $return = $this->createTemplateOverride($override, $htmlPath); - } - - if ($return) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_CREATED') . str_replace(JPATH_ROOT, '', $htmlPath)); - - return true; - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_FAILED'), 'error'); - - return false; - } - } - } - - /** - * Create override folder & file - * - * @param string $overridePath The override location - * @param string $htmlPath The html location - * - * @return boolean True on success. False otherwise. - */ - public function createTemplateOverride($overridePath, $htmlPath) - { - $return = false; - - if (empty($overridePath) || empty($htmlPath)) - { - return $return; - } - - // Get list of template folders - $folders = Folder::folders($overridePath, null, true, true); - - if (!empty($folders)) - { - foreach ($folders as $folder) - { - $htmlFolder = $htmlPath . str_replace($overridePath, '', $folder); - - if (!Folder::exists($htmlFolder)) - { - Folder::create($htmlFolder); - } - } - } - - // Get list of template files (Only get *.php file for template file) - $files = Folder::files($overridePath, '.php', true, true); - - if (empty($files)) - { - return true; - } - - foreach ($files as $file) - { - $overrideFilePath = str_replace($overridePath, '', $file); - $htmlFilePath = $htmlPath . $overrideFilePath; - - if (File::exists($htmlFilePath)) - { - // Generate new unique file name base on current time - $today = Factory::getDate(); - $htmlFilePath = File::stripExt($htmlFilePath) . '-' . $today->format('Ymd-His') . '.' . File::getExt($htmlFilePath); - } - - $return = File::copy($file, $htmlFilePath, '', true); - } - - return $return; - } - - /** - * Delete a particular file. - * - * @param string $file The relative location of the file. - * - * @return boolean True if file deletion is successful, false otherwise - * - * @since 3.2 - */ - public function deleteFile($file) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $filePath = $this->getBasePath() . urldecode(base64_decode($file)); - - $return = File::delete($filePath); - - if (!$return) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_DELETE_ERROR'), 'error'); - - return false; - } - - return true; - } - } - - /** - * Create new file. - * - * @param string $name The name of file. - * @param string $type The extension of the file. - * @param string $location Location for the new file. - * - * @return boolean true if file created successfully, false otherwise - * - * @since 3.2 - */ - public function createFile($name, $type, $location) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $base = $this->getBasePath(); - - if (file_exists(Path::clean($base . '/' . $location . '/' . $name . '.' . $type))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); - - return false; - } - - if (!fopen(Path::clean($base . '/' . $location . '/' . $name . '.' . $type), 'x')) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_CREATE_ERROR'), 'error'); - - return false; - } - - // Check if the format is allowed and will be showed in the backend - $check = $this->checkFormat($type); - - // Add a message if we are not allowed to show this file in the backend. - if (!$check) - { - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_WARNING_FORMAT_WILL_NOT_BE_VISIBLE', $type), 'warning'); - } - - return true; - } - } - - /** - * Upload new file. - * - * @param array $file The uploaded file array. - * @param string $location Location for the new file. - * - * @return boolean True if file uploaded successfully, false otherwise - * - * @since 3.2 - */ - public function uploadFile($file, $location) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath(); - $fileName = File::makeSafe($file['name']); - - $err = null; - - if (!TemplateHelper::canUpload($file, $err)) - { - // Can't upload the file - return false; - } - - if (file_exists(Path::clean($path . '/' . $location . '/' . $file['name']))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); - - return false; - } - - if (!File::upload($file['tmp_name'], Path::clean($path . '/' . $location . '/' . $fileName))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UPLOAD_ERROR'), 'error'); - - return false; - } - - $url = Path::clean($location . '/' . $fileName); - - return $url; - } - } - - /** - * Create new folder. - * - * @param string $name The name of the new folder. - * @param string $location Location for the new folder. - * - * @return boolean True if override folder is created successfully, false otherwise - * - * @since 3.2 - */ - public function createFolder($name, $location) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = Path::clean($location . '/'); - $base = $this->getBasePath(); - - if (file_exists(Path::clean($base . $path . $name))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_EXISTS'), 'error'); - - return false; - } - - if (!Folder::create(Path::clean($base . $path . $name))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_ERROR'), 'error'); - - return false; - } - - return true; - } - } - - /** - * Delete a folder. - * - * @param string $location The name and location of the folder. - * - * @return boolean True if override folder is deleted successfully, false otherwise - * - * @since 3.2 - */ - public function deleteFolder($location) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $base = $this->getBasePath(); - $path = Path::clean($location . '/'); - - if (!file_exists($base . $path)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_NOT_EXISTS'), 'error'); - - return false; - } - - $return = Folder::delete($base . $path); - - if (!$return) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); - - return false; - } - - return true; - } - } - - /** - * Rename a file. - * - * @param string $file The name and location of the old file - * @param string $name The new name of the file. - * - * @return string Encoded string containing the new file location. - * - * @since 3.2 - */ - public function renameFile($file, $name) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath(); - $fileName = base64_decode($file); - $explodeArray = explode('.', $fileName); - $type = end($explodeArray); - $explodeArray = explode('/', $fileName); - $newName = str_replace(end($explodeArray), $name . '.' . $type, $fileName); - - if (file_exists($path . $newName)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); - - return false; - } - - if (!rename($path . $fileName, $path . $newName)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_RENAME_ERROR'), 'error'); - - return false; - } - - return base64_encode($newName); - } - } - - /** - * Get an image address, height and width. - * - * @return array an associative array containing image address, height and width. - * - * @since 3.2 - */ - public function getImage() - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $fileName = base64_decode($app->input->get('file')); - $path = $this->getBasePath(); - - $uri = Uri::root(false) . ltrim(str_replace(JPATH_ROOT, '', $this->getBasePath()), '/'); - - if (file_exists(Path::clean($path . $fileName))) - { - $JImage = new Image(Path::clean($path . $fileName)); - $image['address'] = $uri . $fileName; - $image['path'] = $fileName; - $image['height'] = $JImage->getHeight(); - $image['width'] = $JImage->getWidth(); - } - - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_IMAGE_FILE_NOT_FOUND'), 'error'); - - return false; - } - - return $image; - } - } - - /** - * Crop an image. - * - * @param string $file The name and location of the file - * @param string $w width. - * @param string $h height. - * @param string $x x-coordinate. - * @param string $y y-coordinate. - * - * @return boolean true if image cropped successfully, false otherwise. - * - * @since 3.2 - */ - public function cropImage($file, $w, $h, $x, $y) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath() . base64_decode($file); - - try - { - $image = new Image($path); - $properties = $image->getImageFileProperties($path); - - switch ($properties->mime) - { - case 'image/webp': - $imageType = \IMAGETYPE_WEBP; - break; - case 'image/png': - $imageType = \IMAGETYPE_PNG; - break; - case 'image/gif': - $imageType = \IMAGETYPE_GIF; - break; - default: - $imageType = \IMAGETYPE_JPEG; - } - - $image->crop($w, $h, $x, $y, false); - $image->toFile($path, $imageType); - - return true; - } - catch (\Exception $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - } - } - } - - /** - * Resize an image. - * - * @param string $file The name and location of the file - * @param string $width The new width of the image. - * @param string $height The new height of the image. - * - * @return boolean true if image resize successful, false otherwise. - * - * @since 3.2 - */ - public function resizeImage($file, $width, $height) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath() . base64_decode($file); - - try - { - $image = new Image($path); - $properties = $image->getImageFileProperties($path); - - switch ($properties->mime) - { - case 'image/webp': - $imageType = \IMAGETYPE_WEBP; - break; - case 'image/png': - $imageType = \IMAGETYPE_PNG; - break; - case 'image/gif': - $imageType = \IMAGETYPE_GIF; - break; - default: - $imageType = \IMAGETYPE_JPEG; - } - - $image->resize($width, $height, false, Image::SCALE_FILL); - $image->toFile($path, $imageType); - - return true; - } - catch (\Exception $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - } - } - } - - /** - * Template preview. - * - * @return object object containing the id of the template. - * - * @since 3.2 - */ - public function getPreview() - { - $app = Factory::getApplication(); - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query->select($db->quoteName(['id', 'client_id'])); - $query->from($db->quoteName('#__template_styles')); - $query->where($db->quoteName('template') . ' = :template') - ->bind(':template', $this->template->element); - - $db->setQuery($query); - - try - { - $result = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'warning'); - } - - if (empty($result)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'warning'); - } - else - { - return $result; - } - } - - /** - * Rename a file. - * - * @return mixed array on success, false on failure - * - * @since 3.2 - */ - public function getFont() - { - if ($template = $this->getTemplate()) - { - $app = Factory::getApplication(); - $client = ApplicationHelper::getClientInfo($template->client_id); - $relPath = base64_decode($app->input->get('file')); - $explodeArray = explode('/', $relPath); - $fileName = end($explodeArray); - $path = $this->getBasePath() . base64_decode($app->input->get('file')); - - if (stristr($client->path, 'administrator') == false) - { - $folder = '/templates/'; - } - else - { - $folder = '/administrator/templates/'; - } - - $uri = Uri::root(true) . $folder . $template->element; - - if (file_exists(Path::clean($path))) - { - $font['address'] = $uri . $relPath; - - $font['rel_path'] = $relPath; - - $font['name'] = $fileName; - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error'); - - return false; - } - - return $font; - } - } - - /** - * Copy a file. - * - * @param string $newName The name of the copied file - * @param string $location The final location where the file is to be copied - * @param string $file The name and location of the file - * - * @return boolean true if image resize successful, false otherwise. - * - * @since 3.2 - */ - public function copyFile($newName, $location, $file) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $relPath = base64_decode($file); - $explodeArray = explode('.', $relPath); - $ext = end($explodeArray); - $path = $this->getBasePath(); - $newPath = Path::clean($path . $location . '/' . $newName . '.' . $ext); - - if (file_exists($newPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); - - return false; - } - - if (File::copy($path . $relPath, $newPath)) - { - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_COPY_SUCCESS', $newName . '.' . $ext)); - - return true; - } - else - { - return false; - } - } - } - - /** - * Get the compressed files. - * - * @return array if file exists, false otherwise - * - * @since 3.2 - */ - public function getArchive() - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath() . base64_decode($app->input->get('file')); - - if (file_exists(Path::clean($path))) - { - $files = array(); - $zip = new \ZipArchive; - - if ($zip->open($path) === true) - { - for ($i = 0; $i < $zip->numFiles; $i++) - { - $entry = $zip->getNameIndex($i); - $files[] = $entry; - } - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); - - return false; - } - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error'); - - return false; - } - - return $files; - } - } - - /** - * Extract contents of an archive file. - * - * @param string $file The name and location of the file - * - * @return boolean true if image extraction is successful, false otherwise. - * - * @since 3.2 - */ - public function extractArchive($file) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $relPath = base64_decode($file); - $explodeArray = explode('/', $relPath); - $fileName = end($explodeArray); - $path = $this->getBasePath() . base64_decode($file); - - if (file_exists(Path::clean($path . '/' . $fileName))) - { - $zip = new \ZipArchive; - - if ($zip->open(Path::clean($path . '/' . $fileName)) === true) - { - for ($i = 0; $i < $zip->numFiles; $i++) - { - $entry = $zip->getNameIndex($i); - - if (file_exists(Path::clean($path . '/' . $entry))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXISTS'), 'error'); - - return false; - } - } - - $zip->extractTo($path); - - return true; - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); - - return false; - } - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_NOT_FOUND'), 'error'); - - return false; - } - } - } - - /** - * Check if the extension is allowed and will be shown in the template manager - * - * @param string $ext The extension to check if it is allowed - * - * @return boolean true if the extension is allowed false otherwise - * - * @since 3.6.0 - */ - protected function checkFormat($ext) - { - if (!isset($this->allowedFormats)) - { - $params = ComponentHelper::getParams('com_templates'); - $imageTypes = explode(',', $params->get('image_formats')); - $sourceTypes = explode(',', $params->get('source_formats')); - $fontTypes = explode(',', $params->get('font_formats')); - $archiveTypes = explode(',', $params->get('compressed_formats')); - - $this->allowedFormats = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes); - $this->allowedFormats = array_map('strtolower', $this->allowedFormats); - } - - return in_array(strtolower($ext), $this->allowedFormats); - } - - /** - * Method to get a list of all the files to edit in a template's media folder. - * - * @return array A nested array of relevant files. - * - * @since 4.1.0 - */ - public function getMediaFiles() - { - $result = []; - $template = $this->getTemplate(); - - if (!isset($template->xmldata)) - { - $template->xmldata = TemplatesHelper::parseXMLTemplateFile($template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); - } - - if (!isset($template->xmldata->inheritable) || (isset($template->xmldata->parent) && $template->xmldata->parent === '')) - { - return $result; - } - - $app = Factory::getApplication(); - $path = Path::clean(JPATH_ROOT . '/media/templates/' . ($template->client_id === 0 ? 'site' : 'administrator') . '/' . $template->element . '/'); - $this->mediaElement = $path; - - if (!is_writable($path)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error'); - } - - if (is_dir($path)) - { - $result = $this->getDirectoryTree($path); - } - - return $result; - } - - /** - * Method to resolve the base folder. - * - * @return string The absolute path for the base. - * - * @since 4.1.0 - */ - private function getBasePath() - { - $app = Factory::getApplication(); - $isMedia = $app->input->getInt('isMedia', 0); - - return $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element : - JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element; - } - - /** - * Method to create the templateDetails.xml for the child template - * - * @return boolean true if name is not used, false otherwise - * - * @since 4.1.0 - */ - public function child() - { - $app = Factory::getApplication(); - $template = $this->getTemplate(); - - if (!(array) $template) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - - $client = ApplicationHelper::getClientInfo($template->client_id); - $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml'); - - // Delete new folder if it exists - $toPath = $this->getState('to_path'); - - if (Folder::exists($toPath)) - { - if (!Folder::delete($toPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - } - else - { - if (!Folder::create($toPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - } - - // Copy the template definition from the parent template - if (!File::copy($fromPath, $toPath . '/templateDetails.xml')) - { - return false; - } - - // Check manifest for additional files - $newName = strtolower($this->getState('new_name')); - $template = $this->getTemplate(); - - // Edit XML file - $xmlFile = Path::clean($this->getState('to_path') . '/templateDetails.xml'); - - if (!File::exists($xmlFile)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); - - return false; - } - - try - { - $xml = simplexml_load_string(file_get_contents($xmlFile)); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error'); - - return false; - } - - $user = Factory::getUser(); - unset($xml->languages); - unset($xml->media); - unset($xml->files); - unset($xml->parent); - unset($xml->inheritable); - - // Remove the update parts - unset($xml->update); - unset($xml->updateservers); - - if (isset($xml->creationDate)) - { - $xml->creationDate = (new Date('now'))->format('F Y'); - } - else - { - $xml->addChild('creationDate', (new Date('now'))->format('F Y')); - } - - if (isset($xml->author)) - { - $xml->author = $user->name; - } - else - { - $xml->addChild('author', $user->name); - } - - if (isset($xml->authorEmail)) - { - $xml->authorEmail = $user->email; - } - else - { - $xml->addChild('authorEmail', $user->email); - } - - $files = $xml->addChild('files'); - $files->addChild('filename', 'templateDetails.xml'); - - // Media folder - $media = $xml->addChild('media'); - $media->addAttribute('folder', 'media'); - $media->addAttribute('destination', 'templates/' . ($template->client_id === 0 ? 'site/' : 'administrator/') . $template->element . '_' . $newName); - $media->addChild('folder', 'css'); - $media->addChild('folder', 'js'); - $media->addChild('folder', 'images'); - $media->addChild('folder', 'html'); - $media->addChild('folder', 'scss'); - - $xml->name = $template->element . '_' . $newName; - $xml->inheritable = 0; - $files = $xml->addChild('parent', $template->element); - - $dom = new \DOMDocument; - $dom->preserveWhiteSpace = false; - $dom->formatOutput = true; - $dom->loadXML($xml->asXML()); - - $result = File::write($xmlFile, $dom->saveXML()); - - if (!$result) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - - // Create an empty media folder structure - if (!Folder::create($toPath . '/media') - || !Folder::create($toPath . '/media/css') - || !Folder::create($toPath . '/media/js') - || !Folder::create($toPath . '/media/images') - || !Folder::create($toPath . '/media/html/tinymce') - || !Folder::create($toPath . '/media/scss')) - { - return false; - } - - return true; - } - - /** - * Method to get the parent template existing styles - * - * @return array array of id,titles of the styles - * - * @since 4.1.3 - */ - public function getAllTemplateStyles() - { - $template = $this->getTemplate(); - - if (empty($template->xmldata->inheritable)) - { - return []; - } - - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query->select($db->quoteName(['id', 'title'])) - ->from($db->quoteName('#__template_styles')) - ->where($db->quoteName('client_id') . ' = :client_id', 'AND') - ->where($db->quoteName('template') . ' = :template') - ->orWhere($db->quoteName('parent') . ' = :parent') - ->bind(':client_id', $template->client_id, ParameterType::INTEGER) - ->bind(':template', $template->element) - ->bind(':parent', $template->element); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Method to copy selected styles to the child template - * - * @return boolean true if name is not used, false otherwise - * - * @since 4.1.3 - */ - public function copyStyles() - { - $app = Factory::getApplication(); - $template = $this->getTemplate(); - $newName = strtolower($this->getState('new_name')); - $applyStyles = $this->getState('stylesToCopy'); - - // Get a db connection. - $db = $this->getDbo(); - - // Create a new query object. - $query = $db->getQuery(true); - - $query->select($db->quoteName(['title', 'params'])) - ->from($db->quoteName('#__template_styles')) - ->whereIn($db->quoteName('id'), ArrayHelper::toInteger($applyStyles)); - // Reset the query using our newly populated query object. - $db->setQuery($query); - - try - { - $parentStyle = $db->loadObjectList(); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND'), 'error'); - - return false; - } - - foreach ($parentStyle as $style) - { - $query = $db->getQuery(true); - $styleName = Text::sprintf('COM_TEMPLATES_COPY_CHILD_TEMPLATE_STYLES', ucfirst($template->element . '_' . $newName), $style->title); - - // Insert columns and values - $columns = ['id', 'template', 'client_id', 'home', 'title', 'inheritable', 'parent', 'params']; - $values = [0, $db->quote($template->element . '_' . $newName), (int) $template->client_id, $db->quote('0'), $db->quote($styleName), 0, $db->quote($template->element), $db->quote($style->params)]; - - $query - ->insert($db->quoteName('#__template_styles')) - ->columns($db->quoteName($columns)) - ->values(implode(',', $values)); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error'); - - return false; - } - } - - return true; - } + /** + * The information in a template + * + * @var \stdClass + * @since 1.6 + */ + protected $template = null; + + /** + * The path to the template + * + * @var string + * @since 3.2 + */ + protected $element = null; + + /** + * The path to the static assets + * + * @var string + * @since 4.1.0 + */ + protected $mediaElement = null; + + /** + * Internal method to get file properties. + * + * @param string $path The base path. + * @param string $name The file name. + * + * @return object + * + * @since 1.6 + */ + protected function getFile($path, $name) + { + $temp = new \stdClass(); + + if ($this->getTemplate()) { + $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $path); + $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $path); + $temp->name = $name; + $temp->id = urlencode(base64_encode(str_replace('\\', '//', $path))); + + return $temp; + } + } + + /** + * Method to store file information. + * + * @param string $path The base path. + * @param string $name The file name. + * @param stdClass $template The std class object of template. + * + * @return object stdClass object. + * + * @since 4.0.0 + */ + protected function storeFileInfo($path, $name, $template) + { + $temp = new \stdClass(); + $temp->id = base64_encode($path . $name); + $temp->client = $template->client_id; + $temp->template = $template->element; + $temp->extension_id = $template->extension_id; + + if ($coreFile = $this->getCoreFile($path . $name, $template->client_id)) { + $temp->coreFile = md5_file($coreFile); + } else { + $temp->coreFile = null; + } + + return $temp; + } + + /** + * Method to get all template list. + * + * @return object stdClass object + * + * @since 4.0.0 + */ + public function getTemplateList() + { + // Get a db connection. + $db = $this->getDatabase(); + + // Create a new query object. + $query = $db->getQuery(true); + + // Select the required fields from the table + $query->select( + $this->getState( + 'list.select', + 'a.extension_id, a.name, a.element, a.client_id' + ) + ); + + $query->from($db->quoteName('#__extensions', 'a')) + ->where($db->quoteName('a.enabled') . ' = 1') + ->where($db->quoteName('a.type') . ' = ' . $db->quote('template')); + + // Reset the query. + $db->setQuery($query); + + // Load the results as a list of stdClass objects. + $results = $db->loadObjectList(); + + return $results; + } + + /** + * Method to get all updated file list. + * + * @param boolean $state The optional parameter if you want unchecked list. + * @param boolean $all The optional parameter if you want all list. + * @param boolean $cleanup The optional parameter if you want to clean record which is no more required. + * + * @return object stdClass object + * + * @since 4.0.0 + */ + public function getUpdatedList($state = false, $all = false, $cleanup = false) + { + // Get a db connection. + $db = $this->getDatabase(); + + // Create a new query object. + $query = $db->getQuery(true); + + // Select the required fields from the table + $query->select( + $this->getState( + 'list.select', + 'a.template, a.hash_id, a.extension_id, a.state, a.action, a.client_id, a.created_date, a.modified_date' + ) + ); + + $template = $this->getTemplate(); + + $query->from($db->quoteName('#__template_overrides', 'a')); + + if (!$all) { + $teid = (int) $template->extension_id; + $query->where($db->quoteName('extension_id') . ' = :teid') + ->bind(':teid', $teid, ParameterType::INTEGER); + } + + if ($state) { + $query->where($db->quoteName('state') . ' = 0'); + } + + $query->order($db->quoteName('a.modified_date') . ' DESC'); + + // Reset the query. + $db->setQuery($query); + + // Load the results as a list of stdClass objects. + $pks = $db->loadObjectList(); + + if ($state) { + return $pks; + } + + $results = array(); + + foreach ($pks as $pk) { + $client = ApplicationHelper::getClientInfo($pk->client_id); + $path = Path::clean($client->path . '/templates/' . $pk->template . base64_decode($pk->hash_id)); + + if (file_exists($path)) { + $results[] = $pk; + } elseif ($cleanup) { + $cleanupIds = array(); + $cleanupIds[] = $pk->hash_id; + $this->publish($cleanupIds, -3, $pk->extension_id); + } + } + + return $results; + } + + /** + * Method to get a list of all the core files of override files. + * + * @return array An array of all core files. + * + * @since 4.0.0 + */ + public function getCoreList() + { + // Get list of all templates + $templates = $this->getTemplateList(); + + // Initialize the array variable to store core file list. + $this->coreFileList = array(); + + $app = Factory::getApplication(); + + foreach ($templates as $template) { + $client = ApplicationHelper::getClientInfo($template->client_id); + $element = Path::clean($client->path . '/templates/' . $template->element . '/'); + $path = Path::clean($element . 'html/'); + + if (is_dir($path)) { + $this->prepareCoreFiles($path, $element, $template); + } + } + + // Sort list of stdClass array. + usort( + $this->coreFileList, + function ($a, $b) { + return strcmp($a->id, $b->id); + } + ); + + return $this->coreFileList; + } + + /** + * Prepare core files. + * + * @param string $dir The path of the directory to scan. + * @param string $element The path of the template element. + * @param \stdClass $template The stdClass object of template. + * + * @return array + * + * @since 4.0.0 + */ + public function prepareCoreFiles($dir, $element, $template) + { + $dirFiles = scandir($dir); + + foreach ($dirFiles as $key => $value) { + if (in_array($value, array('.', '..', 'node_modules'))) { + continue; + } + + if (is_dir($dir . $value)) { + $relativePath = str_replace($element, '', $dir . $value); + $this->prepareCoreFiles($dir . $value . '/', $element, $template); + } else { + $ext = pathinfo($dir . $value, PATHINFO_EXTENSION); + $allowedFormat = $this->checkFormat($ext); + + if ($allowedFormat === true) { + $relativePath = str_replace($element, '', $dir); + $info = $this->storeFileInfo('/' . $relativePath, $value, $template); + + if ($info) { + $this->coreFileList[] = $info; + } + } + } + } + } + + /** + * Method to update status of list. + * + * @param array $ids The base path. + * @param array $value The file name. + * @param integer $exid The template extension id. + * + * @return integer Number of files changed. + * + * @since 4.0.0 + */ + public function publish($ids, $value, $exid) + { + $db = $this->getDatabase(); + + foreach ($ids as $id) { + if ($value === -3) { + $deleteQuery = $db->getQuery(true) + ->delete($db->quoteName('#__template_overrides')) + ->where($db->quoteName('hash_id') . ' = :hashid') + ->where($db->quoteName('extension_id') . ' = :exid') + ->bind(':hashid', $id) + ->bind(':exid', $exid, ParameterType::INTEGER); + + try { + // Set the query using our newly populated query object and execute it. + $db->setQuery($deleteQuery); + $result = $db->execute(); + } catch (\RuntimeException $e) { + return $e; + } + } elseif ($value === 1 || $value === 0) { + $updateQuery = $db->getQuery(true) + ->update($db->quoteName('#__template_overrides')) + ->set($db->quoteName('state') . ' = :state') + ->where($db->quoteName('hash_id') . ' = :hashid') + ->where($db->quoteName('extension_id') . ' = :exid') + ->bind(':state', $value, ParameterType::INTEGER) + ->bind(':hashid', $id) + ->bind(':exid', $exid, ParameterType::INTEGER); + + try { + // Set the query using our newly populated query object and execute it. + $db->setQuery($updateQuery); + $result = $db->execute(); + } catch (\RuntimeException $e) { + return $e; + } + } + } + + return $result; + } + + /** + * Method to get a list of all the files to edit in a template. + * + * @return array A nested array of relevant files. + * + * @since 1.6 + */ + public function getFiles() + { + $result = array(); + + if ($template = $this->getTemplate()) { + $app = Factory::getApplication(); + $client = ApplicationHelper::getClientInfo($template->client_id); + $path = Path::clean($client->path . '/templates/' . $template->element . '/'); + $lang = Factory::getLanguage(); + + // Load the core and/or local language file(s). + $lang->load('tpl_' . $template->element, $client->path) + || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path)) + || $lang->load('tpl_' . $template->element, $client->path . '/templates/' . $template->element) + || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path . '/templates/' . $template->xmldata->parent)); + $this->element = $path; + + if (!is_writable($path)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error'); + } + + if (is_dir($path)) { + $result = $this->getDirectoryTree($path); + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_TEMPLATE_FOLDER_NOT_FOUND'), 'error'); + + return false; + } + + // Clean up override history + $this->getUpdatedList(false, true, true); + } + + return $result; + } + + /** + * Get the directory tree. + * + * @param string $dir The path of the directory to scan + * + * @return array + * + * @since 3.2 + */ + public function getDirectoryTree($dir) + { + $result = array(); + + $dirFiles = scandir($dir); + + foreach ($dirFiles as $key => $value) { + if (!in_array($value, array('.', '..', 'node_modules'))) { + if (is_dir($dir . $value)) { + $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value); + $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath); + $result[str_replace('\\', '//', $relativePath)] = $this->getDirectoryTree($dir . $value . '/'); + } else { + $ext = pathinfo($dir . $value, PATHINFO_EXTENSION); + $allowedFormat = $this->checkFormat($ext); + + if ($allowedFormat == true) { + $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value); + $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath); + $result[] = $this->getFile($relativePath, $value); + } + } + } + } + + return $result; + } + + /** + * Method to get the core file of override file + * + * @param string $file Override file + * @param integer $client_id Client Id + * + * @return string $corefile The full path and file name for the target file, or boolean false if the file is not found in any of the paths. + * + * @since 4.0.0 + */ + public function getCoreFile($file, $client_id) + { + $app = Factory::getApplication(); + $filePath = Path::clean($file); + $explodeArray = explode(DIRECTORY_SEPARATOR, $filePath); + + // Only allow html/ folder + if ($explodeArray['1'] !== 'html') { + return false; + } + + $fileName = basename($filePath); + $type = $explodeArray['2']; + $client = ApplicationHelper::getClientInfo($client_id); + + $componentPath = Path::clean($client->path . '/components/'); + $modulePath = Path::clean($client->path . '/modules/'); + $layoutPath = Path::clean(JPATH_ROOT . '/layouts/'); + + // For modules + if (stristr($type, 'mod_') !== false) { + $folder = $explodeArray['2']; + $htmlPath = Path::clean($modulePath . $folder . '/tmpl/'); + $fileName = $this->getSafeName($fileName); + $coreFile = Path::find($htmlPath, $fileName); + + return $coreFile; + } elseif (stristr($type, 'com_') !== false) { + // For components + $folder = $explodeArray['2']; + $subFolder = $explodeArray['3']; + $fileName = $this->getSafeName($fileName); + + // The new scheme, if a view has a tmpl folder + $newHtmlPath = Path::clean($componentPath . $folder . '/tmpl/' . $subFolder . '/'); + + if (!$coreFile = Path::find($newHtmlPath, $fileName)) { + // The old scheme, the views are directly in the component/tmpl folder + $oldHtmlPath = Path::clean($componentPath . $folder . '/views/' . $subFolder . '/tmpl/'); + $coreFile = Path::find($oldHtmlPath, $fileName); + + return $coreFile; + } + + return $coreFile; + } elseif (stristr($type, 'layouts') !== false) { + // For Layouts + $subtype = $explodeArray['3']; + + if (stristr($subtype, 'com_')) { + $folder = $explodeArray['3']; + $subFolder = array_slice($explodeArray, 4, -1); + $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder); + $htmlPath = Path::clean($componentPath . $folder . '/layouts/' . $subFolder); + $fileName = $this->getSafeName($fileName); + $coreFile = Path::find($htmlPath, $fileName); + + return $coreFile; + } elseif (stristr($subtype, 'joomla') || stristr($subtype, 'libraries') || stristr($subtype, 'plugins')) { + $subFolder = array_slice($explodeArray, 3, -1); + $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder); + $htmlPath = Path::clean($layoutPath . $subFolder); + $fileName = $this->getSafeName($fileName); + $coreFile = Path::find($htmlPath, $fileName); + + return $coreFile; + } + } + + return false; + } + + /** + * Creates a safe file name for the given name. + * + * @param string $name The filename + * + * @return string $fileName The filtered name without Date + * + * @since 4.0.0 + */ + private function getSafeName($name) + { + if (strpos($name, '-') !== false && preg_match('/[0-9]/', $name)) { + // Get the extension + $extension = File::getExt($name); + + // Remove ( Date ) from file + $explodeArray = explode('-', $name); + $size = count($explodeArray); + $date = $explodeArray[$size - 2] . '-' . str_replace('.' . $extension, '', $explodeArray[$size - 1]); + + if ($this->validateDate($date)) { + $nameWithoutExtension = implode('-', array_slice($explodeArray, 0, -2)); + + // Filtered name + $name = $nameWithoutExtension . '.' . $extension; + } + } + + return $name; + } + + /** + * Validate Date in file name. + * + * @param string $date Date to validate. + * + * @return boolean Return true if date is valid and false if not. + * + * @since 4.0.0 + */ + private function validateDate($date) + { + $format = 'Ymd-His'; + $valid = Date::createFromFormat($format, $date); + + return $valid && $valid->format($format) === $date; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState('extension.id', $pk); + + // Load the parameters. + $params = ComponentHelper::getParams('com_templates'); + $this->setState('params', $params); + } + + /** + * Method to get the template information. + * + * @return mixed Object if successful, false if not and internal error is set. + * + * @since 1.6 + */ + public function &getTemplate() + { + if (empty($this->template)) { + $pk = (int) $this->getState('extension.id'); + $db = $this->getDatabase(); + $app = Factory::getApplication(); + + // Get the template information. + $query = $db->getQuery(true) + ->select($db->quoteName(['extension_id', 'client_id', 'element', 'name', 'manifest_cache'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('extension_id') . ' = :pk') + ->where($db->quoteName('type') . ' = ' . $db->quote('template')) + ->bind(':pk', $pk, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $result = $db->loadObject(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'warning'); + $this->template = false; + + return false; + } + + if (empty($result)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'error'); + $this->template = false; + } else { + $this->template = $result; + + // Client ID is not always an integer, so enforce here + $this->template->client_id = (int) $this->template->client_id; + + if (!isset($this->template->xmldata)) { + $this->template->xmldata = TemplatesHelper::parseXMLTemplateFile($this->template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $this->template->name); + } + } + } + + return $this->template; + } + + /** + * Method to check if new template name already exists + * + * @return boolean true if name is not used, false otherwise + * + * @since 2.5 + */ + public function checkNewName() + { + $db = $this->getDatabase(); + $name = $this->getState('new_name'); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('name') . ' = :name') + ->bind(':name', $name); + $db->setQuery($query); + + return ($db->loadResult() == 0); + } + + /** + * Method to check if new template name already exists + * + * @return string name of current template + * + * @since 2.5 + */ + public function getFromName() + { + return $this->getTemplate()->element; + } + + /** + * Method to check if new template name already exists + * + * @return boolean true if name is not used, false otherwise + * + * @since 2.5 + */ + public function copy() + { + $app = Factory::getApplication(); + + if ($template = $this->getTemplate()) { + $client = ApplicationHelper::getClientInfo($template->client_id); + $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/'); + + // Delete new folder if it exists + $toPath = $this->getState('to_path'); + + if (Folder::exists($toPath)) { + if (!Folder::delete($toPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + } + + // Copy all files from $fromName template to $newName folder + if (!Folder::copy($fromPath, $toPath)) { + return false; + } + + // Check manifest for additional files + $manifest = simplexml_load_file($toPath . '/templateDetails.xml'); + + // Copy language files from global folder + if ($languages = $manifest->languages) { + $folder = (string) $languages->attributes()->folder; + $languageFiles = $languages->language; + + Folder::create($toPath . '/' . $folder . '/' . $languageFiles->attributes()->tag); + + foreach ($languageFiles as $languageFile) { + $src = Path::clean($client->path . '/language/' . $languageFile); + $dst = Path::clean($toPath . '/' . $folder . '/' . $languageFile); + + if (File::exists($src)) { + File::copy($src, $dst); + } + } + } + + // Copy media files + if ($media = $manifest->media) { + $folder = (string) $media->attributes()->folder; + $destination = (string) $media->attributes()->destination; + + Folder::copy(JPATH_SITE . '/media/' . $destination, $toPath . '/' . $folder); + } + + // Adjust to new template name + if (!$this->fixTemplateName()) { + return false; + } + + return true; + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); + + return false; + } + } + + /** + * Method to delete tmp folder + * + * @return boolean true if delete successful, false otherwise + * + * @since 2.5 + */ + public function cleanup() + { + // Clear installation messages + $app = Factory::getApplication(); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + // Delete temporary directory + return Folder::delete($this->getState('to_path')); + } + + /** + * Method to rename the template in the XML files and rename the language files + * + * @return boolean true if successful, false otherwise + * + * @since 2.5 + */ + protected function fixTemplateName() + { + // Rename Language files + // Get list of language files + $result = true; + $files = Folder::files($this->getState('to_path'), '\.ini$', true, true); + $newName = strtolower($this->getState('new_name')); + $template = $this->getTemplate(); + $oldName = $template->element; + $manifest = json_decode($template->manifest_cache); + + foreach ($files as $file) { + $newFile = '/' . str_replace($oldName, $newName, basename($file)); + $result = File::move($file, dirname($file) . $newFile) && $result; + } + + // Edit XML file + $xmlFile = $this->getState('to_path') . '/templateDetails.xml'; + + if (File::exists($xmlFile)) { + $contents = file_get_contents($xmlFile); + $pattern[] = '#\s*' . $manifest->name . '\s*#i'; + $replace[] = '' . $newName . ''; + $pattern[] = '##'; + $replace[] = ''; + $pattern[] = '##'; + $replace[] = ''; + $contents = preg_replace($pattern, $replace, $contents); + $result = File::write($xmlFile, $contents) && $result; + } + + return $result; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $app = Factory::getApplication(); + + // Codemirror or Editor None should be enabled + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from('#__extensions as a') + ->where( + '(a.name =' . $db->quote('plg_editors_codemirror') . + ' AND a.enabled = 1) OR (a.name =' . + $db->quote('plg_editors_none') . + ' AND a.enabled = 1)' + ); + $db->setQuery($query); + $state = $db->loadResult(); + + if ((int) $state < 1) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EDITOR_DISABLED'), 'warning'); + } + + // Get the form. + $form = $this->loadForm('com_templates.source', 'source', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $data = $this->getSource(); + + $this->preprocessData('com_templates.source', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function &getSource() + { + $app = Factory::getApplication(); + $item = new \stdClass(); + + if (!$this->template) { + $this->getTemplate(); + } + + if ($this->template) { + $input = Factory::getApplication()->input; + $fileName = base64_decode($input->get('file')); + $fileName = str_replace('//', '/', $fileName); + $isMedia = $input->getInt('isMedia', 0); + + $fileName = $isMedia ? Path::clean(JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName) + : Path::clean(JPATH_ROOT . ($this->template->client_id === 0 ? '' : '/administrator') . '/templates/' . $this->template->element . $fileName); + + try { + $filePath = Path::check($fileName); + } catch (\Exception $e) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error'); + + return; + } + + if (file_exists($filePath)) { + $item->extension_id = $this->getState('extension.id'); + $item->filename = Path::clean($fileName); + $item->source = file_get_contents($filePath); + $item->filePath = Path::clean($filePath); + $ds = DIRECTORY_SEPARATOR; + $cleanFileName = str_replace(JPATH_ROOT . ($this->template->client_id === 1 ? $ds . 'administrator' . $ds : $ds) . 'templates' . $ds . $this->template->element, '', $fileName); + + if ($coreFile = $this->getCoreFile($cleanFileName, $this->template->client_id)) { + $item->coreFile = $coreFile; + $item->core = file_get_contents($coreFile); + } + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error'); + } + } + + return $item; + } + + /** + * Method to store the source file contents. + * + * @param array $data The source data to save. + * + * @return boolean True on success, false otherwise and internal error set. + * + * @since 1.6 + */ + public function save($data) + { + // Get the template. + $template = $this->getTemplate(); + + if (empty($template)) { + return false; + } + + $app = Factory::getApplication(); + $fileName = base64_decode($app->input->get('file')); + $isMedia = $app->input->getInt('isMedia', 0); + $fileName = $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName : + JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . $fileName; + + $filePath = Path::clean($fileName); + + // Include the extension plugins for the save events. + PluginHelper::importPlugin('extension'); + + $user = get_current_user(); + chown($filePath, $user); + Path::setPermissions($filePath, '0644'); + + // Try to make the template file writable. + if (!is_writable($filePath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_WRITABLE'), 'warning'); + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_PERMISSIONS', Path::getPermissions($filePath)), 'warning'); + + if (!Path::isOwner($filePath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_CHECK_FILE_OWNERSHIP'), 'warning'); + } + + return false; + } + + // Make sure EOL is Unix + $data['source'] = str_replace(array("\r\n", "\r"), "\n", $data['source']); + + // If the asset file for the template ensure we have valid template so we don't instantly destroy it + if ($fileName === '/joomla.asset.json' && json_decode($data['source']) === null) { + $this->setError(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_INVALID_JSON')); + + return false; + } + + $return = File::write($filePath, $data['source']); + + if (!$return) { + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_ERROR_FAILED_TO_SAVE_FILENAME', $fileName), 'error'); + + return false; + } + + // Get the extension of the changed file. + $explodeArray = explode('.', $fileName); + $ext = end($explodeArray); + + if ($ext == 'less') { + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_COMPILE_LESS', $fileName)); + } + + return true; + } + + /** + * Get overrides folder. + * + * @param string $name The name of override. + * @param string $path Location of override. + * + * @return object containing override name and path. + * + * @since 3.2 + */ + public function getOverridesFolder($name, $path) + { + $folder = new \stdClass(); + $folder->name = $name; + $folder->path = base64_encode($path . $name); + + return $folder; + } + + /** + * Get a list of overrides. + * + * @return array containing overrides. + * + * @since 3.2 + */ + public function getOverridesList() + { + if ($template = $this->getTemplate()) { + $client = ApplicationHelper::getClientInfo($template->client_id); + $componentPath = Path::clean($client->path . '/components/'); + $modulePath = Path::clean($client->path . '/modules/'); + $pluginPath = Path::clean(JPATH_ROOT . '/plugins/'); + $layoutPath = Path::clean(JPATH_ROOT . '/layouts/'); + $components = Folder::folders($componentPath); + + foreach ($components as $component) { + // Collect the folders with views + $folders = Folder::folders($componentPath . '/' . $component, '^view[s]?$', false, true); + $folders = array_merge($folders, Folder::folders($componentPath . '/' . $component, '^tmpl?$', false, true)); + + if (!$folders) { + continue; + } + + foreach ($folders as $folder) { + // The subfolders are views + $views = Folder::folders($folder); + + foreach ($views as $view) { + // The old scheme, if a view has a tmpl folder + $path = $folder . '/' . $view . '/tmpl'; + + // The new scheme, the views are directly in the component/tmpl folder + if (!is_dir($path) && substr($folder, -4) == 'tmpl') { + $path = $folder . '/' . $view; + } + + // Check if the folder exists + if (!is_dir($path)) { + continue; + } + + $result['components'][$component][] = $this->getOverridesFolder($view, Path::clean($folder . '/')); + } + } + } + + foreach (Folder::folders($pluginPath) as $pluginGroup) { + foreach (Folder::folders($pluginPath . '/' . $pluginGroup) as $plugin) { + if (file_exists($pluginPath . '/' . $pluginGroup . '/' . $plugin . '/tmpl/')) { + $pluginLayoutPath = Path::clean($pluginPath . '/' . $pluginGroup . '/'); + $result['plugins'][$pluginGroup][] = $this->getOverridesFolder($plugin, $pluginLayoutPath); + } + } + } + + $modules = Folder::folders($modulePath); + + foreach ($modules as $module) { + $result['modules'][] = $this->getOverridesFolder($module, $modulePath); + } + + $layoutFolders = Folder::folders($layoutPath); + + foreach ($layoutFolders as $layoutFolder) { + $layoutFolderPath = Path::clean($layoutPath . '/' . $layoutFolder . '/'); + $layouts = Folder::folders($layoutFolderPath); + + foreach ($layouts as $layout) { + $result['layouts'][$layoutFolder][] = $this->getOverridesFolder($layout, $layoutFolderPath); + } + } + + // Check for layouts in component folders + foreach ($components as $component) { + if (file_exists($componentPath . '/' . $component . '/layouts/')) { + $componentLayoutPath = Path::clean($componentPath . '/' . $component . '/layouts/'); + + if ($componentLayoutPath) { + $layouts = Folder::folders($componentLayoutPath); + + foreach ($layouts as $layout) { + $result['layouts'][$component][] = $this->getOverridesFolder($layout, $componentLayoutPath); + } + } + } + } + } + + if (!empty($result)) { + return $result; + } + } + + /** + * Create overrides. + * + * @param string $override The override location. + * + * @return boolean true if override creation is successful, false otherwise + * + * @since 3.2 + */ + public function createOverride($override) + { + if ($template = $this->getTemplate()) { + $app = Factory::getApplication(); + $explodeArray = explode(DIRECTORY_SEPARATOR, $override); + $name = end($explodeArray); + $client = ApplicationHelper::getClientInfo($template->client_id); + + if (stristr($name, 'mod_') != false) { + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $name); + } elseif (stristr($override, 'com_') != false) { + $size = count($explodeArray); + + $url = Path::clean($explodeArray[$size - 3] . '/' . $explodeArray[$size - 1]); + + if ($explodeArray[$size - 2] == 'layouts') { + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $url); + } else { + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $url); + } + } elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) { + $size = count($explodeArray); + $layoutPath = Path::clean('plg_' . $explodeArray[$size - 2] . '_' . $explodeArray[$size - 1]); + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $layoutPath); + } else { + $layoutPath = implode('/', array_slice($explodeArray, -2)); + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $layoutPath); + } + + // Check Html folder, create if not exist + if (!Folder::exists($htmlPath)) { + if (!Folder::create($htmlPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_ERROR'), 'error'); + + return false; + } + } + + if (stristr($name, 'mod_') != false) { + $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath); + } elseif (stristr($override, 'com_') != false && stristr($override, 'layouts') == false) { + $path = $override . '/tmpl'; + + // View can also be in the top level folder + if (!is_dir($path)) { + $path = $override; + } + + $return = $this->createTemplateOverride(Path::clean($path), $htmlPath); + } elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) { + $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath); + } else { + $return = $this->createTemplateOverride($override, $htmlPath); + } + + if ($return) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_CREATED') . str_replace(JPATH_ROOT, '', $htmlPath)); + + return true; + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_FAILED'), 'error'); + + return false; + } + } + } + + /** + * Create override folder & file + * + * @param string $overridePath The override location + * @param string $htmlPath The html location + * + * @return boolean True on success. False otherwise. + */ + public function createTemplateOverride($overridePath, $htmlPath) + { + $return = false; + + if (empty($overridePath) || empty($htmlPath)) { + return $return; + } + + // Get list of template folders + $folders = Folder::folders($overridePath, null, true, true); + + if (!empty($folders)) { + foreach ($folders as $folder) { + $htmlFolder = $htmlPath . str_replace($overridePath, '', $folder); + + if (!Folder::exists($htmlFolder)) { + Folder::create($htmlFolder); + } + } + } + + // Get list of template files (Only get *.php file for template file) + $files = Folder::files($overridePath, '.php', true, true); + + if (empty($files)) { + return true; + } + + foreach ($files as $file) { + $overrideFilePath = str_replace($overridePath, '', $file); + $htmlFilePath = $htmlPath . $overrideFilePath; + + if (File::exists($htmlFilePath)) { + // Generate new unique file name base on current time + $today = Factory::getDate(); + $htmlFilePath = File::stripExt($htmlFilePath) . '-' . $today->format('Ymd-His') . '.' . File::getExt($htmlFilePath); + } + + $return = File::copy($file, $htmlFilePath, '', true); + } + + return $return; + } + + /** + * Delete a particular file. + * + * @param string $file The relative location of the file. + * + * @return boolean True if file deletion is successful, false otherwise + * + * @since 3.2 + */ + public function deleteFile($file) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $filePath = $this->getBasePath() . urldecode(base64_decode($file)); + + $return = File::delete($filePath); + + if (!$return) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_DELETE_ERROR'), 'error'); + + return false; + } + + return true; + } + } + + /** + * Create new file. + * + * @param string $name The name of file. + * @param string $type The extension of the file. + * @param string $location Location for the new file. + * + * @return boolean true if file created successfully, false otherwise + * + * @since 3.2 + */ + public function createFile($name, $type, $location) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $base = $this->getBasePath(); + + if (file_exists(Path::clean($base . '/' . $location . '/' . $name . '.' . $type))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); + + return false; + } + + if (!fopen(Path::clean($base . '/' . $location . '/' . $name . '.' . $type), 'x')) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_CREATE_ERROR'), 'error'); + + return false; + } + + // Check if the format is allowed and will be showed in the backend + $check = $this->checkFormat($type); + + // Add a message if we are not allowed to show this file in the backend. + if (!$check) { + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_WARNING_FORMAT_WILL_NOT_BE_VISIBLE', $type), 'warning'); + } + + return true; + } + } + + /** + * Upload new file. + * + * @param array $file The uploaded file array. + * @param string $location Location for the new file. + * + * @return boolean True if file uploaded successfully, false otherwise + * + * @since 3.2 + */ + public function uploadFile($file, $location) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath(); + $fileName = File::makeSafe($file['name']); + + $err = null; + + if (!TemplateHelper::canUpload($file, $err)) { + // Can't upload the file + return false; + } + + if (file_exists(Path::clean($path . '/' . $location . '/' . $file['name']))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); + + return false; + } + + if (!File::upload($file['tmp_name'], Path::clean($path . '/' . $location . '/' . $fileName))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UPLOAD_ERROR'), 'error'); + + return false; + } + + $url = Path::clean($location . '/' . $fileName); + + return $url; + } + } + + /** + * Create new folder. + * + * @param string $name The name of the new folder. + * @param string $location Location for the new folder. + * + * @return boolean True if override folder is created successfully, false otherwise + * + * @since 3.2 + */ + public function createFolder($name, $location) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = Path::clean($location . '/'); + $base = $this->getBasePath(); + + if (file_exists(Path::clean($base . $path . $name))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_EXISTS'), 'error'); + + return false; + } + + if (!Folder::create(Path::clean($base . $path . $name))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_ERROR'), 'error'); + + return false; + } + + return true; + } + } + + /** + * Delete a folder. + * + * @param string $location The name and location of the folder. + * + * @return boolean True if override folder is deleted successfully, false otherwise + * + * @since 3.2 + */ + public function deleteFolder($location) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $base = $this->getBasePath(); + $path = Path::clean($location . '/'); + + if (!file_exists($base . $path)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_NOT_EXISTS'), 'error'); + + return false; + } + + $return = Folder::delete($base . $path); + + if (!$return) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); + + return false; + } + + return true; + } + } + + /** + * Rename a file. + * + * @param string $file The name and location of the old file + * @param string $name The new name of the file. + * + * @return string Encoded string containing the new file location. + * + * @since 3.2 + */ + public function renameFile($file, $name) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath(); + $fileName = base64_decode($file); + $explodeArray = explode('.', $fileName); + $type = end($explodeArray); + $explodeArray = explode('/', $fileName); + $newName = str_replace(end($explodeArray), $name . '.' . $type, $fileName); + + if (file_exists($path . $newName)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); + + return false; + } + + if (!rename($path . $fileName, $path . $newName)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_RENAME_ERROR'), 'error'); + + return false; + } + + return base64_encode($newName); + } + } + + /** + * Get an image address, height and width. + * + * @return array an associative array containing image address, height and width. + * + * @since 3.2 + */ + public function getImage() + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $fileName = base64_decode($app->input->get('file')); + $path = $this->getBasePath(); + + $uri = Uri::root(false) . ltrim(str_replace(JPATH_ROOT, '', $this->getBasePath()), '/'); + + if (file_exists(Path::clean($path . $fileName))) { + $JImage = new Image(Path::clean($path . $fileName)); + $image['address'] = $uri . $fileName; + $image['path'] = $fileName; + $image['height'] = $JImage->getHeight(); + $image['width'] = $JImage->getWidth(); + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_IMAGE_FILE_NOT_FOUND'), 'error'); + + return false; + } + + return $image; + } + } + + /** + * Crop an image. + * + * @param string $file The name and location of the file + * @param string $w width. + * @param string $h height. + * @param string $x x-coordinate. + * @param string $y y-coordinate. + * + * @return boolean true if image cropped successfully, false otherwise. + * + * @since 3.2 + */ + public function cropImage($file, $w, $h, $x, $y) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath() . base64_decode($file); + + try { + $image = new Image($path); + $properties = $image->getImageFileProperties($path); + + switch ($properties->mime) { + case 'image/webp': + $imageType = \IMAGETYPE_WEBP; + break; + case 'image/png': + $imageType = \IMAGETYPE_PNG; + break; + case 'image/gif': + $imageType = \IMAGETYPE_GIF; + break; + default: + $imageType = \IMAGETYPE_JPEG; + } + + $image->crop($w, $h, $x, $y, false); + $image->toFile($path, $imageType); + + return true; + } catch (\Exception $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + } + } + } + + /** + * Resize an image. + * + * @param string $file The name and location of the file + * @param string $width The new width of the image. + * @param string $height The new height of the image. + * + * @return boolean true if image resize successful, false otherwise. + * + * @since 3.2 + */ + public function resizeImage($file, $width, $height) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath() . base64_decode($file); + + try { + $image = new Image($path); + $properties = $image->getImageFileProperties($path); + + switch ($properties->mime) { + case 'image/webp': + $imageType = \IMAGETYPE_WEBP; + break; + case 'image/png': + $imageType = \IMAGETYPE_PNG; + break; + case 'image/gif': + $imageType = \IMAGETYPE_GIF; + break; + default: + $imageType = \IMAGETYPE_JPEG; + } + + $image->resize($width, $height, false, Image::SCALE_FILL); + $image->toFile($path, $imageType); + + return true; + } catch (\Exception $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + } + } + } + + /** + * Template preview. + * + * @return object object containing the id of the template. + * + * @since 3.2 + */ + public function getPreview() + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($db->quoteName(['id', 'client_id'])); + $query->from($db->quoteName('#__template_styles')); + $query->where($db->quoteName('template') . ' = :template') + ->bind(':template', $this->template->element); + + $db->setQuery($query); + + try { + $result = $db->loadObject(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'warning'); + } + + if (empty($result)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'warning'); + } else { + return $result; + } + } + + /** + * Rename a file. + * + * @return mixed array on success, false on failure + * + * @since 3.2 + */ + public function getFont() + { + if ($template = $this->getTemplate()) { + $app = Factory::getApplication(); + $client = ApplicationHelper::getClientInfo($template->client_id); + $relPath = base64_decode($app->input->get('file')); + $explodeArray = explode('/', $relPath); + $fileName = end($explodeArray); + $path = $this->getBasePath() . base64_decode($app->input->get('file')); + + if (stristr($client->path, 'administrator') == false) { + $folder = '/templates/'; + } else { + $folder = '/administrator/templates/'; + } + + $uri = Uri::root(true) . $folder . $template->element; + + if (file_exists(Path::clean($path))) { + $font['address'] = $uri . $relPath; + + $font['rel_path'] = $relPath; + + $font['name'] = $fileName; + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error'); + + return false; + } + + return $font; + } + } + + /** + * Copy a file. + * + * @param string $newName The name of the copied file + * @param string $location The final location where the file is to be copied + * @param string $file The name and location of the file + * + * @return boolean true if image resize successful, false otherwise. + * + * @since 3.2 + */ + public function copyFile($newName, $location, $file) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $relPath = base64_decode($file); + $explodeArray = explode('.', $relPath); + $ext = end($explodeArray); + $path = $this->getBasePath(); + $newPath = Path::clean($path . $location . '/' . $newName . '.' . $ext); + + if (file_exists($newPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); + + return false; + } + + if (File::copy($path . $relPath, $newPath)) { + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_COPY_SUCCESS', $newName . '.' . $ext)); + + return true; + } else { + return false; + } + } + } + + /** + * Get the compressed files. + * + * @return array if file exists, false otherwise + * + * @since 3.2 + */ + public function getArchive() + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath() . base64_decode($app->input->get('file')); + + if (file_exists(Path::clean($path))) { + $files = array(); + $zip = new \ZipArchive(); + + if ($zip->open($path) === true) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $entry = $zip->getNameIndex($i); + $files[] = $entry; + } + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); + + return false; + } + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error'); + + return false; + } + + return $files; + } + } + + /** + * Extract contents of an archive file. + * + * @param string $file The name and location of the file + * + * @return boolean true if image extraction is successful, false otherwise. + * + * @since 3.2 + */ + public function extractArchive($file) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $relPath = base64_decode($file); + $explodeArray = explode('/', $relPath); + $fileName = end($explodeArray); + $path = $this->getBasePath() . base64_decode($file); + + if (file_exists(Path::clean($path . '/' . $fileName))) { + $zip = new \ZipArchive(); + + if ($zip->open(Path::clean($path . '/' . $fileName)) === true) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $entry = $zip->getNameIndex($i); + + if (file_exists(Path::clean($path . '/' . $entry))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXISTS'), 'error'); + + return false; + } + } + + $zip->extractTo($path); + + return true; + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); + + return false; + } + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_NOT_FOUND'), 'error'); + + return false; + } + } + } + + /** + * Check if the extension is allowed and will be shown in the template manager + * + * @param string $ext The extension to check if it is allowed + * + * @return boolean true if the extension is allowed false otherwise + * + * @since 3.6.0 + */ + protected function checkFormat($ext) + { + if (!isset($this->allowedFormats)) { + $params = ComponentHelper::getParams('com_templates'); + $imageTypes = explode(',', $params->get('image_formats')); + $sourceTypes = explode(',', $params->get('source_formats')); + $fontTypes = explode(',', $params->get('font_formats')); + $archiveTypes = explode(',', $params->get('compressed_formats')); + + $this->allowedFormats = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes); + $this->allowedFormats = array_map('strtolower', $this->allowedFormats); + } + + return in_array(strtolower($ext), $this->allowedFormats); + } + + /** + * Method to get a list of all the files to edit in a template's media folder. + * + * @return array A nested array of relevant files. + * + * @since 4.1.0 + */ + public function getMediaFiles() + { + $result = []; + $template = $this->getTemplate(); + + if (!isset($template->xmldata)) { + $template->xmldata = TemplatesHelper::parseXMLTemplateFile($template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); + } + + if (!isset($template->xmldata->inheritable) || (isset($template->xmldata->parent) && $template->xmldata->parent === '')) { + return $result; + } + + $app = Factory::getApplication(); + $path = Path::clean(JPATH_ROOT . '/media/templates/' . ($template->client_id === 0 ? 'site' : 'administrator') . '/' . $template->element . '/'); + $this->mediaElement = $path; + + if (!is_writable($path)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error'); + } + + if (is_dir($path)) { + $result = $this->getDirectoryTree($path); + } + + return $result; + } + + /** + * Method to resolve the base folder. + * + * @return string The absolute path for the base. + * + * @since 4.1.0 + */ + private function getBasePath() + { + $app = Factory::getApplication(); + $isMedia = $app->input->getInt('isMedia', 0); + + return $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element : + JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element; + } + + /** + * Method to create the templateDetails.xml for the child template + * + * @return boolean true if name is not used, false otherwise + * + * @since 4.1.0 + */ + public function child() + { + $app = Factory::getApplication(); + $template = $this->getTemplate(); + + if (!(array) $template) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + + $client = ApplicationHelper::getClientInfo($template->client_id); + $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml'); + + // Delete new folder if it exists + $toPath = $this->getState('to_path'); + + if (Folder::exists($toPath)) { + if (!Folder::delete($toPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + } else { + if (!Folder::create($toPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + } + + // Copy the template definition from the parent template + if (!File::copy($fromPath, $toPath . '/templateDetails.xml')) { + return false; + } + + // Check manifest for additional files + $newName = strtolower($this->getState('new_name')); + $template = $this->getTemplate(); + + // Edit XML file + $xmlFile = Path::clean($this->getState('to_path') . '/templateDetails.xml'); + + if (!File::exists($xmlFile)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); + + return false; + } + + try { + $xml = simplexml_load_string(file_get_contents($xmlFile)); + } catch (\Exception $e) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error'); + + return false; + } + + $user = Factory::getUser(); + unset($xml->languages); + unset($xml->media); + unset($xml->files); + unset($xml->parent); + unset($xml->inheritable); + + // Remove the update parts + unset($xml->update); + unset($xml->updateservers); + + if (isset($xml->creationDate)) { + $xml->creationDate = (new Date('now'))->format('F Y'); + } else { + $xml->addChild('creationDate', (new Date('now'))->format('F Y')); + } + + if (isset($xml->author)) { + $xml->author = $user->name; + } else { + $xml->addChild('author', $user->name); + } + + if (isset($xml->authorEmail)) { + $xml->authorEmail = $user->email; + } else { + $xml->addChild('authorEmail', $user->email); + } + + $files = $xml->addChild('files'); + $files->addChild('filename', 'templateDetails.xml'); + + // Media folder + $media = $xml->addChild('media'); + $media->addAttribute('folder', 'media'); + $media->addAttribute('destination', 'templates/' . ($template->client_id === 0 ? 'site/' : 'administrator/') . $template->element . '_' . $newName); + $media->addChild('folder', 'css'); + $media->addChild('folder', 'js'); + $media->addChild('folder', 'images'); + $media->addChild('folder', 'html'); + $media->addChild('folder', 'scss'); + + $xml->name = $template->element . '_' . $newName; + $xml->inheritable = 0; + $files = $xml->addChild('parent', $template->element); + + $dom = new \DOMDocument(); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML($xml->asXML()); + + $result = File::write($xmlFile, $dom->saveXML()); + + if (!$result) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + + // Create an empty media folder structure + if ( + !Folder::create($toPath . '/media') + || !Folder::create($toPath . '/media/css') + || !Folder::create($toPath . '/media/js') + || !Folder::create($toPath . '/media/images') + || !Folder::create($toPath . '/media/html/tinymce') + || !Folder::create($toPath . '/media/scss') + ) { + return false; + } + + return true; + } + + /** + * Method to get the parent template existing styles + * + * @return array array of id,titles of the styles + * + * @since 4.1.3 + */ + public function getAllTemplateStyles() + { + $template = $this->getTemplate(); + + if (empty($template->xmldata->inheritable)) { + return []; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($db->quoteName(['id', 'title'])) + ->from($db->quoteName('#__template_styles')) + ->where($db->quoteName('client_id') . ' = :client_id', 'AND') + ->where($db->quoteName('template') . ' = :template') + ->orWhere($db->quoteName('parent') . ' = :parent') + ->bind(':client_id', $template->client_id, ParameterType::INTEGER) + ->bind(':template', $template->element) + ->bind(':parent', $template->element); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Method to copy selected styles to the child template + * + * @return boolean true if name is not used, false otherwise + * + * @since 4.1.3 + */ + public function copyStyles() + { + $app = Factory::getApplication(); + $template = $this->getTemplate(); + $newName = strtolower($this->getState('new_name')); + $applyStyles = $this->getState('stylesToCopy'); + + // Get a db connection. + $db = $this->getDatabase(); + + // Create a new query object. + $query = $db->getQuery(true); + + $query->select($db->quoteName(['title', 'params'])) + ->from($db->quoteName('#__template_styles')) + ->whereIn($db->quoteName('id'), ArrayHelper::toInteger($applyStyles)); + // Reset the query using our newly populated query object. + $db->setQuery($query); + + try { + $parentStyle = $db->loadObjectList(); + } catch (\Exception $e) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND'), 'error'); + + return false; + } + + foreach ($parentStyle as $style) { + $query = $db->getQuery(true); + $styleName = Text::sprintf('COM_TEMPLATES_COPY_CHILD_TEMPLATE_STYLES', ucfirst($template->element . '_' . $newName), $style->title); + + // Insert columns and values + $columns = ['id', 'template', 'client_id', 'home', 'title', 'inheritable', 'parent', 'params']; + $values = [0, $db->quote($template->element . '_' . $newName), (int) $template->client_id, $db->quote('0'), $db->quote($styleName), 0, $db->quote($template->element), $db->quote($style->params)]; + + $query + ->insert($db->quoteName('#__template_styles')) + ->columns($db->quoteName($columns)) + ->values(implode(',', $values)); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\Exception $e) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error'); + + return false; + } + } + + return true; + } } diff --git a/code/administrator/components/com_templates/src/Model/TemplatesModel.php b/code/administrator/components/com_templates/src/Model/TemplatesModel.php index 1ba1a401..62c07f97 100644 --- a/code/administrator/components/com_templates/src/Model/TemplatesModel.php +++ b/code/administrator/components/com_templates/src/Model/TemplatesModel.php @@ -1,4 +1,5 @@ client_id); - $item->xmldata = TemplatesHelper::parseXMLTemplateFile($client->path, $item->element); - $num = $this->updated($item->extension_id); - - if ($num) - { - $item->updated = $num; - } - } - - return $items; - } - - /** - * Check if template extension have any updated override. - * - * @param integer $exid Extension id of template. - * - * @return boolean False if records not found/else integer. - * - * @since 4.0.0 - */ - public function updated($exid) - { - $db = $this->getDbo(); - - // Select the required fields from the table - $query = $db->getQuery(true) - ->select($db->quoteName('template')) - ->from($db->quoteName('#__template_overrides')) - ->where($db->quoteName('extension_id') . ' = :extensionid') - ->where($db->quoteName('state') . ' = 0') - ->bind(':extensionid', $exid, ParameterType::INTEGER); - - // Reset the query. - $db->setQuery($query); - - // Load the results as a list of stdClass objects. - $num = count($db->loadObjectList()); - - if ($num > 0) - { - return $num; - } - - return false; - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.extension_id, a.name, a.element, a.client_id' - ) - ); - $clientId = (int) $this->getState('client_id'); - $query->from($db->quoteName('#__extensions', 'a')) - ->where($db->quoteName('a.client_id') . ' = :clientid') - ->where($db->quoteName('a.enabled') . ' = 1') - ->where($db->quoteName('a.type') . ' = ' . $db->quote('template')) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Filter by search in title. - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . StringHelper::strtolower($search) . '%'; - $query->extendWhere( - 'AND', - [ - 'LOWER(' . $db->quoteName('a.element') . ') LIKE :element', - 'LOWER(' . $db->quoteName('a.name') . ') LIKE :name', - ], - 'OR' - ) - ->bind(':element', $search) - ->bind(':name', $search); - } - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.element')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.element', $direction = 'asc') - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - - // Special case for the client id. - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = (!in_array($clientId, array (0, 1))) ? 0 : $clientId; - $this->setState('client_id', $clientId); - - // Load the parameters. - $params = ComponentHelper::getParams('com_templates'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'folder', 'a.folder', + 'element', 'a.element', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'state', 'a.state', + 'enabled', 'a.enabled', + 'ordering', 'a.ordering', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Override parent getItems to add extra XML metadata. + * + * @return array + * + * @since 1.6 + */ + public function getItems() + { + $items = parent::getItems(); + + foreach ($items as &$item) { + $client = ApplicationHelper::getClientInfo($item->client_id); + $item->xmldata = TemplatesHelper::parseXMLTemplateFile($client->path, $item->element); + $num = $this->updated($item->extension_id); + + if ($num) { + $item->updated = $num; + } + } + + return $items; + } + + /** + * Check if template extension have any updated override. + * + * @param integer $exid Extension id of template. + * + * @return boolean False if records not found/else integer. + * + * @since 4.0.0 + */ + public function updated($exid) + { + $db = $this->getDatabase(); + + // Select the required fields from the table + $query = $db->getQuery(true) + ->select($db->quoteName('template')) + ->from($db->quoteName('#__template_overrides')) + ->where($db->quoteName('extension_id') . ' = :extensionid') + ->where($db->quoteName('state') . ' = 0') + ->bind(':extensionid', $exid, ParameterType::INTEGER); + + // Reset the query. + $db->setQuery($query); + + // Load the results as a list of stdClass objects. + $num = count($db->loadObjectList()); + + if ($num > 0) { + return $num; + } + + return false; + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.extension_id, a.name, a.element, a.client_id' + ) + ); + $clientId = (int) $this->getState('client_id'); + $query->from($db->quoteName('#__extensions', 'a')) + ->where($db->quoteName('a.client_id') . ' = :clientid') + ->where($db->quoteName('a.enabled') . ' = 1') + ->where($db->quoteName('a.type') . ' = ' . $db->quote('template')) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Filter by search in title. + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . StringHelper::strtolower($search) . '%'; + $query->extendWhere( + 'AND', + [ + 'LOWER(' . $db->quoteName('a.element') . ') LIKE :element', + 'LOWER(' . $db->quoteName('a.name') . ') LIKE :name', + ], + 'OR' + ) + ->bind(':element', $search) + ->bind(':name', $search); + } + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.element')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.element', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + + // Special case for the client id. + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = (!in_array($clientId, array (0, 1))) ? 0 : $clientId; + $this->setState('client_id', $clientId); + + // Load the parameters. + $params = ComponentHelper::getParams('com_templates'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } } diff --git a/code/administrator/components/com_templates/src/Service/HTML/Templates.php b/code/administrator/components/com_templates/src/Service/HTML/Templates.php index 6d7fd363..c9e33314 100644 --- a/code/administrator/components/com_templates/src/Service/HTML/Templates.php +++ b/code/administrator/components/com_templates/src/Service/HTML/Templates.php @@ -1,4 +1,5 @@ client_id); - - if (!isset($template->xmldata)) - { - $template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); - } - - if ((isset($template->xmldata->inheritable) && (bool) $template->xmldata->inheritable) || isset($template->xmldata->parent)) - { - if (isset($template->xmldata->parent) && (string) $template->xmldata->parent !== '' && file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png')) - { - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) - { - $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); - $html = ''; - } - elseif ((file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_preview.png'))) - { - $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); - $html = ''; - } - else - { - $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); - } - } - elseif (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) - { - $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); - - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) - { - $html = ''; - } - } - else - { - $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); - } - } - elseif (file_exists($client->path . '/templates/' . $template->element . '/template_thumbnail.png')) - { - $html = HTMLHelper::_('image', (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . '/administrator/') . '/templates/' . $template->element . '/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], false, -1); - - if (file_exists($client->path . '/templates/' . $template->element . '/template_preview.png')) - { - $html = ''; - } - } - else - { - $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); - } - - return $html; - } - - /** - * Renders the html for the modal linked to thumb. - * - * @param string|object $template The name of the template or the template object. - * @param integer $clientId The application client ID the template applies to - * - * @return string The html string - * - * @since 3.4 - * - * @deprecated 5.0 The argument $template should be object and $clientId will be removed - */ - public function thumbModal($template, $clientId = 0) - { - if (is_string($template)) - { - return HTMLHelper::_('image', 'template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], true, -1); - } - - $html = ''; - $thumb = ''; - $preview = ''; - $client = ApplicationHelper::getClientInfo($template->client_id); - - if (!isset($template->xmldata)) - { - $template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); - } - - if ((isset($template->xmldata->inheritable) && (bool) $template->xmldata->inheritable) || isset($template->xmldata->parent)) - { - if (isset($template->xmldata->parent) && (string) $template->xmldata->parent !== '') - { - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) - { - $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . 'administrator') . 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png'; - - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element. '/images/template_preview.png')) - { - $preview = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . $template->element. '/images/template_preview.png'; - } - } - else - { - $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . 'administrator') . 'media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png'; - - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent. '/images/template_preview.png')) - { - $preview = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent. '/images/template_preview.png'; - } - } - } - elseif (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) - { - $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png'; - - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) - { - $preview = Uri::root(true) . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png'; - } - } - } - elseif (file_exists($client->path . '/templates/' . $template->element . '/template_thumbnail.png')) - { - $thumb = (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . 'administrator') . '/templates/' . $template->element . '/template_thumbnail.png'; - - if (file_exists($client->path . '/templates/' . $template->element . '/template_preview.png')) - { - $preview = (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . '/administrator') . '/templates/' . $template->element . '/template_preview.png'; - } - } - - if ($thumb !== '' && $preview !== '') - { - $footer = ''; - - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - $template->name . '-Modal', - array( - 'title' => Text::sprintf('COM_TEMPLATES_SCREENSHOT', ucfirst($template->name)), - 'height' => '500px', - 'width' => '800px', - 'footer' => $footer, - ), - '
' . $template->name . '
' - ); - } - - return $html; - } + /** + * Display the thumb for the template. + * + * @param string|object $template The name of the template or the template object. + * @param integer $clientId The application client ID the template applies to + * + * @return string The html string + * + * @since 1.6 + * + * @deprecated 5.0 The argument $template should be object and $clientId will be removed + */ + public function thumb($template, $clientId = 0) + { + if (is_string($template)) { + return HTMLHelper::_('image', 'template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], true, -1); + } + + $client = ApplicationHelper::getClientInfo($template->client_id); + + if (!isset($template->xmldata)) { + $template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); + } + + if ((isset($template->xmldata->inheritable) && (bool) $template->xmldata->inheritable) || isset($template->xmldata->parent)) { + if (isset($template->xmldata->parent) && (string) $template->xmldata->parent !== '' && file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png')) { + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) { + $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); + $html = ''; + } elseif ((file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_preview.png'))) { + $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); + $html = ''; + } else { + $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); + } + } elseif (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) { + $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); + + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) { + $html = ''; + } + } else { + $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); + } + } elseif (file_exists($client->path . '/templates/' . $template->element . '/template_thumbnail.png')) { + $html = HTMLHelper::_('image', (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . '/administrator/') . '/templates/' . $template->element . '/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], false, -1); + + if (file_exists($client->path . '/templates/' . $template->element . '/template_preview.png')) { + $html = ''; + } + } else { + $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); + } + + return $html; + } + + /** + * Renders the html for the modal linked to thumb. + * + * @param string|object $template The name of the template or the template object. + * @param integer $clientId The application client ID the template applies to + * + * @return string The html string + * + * @since 3.4 + * + * @deprecated 5.0 The argument $template should be object and $clientId will be removed + */ + public function thumbModal($template, $clientId = 0) + { + if (is_string($template)) { + return HTMLHelper::_('image', 'template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], true, -1); + } + + $html = ''; + $thumb = ''; + $preview = ''; + $client = ApplicationHelper::getClientInfo($template->client_id); + + if (!isset($template->xmldata)) { + $template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); + } + + if ((isset($template->xmldata->inheritable) && (bool) $template->xmldata->inheritable) || isset($template->xmldata->parent)) { + if (isset($template->xmldata->parent) && (string) $template->xmldata->parent !== '') { + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) { + $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . 'administrator') . 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png'; + + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) { + $preview = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png'; + } + } else { + $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . 'administrator') . 'media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png'; + + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_preview.png')) { + $preview = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_preview.png'; + } + } + } elseif (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) { + $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png'; + + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) { + $preview = Uri::root(true) . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png'; + } + } + } elseif (file_exists($client->path . '/templates/' . $template->element . '/template_thumbnail.png')) { + $thumb = (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . 'administrator') . '/templates/' . $template->element . '/template_thumbnail.png'; + + if (file_exists($client->path . '/templates/' . $template->element . '/template_preview.png')) { + $preview = (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . '/administrator') . '/templates/' . $template->element . '/template_preview.png'; + } + } + + if ($thumb !== '' && $preview !== '') { + $footer = ''; + + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + $template->name . '-Modal', + array( + 'title' => Text::sprintf('COM_TEMPLATES_SCREENSHOT', ucfirst($template->name)), + 'height' => '500px', + 'width' => '800px', + 'footer' => $footer, + ), + '
' . $template->name . '
' + ); + } + + return $html; + } } diff --git a/code/administrator/components/com_templates/src/Table/StyleTable.php b/code/administrator/components/com_templates/src/Table/StyleTable.php index 857dd4f0..1f3620d7 100644 --- a/code/administrator/components/com_templates/src/Table/StyleTable.php +++ b/code/administrator/components/com_templates/src/Table/StyleTable.php @@ -1,4 +1,5 @@ home == '1') - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE')); - - return false; - } - - return parent::bind($array, $ignore); - } - - /** - * Overloaded check method to ensure data integrity. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (empty($this->title)) - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_STYLE_REQUIRES_TITLE')); - - return false; - } - - return true; - } - - /** - * Overloaded store method to ensure unicity of default style. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function store($updateNulls = false) - { - if ($this->home != '0') - { - $clientId = (int) $this->client_id; - $query = $this->_db->getQuery(true) - ->update($this->_db->quoteName('#__template_styles')) - ->set($this->_db->quoteName('home') . ' = ' . $this->_db->quote('0')) - ->where($this->_db->quoteName('client_id') . ' = :clientid') - ->where($this->_db->quoteName('home') . ' = :home') - ->bind(':clientid', $clientId, ParameterType::INTEGER) - ->bind(':home', $this->home); - $this->_db->setQuery($query); - $this->_db->execute(); - } - - return parent::store($updateNulls); - } - - /** - * Overloaded store method to unsure existence of a default style for a template. - * - * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function delete($pk = null) - { - $k = $this->_tbl_key; - $pk = is_null($pk) ? $this->$k : $pk; - - if (!is_null($pk)) - { - $clientId = (int) $this->client_id; - $query = $this->_db->getQuery(true) - ->select($this->_db->quoteName('id')) - ->from($this->_db->quoteName('#__template_styles')) - ->where($this->_db->quoteName('client_id') . ' = :clientid') - ->where($this->_db->quoteName('template') . ' = :template') - ->bind(':template', $this->template) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - $this->_db->setQuery($query); - $results = $this->_db->loadColumn(); - - if (count($results) == 1 && $results[0] == $pk) - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_CANNOT_DELETE_LAST_STYLE')); - - return false; - } - } - - return parent::delete($pk); - } + /** + * Constructor + * + * @param DatabaseDriver $db A database connector object + * + * @since 1.6 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__template_styles', 'id', $db); + } + + /** + * Overloaded bind function to pre-process the params. + * + * @param array $array Named array + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return null|string null if operation was satisfactory, otherwise returns an error + * + * @since 1.6 + */ + public function bind($array, $ignore = '') + { + if (isset($array['params']) && is_array($array['params'])) { + $registry = new Registry($array['params']); + $array['params'] = (string) $registry; + } + + // Verify that the default style is not unset + if ($array['home'] == '0' && $this->home == '1') { + $this->setError(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE')); + + return false; + } + + return parent::bind($array, $ignore); + } + + /** + * Overloaded check method to ensure data integrity. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (empty($this->title)) { + $this->setError(Text::_('COM_TEMPLATES_ERROR_STYLE_REQUIRES_TITLE')); + + return false; + } + + return true; + } + + /** + * Overloaded store method to ensure unicity of default style. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function store($updateNulls = false) + { + if ($this->home != '0') { + $clientId = (int) $this->client_id; + $query = $this->_db->getQuery(true) + ->update($this->_db->quoteName('#__template_styles')) + ->set($this->_db->quoteName('home') . ' = ' . $this->_db->quote('0')) + ->where($this->_db->quoteName('client_id') . ' = :clientid') + ->where($this->_db->quoteName('home') . ' = :home') + ->bind(':clientid', $clientId, ParameterType::INTEGER) + ->bind(':home', $this->home); + $this->_db->setQuery($query); + $this->_db->execute(); + } + + return parent::store($updateNulls); + } + + /** + * Overloaded store method to unsure existence of a default style for a template. + * + * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function delete($pk = null) + { + $k = $this->_tbl_key; + $pk = is_null($pk) ? $this->$k : $pk; + + if (!is_null($pk)) { + $clientId = (int) $this->client_id; + $query = $this->_db->getQuery(true) + ->select($this->_db->quoteName('id')) + ->from($this->_db->quoteName('#__template_styles')) + ->where($this->_db->quoteName('client_id') . ' = :clientid') + ->where($this->_db->quoteName('template') . ' = :template') + ->bind(':template', $this->template) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + $this->_db->setQuery($query); + $results = $this->_db->loadColumn(); + + if (count($results) == 1 && $results[0] == $pk) { + $this->setError(Text::_('COM_TEMPLATES_ERROR_CANNOT_DELETE_LAST_STYLE')); + + return false; + } + } + + return parent::delete($pk); + } } diff --git a/code/administrator/components/com_templates/src/View/Style/HtmlView.php b/code/administrator/components/com_templates/src/View/Style/HtmlView.php index 8f315cd1..546a1a0d 100644 --- a/code/administrator/components/com_templates/src/View/Style/HtmlView.php +++ b/code/administrator/components/com_templates/src/View/Style/HtmlView.php @@ -1,4 +1,5 @@ item = $this->get('Item'); - $this->state = $this->get('State'); - $this->form = $this->get('Form'); - $this->canDo = ContentHelper::getActions('com_templates'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - $canDo = $this->canDo; - - ToolbarHelper::title( - $isNew ? Text::_('COM_TEMPLATES_MANAGER_ADD_STYLE') - : Text::_('COM_TEMPLATES_MANAGER_EDIT_STYLE'), 'paint-brush thememanager' - ); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('style.apply'); - $toolbarButtons[] = ['save', 'style.save']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'style.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('style.cancel'); - } - else - { - ToolbarHelper::cancel('style.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - - // Get the help information for the template item. - $lang = Factory::getLanguage(); - $help = $this->get('Help'); - - if ($lang->hasKey($help->url)) - { - $debug = $lang->setDebug(false); - $url = Text::_($help->url); - $lang->setDebug($debug); - } - else - { - $url = null; - } - - ToolbarHelper::help($help->key, false, $url); - } + /** + * The CMSObject (on success, false on failure) + * + * @var CMSObject + */ + protected $item; + + /** + * The form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->canDo = ContentHelper::getActions('com_templates'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + $canDo = $this->canDo; + + ToolbarHelper::title( + $isNew ? Text::_('COM_TEMPLATES_MANAGER_ADD_STYLE') + : Text::_('COM_TEMPLATES_MANAGER_EDIT_STYLE'), + 'paint-brush thememanager' + ); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('style.apply'); + $toolbarButtons[] = ['save', 'style.save']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'style.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('style.cancel'); + } else { + ToolbarHelper::cancel('style.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + + // Get the help information for the template item. + $lang = Factory::getLanguage(); + $help = $this->get('Help'); + + if ($lang->hasKey($help->url)) { + $debug = $lang->setDebug(false); + $url = Text::_($help->url); + $lang->setDebug($debug); + } else { + $url = null; + } + + ToolbarHelper::help($help->key, false, $url); + } } diff --git a/code/administrator/components/com_templates/src/View/Style/JsonView.php b/code/administrator/components/com_templates/src/View/Style/JsonView.php index edc7dd22..d14f019e 100644 --- a/code/administrator/components/com_templates/src/View/Style/JsonView.php +++ b/code/administrator/components/com_templates/src/View/Style/JsonView.php @@ -1,4 +1,5 @@ item = $this->get('Item'); - } - catch (\Exception $e) - { - $app = Factory::getApplication(); - $app->enqueueMessage($e->getMessage(), 'error'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + * + * @since 1.6 + */ + public function display($tpl = null) + { + try { + $this->item = $this->get('Item'); + } catch (\Exception $e) { + $app = Factory::getApplication(); + $app->enqueueMessage($e->getMessage(), 'error'); - return false; - } + return false; + } - $paramsList = $this->item->getProperties(); + $paramsList = $this->item->getProperties(); - unset($paramsList['xml']); + unset($paramsList['xml']); - $paramsList = json_encode($paramsList); + $paramsList = json_encode($paramsList); - return $paramsList; - } + return $paramsList; + } } diff --git a/code/administrator/components/com_templates/src/View/Styles/HtmlView.php b/code/administrator/components/com_templates/src/View/Styles/HtmlView.php index 15783e61..d74d1909 100644 --- a/code/administrator/components/com_templates/src/View/Styles/HtmlView.php +++ b/code/administrator/components/com_templates/src/View/Styles/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->preview = ComponentHelper::getParams('com_templates')->get('template_positions_display'); - - // Remove the menu item filter for administrator styles. - if ((int) $this->state->get('client_id') !== 0) - { - unset($this->activeFilters['menuitem']); - $this->filterForm->removeField('menuitem', 'filter'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_templates'); - $clientId = (int) $this->get('State')->get('client_id'); - - // Add a shortcut to the templates list view. - ToolbarHelper::link('index.php?option=com_templates&view=templates&client_id=' . $clientId, 'COM_TEMPLATES_MANAGER_TEMPLATES', 'icon-code thememanager'); - - // Set the title. - if ($clientId === 1) - { - ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_STYLES_ADMIN'), 'paint-brush thememanager'); - } - else - { - ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_STYLES_SITE'), 'paint-brush thememanager'); - } - - if ($canDo->get('core.edit.state')) - { - ToolbarHelper::makeDefault('styles.setDefault', 'COM_TEMPLATES_TOOLBAR_SET_HOME'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.create')) - { - ToolbarHelper::custom('styles.duplicate', 'copy', '', 'JTOOLBAR_DUPLICATE', true); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'styles.delete', 'JTOOLBAR_DELETE'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_templates'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Templates:_Styles'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is the parameter enabled to show template positions in the frontend? + * + * @var boolean + * @since 4.0.0 + */ + public $preview; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->preview = ComponentHelper::getParams('com_templates')->get('template_positions_display'); + + // Remove the menu item filter for administrator styles. + if ((int) $this->state->get('client_id') !== 0) { + unset($this->activeFilters['menuitem']); + $this->filterForm->removeField('menuitem', 'filter'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_templates'); + $clientId = (int) $this->get('State')->get('client_id'); + + // Add a shortcut to the templates list view. + ToolbarHelper::link('index.php?option=com_templates&view=templates&client_id=' . $clientId, 'COM_TEMPLATES_MANAGER_TEMPLATES', 'icon-code thememanager'); + + // Set the title. + if ($clientId === 1) { + ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_STYLES_ADMIN'), 'paint-brush thememanager'); + } else { + ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_STYLES_SITE'), 'paint-brush thememanager'); + } + + if ($canDo->get('core.edit.state')) { + ToolbarHelper::makeDefault('styles.setDefault', 'COM_TEMPLATES_TOOLBAR_SET_HOME'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.create')) { + ToolbarHelper::custom('styles.duplicate', 'copy', '', 'JTOOLBAR_DUPLICATE', true); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'styles.delete', 'JTOOLBAR_DELETE'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_templates'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Templates:_Styles'); + } } diff --git a/code/administrator/components/com_templates/src/View/Template/HtmlView.php b/code/administrator/components/com_templates/src/View/Template/HtmlView.php index 8aa3f7cf..4d1801aa 100644 --- a/code/administrator/components/com_templates/src/View/Template/HtmlView.php +++ b/code/administrator/components/com_templates/src/View/Template/HtmlView.php @@ -1,4 +1,5 @@ file = $app->input->get('file'); - $this->fileName = InputFilter::getInstance()->clean(base64_decode($this->file), 'string'); - $explodeArray = explode('.', $this->fileName); - $ext = end($explodeArray); - $this->files = $this->get('Files'); - $this->mediaFiles = $this->get('MediaFiles'); - $this->state = $this->get('State'); - $this->template = $this->get('Template'); - $this->preview = $this->get('Preview'); - $this->pluginState = PluginHelper::isEnabled('installer', 'override'); - $this->updatedList = $this->get('UpdatedList'); - $this->styles = $this->get('AllTemplateStyles'); - $this->stylesHTML = ''; - - $params = ComponentHelper::getParams('com_templates'); - $imageTypes = explode(',', $params->get('image_formats')); - $sourceTypes = explode(',', $params->get('source_formats')); - $fontTypes = explode(',', $params->get('font_formats')); - $archiveTypes = explode(',', $params->get('compressed_formats')); - - if (in_array($ext, $sourceTypes)) - { - $this->form = $this->get('Form'); - $this->form->setFieldAttribute('source', 'syntax', $ext); - $this->source = $this->get('Source'); - $this->type = 'file'; - } - elseif (in_array($ext, $imageTypes)) - { - try - { - $this->image = $this->get('Image'); - $this->type = 'image'; - } - catch (\RuntimeException $exception) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_GD_EXTENSION_NOT_AVAILABLE')); - $this->type = 'home'; - } - } - elseif (in_array($ext, $fontTypes)) - { - $this->font = $this->get('Font'); - $this->type = 'font'; - } - elseif (in_array($ext, $archiveTypes)) - { - $this->archive = $this->get('Archive'); - $this->type = 'archive'; - } - else - { - $this->type = 'home'; - } - - $this->overridesList = $this->get('OverridesList'); - $this->id = $this->state->get('extension.id'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - $app->enqueueMessage(implode("\n", $errors)); - - return false; - } - - $this->addToolbar(); - - if (!Factory::getUser()->authorise('core.admin')) - { - $this->setLayout('readonly'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @since 1.6 - * - * @return void - */ - protected function addToolbar() - { - $app = Factory::getApplication(); - $user = Factory::getUser(); - $app->input->set('hidemainmenu', true); - - // User is global SuperUser - $isSuperUser = $user->authorise('core.admin'); - - // Get the toolbar object instance - $bar = Toolbar::getInstance('toolbar'); - $explodeArray = explode('.', $this->fileName); - $ext = end($explodeArray); - - ToolbarHelper::title(Text::sprintf('COM_TEMPLATES_MANAGER_VIEW_TEMPLATE', ucfirst($this->template->name)), 'icon-code thememanager'); - - // Only show file edit buttons for global SuperUser - if ($isSuperUser) - { - // Add an Apply and save button - if ($this->type === 'file') - { - ToolbarHelper::apply('template.apply'); - ToolbarHelper::save('template.save'); - } - // Add a Crop and Resize button - elseif ($this->type === 'image') - { - ToolbarHelper::custom('template.cropImage', 'icon-crop', '', 'COM_TEMPLATES_BUTTON_CROP', false); - ToolbarHelper::modal('resizeModal', 'icon-expand', 'COM_TEMPLATES_BUTTON_RESIZE'); - } - // Add an extract button - elseif ($this->type === 'archive') - { - ToolbarHelper::custom('template.extractArchive', 'chevron-down', '', 'COM_TEMPLATES_BUTTON_EXTRACT_ARCHIVE', false); - } - // Add a copy/child template button - elseif ($this->type === 'home') - { - if (isset($this->template->xmldata->inheritable) && (string) $this->template->xmldata->inheritable === '1') - { - ToolbarHelper::modal('childModal', 'icon-copy', 'COM_TEMPLATES_BUTTON_TEMPLATE_CHILD', false); - } - elseif (!isset($this->template->xmldata->parent) || $this->template->xmldata->parent == '') - { - ToolbarHelper::modal('copyModal', 'icon-copy', 'COM_TEMPLATES_BUTTON_COPY_TEMPLATE', false); - } - } - } - - // Add a Template preview button - if ($this->type === 'home') - { - $client = (int) $this->preview->client_id === 1 ? 'administrator/' : ''; - $bar->linkButton('preview') - ->icon('icon-image') - ->text('COM_TEMPLATES_BUTTON_PREVIEW') - ->url(Uri::root() . $client . 'index.php?tp=1&templateStyle=' . $this->preview->id) - ->attributes(['target' => '_new']); - } - - // Only show file manage buttons for global SuperUser - if ($isSuperUser) - { - if ($this->type === 'home') - { - // Add Manage folders button - ToolbarHelper::modal('folderModal', 'icon-folder icon white', 'COM_TEMPLATES_BUTTON_FOLDERS'); - - // Add a new file button - ToolbarHelper::modal('fileModal', 'icon-file', 'COM_TEMPLATES_BUTTON_FILE'); - } - else - { - // Add a Rename file Button - ToolbarHelper::modal('renameModal', 'icon-sync', 'COM_TEMPLATES_BUTTON_RENAME_FILE'); - - // Add a Delete file Button - ToolbarHelper::modal('deleteModal', 'icon-times', 'COM_TEMPLATES_BUTTON_DELETE_FILE', 'btn-danger'); - } - } - - if (count($this->updatedList) !== 0 && $this->pluginState) - { - ToolbarHelper::custom('template.deleteOverrideHistory', 'times', '', 'COM_TEMPLATES_BUTTON_DELETE_LIST_ENTRY', true, 'updateForm'); - } - - if ($this->type === 'home') - { - ToolbarHelper::cancel('template.cancel', 'JTOOLBAR_CLOSE'); - } - else - { - ToolbarHelper::cancel('template.close', 'COM_TEMPLATES_BUTTON_CLOSE_FILE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Templates:_Customise'); - } - - /** - * Method for creating the collapsible tree. - * - * @param array $array The value of the present node for recursion - * - * @return string - * - * @note Uses recursion - * @since 3.2 - */ - protected function directoryTree($array) - { - $temp = $this->files; - $this->files = $array; - $txt = $this->loadTemplate('tree'); - $this->files = $temp; - - return $txt; - } - - /** - * Method for listing the folder tree in modals. - * - * @param array $array The value of the present node for recursion - * - * @return string - * - * @note Uses recursion - * @since 3.2 - */ - protected function folderTree($array) - { - $temp = $this->files; - $this->files = $array; - $txt = $this->loadTemplate('folders'); - $this->files = $temp; - - return $txt; - } - - /** - * Method for creating the collapsible tree. - * - * @param array $array The value of the present node for recursion - * - * @return string - * - * @note Uses recursion - * @since 4.1.0 - */ - protected function mediaTree($array) - { - $temp = $this->mediaFiles; - $this->mediaFiles = $array; - $txt = $this->loadTemplate('tree_media'); - $this->mediaFiles = $temp; - - return $txt; - } - - /** - * Method for listing the folder tree in modals. - * - * @param array $array The value of the present node for recursion - * - * @return string - * - * @note Uses recursion - * @since 4.1.0 - */ - protected function mediaFolderTree($array) - { - $temp = $this->mediaFiles; - $this->mediaFiles = $array; - $txt = $this->loadTemplate('media_folders'); - $this->mediaFiles = $temp; - - return $txt; - } + /** + * The Model state + * + * @var CMSObject + */ + protected $state; + + /** + * The template details + * + * @var \stdClass|false + */ + protected $template; + + /** + * For loading the source form + * + * @var Form + */ + protected $form; + + /** + * For loading source file contents + * + * @var array + */ + protected $source; + + /** + * Extension id + * + * @var integer + */ + protected $id; + + /** + * Encrypted file path + * + * @var string + */ + protected $file; + + /** + * List of available overrides + * + * @var array + */ + protected $overridesList; + + /** + * Name of the present file + * + * @var string + */ + protected $fileName; + + /** + * Type of the file - image, source, font + * + * @var string + */ + protected $type; + + /** + * For loading image information + * + * @var array + */ + protected $image; + + /** + * Template id for showing preview button + * + * @var \stdClass + */ + protected $preview; + + /** + * For loading font information + * + * @var array + */ + protected $font; + + /** + * A nested array containing list of files and folders + * + * @var array + */ + protected $files; + + /** + * An array containing a list of compressed files + * + * @var array + */ + protected $archive; + + /** + * The state of installer override plugin. + * + * @var array + * + * @since 4.0.0 + */ + protected $pluginState; + + /** + * A nested array containing list of files and folders in the media folder + * + * @var array + * + * @since 4.1.0 + */ + protected $mediaFiles; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $this->file = $app->input->get('file', ''); + $this->fileName = InputFilter::getInstance()->clean(base64_decode($this->file), 'string'); + $explodeArray = explode('.', $this->fileName); + $ext = end($explodeArray); + $this->files = $this->get('Files'); + $this->mediaFiles = $this->get('MediaFiles'); + $this->state = $this->get('State'); + $this->template = $this->get('Template'); + $this->preview = $this->get('Preview'); + $this->pluginState = PluginHelper::isEnabled('installer', 'override'); + $this->updatedList = $this->get('UpdatedList'); + $this->styles = $this->get('AllTemplateStyles'); + $this->stylesHTML = ''; + + $params = ComponentHelper::getParams('com_templates'); + $imageTypes = explode(',', $params->get('image_formats')); + $sourceTypes = explode(',', $params->get('source_formats')); + $fontTypes = explode(',', $params->get('font_formats')); + $archiveTypes = explode(',', $params->get('compressed_formats')); + + if (in_array($ext, $sourceTypes)) { + $this->form = $this->get('Form'); + $this->form->setFieldAttribute('source', 'syntax', $ext); + $this->source = $this->get('Source'); + $this->type = 'file'; + } elseif (in_array($ext, $imageTypes)) { + try { + $this->image = $this->get('Image'); + $this->type = 'image'; + } catch (\RuntimeException $exception) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_GD_EXTENSION_NOT_AVAILABLE')); + $this->type = 'home'; + } + } elseif (in_array($ext, $fontTypes)) { + $this->font = $this->get('Font'); + $this->type = 'font'; + } elseif (in_array($ext, $archiveTypes)) { + $this->archive = $this->get('Archive'); + $this->type = 'archive'; + } else { + $this->type = 'home'; + } + + $this->overridesList = $this->get('OverridesList'); + $this->id = $this->state->get('extension.id'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + $app->enqueueMessage(implode("\n", $errors)); + + return false; + } + + $this->addToolbar(); + + if (!$this->getCurrentUser()->authorise('core.admin')) { + $this->setLayout('readonly'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @since 1.6 + * + * @return void + */ + protected function addToolbar() + { + $app = Factory::getApplication(); + $user = $this->getCurrentUser(); + $app->input->set('hidemainmenu', true); + + // User is global SuperUser + $isSuperUser = $user->authorise('core.admin'); + + // Get the toolbar object instance + $bar = Toolbar::getInstance('toolbar'); + $explodeArray = explode('.', $this->fileName); + $ext = end($explodeArray); + + ToolbarHelper::title(Text::sprintf('COM_TEMPLATES_MANAGER_VIEW_TEMPLATE', ucfirst($this->template->name)), 'icon-code thememanager'); + + // Only show file edit buttons for global SuperUser + if ($isSuperUser) { + // Add an Apply and save button + if ($this->type === 'file') { + ToolbarHelper::apply('template.apply'); + ToolbarHelper::save('template.save'); + } elseif ($this->type === 'image') { + // Add a Crop and Resize button + ToolbarHelper::custom('template.cropImage', 'icon-crop', '', 'COM_TEMPLATES_BUTTON_CROP', false); + ToolbarHelper::modal('resizeModal', 'icon-expand', 'COM_TEMPLATES_BUTTON_RESIZE'); + } elseif ($this->type === 'archive') { + // Add an extract button + ToolbarHelper::custom('template.extractArchive', 'chevron-down', '', 'COM_TEMPLATES_BUTTON_EXTRACT_ARCHIVE', false); + } elseif ($this->type === 'home') { + // Add a copy/child template button + if (isset($this->template->xmldata->inheritable) && (string) $this->template->xmldata->inheritable === '1') { + ToolbarHelper::modal('childModal', 'icon-copy', 'COM_TEMPLATES_BUTTON_TEMPLATE_CHILD', false); + } elseif (!isset($this->template->xmldata->parent) || $this->template->xmldata->parent == '') { + ToolbarHelper::modal('copyModal', 'icon-copy', 'COM_TEMPLATES_BUTTON_COPY_TEMPLATE', false); + } + } + } + + // Add a Template preview button + if ($this->type === 'home') { + $client = (int) $this->preview->client_id === 1 ? 'administrator/' : ''; + $bar->linkButton('preview') + ->icon('icon-image') + ->text('COM_TEMPLATES_BUTTON_PREVIEW') + ->url(Uri::root() . $client . 'index.php?tp=1&templateStyle=' . $this->preview->id) + ->attributes(['target' => '_new']); + } + + // Only show file manage buttons for global SuperUser + if ($isSuperUser) { + if ($this->type === 'home') { + // Add Manage folders button + ToolbarHelper::modal('folderModal', 'icon-folder icon white', 'COM_TEMPLATES_BUTTON_FOLDERS'); + + // Add a new file button + ToolbarHelper::modal('fileModal', 'icon-file', 'COM_TEMPLATES_BUTTON_FILE'); + } else { + // Add a Rename file Button + ToolbarHelper::modal('renameModal', 'icon-sync', 'COM_TEMPLATES_BUTTON_RENAME_FILE'); + + // Add a Delete file Button + ToolbarHelper::modal('deleteModal', 'icon-times', 'COM_TEMPLATES_BUTTON_DELETE_FILE', 'btn-danger'); + } + } + + if (count($this->updatedList) !== 0 && $this->pluginState) { + $dropdown = $bar->dropdownButton('override-group') + ->text('COM_TEMPLATES_BUTTON_CHECK') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->form('updateForm') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('template.publish') + ->text('COM_TEMPLATES_BUTTON_CHECK_LIST_ENTRY') + ->form('updateForm') + ->listCheck(true); + $childBar->unpublish('template.unpublish') + ->text('COM_TEMPLATES_BUTTON_UNCHECK_LIST_ENTRY') + ->form('updateForm') + ->listCheck(true); + $childBar->unpublish('template.deleteOverrideHistory') + ->text('COM_TEMPLATES_BUTTON_DELETE_LIST_ENTRY') + ->form('updateForm') + ->listCheck(true); + } + + if ($this->type === 'home') { + ToolbarHelper::cancel('template.cancel', 'JTOOLBAR_CLOSE'); + } else { + ToolbarHelper::cancel('template.close', 'COM_TEMPLATES_BUTTON_CLOSE_FILE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Templates:_Customise'); + } + + /** + * Method for creating the collapsible tree. + * + * @param array $array The value of the present node for recursion + * + * @return string + * + * @note Uses recursion + * @since 3.2 + */ + protected function directoryTree($array) + { + $temp = $this->files; + $this->files = $array; + $txt = $this->loadTemplate('tree'); + $this->files = $temp; + + return $txt; + } + + /** + * Method for listing the folder tree in modals. + * + * @param array $array The value of the present node for recursion + * + * @return string + * + * @note Uses recursion + * @since 3.2 + */ + protected function folderTree($array) + { + $temp = $this->files; + $this->files = $array; + $txt = $this->loadTemplate('folders'); + $this->files = $temp; + + return $txt; + } + + /** + * Method for creating the collapsible tree. + * + * @param array $array The value of the present node for recursion + * + * @return string + * + * @note Uses recursion + * @since 4.1.0 + */ + protected function mediaTree($array) + { + $temp = $this->mediaFiles; + $this->mediaFiles = $array; + $txt = $this->loadTemplate('tree_media'); + $this->mediaFiles = $temp; + + return $txt; + } + + /** + * Method for listing the folder tree in modals. + * + * @param array $array The value of the present node for recursion + * + * @return string + * + * @note Uses recursion + * @since 4.1.0 + */ + protected function mediaFolderTree($array) + { + $temp = $this->mediaFiles; + $this->mediaFiles = $array; + $txt = $this->loadTemplate('media_folders'); + $this->mediaFiles = $temp; + + return $txt; + } } diff --git a/code/administrator/components/com_templates/src/View/Templates/HtmlView.php b/code/administrator/components/com_templates/src/View/Templates/HtmlView.php index fbbed69c..ceef57d2 100644 --- a/code/administrator/components/com_templates/src/View/Templates/HtmlView.php +++ b/code/administrator/components/com_templates/src/View/Templates/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->preview = ComponentHelper::getParams('com_templates')->get('template_positions_display'); - $this->file = base64_encode('home'); - $this->pluginState = PluginHelper::isEnabled('installer', 'override'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_templates'); - $clientId = (int) $this->get('State')->get('client_id'); - - // Add a shortcut to the styles list view. - ToolbarHelper::link('index.php?option=com_templates&view=styles&client_id=' . $clientId, 'COM_TEMPLATES_MANAGER_STYLES_BUTTON', 'brush thememanager'); - - // Set the title. - if ($clientId === 1) - { - ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_TEMPLATES_ADMIN'), 'icon-code thememanager'); - } - else - { - ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_TEMPLATES_SITE'), 'icon-code thememanager'); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_templates'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Templates:_Templates'); - } + /** + * The list of templates + * + * @var array + * @since 1.6 + */ + protected $items; + + /** + * The pagination object + * + * @var object + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var object + * @since 1.6 + */ + protected $state; + + /** + * @var string + * @since 3.2 + */ + protected $file; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is the parameter enabled to show template positions in the frontend? + * + * @var boolean + * @since 4.0.0 + */ + public $preview; + + /** + * The state of installer override plugin. + * + * @var array + * + * @since 4.0.0 + */ + protected $pluginState; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->preview = ComponentHelper::getParams('com_templates')->get('template_positions_display'); + $this->file = base64_encode('home'); + $this->pluginState = PluginHelper::isEnabled('installer', 'override'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_templates'); + $clientId = (int) $this->get('State')->get('client_id'); + + // Add a shortcut to the styles list view. + ToolbarHelper::link('index.php?option=com_templates&view=styles&client_id=' . $clientId, 'COM_TEMPLATES_MANAGER_STYLES_BUTTON', 'brush thememanager'); + + // Set the title. + if ($clientId === 1) { + ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_TEMPLATES_ADMIN'), 'icon-code thememanager'); + } else { + ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_TEMPLATES_SITE'), 'icon-code thememanager'); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_templates'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Templates:_Templates'); + } } diff --git a/code/administrator/components/com_templates/templates.xml b/code/administrator/components/com_templates/templates.xml index bd1fc046..7f90e1af 100644 --- a/code/administrator/components/com_templates/templates.xml +++ b/code/administrator/components/com_templates/templates.xml @@ -2,7 +2,7 @@ com_templates Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_templates/tmpl/style/edit.php b/code/administrator/components/com_templates/tmpl/style/edit.php index f0aa6a48..92103a45 100644 --- a/code/administrator/components/com_templates/tmpl/style/edit.php +++ b/code/administrator/components/com_templates/tmpl/style/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $this->useCoreUI = true; @@ -27,90 +28,90 @@
- - -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - - -
-
-

- item->template); ?> -

-
- - item->client_id == 0 ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); ?> - -
-
-

item->xml->description); ?>

- fieldset = 'description'; - $description = LayoutHelper::render('joomla.edit.fieldset', $this); - ?> - -

- - - -

- -
- fieldset = 'basic'; - $html = LayoutHelper::render('joomla.edit.fieldset', $this); - echo $html ? '
' . $html : ''; - ?> -
-
- fields = array( - 'home', - 'client_id', - 'template' - ); - ?> - - form->renderField('inheritable'); ?> - form->renderField('parent'); ?> -
-
- - - - -
- -
- -
-
- - - - fieldsets = array(); - $this->ignore_fieldsets = array('basic', 'description'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - authorise('core.edit', 'com_menus') && $this->item->client_id == 0 && $this->canDo->get('core.edit.state')) : ?> - -
- -
- loadTemplate('assignment'); ?> -
-
- - - - - - - -
+ + +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + + +
+
+

+ item->template); ?> +

+
+ + item->client_id == 0 ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); ?> + +
+
+

item->xml->description); ?>

+ fieldset = 'description'; + $description = LayoutHelper::render('joomla.edit.fieldset', $this); + ?> + +

+ + + +

+ +
+ fieldset = 'basic'; + $html = LayoutHelper::render('joomla.edit.fieldset', $this); + echo $html ? '
' . $html : ''; + ?> +
+
+ fields = array( + 'home', + 'client_id', + 'template' + ); + ?> + + form->renderField('inheritable'); ?> + form->renderField('parent'); ?> +
+
+ + + + +
+ +
+ +
+
+ + + + fieldsets = array(); + $this->ignore_fieldsets = array('basic', 'description'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + authorise('core.edit', 'com_menus') && $this->item->client_id == 0 && $this->canDo->get('core.edit.state')) : ?> + +
+ +
+ loadTemplate('assignment'); ?> +
+
+ + + + + + + +
diff --git a/code/administrator/components/com_templates/tmpl/style/edit_assignment.php b/code/administrator/components/com_templates/tmpl/style/edit_assignment.php index b0a03841..bd3cae0f 100644 --- a/code/administrator/components/com_templates/tmpl/style/edit_assignment.php +++ b/code/administrator/components/com_templates/tmpl/style/edit_assignment.php @@ -1,4 +1,5 @@
- +
diff --git a/code/administrator/components/com_templates/tmpl/styles/default.php b/code/administrator/components/com_templates/tmpl/styles/default.php index 68b3e91f..37122169 100644 --- a/code/administrator/components/com_templates/tmpl/styles/default.php +++ b/code/administrator/components/com_templates/tmpl/styles/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $clientId = (int) $this->state->get('client_id', 0); @@ -26,127 +28,127 @@ $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
-
-
- $this, 'options' => array('selectorFieldName' => 'client_id'))); ?> - total > 0) : ?> - - - - - - - - - - - - - - - - - items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_templates'); - $canEdit = $user->authorise('core.edit', 'com_templates'); - $canChange = $user->authorise('core.edit.state', 'com_templates'); - ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->title); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - - - preview) : ?> - client_id === 1 ? 'administrator' : 'site'; ?> - - - - - - - - - home == '0' || $item->home == '1') : ?> - home != '0', $i, 'styles.', $canChange && $item->home != '1'); ?> - - - image) : ?> - image . '.gif', $item->language_title, array('title' => Text::sprintf('COM_TEMPLATES_GRID_UNSET_LANGUAGE', $item->language_title)), true); ?> - - home; ?> - - - - image) : ?> - image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> - - home; ?> - - - - home == '1') : ?> - - home != '0' && $item->home != '1') : ?> - escape($item->language_title)); ?> - assigned > 0) : ?> - escape($item->assigned)); ?> - - - - - - escape($item->template)); ?> - - - id; ?> -
+
+
+
+ $this, 'options' => array('selectorFieldName' => 'client_id'))); ?> + total > 0) : ?> + + + + + + + + + + + + + + + + + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_templates'); + $canEdit = $user->authorise('core.edit', 'com_templates'); + $canChange = $user->authorise('core.edit.state', 'com_templates'); + ?> + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + + + preview) : ?> + client_id === 1 ? 'administrator' : 'site'; ?> + + + + + + + + + home == '0' || $item->home == '1') : ?> + home != '0', $i, 'styles.', $canChange && $item->home != '1'); ?> + + + image) : ?> + image . '.gif', $item->language_title, array('title' => Text::sprintf('COM_TEMPLATES_GRID_UNSET_LANGUAGE', $item->language_title)), true); ?> + + home; ?> + + + + image) : ?> + image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> + + home; ?> + + + + home == '1') : ?> + + home != '0' && $item->home != '1') : ?> + escape($item->language_title)); ?> + assigned > 0) : ?> + escape($item->assigned)); ?> + + + + + + escape($item->template)); ?> + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
-
-
+ + + +
+
+
diff --git a/code/administrator/components/com_templates/tmpl/template/default.php b/code/administrator/components/com_templates/tmpl/template/default.php index 0ba74329..79858960 100644 --- a/code/administrator/components/com_templates/tmpl/template/default.php +++ b/code/administrator/components/com_templates/tmpl/template/default.php @@ -1,4 +1,5 @@ useScript('form.validate') - ->useScript('keepalive') - ->useScript('diff') - ->useScript('com_templates.admin-template-compare') - ->useScript('com_templates.admin-template-toggle-switch'); + ->useScript('keepalive') + ->useScript('diff') + ->useScript('com_templates.admin-template-compare') + ->useScript('com_templates.admin-template-toggle-switch'); // No access if not global SuperUser -if (!Factory::getUser()->authorise('core.admin')) -{ - Factory::getApplication()->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'danger'); +if (!Factory::getUser()->authorise('core.admin')) { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'danger'); } -if ($this->type == 'image') -{ - $wa->usePreset('cropperjs'); +if ($this->type == 'image') { + $wa->usePreset('cropperjs'); } $wa->useStyle('com_templates.admin-templates') - ->useScript('com_templates.admin-templates'); + ->useScript('com_templates.admin-templates'); -if ($this->type == 'font') -{ - $wa->addInlineStyle(" +if ($this->type == 'font') { + $wa->addInlineStyle(" @font-face { font-family: previewFont; src: url('" . $this->font['address'] . "') @@ -59,411 +57,411 @@ ?>
- 'editor', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
- type == 'file') : ?> -

get('isMedia', 0) ? '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . str_replace('//', '/', base64_decode($this->file)) : '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . str_replace('//', '/', base64_decode($this->file))), $this->template->element); ?>

- - - type == 'image') : ?> -

image['path'], $this->template->element); ?>

- - - type == 'font') : ?> -

font['rel_path'], $this->template->element); ?>

- - -
- type == 'file' && !empty($this->source->coreFile)) : ?> -
-
- form->renderField('show_core'); ?> - form->renderField('show_diff'); ?> -
-
- -
-
- -
-
- type == 'home') : ?> - -
- - -

-

- - - -

-
- type == 'file') : ?> -
-
- source->filename); ?> - source->coreFile)) : ?> -

- -
- -
- form->getInput('source'); ?> -
- - - form->getInput('extension_id'); ?> - form->getInput('filename'); ?> -
-
- source->coreFile)) : ?> - source->coreFile); ?> - source->filePath); ?> -
-

-
- form->getInput('core'); ?> -
-
-
-

-
-
-
-
-
-
- -
- type == 'archive') : ?> - -
- - - -
- type == 'image') : ?> - escape(basename($this->image['address'])); ?> - -
-
- - - - - - - - -
-
- type == 'font') : ?> -
-
-
-

H1. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

-

H2. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

-

H3. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

-

H4. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

-
H5. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML
-
H6. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML
-

Bold. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

-

Italics. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

-

Unordered List

-
    -
  • Item
  • -
  • Item
  • -
  • Item
    -
      -
    • Item
    • -
    • Item
    • -
    • Item
      -
        -
      • Item
      • -
      • Item
      • -
      • Item
      • -
      -
    • -
    -
  • -
-

Ordered List

-
    -
  1. Item
  2. -
  3. Item
  4. -
  5. Item
    -
      -
    • Item
    • -
    • Item
    • -
    • Item
      -
        -
      • Item
      • -
      • Item
      • -
      • Item
      • -
      -
    • -
    -
  6. -
- - -
-
-
- -
-
-
- - -
-
-
- -
    - - overridesList['modules'] as $module) : ?> -
  • - path - . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; - ?> - -  name; ?> - -
  • - -
-
-
-
-
- -
    - - overridesList['components'] as $key => $value) : ?> -
  • - -   - -
      - -
    • - path - . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; - ?> - -  name; ?> - -
    • - -
    -
  • - -
-
-
-
-
- -
    - - overridesList['plugins'] as $key => $group) : ?> -
  • - -   - -
      - -
    • - path - . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; - ?> - - name; ?> - -
    • - -
    -
  • - -
-
-
-
-
- -
    - - overridesList['layouts'] as $key => $value) : ?> -
  • - -   - -
      - -
    • - path - . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&' . $token . '&isMedia=' . $input->get('isMedia', 0); - ?> - -  name; ?> - -
    • - -
    -
  • - -
-
-
-
- + 'editor', 'recall' => true, 'breakpoint' => 768]); ?> + +
+
+ type == 'file') : ?> +

get('isMedia', 0) ? '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . str_replace('//', '/', base64_decode($this->file)) : '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . str_replace('//', '/', base64_decode($this->file))), $this->template->element); ?>

+ + + type == 'image') : ?> +

image['path'], $this->template->element); ?>

+ + + type == 'font') : ?> +

font['rel_path'], $this->template->element); ?>

+ + +
+ type == 'file' && !empty($this->source->coreFile)) : ?> +
+
+ form->renderField('show_core'); ?> + form->renderField('show_diff'); ?> +
+
+ +
+
+ +
+
+ type == 'home') : ?> + +
+ + +

+

+ + + +

+
+ type == 'file') : ?> +
+
+ source->filename); ?> + source->coreFile)) : ?> +

+ +
+ +
+ form->getInput('source'); ?> +
+ + + form->getInput('extension_id'); ?> + form->getInput('filename'); ?> +
+
+ source->coreFile)) : ?> + source->coreFile); ?> + source->filePath); ?> +
+

+
+ form->getInput('core'); ?> +
+
+
+

+
+
+
+
+
+
+ +
+ type == 'archive') : ?> + +
+ + + +
+ type == 'image') : ?> + escape(basename($this->image['address'])); ?> + +
+
+ + + + + + + + +
+
+ type == 'font') : ?> +
+
+
+

H1. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

+

H2. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

+

H3. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

+

H4. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

+
H5. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML
+
H6. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML
+

Bold. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

+

Italics. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

+

Unordered List

+
    +
  • Item
  • +
  • Item
  • +
  • Item
    +
      +
    • Item
    • +
    • Item
    • +
    • Item
      +
        +
      • Item
      • +
      • Item
      • +
      • Item
      • +
      +
    • +
    +
  • +
+

Ordered List

+
    +
  1. Item
  2. +
  3. Item
  4. +
  5. Item
    +
      +
    • Item
    • +
    • Item
    • +
    • Item
      +
        +
      • Item
      • +
      • Item
      • +
      • Item
      • +
      +
    • +
    +
  6. +
+ + +
+
+
+ +
+
+
+ + +
+
+
+ +
    + + overridesList['modules'] as $module) : ?> +
  • + path + . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; + ?> + +  name; ?> + +
  • + +
+
+
+
+
+ +
    + + overridesList['components'] as $key => $value) : ?> +
  • + +   + +
      + +
    • + path + . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; + ?> + +  name; ?> + +
    • + +
    +
  • + +
+
+
+
+
+ +
    + + overridesList['plugins'] as $key => $group) : ?> +
  • + +   + +
      + +
    • + path + . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; + ?> + + name; ?> + +
    • + +
    +
  • + +
+
+
+
+
+ +
    + + overridesList['layouts'] as $key => $value) : ?> +
  • + +   + +
      + +
    • + path + . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&' . $token . '&isMedia=' . $input->get('isMedia', 0); + ?> + +  name; ?> + +
    • + +
    +
  • + +
+
+
+
+ - -
-
- loadTemplate('description'); ?> -
-
- + pluginState) : ?> + + loadTemplate('updated_files'); ?> + + - pluginState) : ?> - - loadTemplate('updated_files'); ?> - - + +
+
+ loadTemplate('description'); ?> +
+
+ - + - template->xmldata->inheritable) && (string) $this->template->xmldata->inheritable === '1' ? 'child' : 'copy'; - $copyModalData = array( - 'selector' => $taskName . 'Modal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_TEMPLATE_' . strtoupper($taskName)), - 'footer' => $this->loadTemplate('modal_' . $taskName . '_footer') - ), - 'body' => $this->loadTemplate('modal_' . $taskName . '_body') - ); - ?> -
- - -
- type != 'home') : ?> - 'renameModal', - 'params' => array( - 'title' => Text::sprintf('COM_TEMPLATES_RENAME_FILE', str_replace('//', '/', $this->fileName)), - 'footer' => $this->loadTemplate('modal_rename_footer') - ), - 'body' => $this->loadTemplate('modal_rename_body') - ); - ?> -
- - -
- - type != 'home') : ?> - 'deleteModal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_ARE_YOU_SURE'), - 'footer' => $this->loadTemplate('modal_delete_footer') - ), - 'body' => $this->loadTemplate('modal_delete_body') - ); - ?> - - - 'fileModal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_NEW_FILE_HEADER'), - 'footer' => $this->loadTemplate('modal_file_footer'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - ), - 'body' => $this->loadTemplate('modal_file_body') - ); - ?> - - 'folderModal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_MANAGE_FOLDERS'), - 'footer' => $this->loadTemplate('modal_folder_footer'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - ), - 'body' => $this->loadTemplate('modal_folder_body') - ); - ?> - - type == 'image') : ?> - 'resizeModal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_RESIZE_IMAGE'), - 'footer' => $this->loadTemplate('modal_resize_footer') - ), - 'body' => $this->loadTemplate('modal_resize_body') - ); - ?> -
- - -
- + template->xmldata->inheritable) && (string) $this->template->xmldata->inheritable === '1' ? 'child' : 'copy'; + $copyModalData = array( + 'selector' => $taskName . 'Modal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_TEMPLATE_' . strtoupper($taskName)), + 'footer' => $this->loadTemplate('modal_' . $taskName . '_footer') + ), + 'body' => $this->loadTemplate('modal_' . $taskName . '_body') + ); + ?> +
+ + +
+ type != 'home') : ?> + 'renameModal', + 'params' => array( + 'title' => Text::sprintf('COM_TEMPLATES_RENAME_FILE', str_replace('//', '/', $this->fileName)), + 'footer' => $this->loadTemplate('modal_rename_footer') + ), + 'body' => $this->loadTemplate('modal_rename_body') + ); + ?> +
+ + +
+ + type != 'home') : ?> + 'deleteModal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_ARE_YOU_SURE'), + 'footer' => $this->loadTemplate('modal_delete_footer') + ), + 'body' => $this->loadTemplate('modal_delete_body') + ); + ?> + + + 'fileModal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_NEW_FILE_HEADER'), + 'footer' => $this->loadTemplate('modal_file_footer'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + ), + 'body' => $this->loadTemplate('modal_file_body') + ); + ?> + + 'folderModal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_MANAGE_FOLDERS'), + 'footer' => $this->loadTemplate('modal_folder_footer'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + ), + 'body' => $this->loadTemplate('modal_folder_body') + ); + ?> + + type == 'image') : ?> + 'resizeModal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_RESIZE_IMAGE'), + 'footer' => $this->loadTemplate('modal_resize_footer') + ), + 'body' => $this->loadTemplate('modal_resize_body') + ); + ?> +
+ + +
+
diff --git a/code/administrator/components/com_templates/tmpl/template/default_description.php b/code/administrator/components/com_templates/tmpl/template/default_description.php index 24119159..72afa2cb 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_description.php +++ b/code/administrator/components/com_templates/tmpl/template/default_description.php @@ -1,4 +1,5 @@
-
- template); ?> - template); ?> -
-

template->element); ?>

- template->client_id); ?> -

template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->path, $this->template->element); ?>

-

template->xmldata->get('description')); ?>

+
+ template); ?> + template); ?> +
+

template->element); ?>

+ template->client_id); ?> +

template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->path, $this->template->element); ?>

+

template->xmldata->get('description')); ?>

diff --git a/code/administrator/components/com_templates/tmpl/template/default_folders.php b/code/administrator/components/com_templates/tmpl/template/default_folders.php index 400efb3c..1675dab6 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_folders.php +++ b/code/administrator/components/com_templates/tmpl/template/default_folders.php @@ -1,4 +1,5 @@ diff --git a/code/administrator/components/com_templates/tmpl/template/default_media_folders.php b/code/administrator/components/com_templates/tmpl/template/default_media_folders.php index 38ea9c2d..f3491ab3 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_media_folders.php +++ b/code/administrator/components/com_templates/tmpl/template/default_media_folders.php @@ -1,4 +1,5 @@ mediaFiles)) -{ - return; +if (!count($this->mediaFiles)) { + return; } ksort($this->mediaFiles, SORT_STRING); ?> diff --git a/code/administrator/components/com_templates/tmpl/template/default_modal_child_body.php b/code/administrator/components/com_templates/tmpl/template/default_modal_child_body.php index 3b9b83e6..507b0767 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_modal_child_body.php +++ b/code/administrator/components/com_templates/tmpl/template/default_modal_child_body.php @@ -1,4 +1,5 @@ styles) > 0) -{ - foreach ($this->styles as $style) - { - $options[] = HTMLHelper::_('select.option', $style->id, $style->title, 'value', 'text'); - } +if (count($this->styles) > 0) { + foreach ($this->styles as $style) { + $options[] = HTMLHelper::_('select.option', $style->id, $style->title, 'value', 'text'); + } } $fancySelectData = [ - 'autocomplete' => 'off', - 'autofocus' => false, - 'class' => '', - 'description' => '', - 'disabled' => false, - 'group' => false, - 'id' => 'style_ids', - 'hidden' => false, - 'hint' => '', - 'label' => '', - 'labelclass' => '', - 'onchange' => '', - 'onclick' => '', - 'multiple' => true, - 'pattern' => '', - 'readonly' => false, - 'repeat' => false, - 'required' => false, - 'size' => 4, - 'spellcheck' => false, - 'validate' => '', - 'value' => '0', - 'options' => $options, - 'dataAttributes' => [], - 'dataAttribute' => '', - 'name' => 'style_ids[]', + 'autocomplete' => 'off', + 'autofocus' => false, + 'class' => '', + 'description' => '', + 'disabled' => false, + 'group' => false, + 'id' => 'style_ids', + 'hidden' => false, + 'hint' => '', + 'label' => '', + 'labelclass' => '', + 'onchange' => '', + 'onclick' => '', + 'multiple' => true, + 'pattern' => '', + 'readonly' => false, + 'repeat' => false, + 'required' => false, + 'size' => 4, + 'spellcheck' => false, + 'validate' => '', + 'value' => '0', + 'options' => $options, + 'dataAttributes' => [], + 'dataAttribute' => '', + 'name' => 'style_ids[]', ]; ?>
-
-
-
-
- -
-
- - - - -
-
-
-
- -
-
- - - - -
-
-
-
+
+
+
+
+ +
+
+ + + + +
+
+
+
+ +
+
+ + + + +
+
+
+
diff --git a/code/administrator/components/com_templates/tmpl/template/default_modal_child_footer.php b/code/administrator/components/com_templates/tmpl/template/default_modal_child_footer.php index 8f0188e2..f1c4dfb6 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_modal_child_footer.php +++ b/code/administrator/components/com_templates/tmpl/template/default_modal_child_footer.php @@ -1,4 +1,5 @@
-
-
-
-
- -
-
- - - - -
-
-
-
+
+
+
+
+ +
+
+ + + + +
+
+
+
diff --git a/code/administrator/components/com_templates/tmpl/template/default_modal_copy_footer.php b/code/administrator/components/com_templates/tmpl/template/default_modal_copy_footer.php index 9eeeff74..17d0f3f1 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_modal_copy_footer.php +++ b/code/administrator/components/com_templates/tmpl/template/default_modal_copy_footer.php @@ -1,4 +1,5 @@
-
-
-

fileName)); ?>

-
-
+
+
+

fileName)); ?>

+
+
diff --git a/code/administrator/components/com_templates/tmpl/template/default_modal_delete_footer.php b/code/administrator/components/com_templates/tmpl/template/default_modal_delete_footer.php index 89a1b12f..9895aadd 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_modal_delete_footer.php +++ b/code/administrator/components/com_templates/tmpl/template/default_modal_delete_footer.php @@ -1,4 +1,5 @@ input; ?>
- - - - - - - + + + + + + +
diff --git a/code/administrator/components/com_templates/tmpl/template/default_modal_file_body.php b/code/administrator/components/com_templates/tmpl/template/default_modal_file_body.php index d3dbea84..926cd1fe 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_modal_file_body.php +++ b/code/administrator/components/com_templates/tmpl/template/default_modal_file_body.php @@ -19,87 +19,87 @@ $input = Factory::getApplication()->input; ?>
-
-
- -
-
-
- - -
-
- - -
- - - - -
-
-
- - -
- - - -
- state->get('params')->get('upload_limit'); ?> - - -
- type != 'home') : ?> -
-
-
- - - - - -
- -
- -
-
-
+
+
+ +
+
+
+ + +
+
+ + +
+ + + + +
+
+
+ + +
+ + + +
+ state->get('params')->get('upload_limit'); ?> + + +
+ type != 'home') : ?> +
+
+
+ + + + + +
+ +
+ +
+
+
diff --git a/code/administrator/components/com_templates/tmpl/template/default_modal_file_footer.php b/code/administrator/components/com_templates/tmpl/template/default_modal_file_footer.php index 25a77c25..2a58ea61 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_modal_file_footer.php +++ b/code/administrator/components/com_templates/tmpl/template/default_modal_file_footer.php @@ -1,4 +1,5 @@ input; ?>
-
-
- -
-
-
- - - - - -
- -
-
-
-
+
+
+ +
+
+
+ + + + + +
+ +
+
+
+
diff --git a/code/administrator/components/com_templates/tmpl/template/default_modal_folder_footer.php b/code/administrator/components/com_templates/tmpl/template/default_modal_folder_footer.php index 3f0c071c..1fbf670f 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_modal_folder_footer.php +++ b/code/administrator/components/com_templates/tmpl/template/default_modal_folder_footer.php @@ -1,4 +1,5 @@ input; ?>
-
- - - - - -
+
+ + + + + +
diff --git a/code/administrator/components/com_templates/tmpl/template/default_modal_rename_body.php b/code/administrator/components/com_templates/tmpl/template/default_modal_rename_body.php index d954ebb6..531e6d7f 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_modal_rename_body.php +++ b/code/administrator/components/com_templates/tmpl/template/default_modal_rename_body.php @@ -1,4 +1,5 @@
-
-
-
-
- -
-
-
- - .fileName); ?> -
-
-
-
-
+
+
+
+
+ +
+
+
+ + .fileName); ?> +
+
+
+
+
diff --git a/code/administrator/components/com_templates/tmpl/template/default_modal_rename_footer.php b/code/administrator/components/com_templates/tmpl/template/default_modal_rename_footer.php index fd6c82fb..d0bd1b96 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_modal_rename_footer.php +++ b/code/administrator/components/com_templates/tmpl/template/default_modal_rename_footer.php @@ -1,4 +1,5 @@
-
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
diff --git a/code/administrator/components/com_templates/tmpl/template/default_modal_resize_footer.php b/code/administrator/components/com_templates/tmpl/template/default_modal_resize_footer.php index 204efeea..9b4601b2 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_modal_resize_footer.php +++ b/code/administrator/components/com_templates/tmpl/template/default_modal_resize_footer.php @@ -1,4 +1,5 @@ diff --git a/code/administrator/components/com_templates/tmpl/template/default_tree_media.php b/code/administrator/components/com_templates/tmpl/template/default_tree_media.php index c3211429..32c014bd 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_tree_media.php +++ b/code/administrator/components/com_templates/tmpl/template/default_tree_media.php @@ -1,4 +1,5 @@ mediaFiles)) -{ - return; +if (!count($this->mediaFiles)) { + return; } ksort($this->mediaFiles, SORT_STRING); ?>
    - mediaFiles as $key => $value) : ?> - - fileName); - $count = 0; + mediaFiles as $key => $value) : ?> + + fileName); + $count = 0; - $keyArrayCount = count($keyArray); + $keyArrayCount = count($keyArray); - if (count($fileArray) >= $keyArrayCount) - { - for ($i = 0; $i < $keyArrayCount; $i++) - { - if ($keyArray[$i] === $fileArray[$i]) - { - $count++; - } - } + if (count($fileArray) >= $keyArrayCount) { + for ($i = 0; $i < $keyArrayCount; $i++) { + if ($keyArray[$i] === $fileArray[$i]) { + $count++; + } + } - if ($count === $keyArrayCount) - { - $class = 'folder show'; - } - else - { - $class = 'folder'; - } - } - else - { - $class = 'folder'; - } + if ($count === $keyArrayCount) { + $class = 'folder show'; + } else { + $class = 'folder'; + } + } else { + $class = 'folder'; + } - ?> -
  • - -  escape(end($explodeArray)); ?> - - mediaTree($value); ?> -
  • - - -
  • - -  escape($value->name); ?> - -
  • - - + ?> +
  • + +  escape(end($explodeArray)); ?> + + mediaTree($value); ?> +
  • + + +
  • + +  escape($value->name); ?> + +
  • + +
diff --git a/code/administrator/components/com_templates/tmpl/template/default_updated_files.php b/code/administrator/components/com_templates/tmpl/template/default_updated_files.php index 8131e5ae..54d5faab 100644 --- a/code/administrator/components/com_templates/tmpl/template/default_updated_files.php +++ b/code/administrator/components/com_templates/tmpl/template/default_updated_files.php @@ -1,4 +1,5 @@ input; ?> -
-
-
- updatedList) !== 0) : ?> - - - - - - - - - - - - - updatedList as $i => $value) : ?> - - - - - - - - - - -
- - - - - - - - - - - -
- hash_id, false, 'cid', 'cb', '', 'updateForm'); ?> - - state, $i, 'template.', 1, 'cb', null, null, 'updateForm'); ?> - - hash_id); ?> - - created_date; ?> - 0 ? HTMLHelper::_('date', $created_date, Text::_('DATE_FORMAT_FILTER_DATETIME')) : '-'; ?> - - modified_date)) : ?> - - - modified_date, Text::_('DATE_FORMAT_FILTER_DATETIME')); ?> - - - action; ?> -
- - - - -
- - -
- -
-
-
+updatedList) === 0) : ?> +
+ + +
+ +
+ + +
+
+
+
+ + + + + + + + + + + + + updatedList as $i => $value) : ?> + + + + + + + + + + +
+ + + + + + + + + + + +
+ hash_id, false, 'cid', 'cb', '', 'updateForm'); ?> + + state, $i, 'template.', 1, 'cb', null, null, 'updateForm'); ?> + + hash_id); ?> + + created_date; ?> + 0 ? HTMLHelper::_('date', $created_date, Text::_('DATE_FORMAT_FILTER_DATETIME')) : '-'; ?> + + modified_date)) : ?> + + + modified_date, Text::_('DATE_FORMAT_FILTER_DATETIME')); ?> + + + action; ?> +
+ + + +
+
+
+ diff --git a/code/administrator/components/com_templates/tmpl/template/readonly.php b/code/administrator/components/com_templates/tmpl/template/readonly.php index 157cff97..0d9f62bc 100644 --- a/code/administrator/components/com_templates/tmpl/template/readonly.php +++ b/code/administrator/components/com_templates/tmpl/template/readonly.php @@ -1,4 +1,5 @@ input; ?>
- 'description', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
- loadTemplate('description'); ?> -
-
- - - - + 'description', 'recall' => true, 'breakpoint' => 768]); ?> + +
+
+ loadTemplate('description'); ?> +
+
+ + + +
diff --git a/code/administrator/components/com_templates/tmpl/templates/default.php b/code/administrator/components/com_templates/tmpl/templates/default.php index 9e0515b5..6754eebe 100644 --- a/code/administrator/components/com_templates/tmpl/templates/default.php +++ b/code/administrator/components/com_templates/tmpl/templates/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -25,117 +27,119 @@ ?>
-
-
-
- $this, 'options' => array('selectorFieldName' => 'client_id'))); ?> - total > 0) : ?> - - - - - - - - - - pluginState) : ?> - - - - - - items as $i => $item) : ?> - - - - - - - pluginState) : ?> - - - - - -
- , - , - -
- - - - - - - - - - - -
- - - - - name)); ?> -
- preview) : ?> - client_id === 1 ? 'administrator' : 'site'; ?> - - - - - - - -
- xmldata->inheritable) && $item->xmldata->inheritable) : ?> -
- - -
- - xmldata->parent) && (string) $item->xmldata->parent !== '') : ?> -
- - xmldata->parent); ?> -
- -
- escape($item->xmldata->get('version')); ?> - - escape($item->xmldata->get('creationDate')); ?> - - xmldata->get('author')) : ?> -
escape($author); ?>
- - — - - xmldata->get('authorEmail')) : ?> -
escape($email); ?>
- - xmldata->get('authorUrl')) : ?> - - -
- updated)) : ?> - updated); ?> - - - -
+
+
+
+ $this, 'options' => array('selectorFieldName' => 'client_id'))); ?> + total > 0) : ?> + + + + + + + + + + pluginState) : ?> + + + + + + items as $i => $item) : ?> + + + + + + + pluginState) : ?> + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+ + + + + name)); ?> +
+ preview) : ?> + client_id === 1 ? 'administrator' : 'site'; ?> + + + + + + + +
+ xmldata->inheritable) && $item->xmldata->inheritable) : ?> +
+ + +
+ + xmldata->parent) && (string) $item->xmldata->parent !== '') : ?> +
+ + xmldata->parent); ?> +
+ +
+ escape($item->xmldata->get('version')); ?> + + escape($item->xmldata->get('creationDate')); ?> + + xmldata->get('author')) : ?> +
escape($author); ?>
+ + — + + xmldata->get('authorEmail')) : ?> +
escape($email); ?>
+ + xmldata->get('authorUrl')) : ?> + + +
+ updated)) : ?> + + updated); ?> + + + + +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
-
-
+ + + +
+
+
diff --git a/code/administrator/components/com_users/config.xml b/code/administrator/components/com_users/config.xml index f765da4e..4b3314b7 100644 --- a/code/administrator/components/com_users/config.xml +++ b/code/administrator/components/com_users/config.xml @@ -110,33 +110,6 @@
- - - - - - - - - -
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/code/administrator/components/com_users/forms/filter_users.xml b/code/administrator/components/com_users/forms/filter_users.xml index 104fa32b..4bd61774 100644 --- a/code/administrator/components/com_users/forms/filter_users.xml +++ b/code/administrator/components/com_users/forms/filter_users.xml @@ -17,6 +17,16 @@ > + + + + + + > + + - diff --git a/code/administrator/components/com_users/helpers/debug.php b/code/administrator/components/com_users/helpers/debug.php index d1dc18a4..e32e2bd0 100644 --- a/code/administrator/components/com_users/helpers/debug.php +++ b/code/administrator/components/com_users/helpers/debug.php @@ -1,16 +1,21 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\Component\Users\Administrator\Helper\DebugHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Users component debugging helper. * diff --git a/code/administrator/components/com_users/helpers/users.php b/code/administrator/components/com_users/helpers/users.php index f0d556c7..fc9ccccb 100644 --- a/code/administrator/components/com_users/helpers/users.php +++ b/code/administrator/components/com_users/helpers/users.php @@ -1,13 +1,18 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Users component helper. diff --git a/code/administrator/components/com_users/postinstall/multifactorauth.php b/code/administrator/components/com_users/postinstall/multifactorauth.php new file mode 100644 index 00000000..f7bc9fac --- /dev/null +++ b/code/administrator/components/com_users/postinstall/multifactorauth.php @@ -0,0 +1,58 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\Database\DatabaseDriver; +use Joomla\Database\ParameterType; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Post-installation message about the new Multi-factor Authentication: condition check. + * + * Returns true if neither of the two new core MFA plugins are enabled. + * + * @return boolean + * @since 4.2.0 + */ +function com_users_postinstall_mfa_condition(): bool +{ + return count(PluginHelper::getPlugin('multifactorauth')) < 1; +} + +/** + * Post-installation message about the new Multi-factor Authentication: action. + * + * Enables the core MFA plugins. + * + * @return void + * @since 4.2.0 + */ +function com_users_postinstall_mfa_action(): void +{ + /** @var DatabaseDriver $db */ + $db = Factory::getContainer()->get('DatabaseDriver'); + $coreMfaPlugins = ['email', 'totp', 'webauthn', 'yubikey']; + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('multifactorauth')) + ->whereIn($db->quoteName('element'), $coreMfaPlugins, ParameterType::STRING); + $db->setQuery($query); + $db->execute(); + + $url = 'index.php?option=com_plugins&filter[folder]=multifactorauth'; + Factory::getApplication()->redirect($url); +} diff --git a/code/administrator/components/com_users/services/provider.php b/code/administrator/components/com_users/services/provider.php index b2efe403..d444883b 100644 --- a/code/administrator/components/com_users/services/provider.php +++ b/code/administrator/components/com_users/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Users')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Users')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Users')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Users')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Users')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Users')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new UsersComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new UsersComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_users/src/Controller/CallbackController.php b/code/administrator/components/com_users/src/Controller/CallbackController.php new file mode 100644 index 00000000..703b2231 --- /dev/null +++ b/code/administrator/components/com_users/src/Controller/CallbackController.php @@ -0,0 +1,82 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\Controller; + +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Event\MultiFactor\Callback; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\Input\Input; +use RuntimeException; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Multi-factor Authentication plugins' AJAX callback controller + * + * @since 4.2.0 + */ +class CallbackController extends BaseController +{ + /** + * Public constructor + * + * @param array $config Plugin configuration + * @param MVCFactoryInterface|null $factory MVC Factory for the com_users component + * @param CMSApplication|null $app CMS application object + * @param Input|null $input Joomla CMS input object + * + * @since 4.2.0 + */ + public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerDefaultTask('callback'); + } + + /** + * Implement a callback feature, typically used for OAuth2 authentication + * + * @param bool $cachable Can this view be cached + * @param array|bool $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link JFilterInput::clean()}. + * + * @return void + * @since 4.2.0 + */ + public function callback($cachable = false, $urlparams = false): void + { + $app = $this->app; + + // Get the Method and make sure it's non-empty + $method = $this->input->getCmd('method', ''); + + if (empty($method)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + PluginHelper::importPlugin('multifactorauth'); + + $event = new Callback($method); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + /** + * The first plugin to handle the request should either redirect or close the application. If we are still here + * no plugin handled the request successfully. Show an error. + */ + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } +} diff --git a/code/administrator/components/com_users/src/Controller/CaptiveController.php b/code/administrator/components/com_users/src/Controller/CaptiveController.php new file mode 100644 index 00000000..6b708d23 --- /dev/null +++ b/code/administrator/components/com_users/src/Controller/CaptiveController.php @@ -0,0 +1,233 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\Controller; + +use Exception; +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Date\Date; +use Joomla\CMS\Event\MultiFactor\NotifyActionLog; +use Joomla\CMS\Event\MultiFactor\Validate; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Uri\Uri; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Users\Administrator\Model\BackupcodesModel; +use Joomla\Component\Users\Administrator\Model\CaptiveModel; +use Joomla\Input\Input; +use RuntimeException; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Captive Multi-factor Authentication page controller + * + * @since 4.2.0 + */ +class CaptiveController extends BaseController +{ + /** + * Public constructor + * + * @param array $config Plugin configuration + * @param MVCFactoryInterface|null $factory MVC Factory for the com_users component + * @param CMSApplication|null $app CMS application object + * @param Input|null $input Joomla CMS input object + * + * @since 4.2.0 + */ + public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('captive', 'display'); + } + + /** + * Displays the captive login page + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function display($cachable = false, $urlparams = false): void + { + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + // Only allow logged in Users + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Get the view object + $viewLayout = $this->input->get('layout', 'default', 'string'); + $view = $this->getView( + 'Captive', + 'html', + '', + [ + 'base_path' => $this->basePath, + 'layout' => $viewLayout, + ] + ); + + $view->document = $this->app->getDocument(); + + // If we're already logged in go to the site's home page + if ((int) $this->app->getSession()->get('com_users.mfa_checked', 0) === 1) { + $url = Route::_('index.php?option=com_users&task=methods.display', false); + + $this->setRedirect($url); + } + + // Pass the model to the view + /** @var CaptiveModel $model */ + $model = $this->getModel('Captive'); + $view->setModel($model, true); + + /** @var BackupcodesModel $codesModel */ + $codesModel = $this->getModel('Backupcodes'); + $view->setModel($codesModel, false); + + try { + // Suppress all modules on the page except those explicitly allowed + $model->suppressAllModules(); + } catch (Exception $e) { + // If we can't kill the modules we can still survive. + } + + // Pass the MFA record ID to the model + $recordId = $this->input->getInt('record_id', null); + $model->setState('record_id', $recordId); + + // Do not go through $this->display() because it overrides the model. + $view->display(); + } + + /** + * Validate the MFA code entered by the user + * + * @param bool $cachable Ignored. This page is never cached. + * @param array $urlparameters Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function validate($cachable = false, $urlparameters = []) + { + // CSRF Check + $this->checkToken($this->input->getMethod()); + + // Get the MFA parameters from the request + $recordId = $this->input->getInt('record_id', null); + $code = $this->input->get('code', null, 'raw'); + /** @var CaptiveModel $model */ + $model = $this->getModel('Captive'); + + // Validate the MFA record + $model->setState('record_id', $recordId); + $record = $model->getRecord(); + + if (empty($record)) { + $event = new NotifyActionLog('onComUsersCaptiveValidateInvalidMethod'); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + throw new RuntimeException(Text::_('COM_USERS_MFA_INVALID_METHOD'), 500); + } + + // Validate the code + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + $event = new Validate($record, $user, $code); + $results = $this->app + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + $isValidCode = false; + + if ($record->method === 'backupcodes') { + /** @var BackupcodesModel $codesModel */ + $codesModel = $this->getModel('Backupcodes'); + $results = [$codesModel->isBackupCode($code, $user)]; + /** + * This is required! Do not remove! + * + * There is a store() call below. It saves the in-memory MFA record to the database. That includes the + * options key which contains the configuration of the Method. For backup codes, these are the actual codes + * you can use. When we check for a backup code validity we also "burn" it, i.e. we remove it from the + * options table and save that to the database. However, this DOES NOT update the $record here. Therefore + * the call to saveRecord() would overwrite the database contents with a record that _includes_ the backup + * code we had just burned. As a result the single use backup codes end up being multiple use. + * + * By doing a getRecord() here, right after we have "burned" any correct backup codes, we resolve this + * issue. The loaded record will reflect the database contents where the options DO NOT include the code we + * just used. Therefore the call to store() will result in the correct database state, i.e. the used backup + * code being removed. + */ + $record = $model->getRecord(); + } + + $isValidCode = array_reduce( + $results, + function (bool $carry, $result) { + return $carry || boolval($result); + }, + false + ); + + if (!$isValidCode) { + // The code is wrong. Display an error and go back. + $captiveURL = Route::_('index.php?option=com_users&view=captive&record_id=' . $recordId, false); + $message = Text::_('COM_USERS_MFA_INVALID_CODE'); + $this->setRedirect($captiveURL, $message, 'error'); + + $event = new NotifyActionLog('onComUsersCaptiveValidateFailed', [$record->title]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + return; + } + + // Update the Last Used, UA and IP columns + $jNow = Date::getInstance(); + + $record->last_used = $jNow->toSql(); + $record->store(); + + // Flag the user as fully logged in + $session = $this->app->getSession(); + $session->set('com_users.mfa_checked', 1); + $session->set('com_users.mandatory_mfa_setup', 0); + + // Get the return URL stored by the plugin in the session + $returnUrl = $session->get('com_users.return_url', ''); + + // If the return URL is not set or not internal to this site redirect to the site's front page + if (empty($returnUrl) || !Uri::isInternal($returnUrl)) { + $returnUrl = Uri::base(); + } + + $this->setRedirect($returnUrl); + + $event = new NotifyActionLog('onComUsersCaptiveValidateSuccess', [$record->title]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + } +} diff --git a/code/administrator/components/com_users/src/Controller/DisplayController.php b/code/administrator/components/com_users/src/Controller/DisplayController.php index 03bd449a..bdfde0e4 100644 --- a/code/administrator/components/com_users/src/Controller/DisplayController.php +++ b/code/administrator/components/com_users/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ get('core.admin'); - - // Default permissions. - default: - return true; - } - } - - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, - * for valid values see {@link \Joomla\CMS\Filter\InputFilter::clean()}. - * - * @return BaseController|boolean This object to support chaining or false on failure. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = array()) - { - $view = $this->input->get('view', 'users'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); - - if (!$this->canView($view)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Check for edit form. - if ($view == 'user' && $layout == 'edit' && !$this->checkEditId('com_users.edit.user', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_users&view=users', false)); - - return false; - } - elseif ($view == 'group' && $layout == 'edit' && !$this->checkEditId('com_users.edit.group', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_users&view=groups', false)); - - return false; - } - elseif ($view == 'level' && $layout == 'edit' && !$this->checkEditId('com_users.edit.level', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_users&view=levels', false)); - - return false; - } - elseif ($view == 'note' && $layout == 'edit' && !$this->checkEditId('com_users.edit.note', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_users&view=notes', false)); - - return false; - } - - return parent::display($cachable, $urlparams); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'users'; + + /** + * Checks whether a user can see this view. + * + * @param string $view The view name. + * + * @return boolean + * + * @since 1.6 + */ + protected function canView($view) + { + $canDo = ContentHelper::getActions('com_users'); + + switch ($view) { + // Special permissions. + case 'groups': + case 'group': + case 'levels': + case 'level': + return $canDo->get('core.admin'); + + // Default permissions. + default: + return true; + } + } + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, + * for valid values see {@link \Joomla\CMS\Filter\InputFilter::clean()}. + * + * @return BaseController|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'users'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); + + if (!$this->canView($view)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Check for edit form. + if ($view == 'user' && $layout == 'edit' && !$this->checkEditId('com_users.edit.user', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_users&view=users', false)); + + return false; + } elseif ($view == 'group' && $layout == 'edit' && !$this->checkEditId('com_users.edit.group', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_users&view=groups', false)); + + return false; + } elseif ($view == 'level' && $layout == 'edit' && !$this->checkEditId('com_users.edit.level', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_users&view=levels', false)); + + return false; + } elseif ($view == 'note' && $layout == 'edit' && !$this->checkEditId('com_users.edit.note', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_users&view=notes', false)); + + return false; + } elseif (in_array($view, ['captive', 'callback', 'methods', 'method'])) { + $controller = $this->factory->createController($view, 'Administrator', [], $this->app, $this->input); + $task = $this->input->get('task', ''); + + return $controller->execute($task); + } + + return parent::display($cachable, $urlparams); + } } diff --git a/code/administrator/components/com_users/src/Controller/GroupController.php b/code/administrator/components/com_users/src/Controller/GroupController.php index d2d55aaf..97a80d27 100644 --- a/code/administrator/components/com_users/src/Controller/GroupController.php +++ b/code/administrator/components/com_users/src/Controller/GroupController.php @@ -1,4 +1,5 @@ app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key)); - } + /** + * Method to check if you can save a new or existing record. + * + * Overrides Joomla\CMS\MVC\Controller\FormController::allowSave to check the core.admin permission. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowSave($data, $key = 'id') + { + return ($this->app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key)); + } - /** - * Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit - * - * Checks that non-Super Admins are not editing Super Admins. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Check if this group is a Super Admin - if (Access::checkGroup($data[$key], 'core.admin')) - { - // If I'm not a Super Admin, then disallow the edit. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - return false; - } - } + /** + * Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit + * + * Checks that non-Super Admins are not editing Super Admins. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Check if this group is a Super Admin + if (Access::checkGroup($data[$key], 'core.admin')) { + // If I'm not a Super Admin, then disallow the edit. + if (!$this->app->getIdentity()->authorise('core.admin')) { + return false; + } + } - return parent::allowEdit($data, $key); - } + return parent::allowEdit($data, $key); + } } diff --git a/code/administrator/components/com_users/src/Controller/GroupsController.php b/code/administrator/components/com_users/src/Controller/GroupsController.php index 9b71d887..18eb3da5 100644 --- a/code/administrator/components/com_users/src/Controller/GroupsController.php +++ b/code/administrator/components/com_users/src/Controller/GroupsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Users\Administrator\Controller; use Joomla\CMS\Access\Exception\NotAllowed; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\AdminController; +// phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * User groups list controller class. @@ -21,120 +25,115 @@ */ class GroupsController extends AdminController { - /** - * @var string The prefix to use with controller messages. - * @since 1.6 - */ - protected $text_prefix = 'COM_USERS_GROUPS'; - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.6 - */ - public function getModel($name = 'Group', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Removes an item. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::delete to check the core.admin permission. - * - * @return void - * - * @since 1.6 - */ - public function delete() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - parent::delete(); - } - - /** - * Method to publish a list of records. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::publish to check the core.admin permission. - * - * @return void - * - * @since 1.6 - */ - public function publish() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - parent::publish(); - } - - /** - * Changes the order of one or more records. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::reorder to check the core.admin permission. - * - * @return boolean True on success - * - * @since 1.6 - */ - public function reorder() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - return parent::reorder(); - } - - /** - * Method to save the submitted ordering values for records. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::saveorder to check the core.admin permission. - * - * @return boolean True on success - * - * @since 1.6 - */ - public function saveorder() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - return parent::saveorder(); - } - - /** - * Check in of one or more records. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::checkin to check the core.admin permission. - * - * @return boolean True on success - * - * @since 1.6 - */ - public function checkin() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - return parent::checkin(); - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_GROUPS'; + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Group', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Removes an item. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::delete to check the core.admin permission. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + parent::delete(); + } + + /** + * Method to publish a list of records. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::publish to check the core.admin permission. + * + * @return void + * + * @since 1.6 + */ + public function publish() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + parent::publish(); + } + + /** + * Changes the order of one or more records. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::reorder to check the core.admin permission. + * + * @return boolean True on success + * + * @since 1.6 + */ + public function reorder() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + return parent::reorder(); + } + + /** + * Method to save the submitted ordering values for records. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::saveorder to check the core.admin permission. + * + * @return boolean True on success + * + * @since 1.6 + */ + public function saveorder() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + return parent::saveorder(); + } + + /** + * Check in of one or more records. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::checkin to check the core.admin permission. + * + * @return boolean True on success + * + * @since 1.6 + */ + public function checkin() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + return parent::checkin(); + } } diff --git a/code/administrator/components/com_users/src/Controller/LevelController.php b/code/administrator/components/com_users/src/Controller/LevelController.php index 439b21c1..502f3c35 100644 --- a/code/administrator/components/com_users/src/Controller/LevelController.php +++ b/code/administrator/components/com_users/src/Controller/LevelController.php @@ -1,4 +1,5 @@ app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key)); - } - - /** - * Overrides JControllerForm::allowEdit - * - * Checks that non-Super Admins are not editing Super Admins. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.8.8 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Check for if Super Admin can edit - $data['id'] = (int) $data['id']; - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__viewlevels')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $data['id'], ParameterType::INTEGER); - $db->setQuery($query); - - $viewlevel = $db->loadAssoc(); - - // Decode level groups - $groups = json_decode($viewlevel['rules']); - - // If this group is super admin and this user is not super admin, canEdit is false - if (!$this->app->getIdentity()->authorise('core.admin') && Access::checkGroup($groups[0], 'core.admin')) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false - ) - ); - - return false; - } - - return parent::allowEdit($data, $key); - } - - /** - * Removes an item. - * - * Overrides Joomla\CMS\MVC\Controller\FormController::delete to check the core.admin permission. - * - * @return void - * - * @since 1.6 - */ - public function delete() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - elseif (empty($ids)) - { - $this->setMessage(Text::_('COM_USERS_NO_LEVELS_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Remove the items. - if ($model->delete($ids)) - { - $this->setMessage(Text::plural('COM_USERS_N_LEVELS_DELETED', count($ids))); - } - } - - $this->setRedirect('index.php?option=com_users&view=levels'); - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_LEVEL'; + + /** + * Method to check if you can save a new or existing record. + * + * Overrides Joomla\CMS\MVC\Controller\FormController::allowSave to check the core.admin permission. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowSave($data, $key = 'id') + { + return ($this->app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key)); + } + + /** + * Overrides JControllerForm::allowEdit + * + * Checks that non-Super Admins are not editing Super Admins. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.8.8 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Check for if Super Admin can edit + $viewLevel = $this->getModel('Level', 'Administrator')->getItem((int) $data['id']); + + // If this group is super admin and this user is not super admin, canEdit is false + if (!$this->app->getIdentity()->authorise('core.admin') && $viewLevel->rules && Access::checkGroup($viewLevel->rules[0], 'core.admin')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + + return false; + } + + return parent::allowEdit($data, $key); + } + + /** + * Removes an item. + * + * Overrides Joomla\CMS\MVC\Controller\FormController::delete to check the core.admin permission. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } elseif (empty($ids)) { + $this->setMessage(Text::_('COM_USERS_NO_LEVELS_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel(); + + // Remove the items. + if ($model->delete($ids)) { + $this->setMessage(Text::plural('COM_USERS_N_LEVELS_DELETED', count($ids))); + } + } + + $this->setRedirect('index.php?option=com_users&view=levels'); + } } diff --git a/code/administrator/components/com_users/src/Controller/LevelsController.php b/code/administrator/components/com_users/src/Controller/LevelsController.php index d4ffb800..c9ad7c36 100644 --- a/code/administrator/components/com_users/src/Controller/LevelsController.php +++ b/code/administrator/components/com_users/src/Controller/LevelsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Users\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Users\Administrator\Controller; use Joomla\CMS\MVC\Controller\AdminController; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * User view levels list controller class. * @@ -19,25 +23,25 @@ */ class LevelsController extends AdminController { - /** - * @var string The prefix to use with controller messages. - * @since 1.6 - */ - protected $text_prefix = 'COM_USERS_LEVELS'; + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_LEVELS'; - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 1.6 - */ - public function getModel($name = 'Level', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Level', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_users/src/Controller/MailController.php b/code/administrator/components/com_users/src/Controller/MailController.php index efd0d595..0d94f6a2 100644 --- a/code/administrator/components/com_users/src/Controller/MailController.php +++ b/code/administrator/components/com_users/src/Controller/MailController.php @@ -1,4 +1,5 @@ app->get('massmailoff', 0) == 1) - { - $this->app->redirect(Route::_('index.php', false)); - } + /** + * Send the mail + * + * @return void + * + * @since 1.6 + */ + public function send() + { + // Redirect to admin index if mass mailer disabled in conf + if ($this->app->get('massmailoff', 0) == 1) { + $this->app->redirect(Route::_('index.php', false)); + } - // Check for request forgeries. - $this->checkToken('request'); + // Check for request forgeries. + $this->checkToken('request'); - $model = $this->getModel('Mail'); + $model = $this->getModel('Mail'); - if ($model->send()) - { - $type = 'message'; - } - else - { - $type = 'error'; - } + if ($model->send()) { + $type = 'message'; + } else { + $type = 'error'; + } - $msg = $model->getError(); - $this->setRedirect('index.php?option=com_users&view=mail', $msg, $type); - } + $msg = $model->getError(); + $this->setRedirect('index.php?option=com_users&view=mail', $msg, $type); + } - /** - * Cancel the mail - * - * @return void - * - * @since 1.6 - */ - public function cancel() - { - // Check for request forgeries. - $this->checkToken('request'); + /** + * Cancel the mail + * + * @return void + * + * @since 1.6 + */ + public function cancel() + { + // Check for request forgeries. + $this->checkToken('request'); - // Clear data from session. - $this->app->setUserState('com_users.display.mail.data', null); + // Clear data from session. + $this->app->setUserState('com_users.display.mail.data', null); - $this->setRedirect('index.php?option=com_users&view=users'); - } + $this->setRedirect('index.php?option=com_users&view=users'); + } } diff --git a/code/administrator/components/com_users/src/Controller/MethodController.php b/code/administrator/components/com_users/src/Controller/MethodController.php new file mode 100644 index 00000000..e2fae399 --- /dev/null +++ b/code/administrator/components/com_users/src/Controller/MethodController.php @@ -0,0 +1,487 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\Controller; + +use Exception; +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Event\MultiFactor\NotifyActionLog; +use Joomla\CMS\Event\MultiFactor\SaveSetup; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController as BaseControllerAlias; +use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\CMS\Router\Route; +use Joomla\CMS\User\User; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; +use Joomla\Component\Users\Administrator\Model\BackupcodesModel; +use Joomla\Component\Users\Administrator\Model\MethodModel; +use Joomla\Component\Users\Administrator\Table\MfaTable; +use Joomla\Input\Input; +use RuntimeException; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Multi-factor Authentication method controller + * + * @since 4.2.0 + */ +class MethodController extends BaseControllerAlias +{ + /** + * Public constructor + * + * @param array $config Plugin configuration + * @param MVCFactoryInterface|null $factory MVC Factory for the com_users component + * @param CMSApplication|null $app CMS application object + * @param Input|null $input Joomla CMS input object + * + * @since 4.2.0 + */ + public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) + { + // We have to tell Joomla what is the name of the view, otherwise it defaults to the name of the *component*. + $config['default_view'] = 'method'; + $config['default_task'] = 'add'; + + parent::__construct($config, $factory, $app, $input); + } + + /** + * Execute a task by triggering a Method in the derived class. + * + * @param string $task The task to perform. If no matching task is found, the '__default' task is executed, if + * defined. + * + * @return mixed The value returned by the called Method. + * + * @throws Exception + * @since 4.2.0 + */ + public function execute($task) + { + if (empty($task) || $task === 'display') { + $task = 'add'; + } + + return parent::execute($task); + } + + /** + * Add a new MFA Method + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function add($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + + $this->assertCanEdit($user); + + // Also make sure the Method really does exist + $method = $this->input->getCmd('method'); + $this->assertMethodExists($method); + + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + $model->setState('method', $method); + + // Pass the return URL to the view + $returnURL = $this->input->getBase64('returnurl'); + $viewLayout = $this->input->get('layout', 'default', 'string'); + $view = $this->getView('Method', 'html'); + $view->setLayout($viewLayout); + $view->returnURL = $returnURL; + $view->user = $user; + $view->document = $this->app->getDocument(); + + $view->setModel($model, true); + + $event = new NotifyActionLog('onComUsersControllerMethodBeforeAdd', [$user, $method]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + $view->display(); + } + + /** + * Edit an existing MFA Method + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function edit($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + + $this->assertCanEdit($user); + + // Also make sure the Method really does exist + $id = $this->input->getInt('id'); + $record = $this->assertValidRecordId($id, $user); + + if ($id <= 0) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + $model->setState('id', $id); + + // Pass the return URL to the view + $returnURL = $this->input->getBase64('returnurl'); + $viewLayout = $this->input->get('layout', 'default', 'string'); + $view = $this->getView('Method', 'html'); + $view->setLayout($viewLayout); + $view->returnURL = $returnURL; + $view->user = $user; + $view->document = $this->app->getDocument(); + + $view->setModel($model, true); + + $event = new NotifyActionLog('onComUsersControllerMethodBeforeEdit', [$id, $user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + $view->display(); + } + + /** + * Regenerate backup codes + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function regenerateBackupCodes($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $this->assertCanEdit($user); + + /** @var BackupcodesModel $model */ + $model = $this->getModel('Backupcodes'); + $model->regenerateBackupCodes($user); + + $backupCodesRecord = $model->getBackupCodesRecord($user); + + // Redirect + $redirectUrl = 'index.php?option=com_users&task=method.edit&user_id=' . $userId . '&id=' . $backupCodesRecord->id; + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $redirectUrl .= '&returnurl=' . $returnURL; + } + + $this->setRedirect(Route::_($redirectUrl, false)); + + $event = new NotifyActionLog('onComUsersControllerMethodAfterRegenerateBackupCodes'); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + } + + /** + * Delete an existing MFA Method + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @since 4.2.0 + */ + public function delete($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $this->assertCanDelete($user); + + // Also make sure the Method really does exist + $id = $this->input->getInt('id'); + $record = $this->assertValidRecordId($id, $user); + + if ($id <= 0) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $type = null; + $message = null; + + $event = new NotifyActionLog('onComUsersControllerMethodBeforeDelete', [$id, $user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + try { + $record->delete(); + } catch (Exception $e) { + $message = $e->getMessage(); + $type = 'error'; + } + + // Redirect + $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $url = base64_decode($returnURL); + } + + $this->setRedirect($url, $message, $type); + } + + /** + * Save the MFA Method + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @since 4.2.0 + */ + public function save($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $this->assertCanEdit($user); + + // Redirect + $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $url = base64_decode($returnURL); + } + + // The record must either be new (ID zero) or exist + $id = $this->input->getInt('id', 0); + $record = $this->assertValidRecordId($id, $user); + + // If it's a new record we need to read the Method from the request and update the (not yet created) record. + if ($record->id == 0) { + $methodName = $this->input->getCmd('method'); + $this->assertMethodExists($methodName); + $record->method = $methodName; + } + + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + + // Ask the plugin to validate the input by calling onUserMultifactorSaveSetup + $result = []; + $input = $this->app->input; + + $event = new NotifyActionLog('onComUsersControllerMethodBeforeSave', [$id, $user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + try { + $event = new SaveSetup($record, $input); + $pluginResults = $this->app + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + foreach ($pluginResults as $pluginResult) { + $result = array_merge($result, $pluginResult); + } + } catch (RuntimeException $e) { + // Go back to the edit page + $nonSefUrl = 'index.php?option=com_users&task=method.'; + + if ($id) { + $nonSefUrl .= 'edit&id=' . (int) $id; + } else { + $nonSefUrl .= 'add&method=' . $record->method; + } + + $nonSefUrl .= '&user_id=' . $userId; + + if (!empty($returnURL)) { + $nonSefUrl .= '&returnurl=' . urlencode($returnURL); + } + + $url = Route::_($nonSefUrl, false); + $this->setRedirect($url, $e->getMessage(), 'error'); + + return; + } + + // Update the record's options with the plugin response + $title = $this->input->getString('title', null); + $title = trim($title); + + if (empty($title)) { + $method = $model->getMethod($record->method); + $title = $method['display']; + } + + // Update the record's "default" flag + $default = $this->input->getBool('default', false); + $record->title = $title; + $record->options = $result; + $record->default = $default ? 1 : 0; + + // Ask the model to save the record + $saved = $record->store(); + + if (!$saved) { + // Go back to the edit page + $nonSefUrl = 'index.php?option=com_users&task=method.'; + + if ($id) { + $nonSefUrl .= 'edit&id=' . (int) $id; + } else { + $nonSefUrl .= 'add'; + } + + $nonSefUrl .= '&user_id=' . $userId; + + if (!empty($returnURL)) { + $nonSefUrl .= '&returnurl=' . urlencode($returnURL); + } + + $url = Route::_($nonSefUrl, false); + $this->setRedirect($url, $record->getError(), 'error'); + + return; + } + + $this->setRedirect($url); + } + + /** + * Assert that the provided ID is a valid record identified for the given user + * + * @param int $id Record ID to check + * @param User|null $user User record. Null to use current user. + * + * @return MfaTable The loaded record + * @since 4.2.0 + */ + private function assertValidRecordId($id, ?User $user = null): MfaTable + { + if (is_null($user)) { + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + + $model->setState('id', $id); + + $record = $model->getRecord($user); + + if (is_null($record) || ($record->id != $id) || ($record->user_id != $user->id)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + return $record; + } + + /** + * Assert that the user can add / edit MFA methods. + * + * @param User|null $user User record. Null to use current user. + * + * @return void + * @throws RuntimeException|Exception + * @since 4.2.0 + */ + private function assertCanEdit(?User $user = null): void + { + if (!MfaHelper::canAddEditMethod($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + /** + * Assert that the user can delete MFA records / disable MFA. + * + * @param User|null $user User record. Null to use current user. + * + * @return void + * @throws RuntimeException|Exception + * @since 4.2.0 + */ + private function assertCanDelete(?User $user = null): void + { + if (!MfaHelper::canDeleteMethod($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + /** + * Assert that the specified MFA Method exists, is activated and enabled for the current user + * + * @param string|null $method The Method to check + * + * @return void + * @since 4.2.0 + */ + private function assertMethodExists(?string $method): void + { + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + + if (empty($method) || !$model->methodExists($method)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + /** + * Assert that there is a logged in user. + * + * @return void + * @since 4.2.0 + */ + private function assertLoggedInUser(): void + { + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } +} diff --git a/code/administrator/components/com_users/src/Controller/MethodsController.php b/code/administrator/components/com_users/src/Controller/MethodsController.php new file mode 100644 index 00000000..014c2aa4 --- /dev/null +++ b/code/administrator/components/com_users/src/Controller/MethodsController.php @@ -0,0 +1,213 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\Controller; + +use Exception; +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Event\MultiFactor\NotifyActionLog; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Uri\Uri; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; +use Joomla\Component\Users\Administrator\Model\MethodsModel; +use Joomla\Input\Input; +use RuntimeException; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Multi-factor Authentication methods selection and management controller + * + * @since 4.2.0 + */ +class MethodsController extends BaseController +{ + /** + * Public constructor + * + * @param array $config Plugin configuration + * @param MVCFactoryInterface|null $factory MVC Factory for the com_users component + * @param CMSApplication|null $app CMS application object + * @param Input|null $input Joomla CMS input object + * + * @since 4.2.0 + */ + public function __construct($config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) + { + // We have to tell Joomla what is the name of the view, otherwise it defaults to the name of the *component*. + $config['default_view'] = 'Methods'; + + parent::__construct($config, $factory, $app, $input); + } + + /** + * Disable Multi-factor Authentication for the current user + * + * @param bool $cachable Can this view be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link JFilterInput::clean()}. + * + * @return void + * @since 4.2.0 + */ + public function disable($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = ($userId === null) + ? $this->app->getIdentity() + : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if (!MfaHelper::canDeleteMethod($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Delete all MFA Methods for the user + /** @var MethodsModel $model */ + $model = $this->getModel('Methods'); + $type = null; + $message = null; + + $event = new NotifyActionLog('onComUsersControllerMethodsBeforeDisable', [$user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + try { + $model->deleteAll($user); + } catch (Exception $e) { + $message = $e->getMessage(); + $type = 'error'; + } + + // Redirect + $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $url = base64_decode($returnURL); + } + + $this->setRedirect($url, $message, $type); + } + + /** + * List all available Multi-factor Authentication Methods available and guide the user to setting them up + * + * @param bool $cachable Can this view be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link JFilterInput::clean()}. + * + * @return void + * @since 4.2.0 + */ + public function display($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = ($userId === null) + ? $this->app->getIdentity() + : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if (!MfaHelper::canShowConfigurationInterface($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $returnURL = $this->input->getBase64('returnurl'); + $viewLayout = $this->input->get('layout', 'default', 'string'); + $view = $this->getView('Methods', 'html'); + $view->setLayout($viewLayout); + $view->returnURL = $returnURL; + $view->user = $user; + $view->document = $this->app->getDocument(); + + $methodsModel = $this->getModel('Methods'); + $view->setModel($methodsModel, true); + + $backupCodesModel = $this->getModel('Backupcodes'); + $view->setModel($backupCodesModel, false); + + $view->display(); + } + + /** + * Disable Multi-factor Authentication for the current user + * + * @param bool $cachable Can this view be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link JFilterInput::clean()}. + * + * @return void + * @since 4.2.0 + */ + public function doNotShowThisAgain($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = ($userId === null) + ? $this->app->getIdentity() + : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if (!MfaHelper::canAddEditMethod($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $event = new NotifyActionLog('onComUsersControllerMethodsBeforeDoNotShowThisAgain', [$user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + /** @var MethodsModel $model */ + $model = $this->getModel('Methods'); + $model->setFlag($user, true); + + // Redirect + $url = Uri::base(); + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $url = base64_decode($returnURL); + } + + $this->setRedirect($url); + } + + /** + * Assert that there is a user currently logged in + * + * @return void + * @since 4.2.0 + */ + private function assertLoggedInUser(): void + { + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } +} diff --git a/code/administrator/components/com_users/src/Controller/NoteController.php b/code/administrator/components/com_users/src/Controller/NoteController.php index 34b5ccc3..31728104 100644 --- a/code/administrator/components/com_users/src/Controller/NoteController.php +++ b/code/administrator/components/com_users/src/Controller/NoteController.php @@ -1,4 +1,5 @@ input->get('u_id', 0, 'int'); - - if ($userId) - { - $append .= '&u_id=' . $userId; - } - - return $append; - } + use VersionableControllerTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 2.5 + */ + protected $text_prefix = 'COM_USERS_NOTE'; + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $key The name of the primary key variable. + * + * @return string The arguments to append to the redirect URL. + * + * @since 2.5 + */ + protected function getRedirectToItemAppend($recordId = null, $key = 'id') + { + $append = parent::getRedirectToItemAppend($recordId, $key); + + $userId = $this->input->get('u_id', 0, 'int'); + + if ($userId) { + $append .= '&u_id=' . $userId; + } + + return $append; + } } diff --git a/code/administrator/components/com_users/src/Controller/NotesController.php b/code/administrator/components/com_users/src/Controller/NotesController.php index cb49b923..a053e6a1 100644 --- a/code/administrator/components/com_users/src/Controller/NotesController.php +++ b/code/administrator/components/com_users/src/Controller/NotesController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 2.5 + */ + public function getModel($name = 'Note', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/code/administrator/components/com_users/src/Controller/UserController.php b/code/administrator/components/com_users/src/Controller/UserController.php index 5694b46d..c4a884dd 100644 --- a/code/administrator/components/com_users/src/Controller/UserController.php +++ b/code/administrator/components/com_users/src/Controller/UserController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Users\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Users\Administrator\Controller; use Joomla\CMS\Access\Access; use Joomla\CMS\MVC\Controller\FormController; @@ -16,6 +16,10 @@ use Joomla\CMS\Router\Route; use Joomla\CMS\Uri\Uri; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * User controller class. * @@ -23,139 +27,132 @@ */ class UserController extends FormController { - /** - * @var string The prefix to use with controller messages. - * @since 1.6 - */ - protected $text_prefix = 'COM_USERS_USER'; - - /** - * Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit - * - * Checks that non-Super Admins are not editing Super Admins. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean True if allowed, false otherwise. - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Check if this person is a Super Admin - if (Access::check($data[$key], 'core.admin')) - { - // If I'm not a Super Admin, then disallow the edit. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - return false; - } - } - - // Allow users to edit their own account - if (isset($data[$key]) && (int) $this->app->getIdentity()->id === (int) $data[$key]) - { - return true; - } - - return parent::allowEdit($data, $key); - } - - /** - * Override parent cancel to redirect when using status edit account. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 4.0.0 - */ - public function cancel($key = null) - { - $result = parent::cancel(); - - if ($return = $this->input->get('return', '', 'BASE64')) - { - $return = base64_decode($return); - - // Don't redirect to an external URL. - if (!Uri::isInternal($return)) - { - $return = Uri::base(); - } - - $this->app->redirect($return); - } - - return $result; - } - - /** - * Override parent save to redirect when using status edit account. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 4.0.0 - */ - public function save($key = null, $urlVar = null) - { - $result = parent::save($key, $urlVar); - - $task = $this->getTask(); - - if ($task === 'save' && $return = $this->input->get('return', '', 'BASE64')) - { - $return = base64_decode($return); - - // Don't redirect to an external URL. - if (!Uri::isInternal($return)) - { - $return = Uri::base(); - } - - $this->setRedirect($return); - } - - return $result; - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True on success, false on failure - * - * @since 2.5 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('User', 'Administrator', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_users&view=users' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.1 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_USER'; + + /** + * Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit + * + * Checks that non-Super Admins are not editing Super Admins. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean True if allowed, false otherwise. + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Check if this person is a Super Admin + if (Access::check($data[$key], 'core.admin')) { + // If I'm not a Super Admin, then disallow the edit. + if (!$this->app->getIdentity()->authorise('core.admin')) { + return false; + } + } + + // Allow users to edit their own account + if (isset($data[$key]) && (int) $this->app->getIdentity()->id === (int) $data[$key]) { + return true; + } + + return parent::allowEdit($data, $key); + } + + /** + * Override parent cancel to redirect when using status edit account. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 4.0.0 + */ + public function cancel($key = null) + { + $result = parent::cancel(); + + if ($return = $this->input->get('return', '', 'BASE64')) { + $return = base64_decode($return); + + // Don't redirect to an external URL. + if (!Uri::isInternal($return)) { + $return = Uri::base(); + } + + $this->app->redirect($return); + } + + return $result; + } + + /** + * Override parent save to redirect when using status edit account. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 4.0.0 + */ + public function save($key = null, $urlVar = null) + { + $result = parent::save($key, $urlVar); + + $task = $this->getTask(); + + if ($task === 'save' && $return = $this->input->get('return', '', 'BASE64')) { + $return = base64_decode($return); + + // Don't redirect to an external URL. + if (!Uri::isInternal($return)) { + $return = Uri::base(); + } + + $this->setRedirect($return); + } + + return $result; + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True on success, false on failure + * + * @since 2.5 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('User', 'Administrator', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_users&view=users' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.1 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + } } diff --git a/code/administrator/components/com_users/src/Controller/UsersController.php b/code/administrator/components/com_users/src/Controller/UsersController.php index f5d9f0b0..54b2b743 100644 --- a/code/administrator/components/com_users/src/Controller/UsersController.php +++ b/code/administrator/components/com_users/src/Controller/UsersController.php @@ -1,4 +1,5 @@ registerTask('block', 'changeBlock'); - $this->registerTask('unblock', 'changeBlock'); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.6 - */ - public function getModel($name = 'User', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to change the block status on a record. - * - * @return void - * - * @since 1.6 - */ - public function changeBlock() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('block' => 1, 'unblock' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Change the state of the records. - if (!$model->block($ids, $value)) - { - $this->setMessage($model->getError(), 'error'); - } - else - { - if ($value == 1) - { - $this->setMessage(Text::plural('COM_USERS_N_USERS_BLOCKED', count($ids))); - } - elseif ($value == 0) - { - $this->setMessage(Text::plural('COM_USERS_N_USERS_UNBLOCKED', count($ids))); - } - } - } - - $this->setRedirect('index.php?option=com_users&view=users'); - } - - /** - * Method to activate a record. - * - * @return void - * - * @since 1.6 - */ - public function activate() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'error'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Change the state of the records. - if (!$model->activate($ids)) - { - $this->setMessage($model->getError(), 'error'); - } - else - { - $this->setMessage(Text::plural('COM_USERS_N_USERS_ACTIVATED', count($ids))); - } - } - - $this->setRedirect('index.php?option=com_users&view=users'); - } - - /** - * Method to get the number of active users - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Users'); - - $model->setState('filter.state', 0); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_USERS_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_USERS_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_USERS'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The CMSApplication for the dispatcher + * @param Input $input Input + * + * @since 1.6 + * @see BaseController + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('block', 'changeBlock'); + $this->registerTask('unblock', 'changeBlock'); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'User', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to change the block status on a record. + * + * @return void + * + * @since 1.6 + */ + public function changeBlock() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('block' => 1, 'unblock' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel(); + + // Change the state of the records. + if (!$model->block($ids, $value)) { + $this->setMessage($model->getError(), 'error'); + } else { + if ($value == 1) { + $this->setMessage(Text::plural('COM_USERS_N_USERS_BLOCKED', count($ids))); + } elseif ($value == 0) { + $this->setMessage(Text::plural('COM_USERS_N_USERS_UNBLOCKED', count($ids))); + } + } + } + + $this->setRedirect('index.php?option=com_users&view=users'); + } + + /** + * Method to activate a record. + * + * @return void + * + * @since 1.6 + */ + public function activate() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'error'); + } else { + // Get the model. + $model = $this->getModel(); + + // Change the state of the records. + if (!$model->activate($ids)) { + $this->setMessage($model->getError(), 'error'); + } else { + $this->setMessage(Text::plural('COM_USERS_N_USERS_ACTIVATED', count($ids))); + } + } + + $this->setRedirect('index.php?option=com_users&view=users'); + } + + /** + * Method to get the number of active users + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Users'); + + $model->setState('filter.state', 0); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_USERS_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_USERS_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } } diff --git a/code/administrator/components/com_users/src/DataShape/CaptiveRenderOptions.php b/code/administrator/components/com_users/src/DataShape/CaptiveRenderOptions.php new file mode 100644 index 00000000..3665363c --- /dev/null +++ b/code/administrator/components/com_users/src/DataShape/CaptiveRenderOptions.php @@ -0,0 +1,200 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\DataShape; + +use InvalidArgumentException; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * @property string $pre_message Custom HTML to display above the MFA form + * @property string $field_type How to render the MFA code field. "input" or "custom". + * @property string $input_type The type attribute for the HTML input box. Typically "text" or "password". + * @property string $placeholder Placeholder text for the HTML input box. Leave empty if you don't need it. + * @property string $label Label to show above the HTML input box. Leave empty if you don't need it. + * @property string $html Custom HTML. Only used when field_type = custom. + * @property string $post_message Custom HTML to display below the MFA form + * @property bool $hide_submit Should I hide the default Submit button? + * @property bool $allowEntryBatching Is this method validating against all configured authenticators of this type? + * @property string $help_url URL for help content + * + * @since 4.2.0 + */ +class CaptiveRenderOptions extends DataShapeObject +{ + /** + * Display a standard HTML5 input field. Use the input_type, placeholder and label properties to set it up. + * + * @since 4.2.0 + */ + public const FIELD_INPUT = 'input'; + + /** + * Display a custom HTML document. Use the html property to set it up. + * + * @since 4.2.0 + */ + public const FIELD_CUSTOM = 'custom'; + + /** + * Custom HTML to display above the MFA form + * + * @var string + * @since 4.2.0 + */ + protected $pre_message = ''; + + /** + * How to render the MFA code field. "input" (HTML input element) or "custom" (custom HTML) + * + * @var string + * @since 4.2.0 + */ + protected $field_type = 'input'; + + /** + * The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type. + * + * @var string + * @since 4.2.0 + */ + protected $input_type = ''; + + /** + * Attributes other than type and id which will be added to the HTML input box. + * + * @var array + * @@since 4.2.0 + */ + protected $input_attributes = []; + + /** + * Placeholder text for the HTML input box. Leave empty if you don't need it. + * + * @var string + * @since 4.2.0 + */ + protected $placeholder = ''; + + /** + * Label to show above the HTML input box. Leave empty if you don't need it. + * + * @var string + * @since 4.2.0 + */ + protected $label = ''; + + /** + * Custom HTML. Only used when field_type = custom. + * + * @var string + * @since 4.2.0 + */ + protected $html = ''; + + /** + * Custom HTML to display below the MFA form + * + * @var string + * @since 4.2.0 + */ + protected $post_message = ''; + + /** + * Should I hide the default Submit button? + * + * @var boolean + * @since 4.2.0 + */ + protected $hide_submit = false; + + /** + * Additional CSS classes for the submit button (apply the MFA setup) + * + * @var string + * @since 4.2.0 + */ + protected $submit_class = ''; + + /** + * Icon class to use for the submit button + * + * @var string + * @since 4.2.0 + */ + protected $submit_icon = 'icon icon-rightarrow icon-arrow-right'; + + /** + * Language key to use for the text on the submit button + * + * @var string + * @since 4.2.0 + */ + protected $submit_text = 'COM_USERS_MFA_VALIDATE'; + + /** + * Is this MFA method validating against all configured authenticators of the same type? + * + * @var boolean + * @since 4.2.0 + */ + protected $allowEntryBatching = true; + + /** + * URL for help content + * + * @var string + * @since 4.2.0 + */ + protected $help_url = ''; + + /** + * Setter for the field_type property + * + * @param string $value One of self::FIELD_INPUT, self::FIELD_CUSTOM + * + * @since 4.2.0 + * @throws InvalidArgumentException + */ + // phpcs:ignore + protected function setField_type(string $value) + { + if (!in_array($value, [self::FIELD_INPUT, self::FIELD_CUSTOM])) { + throw new InvalidArgumentException('Invalid value for property field_type.'); + } + + $this->field_type = $value; + } + + /** + * Setter for the input_attributes property. + * + * @param array $value The value to set + * + * @return void + * @@since 4.2.0 + */ + // phpcs:ignore + protected function setInput_attributes(array $value) + { + $forbiddenAttributes = ['id', 'type', 'name', 'value']; + + foreach ($forbiddenAttributes as $key) { + if (isset($value[$key])) { + unset($value[$key]); + } + } + + $this->input_attributes = $value; + } +} diff --git a/code/administrator/components/com_users/src/DataShape/DataShapeObject.php b/code/administrator/components/com_users/src/DataShape/DataShapeObject.php new file mode 100644 index 00000000..908e36ce --- /dev/null +++ b/code/administrator/components/com_users/src/DataShape/DataShapeObject.php @@ -0,0 +1,198 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\DataShape; + +use InvalidArgumentException; + +/** + * Generic helper for handling data shapes in com_users + * + * @since 4.2.0 + */ +abstract class DataShapeObject implements \ArrayAccess +{ + /** + * Public constructor + * + * @param array $array The data to initialise this object with + * + * @since 4.2.0 + */ + public function __construct(array $array = []) + { + if (!is_array($array) && !($array instanceof self)) { + throw new InvalidArgumentException(sprintf('%s needs an array or a %s object', __METHOD__, __CLASS__)); + } + + foreach (($array instanceof self) ? $array->asArray() : $array as $k => $v) { + $this[$k] = $v; + } + } + + /** + * Get the data shape as a key-value array + * + * @return array + * + * @since 4.2.0 + */ + public function asArray(): array + { + return get_object_vars($this); + } + + /** + * Merge another data shape object or key-value array into this object. + * + * @param array|self $newValues The object or array to merge into self. + * + * @return $this + * + * @since 4.2.0 + */ + public function merge($newValues): self + { + if (!is_array($newValues) && !($newValues instanceof self)) { + throw new InvalidArgumentException(sprintf('%s needs an array or a %s object', __METHOD__, __CLASS__)); + } + + foreach (($newValues instanceof self) ? $newValues->asArray() : $newValues as $k => $v) { + if (!isset($this->{$k})) { + continue; + } + + $this[$k] = $v; + } + + return $this; + } + + /** + * Magic getter + * + * @param string $name The name of the property to retrieve + * + * @return mixed + * + * @since 4.2.0 + */ + public function __get($name) + { + $methodName = 'get' . ucfirst($name); + + if (method_exists($this, $methodName)) { + return $this->{$methodName}; + } + + if (property_exists($this, $name)) { + return $this->{$name}; + } + + throw new InvalidArgumentException(sprintf('Property %s not found in %s', $name, __CLASS__)); + } + + /** + * Magic Setter + * + * @param string $name The property to set the value for + * @param mixed $value The property value to set it to + * + * @return mixed + * @since 4.2.0 + */ + public function __set($name, $value) + { + $methodName = 'set' . ucfirst($name); + + if (method_exists($this, $methodName)) { + return $this->{$methodName}($value); + } + + if (property_exists($this, $name)) { + $this->{$name} = $value; + } + + throw new InvalidArgumentException(sprintf('Property %s not found in %s', $name, __CLASS__)); + } + + /** + * Is a property set? + * + * @param string $name Property name + * + * @return boolean Does it exist in the object? + * @since 4.2.0 + */ + #[\ReturnTypeWillChange] + public function __isset($name) + { + $methodName = 'get' . ucfirst($name); + + return method_exists($this, $methodName) || property_exists($this, $name); + } + + /** + * Does the property exist (array access)? + * + * @param string $offset Property name + * + * @return boolean + * @since 4.2.0 + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + return isset($this->{$offset}); + } + + /** + * Get the value of a property (array access). + * + * @param string $offset Property name + * + * @return mixed + * @since 4.2.0 + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->{$offset}; + } + + /** + * Set the value of a property (array access). + * + * @param string $offset Property name + * @param mixed $value Property value + * + * @return void + * @since 4.2.0 + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + $this->{$offset} = $value; + } + + /** + * Unset a property (array access). + * + * @param string $offset Property name + * + * @return mixed + * @since 4.2.0 + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + throw new \LogicException(sprintf('You cannot unset members of %s', __CLASS__)); + } +} diff --git a/code/administrator/components/com_users/src/DataShape/MethodDescriptor.php b/code/administrator/components/com_users/src/DataShape/MethodDescriptor.php new file mode 100644 index 00000000..a45a1ec2 --- /dev/null +++ b/code/administrator/components/com_users/src/DataShape/MethodDescriptor.php @@ -0,0 +1,120 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\DataShape; + +use Joomla\Component\Users\Administrator\Table\MfaTable; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * @property string $name Internal code of this MFA Method + * @property string $display User-facing name for this MFA Method + * @property string $shortinfo Short description of this MFA Method displayed to the user + * @property string $image URL to the logo image for this Method + * @property bool $canDisable Are we allowed to disable it? + * @property bool $allowMultiple Are we allowed to have multiple instances of it per user? + * @property string $help_url URL for help content + * @property bool $allowEntryBatching Allow authentication against all entries of this MFA Method. + * + * @since 4.2.0 + */ +class MethodDescriptor extends DataShapeObject +{ + /** + * Internal code of this MFA Method + * + * @var string + * @since 4.2.0 + */ + protected $name = ''; + + /** + * User-facing name for this MFA Method + * + * @var string + * @since 4.2.0 + */ + protected $display = ''; + + /** + * Short description of this MFA Method displayed to the user + * + * @var string + * @since 4.2.0 + */ + protected $shortinfo = ''; + + /** + * URL to the logo image for this Method + * + * @var string + * @since 4.2.0 + */ + protected $image = ''; + + /** + * Are we allowed to disable it? + * + * @var boolean + * @since 4.2.0 + */ + protected $canDisable = true; + + /** + * Are we allowed to have multiple instances of it per user? + * + * @var boolean + * @since 4.2.0 + */ + protected $allowMultiple = false; + + /** + * URL for help content + * + * @var string + * @since 4.2.0 + */ + protected $help_url = ''; + + /** + * Allow authentication against all entries of this MFA Method. + * + * Otherwise authentication takes place against a SPECIFIC entry at a time. + * + * @var boolean + * @since 4.2.0 + */ + protected $allowEntryBatching = false; + + /** + * Active authentication methods, used internally only + * + * @var MfaTable[] + * @since 4.2.0 + * @internal + */ + protected $active = []; + + /** + * Adds an active MFA method + * + * @param MfaTable $record The MFA method record to add + * + * @return void + * @since 4.2.0 + */ + public function addActiveMethod(MfaTable $record) + { + $this->active[$record->id] = $record; + } +} diff --git a/code/administrator/components/com_users/src/DataShape/SetupRenderOptions.php b/code/administrator/components/com_users/src/DataShape/SetupRenderOptions.php new file mode 100644 index 00000000..446e828c --- /dev/null +++ b/code/administrator/components/com_users/src/DataShape/SetupRenderOptions.php @@ -0,0 +1,243 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\DataShape; + +use InvalidArgumentException; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Data shape for Method Setup Render Options + * + * @property string $default_title Default title if you are setting up this MFA Method for the first time + * @property string $pre_message Custom HTML to display above the MFA setup form + * @property string $table_heading Heading for displayed tabular data. Typically used to display a list of fixed MFA + * codes, TOTP setup parameters etc + * @property array $tabular_data Any tabular data to display (label => custom HTML). See above + * @property array $hidden_data Hidden fields to include in the form (name => value) + * @property string $field_type How to render the MFA setup code field. "input" (HTML input element) or "custom" + * (custom HTML) + * @property string $input_type The type attribute for the HTML input box. Typically "text" or "password". Use any + * HTML5 input type. + * @property string $input_value Pre-filled value for the HTML input box. Typically used for fixed codes, the fixed + * YubiKey ID etc. + * @property string $placeholder Placeholder text for the HTML input box. Leave empty if you don't need it. + * @property string $label Label to show above the HTML input box. Leave empty if you don't need it. + * @property string $html Custom HTML. Only used when field_type = custom. + * @property bool $show_submit Should I show the submit button (apply the MFA setup)? + * @property string $submit_class Additional CSS classes for the submit button (apply the MFA setup) + * @property string $post_message Custom HTML to display below the MFA setup form + * @property string $help_url A URL with help content for this Method to display to the user + * + * @since 4.2.0 + */ +class SetupRenderOptions extends DataShapeObject +{ + /** + * Display a standard HTML5 input field. Use the input_type, placeholder and label properties to set it up. + * + * @since 4.2.0 + */ + public const FIELD_INPUT = 'input'; + + /** + * Display a custom HTML document. Use the html property to set it up. + * + * @since 4.2.0 + */ + public const FIELD_CUSTOM = 'custom'; + + /** + * Default title if you are setting up this MFA Method for the first time + * + * @var string + * @since 4.2.0 + */ + protected $default_title = ''; + + /** + * Custom HTML to display above the MFA setup form parameters etc + * + * @var string + * @since 4.2.0 + */ + protected $pre_message = ''; + + /** + * Heading for displayed tabular data. Typically used to display a list of fixed MFA codes, TOTP setup + * + * @var string + * @since 4.2.0 + */ + protected $table_heading = ''; + + /** + * Any tabular data to display (label => custom HTML). See above + * + * @var array + * @since 4.2.0 + */ + protected $tabular_data = []; + + /** + * Hidden fields to include in the form (name => value) + * + * @var array + * @since 4.2.0 + */ + protected $hidden_data = []; + + /** + * How to render the MFA setup code field. "input" (HTML input element) or "custom" (custom HTML) + * + * @var string + * @since 4.2.0 + */ + protected $field_type = 'input'; + + /** + * The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type. + * + * @var string + * @since 4.2.0 + */ + protected $input_type = 'text'; + + /** + * Attributes other than type and id which will be added to the HTML input box. + * + * @var array + * @@since 4.2.0 + */ + protected $input_attributes = []; + + /** + * Pre-filled value for the HTML input box. Typically used for fixed codes, the fixed YubiKey ID etc. + * + * @var string + * @since 4.2.0 + */ + protected $input_value = ''; + + /** + * Placeholder text for the HTML input box. Leave empty if you don't need it. + * + * @var string + * @since 4.2.0 + */ + protected $placeholder = ''; + + /** + * Label to show above the HTML input box. Leave empty if you don't need it. + * + * @var string + * @since 4.2.0 + */ + protected $label = ''; + + /** + * Custom HTML. Only used when field_type = custom. + * + * @var string + * @since 4.2.0 + */ + protected $html = ''; + + /** + * Should I show the submit button (apply the MFA setup)? + * + * @var boolean + * @since 4.2.0 + */ + protected $show_submit = true; + + /** + * Additional CSS classes for the submit button (apply the MFA setup) + * + * @var string + * @since 4.2.0 + */ + protected $submit_class = ''; + + /** + * Icon class to use for the submit button + * + * @var string + * @since 4.2.0 + */ + protected $submit_icon = 'icon icon-ok'; + + /** + * Language key to use for the text on the submit button + * + * @var string + * @since 4.2.0 + */ + protected $submit_text = 'JSAVE'; + + /** + * Custom HTML to display below the MFA setup form + * + * @var string + * @since 4.2.0 + */ + protected $post_message = ''; + + /** + * A URL with help content for this Method to display to the user + * + * @var string + * @since 4.2.0 + */ + protected $help_url = ''; + + /** + * Setter for the field_type property + * + * @param string $value One of self::FIELD_INPUT, self::FIELD_CUSTOM + * + * @since 4.2.0 + * @throws InvalidArgumentException + */ + // phpcs:ignore + protected function setField_type($value) + { + if (!in_array($value, [self::FIELD_INPUT, self::FIELD_CUSTOM])) { + throw new InvalidArgumentException('Invalid value for property field_type.'); + } + + $this->field_type = $value; + } + + /** + * Setter for the input_attributes property. + * + * @param array $value The value to set + * + * @return void + * @since 4.2.0 + */ + // phpcs:ignore + protected function setInput_attributes(array $value) + { + $forbiddenAttributes = ['id', 'type', 'name', 'value']; + + foreach ($forbiddenAttributes as $key) { + if (isset($value[$key])) { + unset($value[$key]); + } + } + + $this->input_attributes = $value; + } +} diff --git a/code/administrator/components/com_users/src/Dispatcher/Dispatcher.php b/code/administrator/components/com_users/src/Dispatcher/Dispatcher.php index 25352ba4..7f7ca530 100644 --- a/code/administrator/components/com_users/src/Dispatcher/Dispatcher.php +++ b/code/administrator/components/com_users/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->getCmd('task'); - $view = $this->input->getCmd('view'); - $layout = $this->input->getCmd('layout'); - $allowedTasks = ['user.edit', 'user.apply', 'user.save', 'user.cancel']; - - // Allow users to edit their own account - if (in_array($task, $allowedTasks, true) || ($view === 'user' && $layout === 'edit')) - { - $user = $this->app->getIdentity(); - $id = $this->input->getInt('id'); - - if ((int) $user->id === $id) - { - return; - } - } - - parent::checkAccess(); - } + /** + * Override checkAccess to allow users edit profile without having to have core.manager permission + * + * @return void + * + * @since 4.0.0 + */ + protected function checkAccess() + { + $task = $this->input->getCmd('task'); + $view = $this->input->getCmd('view'); + $layout = $this->input->getCmd('layout'); + $allowedTasks = ['user.edit', 'user.apply', 'user.save', 'user.cancel']; + + // Allow users to edit their own account + if (in_array($task, $allowedTasks, true) || ($view === 'user' && $layout === 'edit')) { + $user = $this->app->getIdentity(); + $id = $this->input->getInt('id'); + + if ((int) $user->id === $id) { + return; + } + } + + /** + * Special case: Multi-factor Authentication + * + * We allow access to all MFA views and tasks. Access control for MFA tasks is performed in + * the Controllers since what is allowed depends on who is logged in and whose account you + * are trying to modify. Implementing these checks in the Dispatcher would violate the + * separation of concerns. + */ + $allowedViews = ['callback', 'captive', 'method', 'methods']; + $isAllowedTask = array_reduce( + $allowedViews, + function ($carry, $taskPrefix) use ($task) { + return $carry || strpos($task ?? '', $taskPrefix . '.') === 0; + }, + false + ); + + if (in_array(strtolower($view ?? ''), $allowedViews) || $isAllowedTask) { + return; + } + + parent::checkAccess(); + } } diff --git a/code/administrator/components/com_users/src/Extension/UsersComponent.php b/code/administrator/components/com_users/src/Extension/UsersComponent.php index 0b9dcd89..cb6c18a0 100644 --- a/code/administrator/components/com_users/src/Extension/UsersComponent.php +++ b/code/administrator/components/com_users/src/Extension/UsersComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('users', new Users); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('users', new Users()); + } - /** - * Returns a valid section for the given section. If it is not valid then null is returned. - * - * @param string $section The section to get the mapping for - * @param object|null $item The content item or null - * - * @return string|null The new section or null - * - * @since 4.0.0 - */ - public function validateSection($section, $item = null) - { - if (Factory::getApplication()->isClient('site')) - { - switch ($section) - { - case 'registration': - case 'profile': - return 'user'; - } - } + /** + * Returns a valid section for the given section. If it is not valid then null is returned. + * + * @param string $section The section to get the mapping for + * @param object|null $item The content item or null + * + * @return string|null The new section or null + * + * @since 4.0.0 + */ + public function validateSection($section, $item = null) + { + if (Factory::getApplication()->isClient('site')) { + switch ($section) { + case 'registration': + case 'profile': + return 'user'; + } + } - if ($section === 'user') - { - return $section; - } + if ($section === 'user') { + return $section; + } - // We don't know other sections. - return null; - } + // We don't know other sections. + return null; + } - /** - * Returns valid contexts. - * - * @return array Associative array with contexts as keys and translated strings as values - * - * @since 4.0.0 - */ - public function getContexts(): array - { - $language = Factory::getApplication()->getLanguage(); - $language->load('com_users', JPATH_ADMINISTRATOR); + /** + * Returns valid contexts. + * + * @return array Associative array with contexts as keys and translated strings as values + * + * @since 4.0.0 + */ + public function getContexts(): array + { + $language = Factory::getApplication()->getLanguage(); + $language->load('com_users', JPATH_ADMINISTRATOR); - return [ - 'com_users.user' => $language->_('COM_USERS'), - ]; - } + return [ + 'com_users.user' => $language->_('COM_USERS'), + ]; + } } diff --git a/code/administrator/components/com_users/src/Field/GroupparentField.php b/code/administrator/components/com_users/src/Field/GroupparentField.php index 8bf85328..b61a98b4 100644 --- a/code/administrator/components/com_users/src/Field/GroupparentField.php +++ b/code/administrator/components/com_users/src/Field/GroupparentField.php @@ -1,4 +1,5 @@ $userGroupsOptionsData) - { - if ((int) $userGroupsOptionsData->parent_id === (int) $fatherId) - { - unset($userGroupsOptions[$userGroupsOptionsId]); + /** + * Method to clean the Usergroup Options from all children starting by a given father + * + * @param array $userGroupsOptions The usergroup options to clean + * @param integer $fatherId The father ID to start with + * + * @return array The cleaned field options + * + * @since 3.9.4 + */ + private function cleanOptionsChildrenByFather($userGroupsOptions, $fatherId) + { + foreach ($userGroupsOptions as $userGroupsOptionsId => $userGroupsOptionsData) { + if ((int) $userGroupsOptionsData->parent_id === (int) $fatherId) { + unset($userGroupsOptions[$userGroupsOptionsId]); - $userGroupsOptions = $this->cleanOptionsChildrenByFather($userGroupsOptions, $userGroupsOptionsId); - } - } + $userGroupsOptions = $this->cleanOptionsChildrenByFather($userGroupsOptions, $userGroupsOptionsId); + } + } - return $userGroupsOptions; - } + return $userGroupsOptions; + } - /** - * Method to get the field options. - * - * @return array The field option objects - * - * @since 1.6 - */ - protected function getOptions() - { - $options = UserGroupsHelper::getInstance()->getAll(); - $currentGroupId = (int) Factory::getApplication()->input->get('id', 0, 'int'); + /** + * Method to get the field options. + * + * @return array The field option objects + * + * @since 1.6 + */ + protected function getOptions() + { + $options = UserGroupsHelper::getInstance()->getAll(); + $currentGroupId = (int) Factory::getApplication()->input->get('id', 0, 'int'); - // Prevent to set yourself as parent - if ($currentGroupId) - { - unset($options[$currentGroupId]); - } + // Prevent to set yourself as parent + if ($currentGroupId) { + unset($options[$currentGroupId]); + } - // We should not remove any groups when we are creating a new group - if ($currentGroupId !== 0) - { - // Prevent parenting direct children and children of children of this item. - $options = $this->cleanOptionsChildrenByFather($options, $currentGroupId); - } + // We should not remove any groups when we are creating a new group + if ($currentGroupId !== 0) { + // Prevent parenting direct children and children of children of this item. + $options = $this->cleanOptionsChildrenByFather($options, $currentGroupId); + } - $options = array_values($options); - $isSuperAdmin = Factory::getUser()->authorise('core.admin'); + $options = array_values($options); + $isSuperAdmin = Factory::getUser()->authorise('core.admin'); - // Pad the option text with spaces using depth level as a multiplier. - for ($i = 0, $n = count($options); $i < $n; $i++) - { - // Show groups only if user is super admin or group is not super admin - if ($isSuperAdmin || !Access::checkGroup($options[$i]->id, 'core.admin')) - { - $options[$i]->value = $options[$i]->id; - $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->title; - } - else - { - unset($options[$i]); - } - } + // Pad the option text with spaces using depth level as a multiplier. + for ($i = 0, $n = count($options); $i < $n; $i++) { + // Show groups only if user is super admin or group is not super admin + if ($isSuperAdmin || !Access::checkGroup($options[$i]->id, 'core.admin')) { + $options[$i]->value = $options[$i]->id; + $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->title; + } else { + unset($options[$i]); + } + } - // Merge any additional options in the XML definition. - return array_merge(parent::getOptions(), $options); - } + // Merge any additional options in the XML definition. + return array_merge(parent::getOptions(), $options); + } } diff --git a/code/administrator/components/com_users/src/Field/LevelsField.php b/code/administrator/components/com_users/src/Field/LevelsField.php index ddf70480..c0f06964 100644 --- a/code/administrator/components/com_users/src/Field/LevelsField.php +++ b/code/administrator/components/com_users/src/Field/LevelsField.php @@ -1,4 +1,5 @@ + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\Field; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Select modules positions. + * + * Reuses the same field from com_modules. Don't lose it; reuse it! + * + * @since 4.2.0 + */ +class ModulesPositionField extends \Joomla\Component\Modules\Administrator\Field\ModulesPositionField +{ +} diff --git a/code/administrator/components/com_users/src/Helper/DebugHelper.php b/code/administrator/components/com_users/src/Helper/DebugHelper.php index fa30388b..ffc6d06e 100644 --- a/code/administrator/components/com_users/src/Helper/DebugHelper.php +++ b/code/administrator/components/com_users/src/Helper/DebugHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('name AS text, element AS value') - ->from('#__extensions') - ->where('enabled >= 1') - ->where('type =' . $db->quote('component')); - - $items = $db->setQuery($query)->loadObjectList(); - - if (count($items)) - { - $lang = Factory::getLanguage(); - - foreach ($items as &$item) - { - // Load language - $extension = $item->value; - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) - || $lang->load("$extension.sys", $source); - - // Translate component name - $item->text = Text::_($item->text); - } - - // Sort by component name - $items = ArrayHelper::sortObjects($items, 'text', 1, true, true); - } - - return $items; - } - - /** - * Get a list of the actions for the component or code actions. - * - * @param string $component The name of the component. - * - * @return array - * - * @since 1.6 - */ - public static function getDebugActions($component = null) - { - $actions = array(); - - // Try to get actions for the component - if (!empty($component)) - { - $component_actions = Access::getActionsFromFile(JPATH_ADMINISTRATOR . '/components/' . $component . '/access.xml'); - - if (!empty($component_actions)) - { - foreach ($component_actions as &$action) - { - $descr = (string) $action->title; - - if (!empty($action->description)) - { - $descr = (string) $action->description; - } - - $actions[$action->title] = array($action->name, $descr); - } - } - } - - // Use default actions from configuration if no component selected or component doesn't have actions - if (empty($actions)) - { - $filename = JPATH_ADMINISTRATOR . '/components/com_config/forms/application.xml'; - - if (is_file($filename)) - { - $xml = simplexml_load_file($filename); - - foreach ($xml->children()->fieldset as $fieldset) - { - if ('permissions' == (string) $fieldset['name']) - { - foreach ($fieldset->children() as $field) - { - if ('rules' == (string) $field['name']) - { - foreach ($field->children() as $action) - { - $descr = (string) $action['title']; - - if (isset($action['description']) && !empty($action['description'])) - { - $descr = (string) $action['description']; - } - - $actions[(string) $action['title']] = array( - (string) $action['name'], - $descr - ); - } - - break; - } - } - } - } - - // Load language - $lang = Factory::getLanguage(); - $extension = 'com_config'; - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - - $lang->load($extension, JPATH_ADMINISTRATOR, null, false, false) - || $lang->load($extension, $source, null, false, false) - || $lang->load($extension, JPATH_ADMINISTRATOR, $lang->getDefault(), false, false) - || $lang->load($extension, $source, $lang->getDefault(), false, false); - } - } - - return $actions; - } - - /** - * Get a list of filter options for the levels. - * - * @return array An array of \JHtmlOption elements. - */ - public static function getLevelsOptions() - { - // Build the filter options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '1', Text::sprintf('COM_USERS_OPTION_LEVEL_COMPONENT', 1)); - $options[] = HTMLHelper::_('select.option', '2', Text::sprintf('COM_USERS_OPTION_LEVEL_CATEGORY', 2)); - $options[] = HTMLHelper::_('select.option', '3', Text::sprintf('COM_USERS_OPTION_LEVEL_DEEPER', 3)); - $options[] = HTMLHelper::_('select.option', '4', '4'); - $options[] = HTMLHelper::_('select.option', '5', '5'); - $options[] = HTMLHelper::_('select.option', '6', '6'); - - return $options; - } + /** + * Get a list of the components. + * + * @return array + * + * @since 1.6 + */ + public static function getComponents() + { + // Initialise variable. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('name AS text, element AS value') + ->from('#__extensions') + ->where('enabled >= 1') + ->where('type =' . $db->quote('component')); + + $items = $db->setQuery($query)->loadObjectList(); + + if (count($items)) { + $lang = Factory::getLanguage(); + + foreach ($items as &$item) { + // Load language + $extension = $item->value; + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) + || $lang->load("$extension.sys", $source); + + // Translate component name + $item->text = Text::_($item->text); + } + + // Sort by component name + $items = ArrayHelper::sortObjects($items, 'text', 1, true, true); + } + + return $items; + } + + /** + * Get a list of the actions for the component or code actions. + * + * @param string $component The name of the component. + * + * @return array + * + * @since 1.6 + */ + public static function getDebugActions($component = null) + { + $actions = array(); + + // Try to get actions for the component + if (!empty($component)) { + $component_actions = Access::getActionsFromFile(JPATH_ADMINISTRATOR . '/components/' . $component . '/access.xml'); + + if (!empty($component_actions)) { + foreach ($component_actions as &$action) { + $descr = (string) $action->title; + + if (!empty($action->description)) { + $descr = (string) $action->description; + } + + $actions[$action->title] = array($action->name, $descr); + } + } + } + + // Use default actions from configuration if no component selected or component doesn't have actions + if (empty($actions)) { + $filename = JPATH_ADMINISTRATOR . '/components/com_config/forms/application.xml'; + + if (is_file($filename)) { + $xml = simplexml_load_file($filename); + + foreach ($xml->children()->fieldset as $fieldset) { + if ('permissions' == (string) $fieldset['name']) { + foreach ($fieldset->children() as $field) { + if ('rules' == (string) $field['name']) { + foreach ($field->children() as $action) { + $descr = (string) $action['title']; + + if (isset($action['description']) && !empty($action['description'])) { + $descr = (string) $action['description']; + } + + $actions[(string) $action['title']] = array( + (string) $action['name'], + $descr + ); + } + + break; + } + } + } + } + + // Load language + $lang = Factory::getLanguage(); + $extension = 'com_config'; + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + + $lang->load($extension, JPATH_ADMINISTRATOR, null, false, false) + || $lang->load($extension, $source, null, false, false) + || $lang->load($extension, JPATH_ADMINISTRATOR, $lang->getDefault(), false, false) + || $lang->load($extension, $source, $lang->getDefault(), false, false); + } + } + + return $actions; + } + + /** + * Get a list of filter options for the levels. + * + * @return array An array of \JHtmlOption elements. + */ + public static function getLevelsOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', Text::sprintf('COM_USERS_OPTION_LEVEL_COMPONENT', 1)); + $options[] = HTMLHelper::_('select.option', '2', Text::sprintf('COM_USERS_OPTION_LEVEL_CATEGORY', 2)); + $options[] = HTMLHelper::_('select.option', '3', Text::sprintf('COM_USERS_OPTION_LEVEL_DEEPER', 3)); + $options[] = HTMLHelper::_('select.option', '4', '4'); + $options[] = HTMLHelper::_('select.option', '5', '5'); + $options[] = HTMLHelper::_('select.option', '6', '6'); + + return $options; + } } diff --git a/code/administrator/components/com_users/src/Helper/Mfa.php b/code/administrator/components/com_users/src/Helper/Mfa.php new file mode 100644 index 00000000..42f231f1 --- /dev/null +++ b/code/administrator/components/com_users/src/Helper/Mfa.php @@ -0,0 +1,361 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\Helper; + +use Exception; +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Document\HtmlDocument; +use Joomla\CMS\Event\MultiFactor\GetMethod; +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Uri\Uri; +use Joomla\CMS\User\User; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor; +use Joomla\Component\Users\Administrator\Model\BackupcodesModel; +use Joomla\Component\Users\Administrator\Model\MethodsModel; +use Joomla\Component\Users\Administrator\Table\MfaTable; +use Joomla\Component\Users\Administrator\View\Methods\HtmlView; +use Joomla\Database\DatabaseDriver; +use Joomla\Database\ParameterType; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Helper functions for captive MFA handling + * + * @since 4.2.0 + */ +abstract class Mfa +{ + /** + * Cache of all currently active MFAs + * + * @var array|null + * @since 4.2.0 + */ + protected static $allMFAs = null; + + /** + * Are we inside the administrator application + * + * @var boolean + * @since 4.2.0 + */ + protected static $isAdmin = null; + + /** + * Get the HTML for the Multi-factor Authentication configuration interface for a user. + * + * This helper method uses a sort of primitive HMVC to display the com_users' Methods page which + * renders the MFA configuration interface. + * + * @param User $user The user we are going to show the configuration UI for. + * + * @return string|null The HTML of the UI; null if we cannot / must not show it. + * @throws Exception + * @since 4.2.0 + */ + public static function getConfigurationInterface(User $user): ?string + { + // Check the conditions + if (!self::canShowConfigurationInterface($user)) { + return null; + } + + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + + if (!$app->input->getCmd('option', '') === 'com_users') { + $app->getLanguage()->load('com_users'); + $app->getDocument() + ->getWebAssetManager() + ->getRegistry() + ->addExtensionRegistryFile('com_users'); + } + + // Get a model + /** @var MVCFactoryInterface $factory */ + $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); + + /** @var MethodsModel $methodsModel */ + $methodsModel = $factory->createModel('Methods', 'Administrator'); + /** @var BackupcodesModel $methodsModel */ + $backupCodesModel = $factory->createModel('Backupcodes', 'Administrator'); + + // Get a view object + $appRoot = $app->isClient('site') ? \JPATH_SITE : \JPATH_ADMINISTRATOR; + $prefix = $app->isClient('site') ? 'Site' : 'Administrator'; + /** @var HtmlView $view */ + $view = $factory->createView( + 'Methods', + $prefix, + 'Html', + [ + 'base_path' => $appRoot . '/components/com_users', + ] + ); + $view->setModel($methodsModel, true); + /** @noinspection PhpParamsInspection */ + $view->setModel($backupCodesModel); + $view->document = $app->getDocument(); + $view->returnURL = base64_encode(Uri::getInstance()->toString()); + $view->user = $user; + $view->set('forHMVC', true); + + @ob_start(); + + try { + $view->display(); + } catch (\Throwable $e) { + @ob_end_clean(); + + /** + * This is intentional! When you are developing a Multi-factor Authentication plugin you + * will inevitably mess something up and end up with an error. This would cause the + * entire MFA configuration page to disappear. No problem! Set Debug System to Yes in + * Global Configuration and you can see the error exception which will help you solve + * your problem. + */ + if (defined('JDEBUG') && JDEBUG) { + throw $e; + } + + return null; + } + + return @ob_get_clean(); + } + + /** + * Get a list of all of the MFA Methods + * + * @return MethodDescriptor[] + * @since 4.2.0 + */ + public static function getMfaMethods(): array + { + PluginHelper::importPlugin('multifactorauth'); + + if (is_null(self::$allMFAs)) { + // Get all the plugin results + $event = new GetMethod(); + $temp = Factory::getApplication() + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + // Normalize the results + self::$allMFAs = []; + + foreach ($temp as $method) { + if (!is_array($method) && !($method instanceof MethodDescriptor)) { + continue; + } + + $method = $method instanceof MethodDescriptor + ? $method : new MethodDescriptor($method); + + if (empty($method['name'])) { + continue; + } + + self::$allMFAs[$method['name']] = $method; + } + } + + return self::$allMFAs; + } + + /** + * Is the current user allowed to add/edit MFA methods for $user? + * + * This is only allowed if I am adding / editing methods for myself. + * + * If the target user is a member of any group disallowed to use MFA this will return false. + * + * @param User|null $user The user you want to know if we're allowed to edit + * + * @return boolean + * @throws Exception + * @since 4.2.0 + */ + public static function canAddEditMethod(?User $user = null): bool + { + // Cannot do MFA operations on no user or a guest user. + if (is_null($user) || $user->guest) { + return false; + } + + // If the user is in a user group which disallows MFA we cannot allow adding / editing methods. + $neverMFAGroups = ComponentHelper::getParams('com_users')->get('neverMFAUserGroups', []); + $neverMFAGroups = is_array($neverMFAGroups) ? $neverMFAGroups : []; + + if (count(array_intersect($user->getAuthorisedGroups(), $neverMFAGroups))) { + return false; + } + + // Check if this is the same as the logged-in user. + $myUser = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + return $myUser->id === $user->id; + } + + /** + * Is the current user allowed to delete MFA methods / disable MFA for $user? + * + * This is allowed if: + * - The user being queried is the same as the logged-in user + * - The logged-in user is a Super User AND the queried user is NOT a Super User. + * + * Note that Super Users can be edited by their own user only for security reasons. If a Super + * User gets locked out they must use the Backup Codes to regain access. If that's not possible, + * they will need to delete their records from the `#__user_mfa` table. + * + * @param User|null $user The user being queried. + * + * @return boolean + * @throws Exception + * @since 4.2.0 + */ + public static function canDeleteMethod(?User $user = null): bool + { + // Cannot do MFA operations on no user or a guest user. + if (is_null($user) || $user->guest) { + return false; + } + + $myUser = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + return $myUser->id === $user->id + || ($myUser->authorise('core.admin') && !$user->authorise('core.admin')); + } + + /** + * Return all MFA records for a specific user + * + * @param int|null $userId User ID. NULL for currently logged in user. + * + * @return MfaTable[] + * @throws Exception + * + * @since 4.2.0 + */ + public static function getUserMfaRecords(?int $userId): array + { + if (empty($userId)) { + $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); + $userId = $user->id ?: 0; + } + + /** @var DatabaseDriver $db */ + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $userId, ParameterType::INTEGER); + + try { + $ids = $db->setQuery($query)->loadColumn() ?: []; + } catch (Exception $e) { + $ids = []; + } + + if (empty($ids)) { + return []; + } + + /** @var MVCFactoryInterface $factory */ + $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); + + // Map all results to MFA table objects + $records = array_map( + function ($id) use ($factory) { + /** @var MfaTable $record */ + $record = $factory->createTable('Mfa', 'Administrator'); + $loaded = $record->load($id); + + return $loaded ? $record : null; + }, + $ids + ); + + // Let's remove Methods we couldn't decrypt when reading from the database. + $hasBackupCodes = false; + + $records = array_filter( + $records, + function ($record) use (&$hasBackupCodes) { + $isValid = !is_null($record) && (!empty($record->options)); + + if ($isValid && ($record->method === 'backupcodes')) { + $hasBackupCodes = true; + } + + return $isValid; + } + ); + + // If the only Method is backup codes it's as good as having no records + if ((count($records) === 1) && $hasBackupCodes) { + return []; + } + + return $records; + } + + /** + * Are the conditions for showing the MFA configuration interface met? + * + * @param User|null $user The user to be configured + * + * @return boolean + * @throws Exception + * @since 4.2.0 + */ + public static function canShowConfigurationInterface(?User $user = null): bool + { + // If I have no user to check against that's all the checking I can do. + if (empty($user)) { + return false; + } + + // I need at least one MFA method plugin for the setup interface to make any sense. + $plugins = PluginHelper::getPlugin('multifactorauth'); + + if (count($plugins) < 1) { + return false; + } + + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + + // We can only show a configuration page in the front- or backend application. + if (!$app->isClient('site') && !$app->isClient('administrator')) { + return false; + } + + // Only show the configuration page if we have an HTML document + if (!($app->getDocument() instanceof HtmlDocument)) { + return false; + } + + // I must be able to add, edit or delete the user's MFA settings + return self::canAddEditMethod($user) || self::canDeleteMethod($user); + } +} diff --git a/code/administrator/components/com_users/src/Helper/UsersHelper.php b/code/administrator/components/com_users/src/Helper/UsersHelper.php index ca1eb746..6f131928 100644 --- a/code/administrator/components/com_users/src/Helper/UsersHelper.php +++ b/code/administrator/components/com_users/src/Helper/UsersHelper.php @@ -1,4 +1,5 @@ getAll(); - - foreach ($options as &$option) - { - $option->value = $option->id; - $option->text = str_repeat('- ', $option->level) . $option->title; - } - - return $options; - } - - /** - * Creates a list of range options used in filter select list - * used in com_users on users view - * - * @return array - * - * @since 2.5 - */ - public static function getRangeOptions() - { - $options = array( - HTMLHelper::_('select.option', 'today', Text::_('COM_USERS_OPTION_RANGE_TODAY')), - HTMLHelper::_('select.option', 'past_week', Text::_('COM_USERS_OPTION_RANGE_PAST_WEEK')), - HTMLHelper::_('select.option', 'past_1month', Text::_('COM_USERS_OPTION_RANGE_PAST_1MONTH')), - HTMLHelper::_('select.option', 'past_3month', Text::_('COM_USERS_OPTION_RANGE_PAST_3MONTH')), - HTMLHelper::_('select.option', 'past_6month', Text::_('COM_USERS_OPTION_RANGE_PAST_6MONTH')), - HTMLHelper::_('select.option', 'past_year', Text::_('COM_USERS_OPTION_RANGE_PAST_YEAR')), - HTMLHelper::_('select.option', 'post_year', Text::_('COM_USERS_OPTION_RANGE_POST_YEAR')), - ); - - return $options; - } - - /** - * Creates a list of two factor authentication methods used in com_users - * on user view - * - * @return array - * - * @since 3.2.0 - * @throws \Exception - */ - public static function getTwoFactorMethods() - { - PluginHelper::importPlugin('twofactorauth'); - $identities = Factory::getApplication()->triggerEvent('onUserTwofactorIdentify', array()); - - $options = array( - HTMLHelper::_('select.option', 'none', Text::_('JGLOBAL_OTPMETHOD_NONE'), 'value', 'text'), - ); - - if (!empty($identities)) - { - foreach ($identities as $identity) - { - if (!is_object($identity)) - { - continue; - } - - $options[] = HTMLHelper::_('select.option', $identity->method, $identity->title, 'value', 'text'); - } - } - - return $options; - } - - /** - * Get a list of the User Groups for Viewing Access Levels - * - * @param string $rules User Groups in JSON format - * - * @return string $groups Comma separated list of User Groups - * - * @since 3.6 - */ - public static function getVisibleByGroups($rules) - { - $rules = json_decode($rules); - - if (!$rules) - { - return false; - } - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title', 'text')) - ->from($db->quoteName('#__usergroups')) - ->whereIn($db->quoteName('id'), $rules); - $db->setQuery($query); - - $groups = $db->loadColumn(); - $groups = implode(', ', $groups); - - return $groups; - } - - /** - * Returns a valid section for users. If it is not valid then null - * is returned. - * - * @param string $section The section to get the mapping for - * - * @return string|null The new section - * - * @since 3.7.0 - * @throws \Exception - * @deprecated 5.0 Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::validateSection() instead. - */ - public static function validateSection($section) - { - return Factory::getApplication()->bootComponent('com_users')->validateSection($section, null); - } - - /** - * Returns valid contexts - * - * @return array - * - * @since 3.7.0 - * @deprecated 5.0 Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::getContexts() instead. - */ - public static function getContexts() - { - return Factory::getApplication()->bootComponent('com_users')->getContexts(); - } + /** + * @var CMSObject A cache for the available actions. + * @since 1.6 + */ + protected static $actions; + + /** + * Get a list of filter options for the blocked state of a user. + * + * @return array An array of \JHtmlOption elements. + * + * @since 1.6 + */ + public static function getStateOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JENABLED')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JDISABLED')); + + return $options; + } + + /** + * Get a list of filter options for the activated state of a user. + * + * @return array An array of \JHtmlOption elements. + * + * @since 1.6 + */ + public static function getActiveOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('COM_USERS_ACTIVATED')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('COM_USERS_UNACTIVATED')); + + return $options; + } + + /** + * Get a list of the user groups for filtering. + * + * @return array An array of \JHtmlOption elements. + * + * @since 1.6 + */ + public static function getGroups() + { + $options = UserGroupsHelper::getInstance()->getAll(); + + foreach ($options as &$option) { + $option->value = $option->id; + $option->text = str_repeat('- ', $option->level) . $option->title; + } + + return $options; + } + + /** + * Creates a list of range options used in filter select list + * used in com_users on users view + * + * @return array + * + * @since 2.5 + */ + public static function getRangeOptions() + { + $options = array( + HTMLHelper::_('select.option', 'today', Text::_('COM_USERS_OPTION_RANGE_TODAY')), + HTMLHelper::_('select.option', 'past_week', Text::_('COM_USERS_OPTION_RANGE_PAST_WEEK')), + HTMLHelper::_('select.option', 'past_1month', Text::_('COM_USERS_OPTION_RANGE_PAST_1MONTH')), + HTMLHelper::_('select.option', 'past_3month', Text::_('COM_USERS_OPTION_RANGE_PAST_3MONTH')), + HTMLHelper::_('select.option', 'past_6month', Text::_('COM_USERS_OPTION_RANGE_PAST_6MONTH')), + HTMLHelper::_('select.option', 'past_year', Text::_('COM_USERS_OPTION_RANGE_PAST_YEAR')), + HTMLHelper::_('select.option', 'post_year', Text::_('COM_USERS_OPTION_RANGE_POST_YEAR')), + ); + + return $options; + } + + /** + * No longer used. + * + * @return array + * + * @since 3.2.0 + * @throws \Exception + * + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public static function getTwoFactorMethods() + { + return []; + } + + /** + * Get a list of the User Groups for Viewing Access Levels + * + * @param string $rules User Groups in JSON format + * + * @return string $groups Comma separated list of User Groups + * + * @since 3.6 + */ + public static function getVisibleByGroups($rules) + { + $rules = json_decode($rules); + + if (!$rules) { + return false; + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('title', 'text')) + ->from($db->quoteName('#__usergroups')) + ->whereIn($db->quoteName('id'), $rules); + $db->setQuery($query); + + $groups = $db->loadColumn(); + $groups = implode(', ', $groups); + + return $groups; + } + + /** + * Returns a valid section for users. If it is not valid then null + * is returned. + * + * @param string $section The section to get the mapping for + * + * @return string|null The new section + * + * @since 3.7.0 + * @throws \Exception + * @deprecated 5.0 Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::validateSection() instead. + */ + public static function validateSection($section) + { + return Factory::getApplication()->bootComponent('com_users')->validateSection($section, null); + } + + /** + * Returns valid contexts + * + * @return array + * + * @since 3.7.0 + * @deprecated 5.0 Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::getContexts() instead. + */ + public static function getContexts() + { + return Factory::getApplication()->bootComponent('com_users')->getContexts(); + } } diff --git a/code/administrator/components/com_users/src/Model/BackupcodesModel.php b/code/administrator/components/com_users/src/Model/BackupcodesModel.php new file mode 100644 index 00000000..ca7d38e8 --- /dev/null +++ b/code/administrator/components/com_users/src/Model/BackupcodesModel.php @@ -0,0 +1,288 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\Model; + +use Joomla\CMS\Crypt\Crypt; +use Joomla\CMS\Date\Date; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Model\BaseDatabaseModel; +use Joomla\CMS\User\User; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Users\Administrator\Table\MfaTable; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Model for managing backup codes + * + * @since 4.2.0 + */ +class BackupcodesModel extends BaseDatabaseModel +{ + /** + * Caches the backup codes per user ID + * + * @var array + * @since 4.2.0 + */ + protected $cache = []; + + /** + * Get the backup codes record for the specified user + * + * @param User|null $user The user in question. Use null for the currently logged in user. + * + * @return MfaTable|null Record object or null if none is found + * @throws \Exception + * @since 4.2.0 + */ + public function getBackupCodesRecord(User $user = null): ?MfaTable + { + // Make sure I have a user + if (empty($user)) { + $user = Factory::getApplication()->getIdentity() ?: + Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + $loaded = $record->load( + [ + 'user_id' => $user->id, + 'method' => 'backupcodes', + ] + ); + + if (!$loaded) { + $record = null; + } + + return $record; + } + + /** + * Generate a new set of backup codes for the specified user. The generated codes are immediately saved to the + * database and the internal cache is updated. + * + * @param User|null $user Which user to generate codes for? + * + * @return void + * @throws \Exception + * @since 4.2.0 + */ + public function regenerateBackupCodes(User $user = null): void + { + // Make sure I have a user + if (empty($user)) { + $user = Factory::getApplication()->getIdentity() ?: + Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + // Generate backup codes + $backupCodes = []; + + for ($i = 0; $i < 10; $i++) { + // Each backup code is 2 groups of 4 digits + $backupCodes[$i] = sprintf('%04u%04u', random_int(0, 9999), random_int(0, 9999)); + } + + // Save the backup codes to the database and update the cache + $this->saveBackupCodes($backupCodes, $user); + } + + /** + * Saves the backup codes to the database + * + * @param array $codes An array of exactly 10 elements + * @param User|null $user The user for which to save the backup codes + * + * @return boolean + * @throws \Exception + * @since 4.2.0 + */ + public function saveBackupCodes(array $codes, ?User $user = null): bool + { + // Make sure I have a user + if (empty($user)) { + $user = Factory::getApplication()->getIdentity() ?: + Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + // Try to load existing backup codes + $existingCodes = $this->getBackupCodes($user); + $jNow = Date::getInstance(); + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + + if (is_null($existingCodes)) { + $record->reset(); + + $newData = [ + 'user_id' => $user->id, + 'title' => Text::_('COM_USERS_USER_BACKUPCODES'), + 'method' => 'backupcodes', + 'default' => 0, + 'created_on' => $jNow->toSql(), + 'options' => $codes, + ]; + } else { + $record->load( + [ + 'user_id' => $user->id, + 'method' => 'backupcodes', + ] + ); + + $newData = [ + 'options' => $codes, + ]; + } + + $saved = $record->save($newData); + + if (!$saved) { + return false; + } + + // Finally, update the cache + $this->cache[$user->id] = $codes; + + return true; + } + + /** + * Returns the backup codes for the specified user. Cached values will be preferentially returned, therefore you + * MUST go through this model's Methods ONLY when dealing with backup codes. + * + * @param User|null $user The user for which you want the backup codes + * + * @return array|null The backup codes, or null if they do not exist + * @throws \Exception + * @since 4.2.0 + */ + public function getBackupCodes(User $user = null): ?array + { + // Make sure I have a user + if (empty($user)) { + $user = Factory::getApplication()->getIdentity() ?: + Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + if (isset($this->cache[$user->id])) { + return $this->cache[$user->id]; + } + + // If there is no cached record try to load it from the database + $this->cache[$user->id] = null; + + // Try to load the record + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + $loaded = $record->load( + [ + 'user_id' => $user->id, + 'method' => 'backupcodes', + ] + ); + + if ($loaded) { + $this->cache[$user->id] = $record->options; + } + + return $this->cache[$user->id]; + } + + /** + * Check if the provided string is a backup code. If it is, it will be removed from the list (replaced with an empty + * string) and the codes will be saved to the database. All comparisons are performed in a timing safe manner. + * + * @param string $code The code to check + * @param User|null $user The user to check against + * + * @return boolean + * @throws \Exception + * @since 4.2.0 + */ + public function isBackupCode($code, ?User $user = null): bool + { + // Load the backup codes + $codes = $this->getBackupCodes($user) ?: array_fill(0, 10, ''); + + // Keep only the numbers in the provided $code + $code = filter_var($code, FILTER_SANITIZE_NUMBER_INT); + $code = trim($code); + + // Check if the code is in the array. We always check against ten codes to prevent timing attacks which + // determine the amount of codes. + $result = false; + + // The two arrays let us always add an element to an array, therefore having PHP expend the same amount of time + // for the correct code, the incorrect codes and the fake codes. + $newArray = []; + $dummyArray = []; + + $realLength = count($codes); + $restLength = 10 - $realLength; + + for ($i = 0; $i < $realLength; $i++) { + if (hash_equals($codes[$i], $code)) { + // This may seem redundant but makes sure both branches of the if-block are isochronous + $result = $result || true; + $newArray[] = ''; + $dummyArray[] = $codes[$i]; + } else { + // This may seem redundant but makes sure both branches of the if-block are isochronous + $result = $result || false; + $dummyArray[] = ''; + $newArray[] = $codes[$i]; + } + } + + /** + * This is an intentional waste of time, symmetrical to the code above, making sure + * evaluating each of the total of ten elements takes the same time. This code should never + * run UNLESS someone messed up with our backup codes array and it no longer contains 10 + * elements. + */ + $otherResult = false; + + $temp1 = ''; + + for ($i = 0; $i < 10; $i++) { + $temp1[$i] = random_int(0, 99999999); + } + + for ($i = 0; $i < $restLength; $i++) { + if (Crypt::timingSafeCompare($temp1[$i], $code)) { + $otherResult = $otherResult || true; + $newArray[] = ''; + $dummyArray[] = $temp1[$i]; + } else { + $otherResult = $otherResult || false; + $newArray[] = ''; + $dummyArray[] = $temp1[$i]; + } + } + + // This last check makes sure than an empty code does not validate + $result = $result && !hash_equals('', $code); + + // Save the backup codes + $this->saveBackupCodes($newArray, $user); + + // Finally return the result + return $result; + } +} diff --git a/code/administrator/components/com_users/src/Model/CaptiveModel.php b/code/administrator/components/com_users/src/Model/CaptiveModel.php new file mode 100644 index 00000000..4e6730c3 --- /dev/null +++ b/code/administrator/components/com_users/src/Model/CaptiveModel.php @@ -0,0 +1,415 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\Model; + +use Exception; +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Event\MultiFactor\Captive; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Model\BaseDatabaseModel; +use Joomla\CMS\User\User; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Users\Administrator\DataShape\CaptiveRenderOptions; +use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; +use Joomla\Component\Users\Administrator\Table\MfaTable; +use Joomla\Event\Event; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Captive Multi-factor Authentication page's model + * + * @since 4.2.0 + */ +class CaptiveModel extends BaseDatabaseModel +{ + /** + * Cache of the names of the currently active MFA Methods + * + * @var array|null + * @since 4.2.0 + */ + protected $activeMFAMethodNames = null; + + /** + * Prevents Joomla from displaying any modules. + * + * This is implemented with a trick. If you use jdoc tags to load modules the JDocumentRendererHtmlModules + * uses JModuleHelper::getModules() to load the list of modules to render. This goes through JModuleHelper::load() + * which triggers the onAfterModuleList event after cleaning up the module list from duplicates. By resetting + * the list to an empty array we force Joomla to not display any modules. + * + * Similar code paths are followed by any canonical code which tries to load modules. So even if your template does + * not use jdoc tags this code will still work as expected. + * + * @param CMSApplication|null $app The CMS application to manipulate + * + * @return void + * @throws Exception + * + * @since 4.2.0 + */ + public function suppressAllModules(CMSApplication $app = null): void + { + if (is_null($app)) { + $app = Factory::getApplication(); + } + + $app->registerEvent('onAfterModuleList', [$this, 'onAfterModuleList']); + } + + /** + * Get the MFA records for the user which correspond to active plugins + * + * @param User|null $user The user for which to fetch records. Skip to use the current user. + * @param bool $includeBackupCodes Should I include the backup codes record? + * + * @return array + * @throws Exception + * + * @since 4.2.0 + */ + public function getRecords(User $user = null, bool $includeBackupCodes = false): array + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + // Get the user's MFA records + $records = MfaHelper::getUserMfaRecords($user->id); + + // No MFA Methods? Then we obviously don't need to display a Captive login page. + if (empty($records)) { + return []; + } + + // Get the enabled MFA Methods' names + $methodNames = $this->getActiveMethodNames(); + + // Filter the records based on currently active MFA Methods + $ret = []; + + $methodNames[] = 'backupcodes'; + $methodNames = array_unique($methodNames); + + if (!$includeBackupCodes) { + $methodNames = array_filter( + $methodNames, + function ($method) { + return $method != 'backupcodes'; + } + ); + } + + foreach ($records as $record) { + // Backup codes must not be included in the list. We add them in the View, at the end of the list. + if (in_array($record->method, $methodNames)) { + $ret[$record->id] = $record; + } + } + + return $ret; + } + + /** + * Return all the active MFA Methods' names + * + * @return array + * @since 4.2.0 + */ + private function getActiveMethodNames(): ?array + { + if (!is_null($this->activeMFAMethodNames)) { + return $this->activeMFAMethodNames; + } + + // Let's get a list of all currently active MFA Methods + $mfaMethods = MfaHelper::getMfaMethods(); + + // If no MFA Method is active we can't really display a Captive login page. + if (empty($mfaMethods)) { + $this->activeMFAMethodNames = []; + + return $this->activeMFAMethodNames; + } + + // Get a list of just the Method names + $this->activeMFAMethodNames = []; + + foreach ($mfaMethods as $mfaMethod) { + $this->activeMFAMethodNames[] = $mfaMethod['name']; + } + + return $this->activeMFAMethodNames; + } + + /** + * Get the currently selected MFA record for the current user. If the record ID is empty, it does not correspond to + * the currently logged in user or does not correspond to an active plugin null is returned instead. + * + * @param User|null $user The user for which to fetch records. Skip to use the current user. + * + * @return MfaTable|null + * @throws Exception + * + * @since 4.2.0 + */ + public function getRecord(?User $user = null): ?MfaTable + { + $id = (int) $this->getState('record_id', null); + + if ($id <= 0) { + return null; + } + + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + $loaded = $record->load( + [ + 'user_id' => $user->id, + 'id' => $id, + ] + ); + + if (!$loaded) { + return null; + } + + $methodNames = $this->getActiveMethodNames(); + + if (!in_array($record->method, $methodNames) && ($record->method != 'backupcodes')) { + return null; + } + + return $record; + } + + /** + * Load the Captive login page render options for a specific MFA record + * + * @param MfaTable $record The MFA record to process + * + * @return CaptiveRenderOptions The rendering options + * @since 4.2.0 + */ + public function loadCaptiveRenderOptions(?MfaTable $record): CaptiveRenderOptions + { + $renderOptions = new CaptiveRenderOptions(); + + if (empty($record)) { + return $renderOptions; + } + + $event = new Captive($record); + $results = Factory::getApplication() + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + if (empty($results)) { + if ($record->method === 'backupcodes') { + return $renderOptions->merge( + [ + 'pre_message' => Text::_('COM_USERS_USER_BACKUPCODES_CAPTIVE_PROMPT'), + 'input_type' => 'number', + 'label' => Text::_('COM_USERS_USER_BACKUPCODE'), + ] + ); + } + + return $renderOptions; + } + + foreach ($results as $result) { + if (empty($result)) { + continue; + } + + return $renderOptions->merge($result); + } + + return $renderOptions; + } + + /** + * Returns the title to display in the Captive login page, or an empty string if no title is to be displayed. + * + * @return string + * @since 4.2.0 + */ + public function getPageTitle(): string + { + // In the frontend we can choose if we will display a title + $showTitle = (bool) ComponentHelper::getParams('com_users') + ->get('frontend_show_title', 1); + + if (!$showTitle) { + return ''; + } + + return Text::_('COM_USERS_USER_MULTIFACTOR_AUTH'); + } + + /** + * Translate a MFA Method's name into its human-readable, display name + * + * @param string $name The internal MFA Method name + * + * @return string + * @since 4.2.0 + */ + public function translateMethodName(string $name): string + { + static $map = null; + + if (!is_array($map)) { + $map = []; + $mfaMethods = MfaHelper::getMfaMethods(); + + if (!empty($mfaMethods)) { + foreach ($mfaMethods as $mfaMethod) { + $map[$mfaMethod['name']] = $mfaMethod['display']; + } + } + } + + if ($name == 'backupcodes') { + return Text::_('COM_USERS_USER_BACKUPCODES'); + } + + return $map[$name] ?? $name; + } + + /** + * Translate a MFA Method's name into the relative URL if its logo image + * + * @param string $name The internal MFA Method name + * + * @return string + * @since 4.2.0 + */ + public function getMethodImage(string $name): string + { + static $map = null; + + if (!is_array($map)) { + $map = []; + $mfaMethods = MfaHelper::getMfaMethods(); + + if (!empty($mfaMethods)) { + foreach ($mfaMethods as $mfaMethod) { + $map[$mfaMethod['name']] = $mfaMethod['image']; + } + } + } + + if ($name == 'backupcodes') { + return 'media/com_users/images/emergency.svg'; + } + + return $map[$name] ?? $name; + } + + /** + * Process the modules list on Joomla! 4. + * + * Joomla! 4.x is passing an Event object. The first argument of the event object is the array of modules. After + * filtering it we have to overwrite the event argument (NOT just return the new list of modules). If a future + * version of Joomla! uses immutable events we'll have to use Reflection to do that or Joomla! would have to fix + * the way this event is handled, taking its return into account. For now, we just abuse the mutable event + * properties - a feature of the event objects we discussed in the Joomla! 4 Working Group back in August 2015. + * + * @param Event $event The Joomla! event object + * + * @return void + * @throws Exception + * + * @since 4.2.0 + */ + public function onAfterModuleList(Event $event): void + { + $modules = $event->getArgument(0); + + if (empty($modules)) { + return; + } + + $this->filterModules($modules); + + $event->setArgument(0, $modules); + } + + /** + * This is the Method which actually filters the sites modules based on the allowed module positions specified by + * the user. + * + * @param array $modules The list of the site's modules. Passed by reference. + * + * @return void The by-reference value is modified instead. + * @since 4.2.0 + * @throws Exception + */ + private function filterModules(array &$modules): void + { + $allowedPositions = $this->getAllowedModulePositions(); + + if (empty($allowedPositions)) { + $modules = []; + + return; + } + + $filtered = []; + + foreach ($modules as $module) { + if (in_array($module->position, $allowedPositions)) { + $filtered[] = $module; + } + } + + $modules = $filtered; + } + + /** + * Get a list of module positions we are allowed to display + * + * @return array + * @throws Exception + * + * @since 4.2.0 + */ + private function getAllowedModulePositions(): array + { + $isAdmin = Factory::getApplication()->isClient('administrator'); + + // Load the list of allowed module positions from the component's settings. May be different for front- and back-end + $configKey = 'allowed_positions_' . ($isAdmin ? 'backend' : 'frontend'); + $res = ComponentHelper::getParams('com_users')->get($configKey, []); + + // In the backend we must always add the 'title' module position + if ($isAdmin) { + $res[] = 'title'; + $res[] = 'toolbar'; + } + + return $res; + } +} diff --git a/code/administrator/components/com_users/src/Model/DebuggroupModel.php b/code/administrator/components/com_users/src/Model/DebuggroupModel.php index 84498e85..8076436c 100644 --- a/code/administrator/components/com_users/src/Model/DebuggroupModel.php +++ b/code/administrator/components/com_users/src/Model/DebuggroupModel.php @@ -1,4 +1,5 @@ getState('filter.component'); - - return DebugHelper::getDebugActions($component); - } - - /** - * Override getItems method. - * - * @return array - * - * @since 1.6 - */ - public function getItems() - { - $groupId = $this->getState('group_id'); - - if (($assets = parent::getItems()) && $groupId) - { - $actions = $this->getDebugActions(); - - foreach ($assets as &$asset) - { - $asset->checks = array(); - - foreach ($actions as $action) - { - $name = $action[0]; - $asset->checks[$name] = Access::checkGroup($groupId, $name, $asset->name); - } - } - } - - return $assets; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.lft', $direction = 'asc') - { - $app = Factory::getApplication(); - - // Adjust the context to support modal layouts. - $layout = $app->input->get('layout', 'default'); - - if ($layout) - { - $this->context .= '.' . $layout; - } - - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('group_id', $this->getUserStateFromRequest($this->context . '.group_id', 'group_id', 0, 'int', false)); - - $levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd'); - $this->setState('filter.level_start', $levelStart); - - $value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd'); - - if ($value > 0 && $value < $levelStart) - { - $value = $levelStart; - } - - $this->setState('filter.level_end', $value); - - $this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_users'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('group_id'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.level_start'); - $id .= ':' . $this->getState('filter.level_end'); - $id .= ':' . $this->getState('filter.component'); - - return parent::getStoreId($id); - } - - /** - * Get the group being debugged. - * - * @return CMSObject - * - * @since 1.6 - */ - public function getGroup() - { - $groupId = (int) $this->getState('group_id'); - - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName(['id', 'title'])) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $groupId, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $group = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $group; - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.name, a.title, a.level, a.lft, a.rgt' - ) - ); - $query->from($db->quoteName('#__assets', 'a')); - - // Filter the items over the search string if set. - if ($this->getState('filter.search')) - { - $search = '%' . trim($this->getState('filter.search')) . '%'; - - // Add the clauses to the query. - $query->where( - '(' . $db->quoteName('a.name') . ' LIKE :name' - . ' OR ' . $db->quoteName('a.title') . ' LIKE :title)' - ) - ->bind(':name', $search) - ->bind(':title', $search); - } - - // Filter on the start and end levels. - $levelStart = (int) $this->getState('filter.level_start'); - $levelEnd = (int) $this->getState('filter.level_end'); - - if ($levelEnd > 0 && $levelEnd < $levelStart) - { - $levelEnd = $levelStart; - } - - if ($levelStart > 0) - { - $query->where($db->quoteName('a.level') . ' >= :levelStart') - ->bind(':levelStart', $levelStart, ParameterType::INTEGER); - } - - if ($levelEnd > 0) - { - $query->where($db->quoteName('a.level') . ' <= :levelEnd') - ->bind(':levelEnd', $levelEnd, ParameterType::INTEGER); - } - - // Filter the items over the component if set. - if ($this->getState('filter.component')) - { - $component = $this->getState('filter.component'); - $lcomponent = $component . '.%'; - $query->where( - '(' . $db->quoteName('a.name') . ' = :component' - . ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)' - ) - ->bind(':component', $component) - ->bind(':lcomponent', $lcomponent); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'a.title', + 'component', 'a.name', + 'a.lft', + 'a.id', + 'level_start', 'level_end', 'a.level', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Get a list of the actions. + * + * @return array + * + * @since 1.6 + */ + public function getDebugActions() + { + $component = $this->getState('filter.component'); + + return DebugHelper::getDebugActions($component); + } + + /** + * Override getItems method. + * + * @return array + * + * @since 1.6 + */ + public function getItems() + { + $groupId = $this->getState('group_id'); + + if (($assets = parent::getItems()) && $groupId) { + $actions = $this->getDebugActions(); + + foreach ($assets as &$asset) { + $asset->checks = array(); + + foreach ($actions as $action) { + $name = $action[0]; + $asset->checks[$name] = Access::checkGroup($groupId, $name, $asset->name); + } + } + } + + return $assets; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Adjust the context to support modal layouts. + $layout = $app->input->get('layout', 'default'); + + if ($layout) { + $this->context .= '.' . $layout; + } + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('group_id', $this->getUserStateFromRequest($this->context . '.group_id', 'group_id', 0, 'int', false)); + + $levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd'); + $this->setState('filter.level_start', $levelStart); + + $value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd'); + + if ($value > 0 && $value < $levelStart) { + $value = $levelStart; + } + + $this->setState('filter.level_end', $value); + + $this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('group_id'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.level_start'); + $id .= ':' . $this->getState('filter.level_end'); + $id .= ':' . $this->getState('filter.component'); + + return parent::getStoreId($id); + } + + /** + * Get the group being debugged. + * + * @return CMSObject + * + * @since 1.6 + */ + public function getGroup() + { + $groupId = (int) $this->getState('group_id'); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'title'])) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $groupId, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $group = $db->loadObject(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $group; + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.name, a.title, a.level, a.lft, a.rgt' + ) + ); + $query->from($db->quoteName('#__assets', 'a')); + + // Filter the items over the search string if set. + if ($this->getState('filter.search')) { + $search = '%' . trim($this->getState('filter.search')) . '%'; + + // Add the clauses to the query. + $query->where( + '(' . $db->quoteName('a.name') . ' LIKE :name' + . ' OR ' . $db->quoteName('a.title') . ' LIKE :title)' + ) + ->bind(':name', $search) + ->bind(':title', $search); + } + + // Filter on the start and end levels. + $levelStart = (int) $this->getState('filter.level_start'); + $levelEnd = (int) $this->getState('filter.level_end'); + + if ($levelEnd > 0 && $levelEnd < $levelStart) { + $levelEnd = $levelStart; + } + + if ($levelStart > 0) { + $query->where($db->quoteName('a.level') . ' >= :levelStart') + ->bind(':levelStart', $levelStart, ParameterType::INTEGER); + } + + if ($levelEnd > 0) { + $query->where($db->quoteName('a.level') . ' <= :levelEnd') + ->bind(':levelEnd', $levelEnd, ParameterType::INTEGER); + } + + // Filter the items over the component if set. + if ($this->getState('filter.component')) { + $component = $this->getState('filter.component'); + $lcomponent = $component . '.%'; + $query->where( + '(' . $db->quoteName('a.name') . ' = :component' + . ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)' + ) + ->bind(':component', $component) + ->bind(':lcomponent', $lcomponent); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } } diff --git a/code/administrator/components/com_users/src/Model/DebuguserModel.php b/code/administrator/components/com_users/src/Model/DebuguserModel.php index 4be8ab54..22bb90b1 100644 --- a/code/administrator/components/com_users/src/Model/DebuguserModel.php +++ b/code/administrator/components/com_users/src/Model/DebuguserModel.php @@ -1,4 +1,5 @@ getState('filter.component'); - - return DebugHelper::getDebugActions($component); - } - - /** - * Override getItems method. - * - * @return array - * - * @since 1.6 - */ - public function getItems() - { - $userId = $this->getState('user_id'); - $user = Factory::getUser($userId); - - if (($assets = parent::getItems()) && $userId) - { - $actions = $this->getDebugActions(); - - foreach ($assets as &$asset) - { - $asset->checks = array(); - - foreach ($actions as $action) - { - $name = $action[0]; - $asset->checks[$name] = $user->authorise($name, $asset->name); - } - } - } - - return $assets; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState($ordering = 'a.lft', $direction = 'asc') - { - $app = Factory::getApplication(); - - // Adjust the context to support modal layouts. - $layout = $app->input->get('layout', 'default'); - - if ($layout) - { - $this->context .= '.' . $layout; - } - - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('user_id', $this->getUserStateFromRequest($this->context . '.user_id', 'user_id', 0, 'int', false)); - - $levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd'); - $this->setState('filter.level_start', $levelStart); - - $value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd'); - - if ($value > 0 && $value < $levelStart) - { - $value = $levelStart; - } - - $this->setState('filter.level_end', $value); - - $this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_users'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('user_id'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.level_start'); - $id .= ':' . $this->getState('filter.level_end'); - $id .= ':' . $this->getState('filter.component'); - - return parent::getStoreId($id); - } - - /** - * Get the user being debugged. - * - * @return User - * - * @since 1.6 - */ - public function getUser() - { - $userId = $this->getState('user_id'); - - return Factory::getUser($userId); - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.name, a.title, a.level, a.lft, a.rgt' - ) - ); - $query->from($db->quoteName('#__assets', 'a')); - - // Filter the items over the search string if set. - if ($this->getState('filter.search')) - { - $search = '%' . trim($this->getState('filter.search')) . '%'; - - // Add the clauses to the query. - $query->where( - '(' . $db->quoteName('a.name') . ' LIKE :name' - . ' OR ' . $db->quoteName('a.title') . ' LIKE :title)' - ) - ->bind(':name', $search) - ->bind(':title', $search); - } - - // Filter on the start and end levels. - $levelStart = (int) $this->getState('filter.level_start'); - $levelEnd = (int) $this->getState('filter.level_end'); - - if ($levelEnd > 0 && $levelEnd < $levelStart) - { - $levelEnd = $levelStart; - } - - if ($levelStart > 0) - { - $query->where($db->quoteName('a.level') . ' >= :levelStart') - ->bind(':levelStart', $levelStart, ParameterType::INTEGER); - } - - if ($levelEnd > 0) - { - $query->where($db->quoteName('a.level') . ' <= :levelEnd') - ->bind(':levelEnd', $levelEnd, ParameterType::INTEGER); - } - - // Filter the items over the component if set. - if ($this->getState('filter.component')) - { - $component = $this->getState('filter.component'); - $lcomponent = $component . '.%'; - $query->where( - '(' . $db->quoteName('a.name') . ' = :component' - . ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)' - ) - ->bind(':component', $component) - ->bind(':lcomponent', $lcomponent); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'a.title', + 'component', 'a.name', + 'a.lft', + 'a.id', + 'level_start', 'level_end', 'a.level', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Get a list of the actions. + * + * @return array + * + * @since 1.6 + */ + public function getDebugActions() + { + $component = $this->getState('filter.component'); + + return DebugHelper::getDebugActions($component); + } + + /** + * Override getItems method. + * + * @return array + * + * @since 1.6 + */ + public function getItems() + { + $userId = $this->getState('user_id'); + $user = Factory::getUser($userId); + + if (($assets = parent::getItems()) && $userId) { + $actions = $this->getDebugActions(); + + foreach ($assets as &$asset) { + $asset->checks = array(); + + foreach ($actions as $action) { + $name = $action[0]; + $asset->checks[$name] = $user->authorise($name, $asset->name); + } + } + } + + return $assets; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Adjust the context to support modal layouts. + $layout = $app->input->get('layout', 'default'); + + if ($layout) { + $this->context .= '.' . $layout; + } + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('user_id', $this->getUserStateFromRequest($this->context . '.user_id', 'user_id', 0, 'int', false)); + + $levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd'); + $this->setState('filter.level_start', $levelStart); + + $value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd'); + + if ($value > 0 && $value < $levelStart) { + $value = $levelStart; + } + + $this->setState('filter.level_end', $value); + + $this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('user_id'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.level_start'); + $id .= ':' . $this->getState('filter.level_end'); + $id .= ':' . $this->getState('filter.component'); + + return parent::getStoreId($id); + } + + /** + * Get the user being debugged. + * + * @return User + * + * @since 1.6 + */ + public function getUser() + { + $userId = $this->getState('user_id'); + + return Factory::getUser($userId); + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.name, a.title, a.level, a.lft, a.rgt' + ) + ); + $query->from($db->quoteName('#__assets', 'a')); + + // Filter the items over the search string if set. + if ($this->getState('filter.search')) { + $search = '%' . trim($this->getState('filter.search')) . '%'; + + // Add the clauses to the query. + $query->where( + '(' . $db->quoteName('a.name') . ' LIKE :name' + . ' OR ' . $db->quoteName('a.title') . ' LIKE :title)' + ) + ->bind(':name', $search) + ->bind(':title', $search); + } + + // Filter on the start and end levels. + $levelStart = (int) $this->getState('filter.level_start'); + $levelEnd = (int) $this->getState('filter.level_end'); + + if ($levelEnd > 0 && $levelEnd < $levelStart) { + $levelEnd = $levelStart; + } + + if ($levelStart > 0) { + $query->where($db->quoteName('a.level') . ' >= :levelStart') + ->bind(':levelStart', $levelStart, ParameterType::INTEGER); + } + + if ($levelEnd > 0) { + $query->where($db->quoteName('a.level') . ' <= :levelEnd') + ->bind(':levelEnd', $levelEnd, ParameterType::INTEGER); + } + + // Filter the items over the component if set. + if ($this->getState('filter.component')) { + $component = $this->getState('filter.component'); + $lcomponent = $component . '.%'; + $query->where( + '(' . $db->quoteName('a.name') . ' = :component' + . ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)' + ) + ->bind(':component', $component) + ->bind(':lcomponent', $lcomponent); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } } diff --git a/code/administrator/components/com_users/src/Model/GroupModel.php b/code/administrator/components/com_users/src/Model/GroupModel.php index 5b770fd1..7e8df434 100644 --- a/code/administrator/components/com_users/src/Model/GroupModel.php +++ b/code/administrator/components/com_users/src/Model/GroupModel.php @@ -1,4 +1,5 @@ 'onUserAfterDeleteGroup', - 'event_after_save' => 'onUserAfterSaveGroup', - 'event_before_delete' => 'onUserBeforeDeleteGroup', - 'event_before_save' => 'onUserBeforeSaveGroup', - 'events_map' => array('delete' => 'user', 'save' => 'user') - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'Usergroup', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - $return = Table::getInstance($type, $prefix, $config); - - return $return; - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.group', 'group', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_users.edit.group.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_users.group', $data); - - return $data; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = '') - { - $obj = is_array($data) ? ArrayHelper::toObject($data, CMSObject::class) : $data; - - if (isset($obj->parent_id) && $obj->parent_id == 0 && $obj->id > 0) - { - $form->setFieldAttribute('parent_id', 'type', 'hidden'); - $form->setFieldAttribute('parent_id', 'hidden', 'true'); - } - - parent::preprocessForm($form, $data, 'user'); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - // Include the user plugins for events. - PluginHelper::importPlugin($this->events_map['save']); - - /** - * Check the super admin permissions for group - * We get the parent group permissions and then check the group permissions manually - * We have to calculate the group permissions manually because we haven't saved the group yet - */ - $parentSuperAdmin = Access::checkGroup($data['parent_id'], 'core.admin'); - - // Get core.admin rules from the root asset - $rules = Access::getAssetRules('root.1')->getData('core.admin'); - - // Get the value for the current group (will be true (allowed), false (denied), or null (inherit) - $groupSuperAdmin = $rules['core.admin']->allow($data['id']); - - // We only need to change the $groupSuperAdmin if the parent is true or false. Otherwise, the value set in the rule takes effect. - if ($parentSuperAdmin === false) - { - // If parent is false (Denied), effective value will always be false - $groupSuperAdmin = false; - } - elseif ($parentSuperAdmin === true) - { - // If parent is true (allowed), group is true unless explicitly set to false - $groupSuperAdmin = ($groupSuperAdmin === false) ? false : true; - } - - // Check for non-super admin trying to save with super admin group - $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); - - if (!$iAmSuperAdmin && $groupSuperAdmin) - { - $this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN')); - - return false; - } - - /** - * Check for super-admin changing self to be non-super-admin - * First, are we a super admin - */ - if ($iAmSuperAdmin) - { - // Next, are we a member of the current group? - $myGroups = Access::getGroupsByUser(Factory::getUser()->get('id'), false); - - if (in_array($data['id'], $myGroups)) - { - // Now, would we have super admin permissions without the current group? - $otherGroups = array_diff($myGroups, array($data['id'])); - $otherSuperAdmin = false; - - foreach ($otherGroups as $otherGroup) - { - $otherSuperAdmin = $otherSuperAdmin ?: Access::checkGroup($otherGroup, 'core.admin'); - } - - /** - * If we would not otherwise have super admin permissions - * and the current group does not have super admin permissions, throw an exception - */ - if ((!$otherSuperAdmin) && (!$groupSuperAdmin)) - { - $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF')); - - return false; - } - } - } - - if (Factory::getApplication()->input->get('task') == 'save2copy') - { - $data['title'] = $this->generateGroupTitle($data['parent_id'], $data['title']); - } - - // Proceed with the save - return parent::save($data); - } - - /** - * Method to delete rows. - * - * @param array &$pks An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function delete(&$pks) - { - // Typecast variable. - $pks = (array) $pks; - $user = Factory::getUser(); - $groups = Access::getGroupsByUser($user->get('id')); - - // Get a row instance. - $table = $this->getTable(); - - // Load plugins. - PluginHelper::importPlugin($this->events_map['delete']); - - // Check if I am a Super Admin - $iAmSuperAdmin = $user->authorise('core.admin'); - - foreach ($pks as $pk) - { - // Do not allow to delete groups to which the current user belongs - if (in_array($pk, $groups)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_DELETE_ERROR_INVALID_GROUP'), 'error'); - - return false; - } - elseif (!$table->load($pk)) - { - // Item is not in the table. - $this->setError($table->getError()); - - return false; - } - } - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - // Access checks. - $allow = $user->authorise('core.edit.state', 'com_users'); - - // Don't allow non-super-admin to delete a super admin - $allow = (!$iAmSuperAdmin && Access::checkGroup($pk, 'core.admin')) ? false : $allow; - - if ($allow) - { - // Fire the before delete event. - Factory::getApplication()->triggerEvent($this->event_before_delete, array($table->getProperties())); - - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - else - { - // Trigger the after delete event. - Factory::getApplication()->triggerEvent($this->event_after_delete, array($table->getProperties(), true, $this->getError())); - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - } - } - } - - return true; - } - - /** - * Method to generate the title of group on Save as Copy action - * - * @param integer $parentId The id of the parent. - * @param string $title The title of group - * - * @return string Contains the modified title. - * - * @since 3.3.7 - */ - protected function generateGroupTitle($parentId, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('title' => $title, 'parent_id' => $parentId))) - { - if ($title == $table->title) - { - $title = StringHelper::increment($title); - } - } - - return $title; - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_after_delete' => 'onUserAfterDeleteGroup', + 'event_after_save' => 'onUserAfterSaveGroup', + 'event_before_delete' => 'onUserBeforeDeleteGroup', + 'event_before_save' => 'onUserBeforeSaveGroup', + 'events_map' => array('delete' => 'user', 'save' => 'user') + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'Usergroup', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + $return = Table::getInstance($type, $prefix, $config); + + return $return; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.group', 'group', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_users.edit.group.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_users.group', $data); + + return $data; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = '') + { + $obj = is_array($data) ? ArrayHelper::toObject($data, CMSObject::class) : $data; + + if (isset($obj->parent_id) && $obj->parent_id == 0 && $obj->id > 0) { + $form->setFieldAttribute('parent_id', 'type', 'hidden'); + $form->setFieldAttribute('parent_id', 'hidden', 'true'); + } + + parent::preprocessForm($form, $data, 'user'); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + // Include the user plugins for events. + PluginHelper::importPlugin($this->events_map['save']); + + /** + * Check the super admin permissions for group + * We get the parent group permissions and then check the group permissions manually + * We have to calculate the group permissions manually because we haven't saved the group yet + */ + $parentSuperAdmin = Access::checkGroup($data['parent_id'], 'core.admin'); + + // Get core.admin rules from the root asset + $rules = Access::getAssetRules('root.1')->getData('core.admin'); + + // Get the value for the current group (will be true (allowed), false (denied), or null (inherit) + $groupSuperAdmin = $rules['core.admin']->allow($data['id']); + + // We only need to change the $groupSuperAdmin if the parent is true or false. Otherwise, the value set in the rule takes effect. + if ($parentSuperAdmin === false) { + // If parent is false (Denied), effective value will always be false + $groupSuperAdmin = false; + } elseif ($parentSuperAdmin === true) { + // If parent is true (allowed), group is true unless explicitly set to false + $groupSuperAdmin = ($groupSuperAdmin === false) ? false : true; + } + + // Check for non-super admin trying to save with super admin group + $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); + + if (!$iAmSuperAdmin && $groupSuperAdmin) { + $this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN')); + + return false; + } + + /** + * Check for super-admin changing self to be non-super-admin + * First, are we a super admin + */ + if ($iAmSuperAdmin) { + // Next, are we a member of the current group? + $myGroups = Access::getGroupsByUser(Factory::getUser()->get('id'), false); + + if (in_array($data['id'], $myGroups)) { + // Now, would we have super admin permissions without the current group? + $otherGroups = array_diff($myGroups, array($data['id'])); + $otherSuperAdmin = false; + + foreach ($otherGroups as $otherGroup) { + $otherSuperAdmin = $otherSuperAdmin ?: Access::checkGroup($otherGroup, 'core.admin'); + } + + /** + * If we would not otherwise have super admin permissions + * and the current group does not have super admin permissions, throw an exception + */ + if ((!$otherSuperAdmin) && (!$groupSuperAdmin)) { + $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF')); + + return false; + } + } + } + + if (Factory::getApplication()->input->get('task') == 'save2copy') { + $data['title'] = $this->generateGroupTitle($data['parent_id'], $data['title']); + } + + // Proceed with the save + return parent::save($data); + } + + /** + * Method to delete rows. + * + * @param array &$pks An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function delete(&$pks) + { + // Typecast variable. + $pks = (array) $pks; + $user = Factory::getUser(); + $groups = Access::getGroupsByUser($user->get('id')); + + // Get a row instance. + $table = $this->getTable(); + + // Load plugins. + PluginHelper::importPlugin($this->events_map['delete']); + + // Check if I am a Super Admin + $iAmSuperAdmin = $user->authorise('core.admin'); + + foreach ($pks as $pk) { + // Do not allow to delete groups to which the current user belongs + if (in_array($pk, $groups)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_DELETE_ERROR_INVALID_GROUP'), 'error'); + + return false; + } elseif (!$table->load($pk)) { + // Item is not in the table. + $this->setError($table->getError()); + + return false; + } + } + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + // Access checks. + $allow = $user->authorise('core.edit.state', 'com_users'); + + // Don't allow non-super-admin to delete a super admin + $allow = (!$iAmSuperAdmin && Access::checkGroup($pk, 'core.admin')) ? false : $allow; + + if ($allow) { + // Fire the before delete event. + Factory::getApplication()->triggerEvent($this->event_before_delete, array($table->getProperties())); + + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } else { + // Trigger the after delete event. + Factory::getApplication()->triggerEvent($this->event_after_delete, array($table->getProperties(), true, $this->getError())); + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + } + } + } + + return true; + } + + /** + * Method to generate the title of group on Save as Copy action + * + * @param integer $parentId The id of the parent. + * @param string $title The title of group + * + * @return string Contains the modified title. + * + * @since 3.3.7 + */ + protected function generateGroupTitle($parentId, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('title' => $title, 'parent_id' => $parentId))) { + if ($title == $table->title) { + $title = StringHelper::increment($title); + } + } + + return $title; + } } diff --git a/code/administrator/components/com_users/src/Model/GroupsModel.php b/code/administrator/components/com_users/src/Model/GroupsModel.php index ec63712e..9bed9050 100644 --- a/code/administrator/components/com_users/src/Model/GroupsModel.php +++ b/code/administrator/components/com_users/src/Model/GroupsModel.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Users\Administrator\Model; -\defined('_JEXEC') or die; +namespace Joomla\Component\Users\Administrator\Model; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Helper\UserGroupsHelper; @@ -17,6 +17,10 @@ use Joomla\Database\DatabaseQuery; use Joomla\Database\ParameterType; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Methods supporting a list of user group records. * @@ -24,237 +28,219 @@ */ class GroupsModel extends ListModel { - /** - * Override parent constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * - * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel - * @since 3.2 - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null) - { - if (empty($config['filter_fields'])) - { - $config['filter_fields'] = array( - 'id', 'a.id', - 'parent_id', 'a.parent_id', - 'title', 'a.title', - 'lft', 'a.lft', - 'rgt', 'a.rgt', - ); - } - - parent::__construct($config, $factory); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.lft', $direction = 'asc') - { - // Load the parameters. - $params = ComponentHelper::getParams('com_users'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Gets the list of groups and adds expensive joins to the result set. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getItems() - { - // Get a storage key. - $store = $this->getStoreId(); - - // Try to load the data from internal storage. - if (empty($this->cache[$store])) - { - $items = parent::getItems(); - - // Bail out on an error or empty list. - if (empty($items)) - { - $this->cache[$store] = $items; - - return $items; - } - - try - { - $items = $this->populateExtraData($items); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Add the items to the internal cache. - $this->cache[$store] = $items; - } - - return $this->cache[$store]; - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - $query->from($db->quoteName('#__usergroups') . ' AS a'); - - // Filter the comments over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . trim($search) . '%'; - $query->where($db->quoteName('a.title') . ' LIKE :title'); - $query->bind(':title', $search); - } - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Populate level & path for items. - * - * @param array $items Array of \stdClass objects - * - * @return array - * - * @since 3.6.3 - */ - private function populateExtraData(array $items) - { - // First pass: get list of the group ids and reset the counts. - $groupsByKey = array(); - - foreach ($items as $item) - { - $groupsByKey[(int) $item->id] = $item; - } - - $groupIds = array_keys($groupsByKey); - - $db = $this->getDbo(); - - // Get total enabled users in group. - $query = $db->getQuery(true); - - // Count the objects in the user group. - $query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count') - ->from($db->quoteName('#__user_usergroup_map', 'map')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id')) - ->whereIn($db->quoteName('map.group_id'), $groupIds) - ->where($db->quoteName('u.block') . ' = 0') - ->group($db->quoteName('map.group_id')); - $db->setQuery($query); - - try - { - $countEnabled = $db->loadAssocList('group_id', 'count_enabled'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get total disabled users in group. - $query->clear(); - $query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count') - ->from($db->quoteName('#__user_usergroup_map', 'map')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id')) - ->whereIn($db->quoteName('map.group_id'), $groupIds) - ->where($db->quoteName('u.block') . ' = 1') - ->group($db->quoteName('map.group_id')); - $db->setQuery($query); - - try - { - $countDisabled = $db->loadAssocList('group_id', 'count_disabled'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Inject the values back into the array. - foreach ($groupsByKey as &$item) - { - $item->count_enabled = isset($countEnabled[$item->id]) ? (int) $countEnabled[$item->id]['user_count'] : 0; - $item->count_disabled = isset($countDisabled[$item->id]) ? (int) $countDisabled[$item->id]['user_count'] : 0; - $item->user_count = $item->count_enabled + $item->count_disabled; - } - - $groups = new UserGroupsHelper($groupsByKey); - - return array_values($groups->getAll()); - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'parent_id', 'a.parent_id', + 'title', 'a.title', + 'lft', 'a.lft', + 'rgt', 'a.rgt', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Gets the list of groups and adds expensive joins to the result set. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId(); + + // Try to load the data from internal storage. + if (empty($this->cache[$store])) { + $items = parent::getItems(); + + // Bail out on an error or empty list. + if (empty($items)) { + $this->cache[$store] = $items; + + return $items; + } + + try { + $items = $this->populateExtraData($items); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Add the items to the internal cache. + $this->cache[$store] = $items; + } + + return $this->cache[$store]; + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__usergroups') . ' AS a'); + + // Filter the comments over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . trim($search) . '%'; + $query->where($db->quoteName('a.title') . ' LIKE :title'); + $query->bind(':title', $search); + } + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Populate level & path for items. + * + * @param array $items Array of \stdClass objects + * + * @return array + * + * @since 3.6.3 + */ + private function populateExtraData(array $items) + { + // First pass: get list of the group ids and reset the counts. + $groupsByKey = array(); + + foreach ($items as $item) { + $groupsByKey[(int) $item->id] = $item; + } + + $groupIds = array_keys($groupsByKey); + + $db = $this->getDatabase(); + + // Get total enabled users in group. + $query = $db->getQuery(true); + + // Count the objects in the user group. + $query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count') + ->from($db->quoteName('#__user_usergroup_map', 'map')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id')) + ->whereIn($db->quoteName('map.group_id'), $groupIds) + ->where($db->quoteName('u.block') . ' = 0') + ->group($db->quoteName('map.group_id')); + $db->setQuery($query); + + try { + $countEnabled = $db->loadAssocList('group_id', 'count_enabled'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get total disabled users in group. + $query->clear(); + $query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count') + ->from($db->quoteName('#__user_usergroup_map', 'map')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id')) + ->whereIn($db->quoteName('map.group_id'), $groupIds) + ->where($db->quoteName('u.block') . ' = 1') + ->group($db->quoteName('map.group_id')); + $db->setQuery($query); + + try { + $countDisabled = $db->loadAssocList('group_id', 'count_disabled'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Inject the values back into the array. + foreach ($groupsByKey as &$item) { + $item->count_enabled = isset($countEnabled[$item->id]) ? (int) $countEnabled[$item->id]['user_count'] : 0; + $item->count_disabled = isset($countDisabled[$item->id]) ? (int) $countDisabled[$item->id]['user_count'] : 0; + $item->user_count = $item->count_enabled + $item->count_disabled; + } + + $groups = new UserGroupsHelper($groupsByKey); + + return array_values($groups->getAll()); + } } diff --git a/code/administrator/components/com_users/src/Model/LevelModel.php b/code/administrator/components/com_users/src/Model/LevelModel.php index d01d4c4d..6f229e5c 100644 --- a/code/administrator/components/com_users/src/Model/LevelModel.php +++ b/code/administrator/components/com_users/src/Model/LevelModel.php @@ -1,4 +1,5 @@ rules); - - if ($groups === null) - { - throw new \RuntimeException('Invalid rules schema'); - } - - $isAdmin = Factory::getUser()->authorise('core.admin'); - - // Check permissions - foreach ($groups as $group) - { - if (!$isAdmin && Access::checkGroup($group, 'core.admin')) - { - $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - } - - // Check if the access level is being used by any content. - if ($this->levelsInUse === null) - { - // Populate the list once. - $this->levelsInUse = array(); - - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT access'); - - // Get all the tables and the prefix - $tables = $db->getTableList(); - $prefix = $db->getPrefix(); - - foreach ($tables as $table) - { - // Get all of the columns in the table - $fields = $db->getTableColumns($table); - - /** - * We are looking for the access field. If custom tables are using something other - * than the 'access' field they are on their own unfortunately. - * Also make sure the table prefix matches the live db prefix (eg, it is not a "bak_" table) - */ - if (strpos($table, $prefix) === 0 && isset($fields['access'])) - { - // Lookup the distinct values of the field. - $query->clear('from') - ->from($db->quoteName($table)); - $db->setQuery($query); - - try - { - $values = $db->loadColumn(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $this->levelsInUse = array_merge($this->levelsInUse, $values); - - // @todo Could assemble an array of the tables used by each view level list those, - // giving the user a clue in the error where to look. - } - } - - // Get uniques. - $this->levelsInUse = array_unique($this->levelsInUse); - - // Ok, after all that we are ready to check the record :) - } - - if (in_array($record->id, $this->levelsInUse)) - { - $this->setError(Text::sprintf('COM_USERS_ERROR_VIEW_LEVEL_IN_USE', $record->id, $record->title)); - - return false; - } - - return parent::canDelete($record); - } - - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'ViewLevel', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - $return = Table::getInstance($type, $prefix, $config); - - return $return; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - $result = parent::getItem($pk); - - // Convert the params field to an array. - $result->rules = json_decode($result->rules); - - return $result; - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.level', 'level', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_users.edit.level.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_users.level', $data); - - return $data; - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = '') - { - // TO DO warning! - parent::preprocessForm($form, $data, 'user'); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - if (!isset($data['rules'])) - { - $data['rules'] = array(); - } - - $data['title'] = InputFilter::getInstance()->clean($data['title'], 'TRIM'); - - return parent::save($data); - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see \Joomla\CMS\Form\FormRule - * @see \JFilterInput - * @since 3.8.8 - */ - public function validate($form, $data, $group = null) - { - $isSuperAdmin = Factory::getUser()->authorise('core.admin'); - - // Non Super user should not be able to change the access levels of super user groups - if (!$isSuperAdmin) - { - if (!isset($data['rules']) || !is_array($data['rules'])) - { - $data['rules'] = array(); - } - - $groups = array_values(UserGroupsHelper::getInstance()->getAll()); - - $rules = array(); - - if (!empty($data['id'])) - { - $table = $this->getTable(); - - $table->load($data['id']); - - $rules = json_decode($table->rules); - } - - $rules = ArrayHelper::toInteger($rules); - - for ($i = 0, $n = count($groups); $i < $n; ++$i) - { - if (Access::checkGroup((int) $groups[$i]->id, 'core.admin')) - { - if (in_array((int) $groups[$i]->id, $rules) && !in_array((int) $groups[$i]->id, $data['rules'])) - { - $data['rules'][] = (int) $groups[$i]->id; - } - elseif (!in_array((int) $groups[$i]->id, $rules) && in_array((int) $groups[$i]->id, $data['rules'])) - { - $this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN')); - - return false; - } - } - } - } - - return parent::validate($form, $data, $group); - } + /** + * @var array A list of the access levels in use. + * @since 1.6 + */ + protected $levelsInUse = null; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + $groups = json_decode($record->rules); + + if ($groups === null) { + throw new \RuntimeException('Invalid rules schema'); + } + + $isAdmin = Factory::getUser()->authorise('core.admin'); + + // Check permissions + foreach ($groups as $group) { + if (!$isAdmin && Access::checkGroup($group, 'core.admin')) { + $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } + } + + // Check if the access level is being used by any content. + if ($this->levelsInUse === null) { + // Populate the list once. + $this->levelsInUse = array(); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('DISTINCT access'); + + // Get all the tables and the prefix + $tables = $db->getTableList(); + $prefix = $db->getPrefix(); + + foreach ($tables as $table) { + // Get all of the columns in the table + $fields = $db->getTableColumns($table); + + /** + * We are looking for the access field. If custom tables are using something other + * than the 'access' field they are on their own unfortunately. + * Also make sure the table prefix matches the live db prefix (eg, it is not a "bak_" table) + */ + if (strpos($table, $prefix) === 0 && isset($fields['access'])) { + // Lookup the distinct values of the field. + $query->clear('from') + ->from($db->quoteName($table)); + $db->setQuery($query); + + try { + $values = $db->loadColumn(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->levelsInUse = array_merge($this->levelsInUse, $values); + + // @todo Could assemble an array of the tables used by each view level list those, + // giving the user a clue in the error where to look. + } + } + + // Get uniques. + $this->levelsInUse = array_unique($this->levelsInUse); + + // Ok, after all that we are ready to check the record :) + } + + if (in_array($record->id, $this->levelsInUse)) { + $this->setError(Text::sprintf('COM_USERS_ERROR_VIEW_LEVEL_IN_USE', $record->id, $record->title)); + + return false; + } + + return parent::canDelete($record); + } + + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'ViewLevel', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + $return = Table::getInstance($type, $prefix, $config); + + return $return; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + $result = parent::getItem($pk); + + // Convert the params field to an array. + $result->rules = json_decode($result->rules); + + return $result; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.level', 'level', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_users.edit.level.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_users.level', $data); + + return $data; + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = '') + { + // TO DO warning! + parent::preprocessForm($form, $data, 'user'); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + if (!isset($data['rules'])) { + $data['rules'] = array(); + } + + $data['title'] = InputFilter::getInstance()->clean($data['title'], 'TRIM'); + + return parent::save($data); + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see \Joomla\CMS\Form\FormRule + * @see \JFilterInput + * @since 3.8.8 + */ + public function validate($form, $data, $group = null) + { + $isSuperAdmin = Factory::getUser()->authorise('core.admin'); + + // Non Super user should not be able to change the access levels of super user groups + if (!$isSuperAdmin) { + if (!isset($data['rules']) || !is_array($data['rules'])) { + $data['rules'] = array(); + } + + $groups = array_values(UserGroupsHelper::getInstance()->getAll()); + + $rules = array(); + + if (!empty($data['id'])) { + $table = $this->getTable(); + + $table->load($data['id']); + + $rules = json_decode($table->rules); + } + + $rules = ArrayHelper::toInteger($rules); + + for ($i = 0, $n = count($groups); $i < $n; ++$i) { + if (Access::checkGroup((int) $groups[$i]->id, 'core.admin')) { + if (in_array((int) $groups[$i]->id, $rules) && !in_array((int) $groups[$i]->id, $data['rules'])) { + $data['rules'][] = (int) $groups[$i]->id; + } elseif (!in_array((int) $groups[$i]->id, $rules) && in_array((int) $groups[$i]->id, $data['rules'])) { + $this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN')); + + return false; + } + } + } + } + + return parent::validate($form, $data, $group); + } } diff --git a/code/administrator/components/com_users/src/Model/LevelsModel.php b/code/administrator/components/com_users/src/Model/LevelsModel.php index 0a94fa59..cfb30a83 100644 --- a/code/administrator/components/com_users/src/Model/LevelsModel.php +++ b/code/administrator/components/com_users/src/Model/LevelsModel.php @@ -1,4 +1,5 @@ setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - $query->from($db->quoteName('#__viewlevels') . ' AS a'); - - // Add the level in the tree. - $query->group('a.id, a.title, a.ordering, a.rules'); - - // Filter the items over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . trim($search) . '%'; - $query->where('a.title LIKE :title') - ->bind(':title', $search); - } - } - - $query->group('a.id'); - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to adjust the ordering of a row. - * - * @param integer $pk The ID of the primary key to move. - * @param integer $direction Increment, usually +1 or -1 - * - * @return boolean False on failure or error, true otherwise. - */ - public function reorder($pk, $direction = 0) - { - // Sanitize the id and adjustment. - $pk = (!empty($pk)) ? $pk : (int) $this->getState('level.id'); - $user = Factory::getUser(); - - // Get an instance of the record's table. - $table = Table::getInstance('ViewLevel', 'Joomla\\CMS\Table\\'); - - // Load the row. - if (!$table->load($pk)) - { - $this->setError($table->getError()); - - return false; - } - - // Access checks. - $allow = $user->authorise('core.edit.state', 'com_users'); - - if (!$allow) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - - return false; - } - - // Move the row. - // @todo: Where clause to restrict category. - $table->move($pk); - - return true; - } - - /** - * Saves the manually set order of records. - * - * @param array $pks An array of primary key ids. - * @param integer $order Order position - * - * @return boolean Boolean true on success, boolean false - * - * @throws \Exception - */ - public function saveorder($pks, $order) - { - $table = Table::getInstance('viewlevel', 'Joomla\\CMS\Table\\'); - $user = Factory::getUser(); - $conditions = array(); - - if (empty($pks)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_ERROR_LEVELS_NOLEVELS_SELECTED'), 'error'); - - return false; - } - - // Update ordering values - foreach ($pks as $i => $pk) - { - $table->load((int) $pk); - - // Access checks. - $allow = $user->authorise('core.edit.state', 'com_users'); - - if (!$allow) - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - elseif ($table->ordering != $order[$i]) - { - $table->ordering = $order[$i]; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - } - - // Execute reorder for each category. - foreach ($conditions as $cond) - { - $table->load($cond[0]); - $table->reorder($cond[1]); - } - - return true; - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'ordering', 'a.ordering', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.ordering', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__viewlevels') . ' AS a'); + + // Add the level in the tree. + $query->group('a.id, a.title, a.ordering, a.rules'); + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . trim($search) . '%'; + $query->where('a.title LIKE :title') + ->bind(':title', $search); + } + } + + $query->group('a.id'); + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to adjust the ordering of a row. + * + * @param integer $pk The ID of the primary key to move. + * @param integer $direction Increment, usually +1 or -1 + * + * @return boolean False on failure or error, true otherwise. + */ + public function reorder($pk, $direction = 0) + { + // Sanitize the id and adjustment. + $pk = (!empty($pk)) ? $pk : (int) $this->getState('level.id'); + $user = Factory::getUser(); + + // Get an instance of the record's table. + $table = Table::getInstance('ViewLevel', 'Joomla\\CMS\Table\\'); + + // Load the row. + if (!$table->load($pk)) { + $this->setError($table->getError()); + + return false; + } + + // Access checks. + $allow = $user->authorise('core.edit.state', 'com_users'); + + if (!$allow) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + + return false; + } + + // Move the row. + // @todo: Where clause to restrict category. + $table->move($pk); + + return true; + } + + /** + * Saves the manually set order of records. + * + * @param array $pks An array of primary key ids. + * @param integer $order Order position + * + * @return boolean Boolean true on success, boolean false + * + * @throws \Exception + */ + public function saveorder($pks, $order) + { + $table = Table::getInstance('viewlevel', 'Joomla\\CMS\Table\\'); + $user = Factory::getUser(); + $conditions = array(); + + if (empty($pks)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_ERROR_LEVELS_NOLEVELS_SELECTED'), 'error'); + + return false; + } + + // Update ordering values + foreach ($pks as $i => $pk) { + $table->load((int) $pk); + + // Access checks. + $allow = $user->authorise('core.edit.state', 'com_users'); + + if (!$allow) { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } elseif ($table->ordering != $order[$i]) { + $table->ordering = $order[$i]; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } + } + + // Execute reorder for each category. + foreach ($conditions as $cond) { + $table->load($cond[0]); + $table->reorder($cond[1]); + } + + return true; + } } diff --git a/code/administrator/components/com_users/src/Model/MailModel.php b/code/administrator/components/com_users/src/Model/MailModel.php index f9e2b1f5..4f19f1d7 100644 --- a/code/administrator/components/com_users/src/Model/MailModel.php +++ b/code/administrator/components/com_users/src/Model/MailModel.php @@ -1,4 +1,5 @@ loadForm('com_users.mail', 'mail', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_users.display.mail.data', array()); - - $this->preprocessData('com_users.mail', $data); - - return $data; - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, $group); - } - - /** - * Send the email - * - * @return boolean - * - * @throws \Exception - */ - public function send() - { - $app = Factory::getApplication(); - $data = $app->input->post->get('jform', array(), 'array'); - $user = Factory::getUser(); - $access = new Access; - $db = $this->getDbo(); - $language = Factory::getLanguage(); - - $mode = array_key_exists('mode', $data) ? (int) $data['mode'] : 0; - $subject = array_key_exists('subject', $data) ? $data['subject'] : ''; - $grp = array_key_exists('group', $data) ? (int) $data['group'] : 0; - $recurse = array_key_exists('recurse', $data) ? (int) $data['recurse'] : 0; - $bcc = array_key_exists('bcc', $data) ? (int) $data['bcc'] : 0; - $disabled = array_key_exists('disabled', $data) ? (int) $data['disabled'] : 0; - $message_body = array_key_exists('message', $data) ? $data['message'] : ''; - - // Automatically removes html formatting - if (!$mode) - { - $message_body = InputFilter::getInstance()->clean($message_body, 'string'); - } - - // Check for a message body and subject - if (!$message_body || !$subject) - { - $app->setUserState('com_users.display.mail.data', $data); - $this->setError(Text::_('COM_USERS_MAIL_PLEASE_FILL_IN_THE_FORM_CORRECTLY')); - - return false; - } - - // Get users in the group out of the ACL, if group is provided. - $to = $grp !== 0 ? $access->getUsersByGroup($grp, $recurse) : array(); - - // When group is provided but no users are found in the group. - if ($grp !== 0 && !$to) - { - $rows = array(); - } - else - { - // Get all users email and group except for senders - $uid = (int) $user->id; - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('email'), - $db->quoteName('name'), - ] - ) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('id') . ' != :id') - ->bind(':id', $uid, ParameterType::INTEGER); - - if ($grp !== 0) - { - $query->whereIn($db->quoteName('id'), $to); - } - - if ($disabled === 0) - { - $query->where($db->quoteName('block') . ' = 0'); - } - - $db->setQuery($query); - $rows = $db->loadObjectList(); - } - - // Check to see if there are any users in this group before we continue - if (!$rows) - { - $app->setUserState('com_users.display.mail.data', $data); - - if (in_array($user->id, $to)) - { - $this->setError(Text::_('COM_USERS_MAIL_ONLY_YOU_COULD_BE_FOUND_IN_THIS_GROUP')); - } - else - { - $this->setError(Text::_('COM_USERS_MAIL_NO_USERS_COULD_BE_FOUND_IN_THIS_GROUP')); - } - - return false; - } - - // Get the Mailer - $mailer = new MailTemplate('com_users.massmail.mail', $language->getTag()); - $params = ComponentHelper::getParams('com_users'); - - try - { - // Build email message format. - $data = [ - 'subject' => stripslashes($subject), - 'body' => $message_body, - 'subjectprefix' => $params->get('mailSubjectPrefix', ''), - 'bodysuffix' => $params->get('mailBodySuffix', '') - ]; - $mailer->addTemplateData($data); - - $recipientType = $bcc ? 'bcc' : 'to'; - - // Add recipients - foreach ($rows as $row) - { - $mailer->addRecipient($row->email, $row->name, $recipientType); - } - - if ($bcc) - { - $mailer->addRecipient($app->get('mailfrom'), $app->get('fromname')); - } - - // Send the Mail - $rs = $mailer->send(); - } - catch (MailDisabledException | phpMailerException $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $rs = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $rs = false; - } - } - - // Check for an error - if ($rs !== true) - { - $app->setUserState('com_users.display.mail.data', $data); - $this->setError($mailer->ErrorInfo); - - return false; - } - elseif (empty($rs)) - { - $app->setUserState('com_users.display.mail.data', $data); - $this->setError(Text::_('COM_USERS_MAIL_THE_MAIL_COULD_NOT_BE_SENT')); - - return false; - } - else - { - /** - * Fill the data (specially for the 'mode', 'group' and 'bcc': they could not exist in the array - * when the box is not checked and in this case, the default value would be used instead of the '0' - * one) - */ - $data['mode'] = $mode; - $data['subject'] = $subject; - $data['group'] = $grp; - $data['recurse'] = $recurse; - $data['bcc'] = $bcc; - $data['message'] = $message_body; - $app->setUserState('com_users.display.mail.data', array()); - $app->enqueueMessage(Text::plural('COM_USERS_MAIL_EMAIL_SENT_TO_N_USERS', count($rows)), 'message'); - - return true; - } - } + /** + * Method to get the row form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.mail', 'mail', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_users.display.mail.data', array()); + + $this->preprocessData('com_users.mail', $data); + + return $data; + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, $group); + } + + /** + * Send the email + * + * @return boolean + * + * @throws \Exception + */ + public function send() + { + $app = Factory::getApplication(); + $data = $app->input->post->get('jform', array(), 'array'); + $user = Factory::getUser(); + $access = new Access(); + $db = $this->getDatabase(); + $language = Factory::getLanguage(); + + $mode = array_key_exists('mode', $data) ? (int) $data['mode'] : 0; + $subject = array_key_exists('subject', $data) ? $data['subject'] : ''; + $grp = array_key_exists('group', $data) ? (int) $data['group'] : 0; + $recurse = array_key_exists('recurse', $data) ? (int) $data['recurse'] : 0; + $bcc = array_key_exists('bcc', $data) ? (int) $data['bcc'] : 0; + $disabled = array_key_exists('disabled', $data) ? (int) $data['disabled'] : 0; + $message_body = array_key_exists('message', $data) ? $data['message'] : ''; + + // Automatically removes html formatting + if (!$mode) { + $message_body = InputFilter::getInstance()->clean($message_body, 'string'); + } + + // Check for a message body and subject + if (!$message_body || !$subject) { + $app->setUserState('com_users.display.mail.data', $data); + $this->setError(Text::_('COM_USERS_MAIL_PLEASE_FILL_IN_THE_FORM_CORRECTLY')); + + return false; + } + + // Get users in the group out of the ACL, if group is provided. + $to = $grp !== 0 ? $access->getUsersByGroup($grp, $recurse) : array(); + + // When group is provided but no users are found in the group. + if ($grp !== 0 && !$to) { + $rows = array(); + } else { + // Get all users email and group except for senders + $uid = (int) $user->id; + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('email'), + $db->quoteName('name'), + ] + ) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('id') . ' != :id') + ->bind(':id', $uid, ParameterType::INTEGER); + + if ($grp !== 0) { + $query->whereIn($db->quoteName('id'), $to); + } + + if ($disabled === 0) { + $query->where($db->quoteName('block') . ' = 0'); + } + + $db->setQuery($query); + $rows = $db->loadObjectList(); + } + + // Check to see if there are any users in this group before we continue + if (!$rows) { + $app->setUserState('com_users.display.mail.data', $data); + + if (in_array($user->id, $to)) { + $this->setError(Text::_('COM_USERS_MAIL_ONLY_YOU_COULD_BE_FOUND_IN_THIS_GROUP')); + } else { + $this->setError(Text::_('COM_USERS_MAIL_NO_USERS_COULD_BE_FOUND_IN_THIS_GROUP')); + } + + return false; + } + + // Get the Mailer + $mailer = new MailTemplate('com_users.massmail.mail', $language->getTag()); + $params = ComponentHelper::getParams('com_users'); + + try { + // Build email message format. + $data = [ + 'subject' => stripslashes($subject), + 'body' => $message_body, + 'subjectprefix' => $params->get('mailSubjectPrefix', ''), + 'bodysuffix' => $params->get('mailBodySuffix', '') + ]; + $mailer->addTemplateData($data); + + $recipientType = $bcc ? 'bcc' : 'to'; + + // Add recipients + foreach ($rows as $row) { + $mailer->addRecipient($row->email, $row->name, $recipientType); + } + + if ($bcc) { + $mailer->addRecipient($app->get('mailfrom'), $app->get('fromname')); + } + + // Send the Mail + $rs = $mailer->send(); + } catch (MailDisabledException | phpMailerException $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $rs = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $rs = false; + } + } + + // Check for an error + if ($rs !== true) { + $app->setUserState('com_users.display.mail.data', $data); + $this->setError($mailer->ErrorInfo); + + return false; + } elseif (empty($rs)) { + $app->setUserState('com_users.display.mail.data', $data); + $this->setError(Text::_('COM_USERS_MAIL_THE_MAIL_COULD_NOT_BE_SENT')); + + return false; + } else { + /** + * Fill the data (specially for the 'mode', 'group' and 'bcc': they could not exist in the array + * when the box is not checked and in this case, the default value would be used instead of the '0' + * one) + */ + $data['mode'] = $mode; + $data['subject'] = $subject; + $data['group'] = $grp; + $data['recurse'] = $recurse; + $data['bcc'] = $bcc; + $data['message'] = $message_body; + $app->setUserState('com_users.display.mail.data', array()); + $app->enqueueMessage(Text::plural('COM_USERS_MAIL_EMAIL_SENT_TO_N_USERS', count($rows)), 'message'); + + return true; + } + } } diff --git a/code/administrator/components/com_users/src/Model/MethodModel.php b/code/administrator/components/com_users/src/Model/MethodModel.php new file mode 100644 index 00000000..53760cec --- /dev/null +++ b/code/administrator/components/com_users/src/Model/MethodModel.php @@ -0,0 +1,261 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\Model; + +use Exception; +use Joomla\CMS\Event\MultiFactor\GetSetup; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Model\BaseDatabaseModel; +use Joomla\CMS\User\User; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Users\Administrator\DataShape\SetupRenderOptions; +use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; +use Joomla\Component\Users\Administrator\Table\MfaTable; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Multi-factor Authentication management model + * + * @since 4.2.0 + */ +class MethodModel extends BaseDatabaseModel +{ + /** + * List of MFA Methods + * + * @var array + * @since 4.2.0 + */ + protected $mfaMethods = null; + + /** + * Get the specified MFA Method's record + * + * @param string $method The Method to retrieve. + * + * @return array + * @since 4.2.0 + */ + public function getMethod(string $method): array + { + if (!$this->methodExists($method)) { + return [ + 'name' => $method, + 'display' => '', + 'shortinfo' => '', + 'image' => '', + 'canDisable' => true, + 'allowMultiple' => true, + ]; + } + + return $this->mfaMethods[$method]; + } + + /** + * Is the specified MFA Method available? + * + * @param string $method The Method to check. + * + * @return boolean + * @since 4.2.0 + */ + public function methodExists(string $method): bool + { + if (!is_array($this->mfaMethods)) { + $this->populateMfaMethods(); + } + + return isset($this->mfaMethods[$method]); + } + + /** + * @param User|null $user The user record. Null to use the currently logged in user. + * + * @return array + * @throws Exception + * + * @since 4.2.0 + */ + public function getRenderOptions(?User $user = null): SetupRenderOptions + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); + } + + $renderOptions = new SetupRenderOptions(); + + $event = new GetSetup($this->getRecord($user)); + $results = Factory::getApplication() + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + if (empty($results)) { + return $renderOptions; + } + + foreach ($results as $result) { + if (empty($result)) { + continue; + } + + return $renderOptions->merge($result); + } + + return $renderOptions; + } + + /** + * Get the specified MFA record. It will return a fake default record when no record ID is specified. + * + * @param User|null $user The user record. Null to use the currently logged in user. + * + * @return MfaTable + * @throws Exception + * + * @since 4.2.0 + */ + public function getRecord(User $user = null): MfaTable + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + $defaultRecord = $this->getDefaultRecord($user); + $id = (int) $this->getState('id', 0); + + if ($id <= 0) { + return $defaultRecord; + } + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + $loaded = $record->load( + [ + 'user_id' => $user->id, + 'id' => $id, + ] + ); + + if (!$loaded) { + return $defaultRecord; + } + + if (!$this->methodExists($record->method)) { + return $defaultRecord; + } + + return $record; + } + + /** + * Return the title to use for the page + * + * @return string + * + * @since 4.2.0 + */ + public function getPageTitle(): string + { + $task = $this->getState('task', 'edit'); + + switch ($task) { + case 'mfa': + $key = 'COM_USERS_USER_MULTIFACTOR_AUTH'; + break; + + default: + $key = sprintf('COM_USERS_MFA_%s_PAGE_HEAD', $task); + break; + } + + return Text::_($key); + } + + /** + * @param User|null $user The user record. Null to use the current user. + * + * @return MfaTable + * @throws Exception + * + * @since 4.2.0 + */ + protected function getDefaultRecord(?User $user = null): MfaTable + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + $method = $this->getState('method'); + $title = ''; + + if (is_null($this->mfaMethods)) { + $this->populateMfaMethods(); + } + + if ($method && isset($this->mfaMethods[$method])) { + $title = $this->mfaMethods[$method]['display']; + } + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + + $record->bind( + [ + 'id' => null, + 'user_id' => $user->id, + 'title' => $title, + 'method' => $method, + 'default' => 0, + 'options' => [], + ] + ); + + return $record; + } + + /** + * Populate the list of MFA Methods + * + * @return void + * @since 4.2.0 + */ + private function populateMfaMethods(): void + { + $this->mfaMethods = []; + $mfaMethods = MfaHelper::getMfaMethods(); + + if (empty($mfaMethods)) { + return; + } + + foreach ($mfaMethods as $method) { + $this->mfaMethods[$method['name']] = $method; + } + + // We also need to add the backup codes Method + $this->mfaMethods['backupcodes'] = [ + 'name' => 'backupcodes', + 'display' => Text::_('COM_USERS_USER_BACKUPCODES'), + 'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'), + 'image' => 'media/com_users/images/emergency.svg', + 'canDisable' => false, + 'allowMultiple' => false, + ]; + } +} diff --git a/code/administrator/components/com_users/src/Model/MethodsModel.php b/code/administrator/components/com_users/src/Model/MethodsModel.php new file mode 100644 index 00000000..1fffe951 --- /dev/null +++ b/code/administrator/components/com_users/src/Model/MethodsModel.php @@ -0,0 +1,225 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\Model; + +use DateInterval; +use DateTimeZone; +use Exception; +use Joomla\CMS\Date\Date; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Model\BaseDatabaseModel; +use Joomla\CMS\User\User; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; +use Joomla\Database\ParameterType; +use RuntimeException; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Multi-factor Authentication Methods list page's model + * + * @since 4.2.0 + */ +class MethodsModel extends BaseDatabaseModel +{ + /** + * Returns a list of all available MFA methods and their currently active records for a given user. + * + * @param User|null $user The user object. Skip to use the current user. + * + * @return array + * @throws Exception + * + * @since 4.2.0 + */ + public function getMethods(?User $user = null): array + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + if ($user->guest) { + return []; + } + + // Get an associative array of MFA Methods + $rawMethods = MfaHelper::getMfaMethods(); + $methods = []; + + foreach ($rawMethods as $method) { + $method['active'] = []; + $methods[$method['name']] = $method; + } + + // Put the user MFA records into the Methods array + $userMfaRecords = MfaHelper::getUserMfaRecords($user->id); + + if (!empty($userMfaRecords)) { + foreach ($userMfaRecords as $record) { + if (!isset($methods[$record->method])) { + continue; + } + + $methods[$record->method]->addActiveMethod($record); + } + } + + return $methods; + } + + /** + * Delete all Multi-factor Authentication Methods for the given user. + * + * @param User|null $user The user object to reset MFA for. Null to use the current user. + * + * @return void + * @throws Exception + * + * @since 4.2.0 + */ + public function deleteAll(?User $user = null): void + { + // Make sure we have a user object + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); + } + + // If the user object is a guest (who can't have MFA) we abort with an error + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $user->id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + } + + /** + * Format a relative timestamp. It deals with timestamps today and yesterday in a special manner. Example returns: + * Yesterday, 13:12 + * Today, 08:33 + * January 1, 2015 + * + * @param string $dateTimeText The database time string to use, e.g. "2017-01-13 13:25:36" + * + * @return string The formatted, human-readable date + * @throws Exception + * + * @since 4.2.0 + */ + public function formatRelative(?string $dateTimeText): string + { + if (empty($dateTimeText)) { + return Text::_('JNEVER'); + } + + // The timestamp is given in UTC. Make sure Joomla! parses it as such. + $utcTimeZone = new DateTimeZone('UTC'); + $jDate = new Date($dateTimeText, $utcTimeZone); + $unixStamp = $jDate->toUnix(); + + // I'm pretty sure we didn't have MFA in Joomla back in 1970 ;) + if ($unixStamp < 0) { + return Text::_('JNEVER'); + } + + // I need to display the date in the user's local timezone. That's how you do it. + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + $userTZ = $user->getParam('timezone', 'UTC'); + $tz = new DateTimeZone($userTZ); + $jDate->setTimezone($tz); + + // Default format string: way in the past, the time of the day is not important + $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_PAST'); + $containerString = Text::_('COM_USERS_MFA_LBL_PAST'); + + // If the timestamp is within the last 72 hours we may need a special format + if ($unixStamp > (time() - (72 * 3600))) { + // Is this timestamp today? + $jNow = new Date(); + $jNow->setTimezone($tz); + $checkNow = $jNow->format('Ymd', true); + $checkDate = $jDate->format('Ymd', true); + + if ($checkDate == $checkNow) { + $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_TODAY'); + $containerString = Text::_('COM_USERS_MFA_LBL_TODAY'); + } else { + // Is this timestamp yesterday? + $jYesterday = clone $jNow; + $jYesterday->setTime(0, 0, 0); + $oneSecond = new DateInterval('PT1S'); + $jYesterday->sub($oneSecond); + $checkYesterday = $jYesterday->format('Ymd', true); + + if ($checkDate == $checkYesterday) { + $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_YESTERDAY'); + $containerString = Text::_('COM_USERS_MFA_LBL_YESTERDAY'); + } + } + } + + return sprintf($containerString, $jDate->format($formatString, true)); + } + + /** + * Set the user's "don't show this again" flag. + * + * @param User $user The user to check + * @param bool $flag True to set the flag, false to unset it (it will be set to 0, actually) + * + * @return void + * + * @since 4.2.0 + */ + public function setFlag(User $user, bool $flag = true): void + { + $db = $this->getDatabase(); + $profileKey = 'mfa.dontshow'; + $query = $db->getQuery(true) + ->select($db->quoteName('profile_value')) + ->from($db->quoteName('#__user_profiles')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->where($db->quoteName('profile_key') . ' = :profileKey') + ->bind(':user_id', $user->id, ParameterType::INTEGER) + ->bind(':profileKey', $profileKey, ParameterType::STRING); + + try { + $result = $db->setQuery($query)->loadResult(); + } catch (Exception $e) { + return; + } + + $exists = !is_null($result); + + $object = (object) [ + 'user_id' => $user->id, + 'profile_key' => 'mfa.dontshow', + 'profile_value' => ($flag ? 1 : 0), + 'ordering' => 1, + ]; + + if (!$exists) { + $db->insertObject('#__user_profiles', $object); + } else { + $db->updateObject('#__user_profiles', $object, ['user_id', 'profile_key']); + } + } +} diff --git a/code/administrator/components/com_users/src/Model/NoteModel.php b/code/administrator/components/com_users/src/Model/NoteModel.php index 974f7a62..977ef9fa 100644 --- a/code/administrator/components/com_users/src/Model/NoteModel.php +++ b/code/administrator/components/com_users/src/Model/NoteModel.php @@ -1,4 +1,5 @@ loadForm('com_users.note', 'note', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 2.5 - * @throws \Exception - */ - public function getItem($pk = null) - { - $result = parent::getItem($pk); - - // Get the dispatcher and load the content plugins. - PluginHelper::importPlugin('content'); - - // Load the user plugins for backward compatibility (v3.3.3 and earlier). - PluginHelper::importPlugin('user'); - - // Trigger the data preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.note', $result)); - - return $result; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Get the application - $app = Factory::getApplication(); - - // Check the session for previously entered form data. - $data = $app->getUserState('com_users.edit.note.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Prime some default values. - if ($this->getState('note.id') == 0) - { - $data->set('catid', $app->input->get('catid', $app->getUserState('com_users.notes.filter.category_id'), 'int')); - } - - $userId = $app->input->get('u_id', 0, 'int'); - - if ($userId != 0) - { - $data->user_id = $userId; - } - } - - $this->preprocessData('com_users.note', $data); - - return $data; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 2.5 - * @throws \Exception - */ - protected function populateState() - { - parent::populateState(); - - $userId = Factory::getApplication()->input->get('u_id', 0, 'int'); - $this->setState('note.user_id', $userId); - } + use VersionableModelTrait; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_users.note'; + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|bool A Form object on success, false on failure + * + * @since 2.5 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.note', 'note', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 2.5 + * @throws \Exception + */ + public function getItem($pk = null) + { + $result = parent::getItem($pk); + + // Get the dispatcher and load the content plugins. + PluginHelper::importPlugin('content'); + + // Load the user plugins for backward compatibility (v3.3.3 and earlier). + PluginHelper::importPlugin('user'); + + // Trigger the data preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.note', $result)); + + return $result; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Get the application + $app = Factory::getApplication(); + + // Check the session for previously entered form data. + $data = $app->getUserState('com_users.edit.note.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Prime some default values. + if ($this->getState('note.id') == 0) { + $data->set('catid', $app->input->get('catid', $app->getUserState('com_users.notes.filter.category_id'), 'int')); + } + + $userId = $app->input->get('u_id', 0, 'int'); + + if ($userId != 0) { + $data->user_id = $userId; + } + } + + $this->preprocessData('com_users.note', $data); + + return $data; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 2.5 + * @throws \Exception + */ + protected function populateState() + { + parent::populateState(); + + $userId = Factory::getApplication()->input->get('u_id', 0, 'int'); + $this->setState('note.user_id', $userId); + } } diff --git a/code/administrator/components/com_users/src/Model/NotesModel.php b/code/administrator/components/com_users/src/Model/NotesModel.php index b9ef0a6c..c4d7c7d5 100644 --- a/code/administrator/components/com_users/src/Model/NotesModel.php +++ b/code/administrator/components/com_users/src/Model/NotesModel.php @@ -1,4 +1,5 @@ getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState('list.select', - 'a.id, a.subject, a.checked_out, a.checked_out_time,' . - 'a.catid, a.created_time, a.review_time,' . - 'a.state, a.publish_up, a.publish_down' - ) - ); - $query->from('#__user_notes AS a'); - - // Join over the category - $query->select('c.title AS category_title, c.params AS category_params') - ->join('LEFT', '#__categories AS c ON c.id = a.catid'); - - // Join over the users for the note user. - $query->select('u.name AS user_name') - ->join('LEFT', '#__users AS u ON u.id = a.user_id'); - - // Join over the users for the checked out user. - $query->select('uc.name AS editor') - ->join('LEFT', '#__users AS uc ON uc.id = a.checked_out'); - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search3 = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $search3, ParameterType::INTEGER); - } - elseif (stripos($search, 'uid:') === 0) - { - $search4 = (int) substr($search, 4); - $query->where($db->quoteName('a.user_id') . ' = :id'); - $query->bind(':id', $search4, ParameterType::INTEGER); - } - else - { - $search = '%' . trim($search) . '%'; - $query->where( - '(' . $db->quoteName('a.subject') . ' LIKE :subject' - . ' OR ' . $db->quoteName('u.name') . ' LIKE :name' - . ' OR ' . $db->quoteName('u.username') . ' LIKE :username)' - ); - $query->bind(':subject', $search); - $query->bind(':name', $search); - $query->bind(':username', $search); - } - } - - // Filter by published state - $published = $this->getState('filter.published'); - - if (is_numeric($published)) - { - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $published, ParameterType::INTEGER); - } - elseif ($published !== '*') - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - } - - // Filter by a single category. - $categoryId = (int) $this->getState('filter.category_id'); - - if ($categoryId) - { - $query->where($db->quoteName('a.catid') . ' = :catid') - ->bind(':catid', $categoryId, ParameterType::INTEGER); - } - - // Filter by a single user. - $userId = (int) $this->getState('filter.user_id'); - - if ($userId) - { - // Add the body and where filter. - $query->select('a.body') - ->where($db->quoteName('a.user_id') . ' = :user_id') - ->bind(':user_id', $userId, ParameterType::INTEGER); - } - - // Filter on the level. - if ($level = $this->getState('filter.level')) - { - $level = (int) $level; - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.review_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.category_id'); - $id .= ':' . $this->getState('filter.user_id'); - $id .= ':' . $this->getState('filter.level'); - - return parent::getStoreId($id); - } - - /** - * Gets a user object if the user filter is set. - * - * @return User The User object - * - * @since 2.5 - */ - public function getUser() - { - $user = new User; - - // Filter by search in title - $search = (int) $this->getState('filter.user_id'); - - if ($search != 0) - { - $user->load((int) $search); - } - - return $user; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState($ordering = 'a.review_time', $direction = 'desc') - { - // Adjust the context to support modal layouts. - if ($layout = Factory::getApplication()->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - parent::populateState($ordering, $direction); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + // Set the list ordering fields. + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'user_id', 'a.user_id', + 'u.name', + 'subject', 'a.subject', + 'catid', 'a.catid', 'category_id', + 'state', 'a.state', 'published', + 'c.title', + 'review_time', 'a.review_time', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'level', 'c.level', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery A DatabaseQuery object to retrieve the data set. + * + * @since 2.5 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.subject, a.checked_out, a.checked_out_time,' . + 'a.catid, a.created_time, a.review_time,' . + 'a.state, a.publish_up, a.publish_down' + ) + ); + $query->from('#__user_notes AS a'); + + // Join over the category + $query->select('c.title AS category_title, c.params AS category_params') + ->join('LEFT', '#__categories AS c ON c.id = a.catid'); + + // Join over the users for the note user. + $query->select('u.name AS user_name') + ->join('LEFT', '#__users AS u ON u.id = a.user_id'); + + // Join over the users for the checked out user. + $query->select('uc.name AS editor') + ->join('LEFT', '#__users AS uc ON uc.id = a.checked_out'); + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search3 = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $search3, ParameterType::INTEGER); + } elseif (stripos($search, 'uid:') === 0) { + $search4 = (int) substr($search, 4); + $query->where($db->quoteName('a.user_id') . ' = :id'); + $query->bind(':id', $search4, ParameterType::INTEGER); + } else { + $search = '%' . trim($search) . '%'; + $query->where( + '(' . $db->quoteName('a.subject') . ' LIKE :subject' + . ' OR ' . $db->quoteName('u.name') . ' LIKE :name' + . ' OR ' . $db->quoteName('u.username') . ' LIKE :username)' + ); + $query->bind(':subject', $search); + $query->bind(':name', $search); + $query->bind(':username', $search); + } + } + + // Filter by published state + $published = $this->getState('filter.published'); + + if (is_numeric($published)) { + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $published, ParameterType::INTEGER); + } elseif ($published !== '*') { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + } + + // Filter by a single category. + $categoryId = (int) $this->getState('filter.category_id'); + + if ($categoryId) { + $query->where($db->quoteName('a.catid') . ' = :catid') + ->bind(':catid', $categoryId, ParameterType::INTEGER); + } + + // Filter by a single user. + $userId = (int) $this->getState('filter.user_id'); + + if ($userId) { + // Add the body and where filter. + $query->select('a.body') + ->where($db->quoteName('a.user_id') . ' = :user_id') + ->bind(':user_id', $userId, ParameterType::INTEGER); + } + + // Filter on the level. + if ($level = $this->getState('filter.level')) { + $level = (int) $level; + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.review_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.category_id'); + $id .= ':' . $this->getState('filter.user_id'); + $id .= ':' . $this->getState('filter.level'); + + return parent::getStoreId($id); + } + + /** + * Gets a user object if the user filter is set. + * + * @return User The User object + * + * @since 2.5 + */ + public function getUser() + { + $user = new User(); + + // Filter by search in title + $search = (int) $this->getState('filter.user_id'); + + if ($search != 0) { + $user->load((int) $search); + } + + return $user; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState($ordering = 'a.review_time', $direction = 'desc') + { + // Adjust the context to support modal layouts. + if ($layout = Factory::getApplication()->input->get('layout')) { + $this->context .= '.' . $layout; + } + + parent::populateState($ordering, $direction); + } } diff --git a/code/administrator/components/com_users/src/Model/UserModel.php b/code/administrator/components/com_users/src/Model/UserModel.php index d0003a0b..3f857422 100644 --- a/code/administrator/components/com_users/src/Model/UserModel.php +++ b/code/administrator/components/com_users/src/Model/UserModel.php @@ -1,4 +1,5 @@ 'onUserAfterDelete', - 'event_after_save' => 'onUserAfterSave', - 'event_before_delete' => 'onUserBeforeDelete', - 'event_before_save' => 'onUserBeforeSave', - 'events_map' => array('save' => 'user', 'delete' => 'user', 'validate' => 'user') - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'User', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - $table = Table::getInstance($type, $prefix, $config); - - return $table; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('user.id'); - - if ($this->_item === null) - { - $this->_item = array(); - } - - if (!isset($this->_item[$pk])) - { - $this->_item[$pk] = parent::getItem($pk); - } - - return $this->_item[$pk]; - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.user', 'user', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - $user = Factory::getUser(); - - // If the user needs to change their password, mark the password fields as required - if ($user->requireReset) - { - $form->setFieldAttribute('password', 'required', 'true'); - $form->setFieldAttribute('password2', 'required', 'true'); - } - - // When multilanguage is set, a user's default site language should also be a Content Language - if (Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); - } - - $userId = (int) $form->getValue('id'); - - // The user should not be able to set the requireReset value on their own account - if ($userId === (int) $user->id) - { - $form->removeField('requireReset'); - } - - /** - * If users without core.manage permission editing their own account, remove some fields which they should - * not be allowed to change and prevent them to change user name if configured - */ - if (!$user->authorise('core.manage', 'com_users') && (int) $user->id === $userId) - { - if (!ComponentHelper::getParams('com_users')->get('change_login_name')) - { - $form->setFieldAttribute('username', 'required', 'false'); - $form->setFieldAttribute('username', 'readonly', 'true'); - $form->setFieldAttribute('username', 'description', 'COM_USERS_USER_FIELD_NOCHANGE_USERNAME_DESC'); - } - - $form->removeField('lastResetTime'); - $form->removeField('resetCount'); - $form->removeField('sendEmail'); - $form->removeField('block'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_users.edit.user.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_users.profile', $data, 'user'); - - return $data; - } - - /** - * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - * @throws \Exception - */ - public function save($data) - { - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id'); - $user = User::getInstance($pk); - - $my = Factory::getUser(); - $iAmSuperAdmin = $my->authorise('core.admin'); - - // User cannot modify own user groups - if ((int) $user->id == (int) $my->id && !$iAmSuperAdmin && isset($data['groups'])) - { - // Form was probably tampered with - Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_EDIT_OWN_GROUP'), 'warning'); - - $data['groups'] = null; - } - - if ($data['block'] && $pk == $my->id && !$my->block) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF')); - - return false; - } - - // Make sure user groups is selected when add/edit an account - if (empty($data['groups']) && ((int) $user->id != (int) $my->id || $iAmSuperAdmin)) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_SAVE_ACCOUNT_WITHOUT_GROUPS')); - - return false; - } - - // Make sure that we are not removing ourself from Super Admin group - if ($iAmSuperAdmin && $my->get('id') == $pk) - { - // Check that at least one of our new groups is Super Admin - $stillSuperAdmin = false; - $myNewGroups = $data['groups']; - - foreach ($myNewGroups as $group) - { - $stillSuperAdmin = $stillSuperAdmin ?: Access::checkGroup($group, 'core.admin'); - } - - if (!$stillSuperAdmin) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DEMOTE_SELF')); - - return false; - } - } - - // Handle the two factor authentication setup - if (isset($data['twofactor']['method'])) - { - $twoFactorMethod = $data['twofactor']['method']; - - // Get the current One Time Password (two factor auth) configuration - $otpConfig = $this->getOtpConfig($pk); - - if ($twoFactorMethod != 'none') - { - // Run the plugins - PluginHelper::importPlugin('twofactorauth'); - $otpConfigReplies = Factory::getApplication()->triggerEvent('onUserTwofactorApplyConfiguration', array($twoFactorMethod)); - - // Look for a valid reply - foreach ($otpConfigReplies as $reply) - { - if (!is_object($reply) || empty($reply->method) || ($reply->method != $twoFactorMethod)) - { - continue; - } - - $otpConfig->method = $reply->method; - $otpConfig->config = $reply->config; - - break; - } - - // Save OTP configuration. - $this->setOtpConfig($pk, $otpConfig); - - // Generate one time emergency passwords if required (depleted or not set) - if (empty($otpConfig->otep)) - { - $oteps = $this->generateOteps($pk); - } - } - else - { - $otpConfig->method = 'none'; - $otpConfig->config = array(); - $this->setOtpConfig($pk, $otpConfig); - } - - // Unset the raw data - unset($data['twofactor']); - - // Reload the user record with the updated OTP configuration - $user->load($pk); - } - - // Bind the data. - if (!$user->bind($data)) - { - $this->setError($user->getError()); - - return false; - } - - // Store the data. - if (!$user->save()) - { - $this->setError($user->getError()); - - return false; - } - - // Destroy all active sessions for the user after changing the password or blocking him - if ($data['password2'] || $data['block']) - { - UserHelper::destroyUserSessions($user->id, true); - } - - $this->setState('user.id', $user->id); - - return true; - } - - /** - * Method to delete rows. - * - * @param array &$pks An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function delete(&$pks) - { - $user = Factory::getUser(); - $table = $this->getTable(); - $pks = (array) $pks; - - // Check if I am a Super Admin - $iAmSuperAdmin = $user->authorise('core.admin'); - - PluginHelper::importPlugin($this->events_map['delete']); - - if (in_array($user->id, $pks)) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DELETE_SELF')); - - return false; - } - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - // Access checks. - $allow = $user->authorise('core.delete', 'com_users'); - - // Don't allow non-super-admin to delete a super admin - $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; - - if ($allow) - { - // Get users data for the users to delete. - $user_to_delete = Factory::getUser($pk); - - // Fire the before delete event. - Factory::getApplication()->triggerEvent($this->event_before_delete, array($table->getProperties())); - - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - else - { - // Trigger the after delete event. - Factory::getApplication()->triggerEvent($this->event_after_delete, array($user_to_delete->getProperties(), true, $this->getError())); - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - return true; - } - - /** - * Method to block user records. - * - * @param array &$pks The ids of the items to publish. - * @param integer $value The value of the published state - * - * @return boolean True on success. - * - * @since 1.6 - * @throws \Exception - */ - public function block(&$pks, $value = 1) - { - $app = Factory::getApplication(); - $user = Factory::getUser(); - - // Check if I am a Super Admin - $iAmSuperAdmin = $user->authorise('core.admin'); - $table = $this->getTable(); - $pks = (array) $pks; - - PluginHelper::importPlugin($this->events_map['save']); - - // Prepare the logout options. - $options = array( - 'clientid' => $app->get('shared_session', '0') ? null : 0, - ); - - // Access checks. - foreach ($pks as $i => $pk) - { - if ($value == 1 && $pk == $user->get('id')) - { - // Cannot block yourself. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF'), 'error'); - } - elseif ($table->load($pk)) - { - $old = $table->getProperties(); - $allow = $user->authorise('core.edit.state', 'com_users'); - - // Don't allow non-super-admin to delete a super admin - $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; - - if ($allow) - { - // Skip changing of same state - if ($table->block == $value) - { - unset($pks[$i]); - continue; - } - - $table->block = (int) $value; - - // If unblocking, also change password reset count to zero to unblock reset - if ($table->block === 0) - { - $table->resetCount = 0; - } - - // Allow an exception to be thrown. - try - { - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($old, false, $table->getProperties())); - - if (in_array(false, $result, true)) - { - // Plugin will have to raise its own error or throw an exception. - return false; - } - - // Store the table. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - if ($table->block) - { - UserHelper::destroyUserSessions($table->id); - } - - // Trigger the after save event - Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Log the user out. - if ($value) - { - $app->logout($table->id, $options); - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - } - } - - return true; - } - - /** - * Method to activate user records. - * - * @param array &$pks The ids of the items to activate. - * - * @return boolean True on success. - * - * @since 1.6 - * @throws \Exception - */ - public function activate(&$pks) - { - $user = Factory::getUser(); - - // Check if I am a Super Admin - $iAmSuperAdmin = $user->authorise('core.admin'); - $table = $this->getTable(); - $pks = (array) $pks; - - PluginHelper::importPlugin($this->events_map['save']); - - // Access checks. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - $old = $table->getProperties(); - $allow = $user->authorise('core.edit.state', 'com_users'); - - // Don't allow non-super-admin to delete a super admin - $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; - - if (empty($table->activation)) - { - // Ignore activated accounts. - unset($pks[$i]); - } - elseif ($allow) - { - $table->block = 0; - $table->activation = ''; - - // Allow an exception to be thrown. - try - { - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($old, false, $table->getProperties())); - - if (in_array(false, $result, true)) - { - // Plugin will have to raise it's own error or throw an exception. - return false; - } - - // Store the table. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Fire the after save event - Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - } - } - - return true; - } - - /** - * Method to perform batch operations on an item or a set of items. - * - * @param array $commands An array of commands to perform. - * @param array $pks An array of item ids. - * @param array $contexts An array of item contexts. - * - * @return boolean Returns true on success, false on failure. - * - * @since 2.5 - */ - public function batch($commands, $pks, $contexts) - { - // Sanitize user ids. - $pks = array_unique($pks); - $pks = ArrayHelper::toInteger($pks); - - // Remove any values of zero. - if (array_search(0, $pks, true)) - { - unset($pks[array_search(0, $pks, true)]); - } - - if (empty($pks)) - { - $this->setError(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED')); - - return false; - } - - $done = false; - - if (!empty($commands['group_id'])) - { - $cmd = ArrayHelper::getValue($commands, 'group_action', 'add'); - - if (!$this->batchUser((int) $commands['group_id'], $pks, $cmd)) - { - return false; - } - - $done = true; - } - - if (!empty($commands['reset_id'])) - { - if (!$this->batchReset($pks, $commands['reset_id'])) - { - return false; - } - - $done = true; - } - - if (!$done) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_INSUFFICIENT_BATCH_INFORMATION')); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Batch flag users as being required to reset their passwords - * - * @param array $userIds An array of user IDs on which to operate - * @param string $action The action to perform - * - * @return boolean True on success, false on failure - * - * @since 3.2 - */ - public function batchReset($userIds, $action) - { - $userIds = ArrayHelper::toInteger($userIds); - - // Check if I am a Super Admin - $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); - - // Non-super super user cannot work with super-admin user. - if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) - { - $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER')); - - return false; - } - - // Set the action to perform - if ($action === 'yes') - { - $value = 1; - } - else - { - $value = 0; - } - - // Prune out the current user if they are in the supplied user ID array - $userIds = array_diff($userIds, array(Factory::getUser()->id)); - - if (empty($userIds)) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_REQUIRERESET_SELF')); - - return false; - } - - // Get the DB object - $db = $this->getDbo(); - - $userIds = ArrayHelper::toInteger($userIds); - - $query = $db->getQuery(true); - - // Update the reset flag - $query->update($db->quoteName('#__users')) - ->set($db->quoteName('requireReset') . ' = :requireReset') - ->whereIn($db->quoteName('id'), $userIds) - ->bind(':requireReset', $value, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return true; - } - - /** - * Perform batch operations - * - * @param integer $groupId The group ID which assignments are being edited - * @param array $userIds An array of user IDs on which to operate - * @param string $action The action to perform - * - * @return boolean True on success, false on failure - * - * @since 1.6 - */ - public function batchUser($groupId, $userIds, $action) - { - $userIds = ArrayHelper::toInteger($userIds); - - // Check if I am a Super Admin - $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); - - // Non-super super user cannot work with super-admin user. - if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) - { - $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER')); - - return false; - } - - // Non-super admin cannot work with super-admin group. - if ((!$iAmSuperAdmin && Access::checkGroup($groupId, 'core.admin')) || $groupId < 1) - { - $this->setError(Text::_('COM_USERS_ERROR_INVALID_GROUP')); - - return false; - } - - // Get the DB object - $db = $this->getDbo(); - - switch ($action) - { - // Sets users to a selected group - case 'set': - $doDelete = 'all'; - $doAssign = true; - break; - - // Remove users from a selected group - case 'del': - $doDelete = 'group'; - break; - - // Add users to a selected group - case 'add': - default: - $doAssign = true; - break; - } - - // Remove the users from the group if requested. - if (isset($doDelete)) - { - $query = $db->getQuery(true); - - // Remove users from the group - $query->delete($db->quoteName('#__user_usergroup_map')) - ->whereIn($db->quoteName('user_id'), $userIds); - - // Only remove users from selected group - if ($doDelete == 'group') - { - $query->where($db->quoteName('group_id') . ' = :group_id') - ->bind(':group_id', $groupId, ParameterType::INTEGER); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - // Assign the users to the group if requested. - if (isset($doAssign)) - { - $query = $db->getQuery(true); - - // First, we need to check if the user is already assigned to a group - $query->select($db->quoteName('user_id')) - ->from($db->quoteName('#__user_usergroup_map')) - ->where($db->quoteName('group_id') . ' = :group_id') - ->bind(':group_id', $groupId, ParameterType::INTEGER); - $db->setQuery($query); - $users = $db->loadColumn(); - - // Build the values clause for the assignment query. - $query->clear(); - $groups = false; - - foreach ($userIds as $id) - { - if (!in_array($id, $users)) - { - $query->values($id . ',' . $groupId); - $groups = true; - } - } - - // If we have no users to process, throw an error to notify the user - if (!$groups) - { - $this->setError(Text::_('COM_USERS_ERROR_NO_ADDITIONS')); - - return false; - } - - $query->insert($db->quoteName('#__user_usergroup_map')) - ->columns(array($db->quoteName('user_id'), $db->quoteName('group_id'))); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - return true; - } - - /** - * Gets the available groups. - * - * @return array An array of groups - * - * @since 1.6 - */ - public function getGroups() - { - $user = Factory::getUser(); - - if ($user->authorise('core.edit', 'com_users') && $user->authorise('core.manage', 'com_users')) - { - $model = $this->bootComponent('com_users') - ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]); - - return $model->getItems(); - } - else - { - return null; - } - } - - /** - * Gets the groups this object is assigned to - * - * @param integer $userId The user ID to retrieve the groups for - * - * @return array An array of assigned groups - * - * @since 1.6 - */ - public function getAssignedGroups($userId = null) - { - $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id'); - - if (empty($userId)) - { - $result = array(); - $form = $this->getForm(); - - if ($form) - { - $groupsIDs = $form->getValue('groups'); - } - - if (!empty($groupsIDs)) - { - $result = $groupsIDs; - } - else - { - $params = ComponentHelper::getParams('com_users'); - - if ($groupId = $params->get('new_usertype', $params->get('guest_usergroup', 1))) - { - $result[] = $groupId; - } - } - } - else - { - $result = UserHelper::getUserGroups($userId); - } - - return $result; - } - - /** - * Returns the one time password (OTP) – a.k.a. two factor authentication – - * configuration for a particular user. - * - * @param integer $userId The numeric ID of the user - * - * @return \stdClass An object holding the OTP configuration for this user - * - * @since 3.2 - */ - public function getOtpConfig($userId = null) - { - $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id'); - - // Initialise - $otpConfig = (object) array( - 'method' => 'none', - 'config' => array(), - 'otep' => array() - ); - - /** - * Get the raw data, without going through User (required in order to - * be able to modify the user record before logging in the user). - */ - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__users')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $userId, ParameterType::INTEGER); - $db->setQuery($query); - $item = $db->loadObject(); - - // Make sure this user does have OTP enabled - if (empty($item->otpKey)) - { - return $otpConfig; - } - - // Get the encrypted data - list($method, $config) = explode(':', $item->otpKey, 2); - $encryptedOtep = $item->otep; - - // Get the secret key, yes the thing that is saved in the configuration file - $key = $this->getOtpConfigEncryptionKey(); - - // Cleanup old encryption methods, and convert to using openssl as the adapter to use. - if (strpos($config, '{') === false) - { - /** - * This part of the if statement block of code has been reviewed just before 4.0.0 release and determined that it is wrong, - * and has never worked. - * - * The aim is/was to migrate away from mcrypt encrypted data by decrypting the data and then re-encrypting - * it with the openssl adapter, but there has been a bug for a long time in the constructing of the - * mcrypt Aes class, where the number of parameters passed were wrong, meaning it was actually returning - * an openssl adapter not an mcrypt one. - * - * Rather than fix this just before 4.0.0 release, we will deprecate this block and remove it in 5.0.0 - * - * @deprecated 4.0.0 Will be removed in 5.0.0 - always use the openssl (default) adapter with the Aes class from now on. - */ - - // We use the openssl adapter by default now. - $openssl = new Aes($key, 256); - - /** - * Deal with legacy mcrypt encrypted data - * NOTE THIS NEXT LINE IS WRONG and contains wrong number of params, thus returns the openssl adapter and not the mcrypt adapter. - */ - $mcrypt = new Aes($key, 256, 'cbc', null, 'mcrypt'); - - // Attempt to decrypt using the mcrypt adapter, under normal circumstances this should fail (We no longer use mcrypt adapter to encrypt). - $decryptedConfig = $mcrypt->decryptString($config); - - // If we were able to decrypt using the mcrypt adapter, { will be in the config (JSON String), so lets update to openssl adapter use. - if (strpos($decryptedConfig, '{') !== false) - { - // Data encrypted with mcrypt, decrypt it, and then convert to openssl. - $decryptedOtep = $mcrypt->decryptString($encryptedOtep); - $encryptedOtep = $openssl->encryptString($decryptedOtep); - } - else - { - // Config data seems to be save encrypted, this can happen with 3.6.3 and openssl, lets get the data. - $decryptedConfig = $openssl->decryptString($config); - } - - $otpKey = $method . ':' . $decryptedConfig; - - $query = $db->getQuery(true) - ->update($db->quoteName('#__users')) - ->set($db->quoteName('otep') . ' = :otep') - ->set($db->quoteName('otpKey') . ' = :otpKey') - ->where($db->quoteName('id') . ' = :id') - ->bind(':otep', $encryptedOtep) - ->bind(':otpKey', $otpKey) - ->bind(':id', $userId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } - else - { - $decryptedConfig = $config; - } - - // Create an encryptor class - $aes = new Aes($key, 256); - - // Decrypt the data - $decryptedOtep = $aes->decryptString($encryptedOtep); - - // Remove the null padding added during encryption - $decryptedConfig = rtrim($decryptedConfig, "\0"); - $decryptedOtep = rtrim($decryptedOtep, "\0"); - - // Update the configuration object - $otpConfig->method = $method; - $otpConfig->config = @json_decode($decryptedConfig); - $otpConfig->otep = @json_decode($decryptedOtep); - - /* - * If the decryption failed for any reason we essentially disable the - * two-factor authentication. This prevents impossible to log in sites - * if the site admin changes the site secret for any reason. - */ - if (is_null($otpConfig->config)) - { - $otpConfig->config = array(); - } - - if (is_object($otpConfig->config)) - { - $otpConfig->config = (array) $otpConfig->config; - } - - if (is_null($otpConfig->otep)) - { - $otpConfig->otep = array(); - } - - if (is_object($otpConfig->otep)) - { - $otpConfig->otep = (array) $otpConfig->otep; - } - - // Return the configuration object - return $otpConfig; - } - - /** - * Sets the one time password (OTP) – a.k.a. two factor authentication – - * configuration for a particular user. The $otpConfig object is the same as - * the one returned by the getOtpConfig method. - * - * @param integer $userId The numeric ID of the user - * @param \stdClass $otpConfig The OTP configuration object - * - * @return boolean True on success - * - * @since 3.2 - */ - public function setOtpConfig($userId, $otpConfig) - { - $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id'); - - $updates = (object) array( - 'id' => $userId, - 'otpKey' => '', - 'otep' => '' - ); - - // Create an encryptor class - $key = $this->getOtpConfigEncryptionKey(); - $aes = new Aes($key, 256); - - // Create the encrypted option strings - if (!empty($otpConfig->method) && ($otpConfig->method != 'none')) - { - $decryptedConfig = json_encode($otpConfig->config); - $decryptedOtep = json_encode($otpConfig->otep); - $updates->otpKey = $otpConfig->method . ':' . $decryptedConfig; - $updates->otep = $aes->encryptString($decryptedOtep); - } - - $db = $this->getDbo(); - $result = $db->updateObject('#__users', $updates, 'id'); - - return $result; - } - - /** - * Gets the symmetric encryption key for the OTP configuration data. It - * currently returns the site's secret. - * - * @return string The encryption key - * - * @since 3.2 - */ - public function getOtpConfigEncryptionKey() - { - return Factory::getApplication()->get('secret'); - } - - /** - * Gets the configuration forms for all two-factor authentication methods - * in an array. - * - * @param integer $userId The user ID to load the forms for (optional) - * - * @return array - * - * @since 3.2 - * @throws \Exception - */ - public function getTwofactorform($userId = null) - { - $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id'); - - $otpConfig = $this->getOtpConfig($userId); - - PluginHelper::importPlugin('twofactorauth'); - - return Factory::getApplication()->triggerEvent('onUserTwofactorShowConfiguration', array($otpConfig, $userId)); - } - - /** - * Generates a new set of One Time Emergency Passwords (OTEPs) for a given user. - * - * @param integer $userId The user ID - * @param integer $count How many OTEPs to generate? Default: 10 - * - * @return array The generated OTEPs - * - * @since 3.2 - */ - public function generateOteps($userId, $count = 10) - { - $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id'); - - // Initialise - $oteps = array(); - - // Get the OTP configuration for the user - $otpConfig = $this->getOtpConfig($userId); - - // If two factor authentication is not enabled, abort - if (empty($otpConfig->method) || ($otpConfig->method == 'none')) - { - return $oteps; - } - - $salt = '0123456789'; - $base = strlen($salt); - $length = 16; - - for ($i = 0; $i < $count; $i++) - { - $makepass = ''; - $random = Crypt::genRandomBytes($length + 1); - $shift = ord($random[0]); - - for ($j = 1; $j <= $length; ++$j) - { - $makepass .= $salt[($shift + ord($random[$j])) % $base]; - $shift += ord($random[$j]); - } - - $oteps[] = $makepass; - } - - $otpConfig->otep = $oteps; - - // Save the now modified OTP configuration - $this->setOtpConfig($userId, $otpConfig); - - return $oteps; - } - - /** - * Checks if the provided secret key is a valid two factor authentication - * secret key. If not, it will check it against the list of one time - * emergency passwords (OTEPs). If it's a valid OTEP it will also remove it - * from the user's list of OTEPs. - * - * This method will return true in the following conditions: - * - The two factor authentication is not enabled - * - You have provided a valid secret key for - * - You have provided a valid OTEP - * - * You can define the following options in the $options array: - * otp_config The OTP (one time password, a.k.a. two factor auth) - * configuration object. If not set we'll load it automatically. - * warn_if_not_req Issue a warning if you are checking a secret key against - * a user account which doesn't have any two factor - * authentication method enabled. - * warn_irq_msg The string to use for the warn_if_not_req warning - * - * @param integer $userId The user's numeric ID - * @param string $secretKey The secret key you want to check - * @param array $options Options; see above - * - * @return boolean True if it's a valid secret key for this user. - * - * @since 3.2 - * @throws \Exception - */ - public function isValidSecretKey($userId, $secretKey, $options = array()) - { - // Load the user's OTP (one time password, a.k.a. two factor auth) configuration - if (!array_key_exists('otp_config', $options)) - { - $otpConfig = $this->getOtpConfig($userId); - $options['otp_config'] = $otpConfig; - } - else - { - $otpConfig = $options['otp_config']; - } - - // Check if the user has enabled two factor authentication - if (empty($otpConfig->method) || ($otpConfig->method == 'none')) - { - // Load language - $lang = Factory::getLanguage(); - $extension = 'com_users'; - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, $source); - - $warn = true; - $warnMessage = Text::_('COM_USERS_ERROR_SECRET_CODE_WITHOUT_TFA'); - - if (array_key_exists('warn_if_not_req', $options)) - { - $warn = $options['warn_if_not_req']; - } - - if (array_key_exists('warn_irq_msg', $options)) - { - $warnMessage = $options['warn_irq_msg']; - } - - // Warn the user if they are using a secret code but they have not - // enabled two factor auth in their account. - if (!empty($secretKey) && $warn) - { - try - { - $app = Factory::getApplication(); - $app->enqueueMessage($warnMessage, 'warning'); - } - catch (\Exception $exc) - { - // This happens when we are in CLI mode. In this case - // no warning is issued - return true; - } - } - - return true; - } - - $credentials = array( - 'secretkey' => $secretKey, - ); - - // Try to validate the OTP - PluginHelper::importPlugin('twofactorauth'); - - $otpAuthReplies = Factory::getApplication()->triggerEvent('onUserTwofactorAuthenticate', array($credentials, $options)); - - $check = false; - - /* - * This looks like noob code but DO NOT TOUCH IT and do not convert - * to in_array(). During testing in_array() inexplicably returned - * null when the OTEP begins with a zero! o_O - */ - if (!empty($otpAuthReplies)) - { - foreach ($otpAuthReplies as $authReply) - { - $check = $check || $authReply; - } - } - - // Fall back to one time emergency passwords - if (!$check) - { - $check = $this->isValidOtep($userId, $secretKey, $otpConfig); - } - - return $check; - } - - /** - * Checks if the supplied string is a valid one time emergency password - * (OTEP) for this user. If it is it will be automatically removed from the - * user's list of OTEPs. - * - * @param integer $userId The user ID against which you are checking - * @param string $otep The string you want to test for validity - * @param object $otpConfig Optional; the two factor authentication configuration (automatically fetched if not set) - * - * @return boolean True if it's a valid OTEP or if two factor auth is not - * enabled in this user's account. - * - * @since 3.2 - */ - public function isValidOtep($userId, $otep, $otpConfig = null) - { - if (is_null($otpConfig)) - { - $otpConfig = $this->getOtpConfig($userId); - } - - // Did the user use an OTEP instead? - if (empty($otpConfig->otep)) - { - if (empty($otpConfig->method) || ($otpConfig->method == 'none')) - { - // Two factor authentication is not enabled on this account. - // Any string is assumed to be a valid OTEP. - return true; - } - else - { - /** - * Two factor authentication enabled and no OTEPs defined. The - * user has used them all up. Therefore anything they enter is - * an invalid OTEP. - */ - return false; - } - } - - // Clean up the OTEP (remove dashes, spaces and other funny stuff - // our beloved users may have unwittingly stuffed in it) - $otep = filter_var($otep, FILTER_SANITIZE_NUMBER_INT); - $otep = str_replace('-', '', $otep); - - $check = false; - - // Did we find a valid OTEP? - if (in_array($otep, $otpConfig->otep)) - { - // Remove the OTEP from the array - $otpConfig->otep = array_diff($otpConfig->otep, array($otep)); - - $this->setOtpConfig($userId, $otpConfig); - - // Return true; the OTEP was a valid one - $check = true; - } - - return $check; - } + /** + * An item. + * + * @var array + */ + protected $_item = null; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_after_delete' => 'onUserAfterDelete', + 'event_after_save' => 'onUserAfterSave', + 'event_before_delete' => 'onUserBeforeDelete', + 'event_before_save' => 'onUserBeforeSave', + 'events_map' => array('save' => 'user', 'delete' => 'user', 'validate' => 'user') + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'User', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + $table = Table::getInstance($type, $prefix, $config); + + return $table; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('user.id'); + + if ($this->_item === null) { + $this->_item = array(); + } + + if (!isset($this->_item[$pk])) { + $this->_item[$pk] = parent::getItem($pk); + } + + return $this->_item[$pk]; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.user', 'user', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + $user = Factory::getUser(); + + // If the user needs to change their password, mark the password fields as required + if ($user->requireReset) { + $form->setFieldAttribute('password', 'required', 'true'); + $form->setFieldAttribute('password2', 'required', 'true'); + } + + // When multilanguage is set, a user's default site language should also be a Content Language + if (Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); + } + + $userId = (int) $form->getValue('id'); + + // The user should not be able to set the requireReset value on their own account + if ($userId === (int) $user->id) { + $form->removeField('requireReset'); + } + + /** + * If users without core.manage permission editing their own account, remove some fields which they should + * not be allowed to change and prevent them to change user name if configured + */ + if (!$user->authorise('core.manage', 'com_users') && (int) $user->id === $userId) { + if (!ComponentHelper::getParams('com_users')->get('change_login_name')) { + $form->setFieldAttribute('username', 'required', 'false'); + $form->setFieldAttribute('username', 'readonly', 'true'); + $form->setFieldAttribute('username', 'description', 'COM_USERS_USER_FIELD_NOCHANGE_USERNAME_DESC'); + } + + $form->removeField('lastResetTime'); + $form->removeField('resetCount'); + $form->removeField('sendEmail'); + $form->removeField('block'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_users.edit.user.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_users.profile', $data, 'user'); + + return $data; + } + + /** + * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + * @throws \Exception + */ + public function save($data) + { + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id'); + $user = User::getInstance($pk); + + $my = Factory::getUser(); + $iAmSuperAdmin = $my->authorise('core.admin'); + + // User cannot modify own user groups + if ((int) $user->id == (int) $my->id && !$iAmSuperAdmin && isset($data['groups'])) { + // Form was probably tampered with + Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_EDIT_OWN_GROUP'), 'warning'); + + $data['groups'] = null; + } + + if ($data['block'] && $pk == $my->id && !$my->block) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF')); + + return false; + } + + // Make sure user groups is selected when add/edit an account + if (empty($data['groups']) && ((int) $user->id != (int) $my->id || $iAmSuperAdmin)) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_SAVE_ACCOUNT_WITHOUT_GROUPS')); + + return false; + } + + // Make sure that we are not removing ourself from Super Admin group + if ($iAmSuperAdmin && $my->get('id') == $pk) { + // Check that at least one of our new groups is Super Admin + $stillSuperAdmin = false; + $myNewGroups = $data['groups']; + + foreach ($myNewGroups as $group) { + $stillSuperAdmin = $stillSuperAdmin ?: Access::checkGroup($group, 'core.admin'); + } + + if (!$stillSuperAdmin) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DEMOTE_SELF')); + + return false; + } + } + + // Bind the data. + if (!$user->bind($data)) { + $this->setError($user->getError()); + + return false; + } + + // Store the data. + if (!$user->save()) { + $this->setError($user->getError()); + + return false; + } + + // Destroy all active sessions for the user after changing the password or blocking him + if ($data['password2'] || $data['block']) { + UserHelper::destroyUserSessions($user->id, true); + } + + $this->setState('user.id', $user->id); + + return true; + } + + /** + * Method to delete rows. + * + * @param array &$pks An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function delete(&$pks) + { + $user = Factory::getUser(); + $table = $this->getTable(); + $pks = (array) $pks; + + // Check if I am a Super Admin + $iAmSuperAdmin = $user->authorise('core.admin'); + + PluginHelper::importPlugin($this->events_map['delete']); + + if (in_array($user->id, $pks)) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DELETE_SELF')); + + return false; + } + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + // Access checks. + $allow = $user->authorise('core.delete', 'com_users'); + + // Don't allow non-super-admin to delete a super admin + $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; + + if ($allow) { + // Get users data for the users to delete. + $user_to_delete = Factory::getUser($pk); + + // Fire the before delete event. + Factory::getApplication()->triggerEvent($this->event_before_delete, array($table->getProperties())); + + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } else { + // Trigger the after delete event. + Factory::getApplication()->triggerEvent($this->event_after_delete, array($user_to_delete->getProperties(), true, $this->getError())); + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + return true; + } + + /** + * Method to block user records. + * + * @param array &$pks The ids of the items to publish. + * @param integer $value The value of the published state + * + * @return boolean True on success. + * + * @since 1.6 + * @throws \Exception + */ + public function block(&$pks, $value = 1) + { + $app = Factory::getApplication(); + $user = Factory::getUser(); + + // Check if I am a Super Admin + $iAmSuperAdmin = $user->authorise('core.admin'); + $table = $this->getTable(); + $pks = (array) $pks; + + PluginHelper::importPlugin($this->events_map['save']); + + // Prepare the logout options. + $options = array( + 'clientid' => $app->get('shared_session', '0') ? null : 0, + ); + + // Access checks. + foreach ($pks as $i => $pk) { + if ($value == 1 && $pk == $user->get('id')) { + // Cannot block yourself. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF'), 'error'); + } elseif ($table->load($pk)) { + $old = $table->getProperties(); + $allow = $user->authorise('core.edit.state', 'com_users'); + + // Don't allow non-super-admin to delete a super admin + $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; + + if ($allow) { + // Skip changing of same state + if ($table->block == $value) { + unset($pks[$i]); + continue; + } + + $table->block = (int) $value; + + // If unblocking, also change password reset count to zero to unblock reset + if ($table->block === 0) { + $table->resetCount = 0; + } + + // Allow an exception to be thrown. + try { + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($old, false, $table->getProperties())); + + if (in_array(false, $result, true)) { + // Plugin will have to raise its own error or throw an exception. + return false; + } + + // Store the table. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + if ($table->block) { + UserHelper::destroyUserSessions($table->id); + } + + // Trigger the after save event + Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Log the user out. + if ($value) { + $app->logout($table->id, $options); + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } + } + } + + return true; + } + + /** + * Method to activate user records. + * + * @param array &$pks The ids of the items to activate. + * + * @return boolean True on success. + * + * @since 1.6 + * @throws \Exception + */ + public function activate(&$pks) + { + $user = Factory::getUser(); + + // Check if I am a Super Admin + $iAmSuperAdmin = $user->authorise('core.admin'); + $table = $this->getTable(); + $pks = (array) $pks; + + PluginHelper::importPlugin($this->events_map['save']); + + // Access checks. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + $old = $table->getProperties(); + $allow = $user->authorise('core.edit.state', 'com_users'); + + // Don't allow non-super-admin to delete a super admin + $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; + + if (empty($table->activation)) { + // Ignore activated accounts. + unset($pks[$i]); + } elseif ($allow) { + $table->block = 0; + $table->activation = ''; + + // Allow an exception to be thrown. + try { + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($old, false, $table->getProperties())); + + if (in_array(false, $result, true)) { + // Plugin will have to raise it's own error or throw an exception. + return false; + } + + // Store the table. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Fire the after save event + Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } + } + } + + return true; + } + + /** + * Method to perform batch operations on an item or a set of items. + * + * @param array $commands An array of commands to perform. + * @param array $pks An array of item ids. + * @param array $contexts An array of item contexts. + * + * @return boolean Returns true on success, false on failure. + * + * @since 2.5 + */ + public function batch($commands, $pks, $contexts) + { + // Sanitize user ids. + $pks = array_unique($pks); + $pks = ArrayHelper::toInteger($pks); + + // Remove any values of zero. + if (array_search(0, $pks, true)) { + unset($pks[array_search(0, $pks, true)]); + } + + if (empty($pks)) { + $this->setError(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED')); + + return false; + } + + $done = false; + + if (!empty($commands['group_id'])) { + $cmd = ArrayHelper::getValue($commands, 'group_action', 'add'); + + if (!$this->batchUser((int) $commands['group_id'], $pks, $cmd)) { + return false; + } + + $done = true; + } + + if (!empty($commands['reset_id'])) { + if (!$this->batchReset($pks, $commands['reset_id'])) { + return false; + } + + $done = true; + } + + if (!$done) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_INSUFFICIENT_BATCH_INFORMATION')); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Batch flag users as being required to reset their passwords + * + * @param array $userIds An array of user IDs on which to operate + * @param string $action The action to perform + * + * @return boolean True on success, false on failure + * + * @since 3.2 + */ + public function batchReset($userIds, $action) + { + $userIds = ArrayHelper::toInteger($userIds); + + // Check if I am a Super Admin + $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); + + // Non-super super user cannot work with super-admin user. + if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) { + $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER')); + + return false; + } + + // Set the action to perform + if ($action === 'yes') { + $value = 1; + } else { + $value = 0; + } + + // Prune out the current user if they are in the supplied user ID array + $userIds = array_diff($userIds, array(Factory::getUser()->id)); + + if (empty($userIds)) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_REQUIRERESET_SELF')); + + return false; + } + + // Get the DB object + $db = $this->getDatabase(); + + $userIds = ArrayHelper::toInteger($userIds); + + $query = $db->getQuery(true); + + // Update the reset flag + $query->update($db->quoteName('#__users')) + ->set($db->quoteName('requireReset') . ' = :requireReset') + ->whereIn($db->quoteName('id'), $userIds) + ->bind(':requireReset', $value, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } + + /** + * Perform batch operations + * + * @param integer $groupId The group ID which assignments are being edited + * @param array $userIds An array of user IDs on which to operate + * @param string $action The action to perform + * + * @return boolean True on success, false on failure + * + * @since 1.6 + */ + public function batchUser($groupId, $userIds, $action) + { + $userIds = ArrayHelper::toInteger($userIds); + + // Check if I am a Super Admin + $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); + + // Non-super super user cannot work with super-admin user. + if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) { + $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER')); + + return false; + } + + // Non-super admin cannot work with super-admin group. + if ((!$iAmSuperAdmin && Access::checkGroup($groupId, 'core.admin')) || $groupId < 1) { + $this->setError(Text::_('COM_USERS_ERROR_INVALID_GROUP')); + + return false; + } + + // Get the DB object + $db = $this->getDatabase(); + + switch ($action) { + // Sets users to a selected group + case 'set': + $doDelete = 'all'; + $doAssign = true; + break; + + // Remove users from a selected group + case 'del': + $doDelete = 'group'; + break; + + // Add users to a selected group + case 'add': + default: + $doAssign = true; + break; + } + + // Remove the users from the group if requested. + if (isset($doDelete)) { + /* + * First we need to check that the user is part of more than one group + * otherwise we will end up with a user that is not part of any group + * unless we are moving the user to a new group. + */ + if ($doDelete === 'group') { + $query = $db->getQuery(true); + $query->select($db->quoteName('user_id')) + ->from($db->quoteName('#__user_usergroup_map')) + ->whereIn($db->quoteName('user_id'), $userIds); + + // Add the group by clause to remove users who are only in one group + $query->group($db->quoteName('user_id')) + ->having('COUNT(user_id) > 1'); + $db->setQuery($query); + $users = $db->loadColumn(); + + // If we have no users to process, throw an error to notify the user + if (empty($users)) { + $this->setError(Text::_('COM_USERS_ERROR_ONLY_ONE_GROUP')); + + return false; + } + + // Check to see if the users are in the group to be removed + $query->clear() + ->select($db->quoteName('user_id')) + ->from($db->quoteName('#__user_usergroup_map')) + ->whereIn($db->quoteName('user_id'), $users) + ->where($db->quoteName('group_id') . ' = :group_id') + ->bind(':group_id', $groupId, ParameterType::INTEGER); + $db->setQuery($query); + $users = $db->loadColumn(); + + // If we have no users to process, throw an error to notify the user + if (empty($users)) { + $this->setError(Text::_('COM_USERS_ERROR_NOT_IN_GROUP')); + + return false; + } + + // Finally remove the users from the group + $query->clear() + ->delete($db->quoteName('#__user_usergroup_map')) + ->whereIn($db->quoteName('user_id'), $users) + ->where($db->quoteName('group_id') . '= :group_id') + ->bind(':group_id', $groupId, ParameterType::INTEGER); + $db->setQuery($query); + } elseif ($doDelete === 'all') { + $query = $db->getQuery(true); + $query->delete($db->quoteName('#__user_usergroup_map')) + ->whereIn($db->quoteName('user_id'), $users); + } + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + // Assign the users to the group if requested. + if (isset($doAssign)) { + $query = $db->getQuery(true); + + // First, we need to check if the user is already assigned to a group + $query->select($db->quoteName('user_id')) + ->from($db->quoteName('#__user_usergroup_map')) + ->where($db->quoteName('group_id') . ' = :group_id') + ->bind(':group_id', $groupId, ParameterType::INTEGER); + $db->setQuery($query); + $users = $db->loadColumn(); + + // Build the values clause for the assignment query. + $query->clear(); + $groups = false; + + foreach ($userIds as $id) { + if (!in_array($id, $users)) { + $query->values($id . ',' . $groupId); + $groups = true; + } + } + + // If we have no users to process, throw an error to notify the user + if (!$groups) { + $this->setError(Text::_('COM_USERS_ERROR_NO_ADDITIONS')); + + return false; + } + + $query->insert($db->quoteName('#__user_usergroup_map')) + ->columns(array($db->quoteName('user_id'), $db->quoteName('group_id'))); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + return true; + } + + /** + * Gets the available groups. + * + * @return array An array of groups + * + * @since 1.6 + */ + public function getGroups() + { + $user = Factory::getUser(); + + if ($user->authorise('core.edit', 'com_users') && $user->authorise('core.manage', 'com_users')) { + $model = $this->bootComponent('com_users') + ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]); + + return $model->getItems(); + } else { + return null; + } + } + + /** + * Gets the groups this object is assigned to + * + * @param integer $userId The user ID to retrieve the groups for + * + * @return array An array of assigned groups + * + * @since 1.6 + */ + public function getAssignedGroups($userId = null) + { + $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id'); + + if (empty($userId)) { + $result = array(); + $form = $this->getForm(); + + if ($form) { + $groupsIDs = $form->getValue('groups'); + } + + if (!empty($groupsIDs)) { + $result = $groupsIDs; + } else { + $params = ComponentHelper::getParams('com_users'); + + if ($groupId = $params->get('new_usertype', $params->get('guest_usergroup', 1))) { + $result[] = $groupId; + } + } + } else { + $result = UserHelper::getUserGroups($userId); + } + + return $result; + } + + /** + * No longer used + * + * @param integer $userId Ignored + * + * @return \stdClass + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function getOtpConfig($userId = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getUserMfaRecords() instead.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + // Return the configuration object + return (object) array( + 'method' => 'none', + 'config' => array(), + 'otep' => array() + ); + } + + /** + * No longer used + * + * @param integer $userId Ignored + * @param \stdClass $otpConfig Ignored + * + * @return boolean True on success + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function setOtpConfig($userId, $otpConfig) + { + @trigger_error( + sprintf( + '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return true; + } + + /** + * No longer used + * + * @return string + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function getOtpConfigEncryptionKey() + { + @trigger_error( + sprintf( + '%s() is deprecated. Use \Joomla\CMS\Factory::getApplication()->get(\'secret\') instead', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return Factory::getApplication()->get('secret'); + } + + /** + * No longer used + * + * @param integer $userId Ignored + * + * @return array Empty array + * + * @since 3.2 + * @throws \Exception + * + * @deprecated 4.2.0 Will be removed in 5.0. + */ + public function getTwofactorform($userId = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getConfigurationInterface()', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return []; + } + + /** + * No longer used + * + * @param integer $userId Ignored + * @param integer $count Ignored + * + * @return array Empty array + * + * @since 3.2 + * @deprecated 4.2.0 Wil be removed in 5.0. + */ + public function generateOteps($userId, $count = 10) + { + @trigger_error( + sprintf( + '%s() is deprecated. See \Joomla\Component\Users\Administrator\Model\BackupcodesModel::saveBackupCodes()', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return []; + } + + /** + * No longer used. Always returns true. + * + * @param integer $userId Ignored + * @param string $secretKey Ignored + * @param array $options Ignored + * + * @return boolean Always true + * + * @since 3.2 + * @throws \Exception + * + * @deprecated 4.2.0 Will be removed in 5.0. MFA validation is done in the captive login. + */ + public function isValidSecretKey($userId, $secretKey, $options = array()) + { + @trigger_error( + sprintf( + '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return true; + } + + /** + * No longer used + * + * @param integer $userId Ignored + * @param string $otep Ignored + * @param object $otpConfig Ignored + * + * @return boolean Always true + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function isValidOtep($userId, $otep, $otpConfig = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return true; + } } diff --git a/code/administrator/components/com_users/src/Model/UsersModel.php b/code/administrator/components/com_users/src/Model/UsersModel.php index 95505a31..e8036388 100644 --- a/code/administrator/components/com_users/src/Model/UsersModel.php +++ b/code/administrator/components/com_users/src/Model/UsersModel.php @@ -1,4 +1,5 @@ input->get('layout', 'default', 'cmd')) - { - $this->context .= '.' . $layout; - } - - $groups = json_decode(base64_decode($app->input->get('groups', '', 'BASE64'))); - - if (isset($groups)) - { - $groups = ArrayHelper::toInteger($groups); - } - - $this->setState('filter.groups', $groups); - - $excluded = json_decode(base64_decode($app->input->get('excluded', '', 'BASE64'))); - - if (isset($excluded)) - { - $excluded = ArrayHelper::toInteger($excluded); - } - - $this->setState('filter.excluded', $excluded); - - // Load the parameters. - $params = ComponentHelper::getParams('com_users'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.active'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.group_id'); - $id .= ':' . $this->getState('filter.range'); - - return parent::getStoreId($id); - } - - /** - * Gets the list of users and adds expensive joins to the result set. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getItems() - { - // Get a storage key. - $store = $this->getStoreId(); - - // Try to load the data from internal storage. - if (empty($this->cache[$store])) - { - $groups = $this->getState('filter.groups'); - $groupId = $this->getState('filter.group_id'); - - if (isset($groups) && (empty($groups) || $groupId && !in_array($groupId, $groups))) - { - $items = array(); - } - else - { - $items = parent::getItems(); - } - - // Bail out on an error or empty list. - if (empty($items)) - { - $this->cache[$store] = $items; - - return $items; - } - - // Joining the groups with the main query is a performance hog. - // Find the information only on the result set. - - // First pass: get list of the user ids and reset the counts. - $userIds = array(); - - foreach ($items as $item) - { - $userIds[] = (int) $item->id; - $item->group_count = 0; - $item->group_names = ''; - $item->note_count = 0; - } - - // Get the counts from the database only for the users in the list. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Join over the group mapping table. - $query->select('map.user_id, COUNT(map.group_id) AS group_count') - ->from('#__user_usergroup_map AS map') - ->whereIn($db->quoteName('map.user_id'), $userIds) - ->group('map.user_id') - // Join over the user groups table. - ->join('LEFT', '#__usergroups AS g2 ON g2.id = map.group_id'); - - $db->setQuery($query); - - // Load the counts into an array indexed on the user id field. - try - { - $userGroups = $db->loadObjectList('user_id'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $query->clear() - ->select('n.user_id, COUNT(n.id) As note_count') - ->from('#__user_notes AS n') - ->whereIn($db->quoteName('n.user_id'), $userIds) - ->where('n.state >= 0') - ->group('n.user_id'); - - $db->setQuery($query); - - // Load the counts into an array indexed on the aro.value field (the user id). - try - { - $userNotes = $db->loadObjectList('user_id'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Second pass: collect the group counts into the master items array. - foreach ($items as &$item) - { - if (isset($userGroups[$item->id])) - { - $item->group_count = $userGroups[$item->id]->group_count; - - // Group_concat in other databases is not supported - $item->group_names = $this->_getUserDisplayedGroups($item->id); - } - - if (isset($userNotes[$item->id])) - { - $item->note_count = $userNotes[$item->id]->note_count; - } - } - - // Add the items to the internal cache. - $this->cache[$store] = $items; - } - - return $this->cache[$store]; - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - - $query->from($db->quoteName('#__users') . ' AS a'); - - // If the model is set to check item state, add to the query. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('a.block') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - - // If the model is set to check the activated state, add to the query. - $active = $this->getState('filter.active'); - - if (is_numeric($active)) - { - if ($active == '0') - { - $query->whereIn($db->quoteName('a.activation'), ['', '0']); - } - elseif ($active == '1') - { - $query->where($query->length($db->quoteName('a.activation')) . ' > 1'); - } - } - - // Filter the items over the group id if set. - $groupId = $this->getState('filter.group_id'); - $groups = $this->getState('filter.groups'); - - if ($groupId || isset($groups)) - { - $query->join('LEFT', '#__user_usergroup_map AS map2 ON map2.user_id = a.id') - ->group( - $db->quoteName( - array( - 'a.id', - 'a.name', - 'a.username', - 'a.password', - 'a.block', - 'a.sendEmail', - 'a.registerDate', - 'a.lastvisitDate', - 'a.activation', - 'a.params', - 'a.email', - 'a.lastResetTime', - 'a.resetCount', - 'a.otpKey', - 'a.otep', - 'a.requireReset' - ) - ) - ); - - if ($groupId) - { - $groupId = (int) $groupId; - $query->where($db->quoteName('map2.group_id') . ' = :group_id') - ->bind(':group_id', $groupId, ParameterType::INTEGER); - } - - if (isset($groups)) - { - $query->whereIn($db->quoteName('map2.group_id'), $groups); - } - } - - // Filter the items over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - elseif (stripos($search, 'username:') === 0) - { - $search = '%' . substr($search, 9) . '%'; - $query->where($db->quoteName('a.username') . ' LIKE :username'); - $query->bind(':username', $search); - } - else - { - $search = '%' . trim($search) . '%'; - - // Add the clauses to the query. - $query->where( - '(' . $db->quoteName('a.name') . ' LIKE :name' - . ' OR ' . $db->quoteName('a.username') . ' LIKE :username' - . ' OR ' . $db->quoteName('a.email') . ' LIKE :email)' - ) - ->bind(':name', $search) - ->bind(':username', $search) - ->bind(':email', $search); - } - } - - // Add filter for registration time ranges select list. UI Visitors get a range of predefined - // values. API users can do a full range based on ISO8601 - $range = $this->getState('filter.range'); - $registrationStart = $this->getState('filter.registrationDateStart'); - $registrationEnd = $this->getState('filter.registrationDateEnd'); - - // Apply the range filter. - if ($range || ($registrationStart && $registrationEnd)) - { - if ($range) - { - $dates = $this->buildDateRange($range); - } - else - { - $dates = [ - 'dNow' => $registrationEnd, - 'dStart' => $registrationStart, - ]; - } - - if ($dates['dStart'] !== false) - { - $dStart = $dates['dStart']->format('Y-m-d H:i:s'); - - if ($dates['dNow'] === false) - { - $query->where($db->quoteName('a.registerDate') . ' < :registerDate'); - $query->bind(':registerDate', $dStart); - } - else - { - $dNow = $dates['dNow']->format('Y-m-d H:i:s'); - - $query->where($db->quoteName('a.registerDate') . ' BETWEEN :registerDate1 AND :registerDate2'); - $query->bind(':registerDate1', $dStart); - $query->bind(':registerDate2', $dNow); - } - } - } - - // Add filter for last visit time ranges select list. UI Visitors get a range of predefined - // values. API users can do a full range based on ISO8601 - $lastvisitrange = $this->getState('filter.lastvisitrange'); - $lastVisitStart = $this->getState('filter.lastVisitStart'); - $lastVisitEnd = $this->getState('filter.lastVisitEnd'); - - // Apply the range filter. - if ($lastvisitrange || ($lastVisitStart && $lastVisitEnd)) - { - if ($lastvisitrange) - { - $dates = $this->buildDateRange($lastvisitrange); - } - else - { - $dates = [ - 'dNow' => $lastVisitEnd, - 'dStart' => $lastVisitStart, - ]; - } - - if ($dates['dStart'] === false) - { - $query->where($db->quoteName('a.lastvisitDate') . ' IS NULL'); - } - else - { - $query->where($db->quoteName('a.lastvisitDate') . ' IS NOT NULL'); - - $dStart = $dates['dStart']->format('Y-m-d H:i:s'); - - if ($dates['dNow'] === false) - { - $query->where($db->quoteName('a.lastvisitDate') . ' < :lastvisitDate'); - $query->bind(':lastvisitDate', $dStart); - } - else - { - $dNow = $dates['dNow']->format('Y-m-d H:i:s'); - - $query->where($db->quoteName('a.lastvisitDate') . ' BETWEEN :lastvisitDate1 AND :lastvisitDate2'); - $query->bind(':lastvisitDate1', $dStart); - $query->bind(':lastvisitDate2', $dNow); - } - } - } - - // Filter by excluded users - $excluded = $this->getState('filter.excluded'); - - if (!empty($excluded)) - { - $query->whereNotIn($db->quoteName('id'), $excluded); - } - - // Add the list ordering clause. - $query->order( - $db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Construct the date range to filter on. - * - * @param string $range The textual range to construct the filter for. - * - * @return array The date range to filter on. - * - * @since 3.6.0 - * @throws \Exception - */ - private function buildDateRange($range) - { - // Get UTC for now. - $dNow = new Date; - $dStart = clone $dNow; - - switch ($range) - { - case 'past_week': - $dStart->modify('-7 day'); - break; - - case 'past_1month': - $dStart->modify('-1 month'); - break; - - case 'past_3month': - $dStart->modify('-3 month'); - break; - - case 'past_6month': - $dStart->modify('-6 month'); - $arr = []; - break; - - case 'post_year': - $dNow = false; - case 'past_year': - $dStart->modify('-1 year'); - break; - - case 'today': - // Ranges that need to align with local 'days' need special treatment. - $app = Factory::getApplication(); - $offset = $app->get('offset'); - - // Reset the start time to be the beginning of today, local time. - $dStart = new Date('now', $offset); - $dStart->setTime(0, 0, 0); - - // Now change the timezone back to UTC. - $tz = new \DateTimeZone('GMT'); - $dStart->setTimezone($tz); - break; - case 'never': - $dNow = false; - $dStart = false; - break; - } - - return array('dNow' => $dNow, 'dStart' => $dStart); - } - - /** - * SQL server change - * - * @param integer $userId User identifier - * - * @return string Groups titles imploded :$ - */ - protected function _getUserDisplayedGroups($userId) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups', 'ug')) - ->join('LEFT', $db->quoteName('#__user_usergroup_map', 'map') . ' ON (ug.id = map.group_id)') - ->where($db->quoteName('map.user_id') . ' = :user_id') - ->bind(':user_id', $userId, ParameterType::INTEGER); - - try - { - $result = $db->setQuery($query)->loadColumn(); - } - catch (\RuntimeException $e) - { - $result = array(); - } - - return implode("\n", $result); - } + /** + * A list of filter variables to not merge into the model's state + * + * @var array + * @since 4.0.0 + */ + protected $filterForbiddenList = array('groups', 'excluded'); + + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'username', 'a.username', + 'email', 'a.email', + 'block', 'a.block', + 'sendEmail', 'a.sendEmail', + 'registerDate', 'a.registerDate', + 'lastvisitDate', 'a.lastvisitDate', + 'activation', 'a.activation', + 'active', + 'group_id', + 'range', + 'lastvisitrange', + 'state', + 'mfa' + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout', 'default', 'cmd')) { + $this->context .= '.' . $layout; + } + + $groups = json_decode(base64_decode($app->input->get('groups', '', 'BASE64'))); + + if (isset($groups)) { + $groups = ArrayHelper::toInteger($groups); + } + + $this->setState('filter.groups', $groups); + + $excluded = json_decode(base64_decode($app->input->get('excluded', '', 'BASE64'))); + + if (isset($excluded)) { + $excluded = ArrayHelper::toInteger($excluded); + } + + $this->setState('filter.excluded', $excluded); + + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.active'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.group_id'); + $id .= ':' . $this->getState('filter.range'); + + if (PluginHelper::isEnabled('multifactorauth')) { + $id .= ':' . $this->getState('filter.mfa'); + } + + return parent::getStoreId($id); + } + + /** + * Gets the list of users and adds expensive joins to the result set. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId(); + + // Try to load the data from internal storage. + if (empty($this->cache[$store])) { + $groups = $this->getState('filter.groups'); + $groupId = $this->getState('filter.group_id'); + + if (isset($groups) && (empty($groups) || $groupId && !in_array($groupId, $groups))) { + $items = array(); + } else { + $items = parent::getItems(); + } + + // Bail out on an error or empty list. + if (empty($items)) { + $this->cache[$store] = $items; + + return $items; + } + + // Joining the groups with the main query is a performance hog. + // Find the information only on the result set. + + // First pass: get list of the user ids and reset the counts. + $userIds = array(); + + foreach ($items as $item) { + $userIds[] = (int) $item->id; + + $item->group_count = 0; + $item->group_names = ''; + $item->note_count = 0; + } + + // Get the counts from the database only for the users in the list. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Join over the group mapping table. + $query->select('map.user_id, COUNT(map.group_id) AS group_count') + ->from('#__user_usergroup_map AS map') + ->whereIn($db->quoteName('map.user_id'), $userIds) + ->group('map.user_id') + // Join over the user groups table. + ->join('LEFT', '#__usergroups AS g2 ON g2.id = map.group_id'); + + $db->setQuery($query); + + // Load the counts into an array indexed on the user id field. + try { + $userGroups = $db->loadObjectList('user_id'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + $query->clear() + ->select('n.user_id, COUNT(n.id) As note_count') + ->from('#__user_notes AS n') + ->whereIn($db->quoteName('n.user_id'), $userIds) + ->where('n.state >= 0') + ->group('n.user_id'); + + $db->setQuery($query); + + // Load the counts into an array indexed on the aro.value field (the user id). + try { + $userNotes = $db->loadObjectList('user_id'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Second pass: collect the group counts into the master items array. + foreach ($items as &$item) { + if (isset($userGroups[$item->id])) { + $item->group_count = $userGroups[$item->id]->group_count; + + // Group_concat in other databases is not supported + $item->group_names = $this->getUserDisplayedGroups($item->id); + } + + if (isset($userNotes[$item->id])) { + $item->note_count = $userNotes[$item->id]->note_count; + } + } + + // Add the items to the internal cache. + $this->cache[$store] = $items; + } + + return $this->cache[$store]; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return Form|null The \JForm object or null if the form can't be found + * + * @since 4.2.0 + */ + public function getFilterForm($data = [], $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + if ($form && !PluginHelper::isEnabled('multifactorauth')) { + $form->removeField('mfa', 'filter'); + } + + return $form; + } + + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + + $query->from($db->quoteName('#__users') . ' AS a'); + + // Include MFA information + if (PluginHelper::isEnabled('multifactorauth')) { + $subQuery = $db->getQuery(true) + ->select( + [ + 'MIN(' . $db->quoteName('user_id') . ') AS ' . $db->quoteName('uid'), + 'COUNT(*) AS ' . $db->quoteName('mfaRecords') + ] + ) + ->from($db->quoteName('#__user_mfa')) + ->group($db->quoteName('user_id')); + $query->select($db->quoteName('mfa.mfaRecords')) + ->join( + 'left', + '(' . $subQuery . ') AS ' . $db->quoteName('mfa'), + $db->quoteName('mfa.uid') . ' = ' . $db->quoteName('a.id') + ); + + $mfaState = $this->getState('filter.mfa'); + + if (is_numeric($mfaState)) { + $mfaState = (int) $mfaState; + + if ($mfaState === 1) { + $query->where( + '((' . $db->quoteName('mfa.mfaRecords') . ' > 0) OR (' . + $db->quoteName('a.otpKey') . ' IS NOT NULL AND ' . + $db->quoteName('a.otpKey') . ' != ' . $db->quote('') . '))' + ); + } else { + $query->where( + '((' . $db->quoteName('mfa.mfaRecords') . ' = 0 OR ' . + $db->quoteName('mfa.mfaRecords') . ' IS NULL) AND (' . + $db->quoteName('a.otpKey') . ' IS NULL OR ' . + $db->quoteName('a.otpKey') . ' = ' . $db->quote('') . '))' + ); + } + } + } + + // If the model is set to check item state, add to the query. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $query->where($db->quoteName('a.block') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } + + // If the model is set to check the activated state, add to the query. + $active = $this->getState('filter.active'); + + if (is_numeric($active)) { + if ($active == '0') { + $query->whereIn($db->quoteName('a.activation'), ['', '0']); + } elseif ($active == '1') { + $query->where($query->length($db->quoteName('a.activation')) . ' > 1'); + } + } + + // Filter the items over the group id if set. + $groupId = $this->getState('filter.group_id'); + $groups = $this->getState('filter.groups'); + + if ($groupId || isset($groups)) { + $query->join('LEFT', '#__user_usergroup_map AS map2 ON map2.user_id = a.id') + ->group( + $db->quoteName( + array( + 'a.id', + 'a.name', + 'a.username', + 'a.password', + 'a.block', + 'a.sendEmail', + 'a.registerDate', + 'a.lastvisitDate', + 'a.activation', + 'a.params', + 'a.email', + 'a.lastResetTime', + 'a.resetCount', + 'a.otpKey', + 'a.otep', + 'a.requireReset' + ) + ) + ); + + if ($groupId) { + $groupId = (int) $groupId; + $query->where($db->quoteName('map2.group_id') . ' = :group_id') + ->bind(':group_id', $groupId, ParameterType::INTEGER); + } + + if (isset($groups)) { + $query->whereIn($db->quoteName('map2.group_id'), $groups); + } + } + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } elseif (stripos($search, 'username:') === 0) { + $search = '%' . substr($search, 9) . '%'; + $query->where($db->quoteName('a.username') . ' LIKE :username'); + $query->bind(':username', $search); + } else { + $search = '%' . trim($search) . '%'; + + // Add the clauses to the query. + $query->where( + '(' . $db->quoteName('a.name') . ' LIKE :name' + . ' OR ' . $db->quoteName('a.username') . ' LIKE :username' + . ' OR ' . $db->quoteName('a.email') . ' LIKE :email)' + ) + ->bind(':name', $search) + ->bind(':username', $search) + ->bind(':email', $search); + } + } + + // Add filter for registration time ranges select list. UI Visitors get a range of predefined + // values. API users can do a full range based on ISO8601 + $range = $this->getState('filter.range'); + $registrationStart = $this->getState('filter.registrationDateStart'); + $registrationEnd = $this->getState('filter.registrationDateEnd'); + + // Apply the range filter. + if ($range || ($registrationStart && $registrationEnd)) { + if ($range) { + $dates = $this->buildDateRange($range); + } else { + $dates = [ + 'dNow' => $registrationEnd, + 'dStart' => $registrationStart, + ]; + } + + if ($dates['dStart'] !== false) { + $dStart = $dates['dStart']->format('Y-m-d H:i:s'); + + if ($dates['dNow'] === false) { + $query->where($db->quoteName('a.registerDate') . ' < :registerDate'); + $query->bind(':registerDate', $dStart); + } else { + $dNow = $dates['dNow']->format('Y-m-d H:i:s'); + + $query->where($db->quoteName('a.registerDate') . ' BETWEEN :registerDate1 AND :registerDate2'); + $query->bind(':registerDate1', $dStart); + $query->bind(':registerDate2', $dNow); + } + } + } + + // Add filter for last visit time ranges select list. UI Visitors get a range of predefined + // values. API users can do a full range based on ISO8601 + $lastvisitrange = $this->getState('filter.lastvisitrange'); + $lastVisitStart = $this->getState('filter.lastVisitStart'); + $lastVisitEnd = $this->getState('filter.lastVisitEnd'); + + // Apply the range filter. + if ($lastvisitrange || ($lastVisitStart && $lastVisitEnd)) { + if ($lastvisitrange) { + $dates = $this->buildDateRange($lastvisitrange); + } else { + $dates = [ + 'dNow' => $lastVisitEnd, + 'dStart' => $lastVisitStart, + ]; + } + + if ($dates['dStart'] === false) { + $query->where($db->quoteName('a.lastvisitDate') . ' IS NULL'); + } else { + $query->where($db->quoteName('a.lastvisitDate') . ' IS NOT NULL'); + + $dStart = $dates['dStart']->format('Y-m-d H:i:s'); + + if ($dates['dNow'] === false) { + $query->where($db->quoteName('a.lastvisitDate') . ' < :lastvisitDate'); + $query->bind(':lastvisitDate', $dStart); + } else { + $dNow = $dates['dNow']->format('Y-m-d H:i:s'); + + $query->where($db->quoteName('a.lastvisitDate') . ' BETWEEN :lastvisitDate1 AND :lastvisitDate2'); + $query->bind(':lastvisitDate1', $dStart); + $query->bind(':lastvisitDate2', $dNow); + } + } + } + + // Filter by excluded users + $excluded = $this->getState('filter.excluded'); + + if (!empty($excluded)) { + $query->whereNotIn($db->quoteName('id'), $excluded); + } + + // Add the list ordering clause. + $query->order( + $db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) + ); + + return $query; + } + + /** + * Construct the date range to filter on. + * + * @param string $range The textual range to construct the filter for. + * + * @return array The date range to filter on. + * + * @since 3.6.0 + * @throws \Exception + */ + private function buildDateRange($range) + { + // Get UTC for now. + $dNow = new Date(); + $dStart = clone $dNow; + + switch ($range) { + case 'past_week': + $dStart->modify('-7 day'); + break; + + case 'past_1month': + $dStart->modify('-1 month'); + break; + + case 'past_3month': + $dStart->modify('-3 month'); + break; + + case 'past_6month': + $dStart->modify('-6 month'); + $arr = []; + break; + + case 'post_year': + $dNow = false; + + // No break + + case 'past_year': + $dStart->modify('-1 year'); + break; + + case 'today': + // Ranges that need to align with local 'days' need special treatment. + $app = Factory::getApplication(); + $offset = $app->get('offset'); + + // Reset the start time to be the beginning of today, local time. + $dStart = new Date('now', $offset); + $dStart->setTime(0, 0, 0); + + // Now change the timezone back to UTC. + $tz = new \DateTimeZone('GMT'); + $dStart->setTimezone($tz); + break; + case 'never': + $dNow = false; + $dStart = false; + break; + } + + return array('dNow' => $dNow, 'dStart' => $dStart); + } + + /** + * SQL server change + * + * @param integer $userId User identifier + * + * @return string Groups titles imploded :$ + */ + protected function getUserDisplayedGroups($userId) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups', 'ug')) + ->join('LEFT', $db->quoteName('#__user_usergroup_map', 'map') . ' ON (ug.id = map.group_id)') + ->where($db->quoteName('map.user_id') . ' = :user_id') + ->bind(':user_id', $userId, ParameterType::INTEGER); + + try { + $result = $db->setQuery($query)->loadColumn(); + } catch (\RuntimeException $e) { + $result = array(); + } + + return implode("\n", $result); + } } diff --git a/code/administrator/components/com_users/src/Service/Encrypt.php b/code/administrator/components/com_users/src/Service/Encrypt.php new file mode 100644 index 00000000..fb8685c2 --- /dev/null +++ b/code/administrator/components/com_users/src/Service/Encrypt.php @@ -0,0 +1,132 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\Service; + +use Joomla\CMS\Encrypt\Aes; +use Joomla\CMS\Factory; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Data encryption service. + * + * @since 4.2.0 + */ +class Encrypt +{ + /** + * The encryption engine used by this service + * + * @var Aes + * @since 4.2.0 + */ + private $aes; + + /** + * EncryptService constructor. + * + * @since 4.2.0 + */ + public function __construct() + { + $this->initialize(); + } + + /** + * Encrypt the plaintext $data and return the ciphertext prefixed by ###AES128### + * + * @param string $data The plaintext data + * + * @return string The ciphertext, prefixed by ###AES128### + * + * @since 4.2.0 + */ + public function encrypt(string $data): string + { + if (!is_object($this->aes)) { + return $data; + } + + $this->aes->setPassword($this->getPassword(), false); + $encrypted = $this->aes->encryptString($data, true); + + return '###AES128###' . $encrypted; + } + + /** + * Decrypt the ciphertext, prefixed by ###AES128###, and return the plaintext. + * + * @param string $data The ciphertext, prefixed by ###AES128### + * @param bool $legacy Use legacy key expansion? Use it to decrypt data encrypted with FOF 3. + * + * @return string The plaintext data + * + * @since 4.2.0 + */ + public function decrypt(string $data, bool $legacy = false): string + { + if (substr($data, 0, 12) != '###AES128###') { + return $data; + } + + $data = substr($data, 12); + + if (!is_object($this->aes)) { + return $data; + } + + $this->aes->setPassword($this->getPassword(), $legacy); + $decrypted = $this->aes->decryptString($data, true, $legacy); + + // Decrypted data is null byte padded. We have to remove the padding before proceeding. + return rtrim($decrypted, "\0"); + } + + /** + * Initialize the AES cryptography object + * + * @return void + * @since 4.2.0 + */ + private function initialize(): void + { + if (is_object($this->aes)) { + return; + } + + $password = $this->getPassword(); + + if (empty($password)) { + return; + } + + $this->aes = new Aes('cbc'); + $this->aes->setPassword($password); + } + + /** + * Returns the password used to encrypt information in the component + * + * @return string + * + * @since 4.2.0 + */ + private function getPassword(): string + { + try { + return Factory::getApplication()->get('secret', ''); + } catch (\Exception $e) { + return ''; + } + } +} diff --git a/code/administrator/components/com_users/src/Service/HTML/Users.php b/code/administrator/components/com_users/src/Service/HTML/Users.php index 1f8e9edf..3da339ed 100644 --- a/code/administrator/components/com_users/src/Service/HTML/Users.php +++ b/code/administrator/components/com_users/src/Service/HTML/Users.php @@ -1,4 +1,5 @@ element if the specified file exists, otherwise, a null string - * - * @since 2.5 - * @throws \Exception - */ - public function image($src) - { - $src = preg_replace('#[^A-Z0-9\-_\./]#i', '', $src); - $file = JPATH_SITE . '/' . $src; - - Path::check($file); - - if (!file_exists($file)) - { - return ''; - } - - return ''; - } - - /** - * Displays an icon to add a note for this user. - * - * @param integer $userId The user ID - * - * @return string A link to add a note - * - * @since 2.5 - */ - public function addNote($userId) - { - $title = Text::_('COM_USERS_ADD_NOTE'); - - return '' . $title . ''; - } - - /** - * Displays an icon to filter the notes list on this user. - * - * @param integer $count The number of notes for the user - * @param integer $userId The user ID - * - * @return string A link to apply a filter - * - * @since 2.5 - */ - public function filterNotes($count, $userId) - { - if (empty($count)) - { - return ''; - } - - $title = Text::_('COM_USERS_FILTER_NOTES'); - - return '' . $title . ''; - } - - /** - * Displays a note icon. - * - * @param integer $count The number of notes for the user - * @param integer $userId The user ID - * - * @return string A link to a modal window with the user notes - * - * @since 2.5 - */ - public function notes($count, $userId) - { - if (empty($count)) - { - return ''; - } - - $title = Text::plural('COM_USERS_N_USER_NOTES', $count); - - return ''; - } - - /** - * Renders the modal html. - * - * @param integer $count The number of notes for the user - * @param integer $userId The user ID - * - * @return string The html for the rendered modal - * - * @since 3.4.1 - */ - public function notesModal($count, $userId) - { - if (empty($count)) - { - return ''; - } - - $title = Text::plural('COM_USERS_N_USER_NOTES', $count); - $footer = ''; - - return HTMLHelper::_( - 'bootstrap.renderModal', - 'userModal_' . (int) $userId, - array( - 'title' => $title, - 'backdrop' => 'static', - 'keyboard' => true, - 'closeButton' => true, - 'footer' => $footer, - 'url' => Route::_('index.php?option=com_users&view=notes&tmpl=component&layout=modal&filter[user_id]=' . (int) $userId), - 'height' => '300px', - 'width' => '800px', - ) - ); - - } - - /** - * Build an array of block/unblock user states to be used by jgrid.state, - * State options will be different for any user - * and for currently logged in user - * - * @param boolean $self True if state array is for currently logged in user - * - * @return array a list of possible states to display - * - * @since 3.0 - */ - public function blockStates( $self = false) - { - if ($self) - { - $states = array( - 1 => array( - 'task' => 'unblock', - 'text' => '', - 'active_title' => 'COM_USERS_TOOLBAR_BLOCK', - 'inactive_title' => '', - 'tip' => true, - 'active_class' => 'unpublish', - 'inactive_class' => 'unpublish', - ), - 0 => array( - 'task' => 'block', - 'text' => '', - 'active_title' => '', - 'inactive_title' => 'COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF', - 'tip' => true, - 'active_class' => 'publish', - 'inactive_class' => 'publish', - ) - ); - } - else - { - $states = array( - 1 => array( - 'task' => 'unblock', - 'text' => '', - 'active_title' => 'COM_USERS_TOOLBAR_UNBLOCK', - 'inactive_title' => '', - 'tip' => true, - 'active_class' => 'unpublish', - 'inactive_class' => 'unpublish', - ), - 0 => array( - 'task' => 'block', - 'text' => '', - 'active_title' => 'COM_USERS_TOOLBAR_BLOCK', - 'inactive_title' => '', - 'tip' => true, - 'active_class' => 'publish', - 'inactive_class' => 'publish', - ) - ); - } - - return $states; - } - - /** - * Build an array of activate states to be used by jgrid.state, - * - * @return array a list of possible states to display - * - * @since 3.0 - */ - public function activateStates() - { - $states = array( - 1 => array( - 'task' => 'activate', - 'text' => '', - 'active_title' => 'COM_USERS_TOOLBAR_ACTIVATE', - 'inactive_title' => '', - 'tip' => true, - 'active_class' => 'unpublish', - 'inactive_class' => 'unpublish', - ), - 0 => array( - 'task' => '', - 'text' => '', - 'active_title' => '', - 'inactive_title' => 'COM_USERS_ACTIVATED', - 'tip' => true, - 'active_class' => 'publish', - 'inactive_class' => 'publish', - ) - ); - - return $states; - } - - /** - * Get the sanitized value - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function value($value) - { - if (is_string($value)) - { - $value = trim($value); - } - - if (empty($value)) - { - return Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND'); - } - - elseif (!is_array($value)) - { - return htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); - } - } - - /** - * Get the space symbol - * - * @param mixed $value Value of the field - * - * @return string - * - * @since 1.6 - */ - public function spacer($value) - { - return ''; - } - - /** - * Get the sanitized template style - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function templatestyle($value) - { - if (empty($value)) - { - return static::value($value); - } - else - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__template_styles')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $value, ParameterType::INTEGER); - $db->setQuery($query); - $title = $db->loadResult(); - - if ($title) - { - return htmlspecialchars($title, ENT_COMPAT, 'UTF-8'); - } - else - { - return static::value(''); - } - } - } - - /** - * Get the sanitized language - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function admin_language($value) - { - if (!$value) - { - return static::value($value); - } - - $path = LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR, $value); - $file = $path . '/langmetadata.xml'; - - if (!is_file($file)) - { - // For language packs from before 4.0. - $file = $path . '/' . $value . '.xml'; - - if (!is_file($file)) - { - return static::value($value); - } - } - - $result = LanguageHelper::parseXMLLanguageFile($file); - - if ($result) - { - return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8'); - } - - return static::value($value); - } - - /** - * Get the sanitized language - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function language($value) - { - if (!$value) - { - return static::value($value); - } - - $path = LanguageHelper::getLanguagePath(JPATH_SITE, $value); - $file = $path . '/langmetadata.xml'; - - if (!is_file($file)) - { - // For language packs from before 4.0. - $file = $path . '/' . $value . '.xml'; - - if (!is_file($file)) - { - return static::value($value); - } - } - - $result = LanguageHelper::parseXMLLanguageFile($file); - - if ($result) - { - return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8'); - } - - return static::value($value); - } - - /** - * Get the sanitized editor name - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function editor($value) - { - if (empty($value)) - { - return static::value($value); - } - else - { - $db = Factory::getDbo(); - $lang = Factory::getLanguage(); - $query = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('element') . ' = :element') - ->where($db->quoteName('folder') . ' = ' . $db->quote('editors')) - ->bind(':element', $value); - $db->setQuery($query); - $title = $db->loadResult(); - - if ($title) - { - $lang->load("plg_editors_$value.sys", JPATH_ADMINISTRATOR) - || $lang->load("plg_editors_$value.sys", JPATH_PLUGINS . '/editors/' . $value); - $lang->load($title . '.sys'); - - return Text::_($title); - } - else - { - return static::value(''); - } - } - } + /** + * Display an image. + * + * @param string $src The source of the image + * + * @return string A element if the specified file exists, otherwise, a null string + * + * @since 2.5 + * @throws \Exception + */ + public function image($src) + { + $src = preg_replace('#[^A-Z0-9\-_\./]#i', '', $src); + $file = JPATH_SITE . '/' . $src; + + Path::check($file); + + if (!file_exists($file)) { + return ''; + } + + return ''; + } + + /** + * Displays an icon to add a note for this user. + * + * @param integer $userId The user ID + * + * @return string A link to add a note + * + * @since 2.5 + */ + public function addNote($userId) + { + $title = Text::_('COM_USERS_ADD_NOTE'); + + return '' . $title . ''; + } + + /** + * Displays an icon to filter the notes list on this user. + * + * @param integer $count The number of notes for the user + * @param integer $userId The user ID + * + * @return string A link to apply a filter + * + * @since 2.5 + */ + public function filterNotes($count, $userId) + { + if (empty($count)) { + return ''; + } + + $title = Text::_('COM_USERS_FILTER_NOTES'); + + return '' . $title . ''; + } + + /** + * Displays a note icon. + * + * @param integer $count The number of notes for the user + * @param integer $userId The user ID + * + * @return string A link to a modal window with the user notes + * + * @since 2.5 + */ + public function notes($count, $userId) + { + if (empty($count)) { + return ''; + } + + $title = Text::plural('COM_USERS_N_USER_NOTES', $count); + + return ''; + } + + /** + * Renders the modal html. + * + * @param integer $count The number of notes for the user + * @param integer $userId The user ID + * + * @return string The html for the rendered modal + * + * @since 3.4.1 + */ + public function notesModal($count, $userId) + { + if (empty($count)) { + return ''; + } + + $title = Text::plural('COM_USERS_N_USER_NOTES', $count); + $footer = ''; + + return HTMLHelper::_( + 'bootstrap.renderModal', + 'userModal_' . (int) $userId, + array( + 'title' => $title, + 'backdrop' => 'static', + 'keyboard' => true, + 'closeButton' => true, + 'footer' => $footer, + 'url' => Route::_('index.php?option=com_users&view=notes&tmpl=component&layout=modal&filter[user_id]=' . (int) $userId), + 'height' => '300px', + 'width' => '800px', + ) + ); + } + + /** + * Build an array of block/unblock user states to be used by jgrid.state, + * State options will be different for any user + * and for currently logged in user + * + * @param boolean $self True if state array is for currently logged in user + * + * @return array a list of possible states to display + * + * @since 3.0 + */ + public function blockStates($self = false) + { + if ($self) { + $states = array( + 1 => array( + 'task' => 'unblock', + 'text' => '', + 'active_title' => 'COM_USERS_TOOLBAR_BLOCK', + 'inactive_title' => '', + 'tip' => true, + 'active_class' => 'unpublish', + 'inactive_class' => 'unpublish', + ), + 0 => array( + 'task' => 'block', + 'text' => '', + 'active_title' => '', + 'inactive_title' => 'COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF', + 'tip' => true, + 'active_class' => 'publish', + 'inactive_class' => 'publish', + ) + ); + } else { + $states = array( + 1 => array( + 'task' => 'unblock', + 'text' => '', + 'active_title' => 'COM_USERS_TOOLBAR_UNBLOCK', + 'inactive_title' => '', + 'tip' => true, + 'active_class' => 'unpublish', + 'inactive_class' => 'unpublish', + ), + 0 => array( + 'task' => 'block', + 'text' => '', + 'active_title' => 'COM_USERS_TOOLBAR_BLOCK', + 'inactive_title' => '', + 'tip' => true, + 'active_class' => 'publish', + 'inactive_class' => 'publish', + ) + ); + } + + return $states; + } + + /** + * Build an array of activate states to be used by jgrid.state, + * + * @return array a list of possible states to display + * + * @since 3.0 + */ + public function activateStates() + { + $states = array( + 1 => array( + 'task' => 'activate', + 'text' => '', + 'active_title' => 'COM_USERS_TOOLBAR_ACTIVATE', + 'inactive_title' => '', + 'tip' => true, + 'active_class' => 'unpublish', + 'inactive_class' => 'unpublish', + ), + 0 => array( + 'task' => '', + 'text' => '', + 'active_title' => '', + 'inactive_title' => 'COM_USERS_ACTIVATED', + 'tip' => true, + 'active_class' => 'publish', + 'inactive_class' => 'publish', + ) + ); + + return $states; + } + + /** + * Get the sanitized value + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function value($value) + { + if (is_string($value)) { + $value = trim($value); + } + + if (empty($value)) { + return Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND'); + } elseif (!is_array($value)) { + return htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); + } + } + + /** + * Get the space symbol + * + * @param mixed $value Value of the field + * + * @return string + * + * @since 1.6 + */ + public function spacer($value) + { + return ''; + } + + /** + * Get the sanitized template style + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function templatestyle($value) + { + if (empty($value)) { + return static::value($value); + } else { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__template_styles')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $value, ParameterType::INTEGER); + $db->setQuery($query); + $title = $db->loadResult(); + + if ($title) { + return htmlspecialchars($title, ENT_COMPAT, 'UTF-8'); + } else { + return static::value(''); + } + } + } + + /** + * Get the sanitized language + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function admin_language($value) + { + if (!$value) { + return static::value($value); + } + + $path = LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR, $value); + $file = $path . '/langmetadata.xml'; + + if (!is_file($file)) { + // For language packs from before 4.0. + $file = $path . '/' . $value . '.xml'; + + if (!is_file($file)) { + return static::value($value); + } + } + + $result = LanguageHelper::parseXMLLanguageFile($file); + + if ($result) { + return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8'); + } + + return static::value($value); + } + + /** + * Get the sanitized language + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function language($value) + { + if (!$value) { + return static::value($value); + } + + $path = LanguageHelper::getLanguagePath(JPATH_SITE, $value); + $file = $path . '/langmetadata.xml'; + + if (!is_file($file)) { + // For language packs from before 4.0. + $file = $path . '/' . $value . '.xml'; + + if (!is_file($file)) { + return static::value($value); + } + } + + $result = LanguageHelper::parseXMLLanguageFile($file); + + if ($result) { + return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8'); + } + + return static::value($value); + } + + /** + * Get the sanitized editor name + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function editor($value) + { + if (empty($value)) { + return static::value($value); + } else { + $db = Factory::getDbo(); + $lang = Factory::getLanguage(); + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = :element') + ->where($db->quoteName('folder') . ' = ' . $db->quote('editors')) + ->bind(':element', $value); + $db->setQuery($query); + $title = $db->loadResult(); + + if ($title) { + $lang->load("plg_editors_$value.sys", JPATH_ADMINISTRATOR) + || $lang->load("plg_editors_$value.sys", JPATH_PLUGINS . '/editors/' . $value); + $lang->load($title . '.sys'); + + return Text::_($title); + } else { + return static::value(''); + } + } + } } diff --git a/code/administrator/components/com_users/src/Table/MfaTable.php b/code/administrator/components/com_users/src/Table/MfaTable.php new file mode 100644 index 00000000..28dc39a2 --- /dev/null +++ b/code/administrator/components/com_users/src/Table/MfaTable.php @@ -0,0 +1,423 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\Table; + +use Exception; +use Joomla\CMS\Date\Date; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\CMS\Table\Table; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; +use Joomla\Component\Users\Administrator\Model\BackupcodesModel; +use Joomla\Component\Users\Administrator\Service\Encrypt; +use Joomla\Database\DatabaseDriver; +use Joomla\Database\ParameterType; +use Joomla\Event\DispatcherInterface; +use RuntimeException; +use Throwable; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Table for the Multi-Factor Authentication records + * + * @property int $id Record ID. + * @property int $user_id User ID + * @property string $title Record title. + * @property string $method MFA Method (corresponds to one of the plugins). + * @property int $default Is this the default Method? + * @property array $options Configuration options for the MFA Method. + * @property string $created_on Date and time the record was created. + * @property string $last_used Date and time the record was last used successfully. + * + * @since 4.2.0 + */ +class MfaTable extends Table +{ + /** + * Delete flags per ID, set up onBeforeDelete and used onAfterDelete + * + * @var array + * @since 4.2.0 + */ + private $deleteFlags = []; + + /** + * Encryption service + * + * @var Encrypt + * @since 4.2.0 + */ + private $encryptService; + + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.2.0 + */ + // phpcs:ignore + protected $_supportNullValue = true; + + /** + * Table constructor + * + * @param DatabaseDriver $db Database driver object + * @param DispatcherInterface|null $dispatcher Events dispatcher object + * + * @since 4.2.0 + */ + public function __construct(DatabaseDriver $db, DispatcherInterface $dispatcher = null) + { + parent::__construct('#__user_mfa', 'id', $db, $dispatcher); + + $this->encryptService = new Encrypt(); + } + + /** + * Method to store a row in the database from the Table instance properties. + * + * If a primary key value is set the row with that primary key value will be updated with the instance property values. + * If no primary key value is set a new row will be inserted into the database with the properties from the Table instance. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 4.2.0 + */ + public function store($updateNulls = true) + { + // Encrypt the options before saving them + $this->options = $this->encryptService->encrypt(json_encode($this->options ?: [])); + + // Set last_used date to null if empty or zero date + if (!((int) $this->last_used)) { + $this->last_used = null; + } + + $records = MfaHelper::getUserMfaRecords($this->user_id); + + if ($this->id) { + // Existing record. Remove it from the list of records. + $records = array_filter( + $records, + function ($rec) { + return $rec->id != $this->id; + } + ); + } + + // Update the dates on a new record + if (empty($this->id)) { + $this->created_on = Date::getInstance()->toSql(); + $this->last_used = null; + } + + // Do I need to mark this record as the default? + if ($this->default == 0) { + $hasDefaultRecord = array_reduce( + $records, + function ($carry, $record) { + return $carry || ($record->default == 1); + }, + false + ); + + $this->default = $hasDefaultRecord ? 0 : 1; + } + + // Let's find out if we are saving a new MFA method record without having backup codes yet. + $mustCreateBackupCodes = false; + + if (empty($this->id) && $this->method !== 'backupcodes') { + // Do I have any backup records? + $hasBackupCodes = array_reduce( + $records, + function (bool $carry, $record) { + return $carry || $record->method === 'backupcodes'; + }, + false + ); + + $mustCreateBackupCodes = !$hasBackupCodes; + + // If the only other entry is the backup records one I need to make this the default method + if ($hasBackupCodes && count($records) === 1) { + $this->default = 1; + } + } + + // Store the record + try { + $result = parent::store($updateNulls); + } catch (Throwable $e) { + $this->setError($e->getMessage()); + + $result = false; + } + + // Decrypt the options (they must be decrypted in memory) + $this->decryptOptions(); + + if ($result) { + // If this record is the default unset the default flag from all other records + $this->switchDefaultRecord(); + + // Do I need to generate backup codes? + if ($mustCreateBackupCodes) { + $this->generateBackupCodes(); + } + } + + return $result; + } + + /** + * Method to load a row from the database by primary key and bind the fields to the Table instance properties. + * + * @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. + * If not set the instance property value is used. + * @param boolean $reset True to reset the default values before loading the new row. + * + * @return boolean True if successful. False if row not found. + * + * @since 4.2.0 + * @throws \InvalidArgumentException + * @throws RuntimeException + * @throws \UnexpectedValueException + */ + public function load($keys = null, $reset = true) + { + $result = parent::load($keys, $reset); + + if ($result) { + $this->decryptOptions(); + } + + return $result; + } + + /** + * Method to delete a row from the database table by primary key value. + * + * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. + * + * @return boolean True on success. + * + * @since 4.2.0 + * @throws \UnexpectedValueException + */ + public function delete($pk = null) + { + $record = $this; + + if ($pk != $this->id) { + $record = clone $this; + $record->reset(); + $result = $record->load($pk); + + if (!$result) { + // If the record does not exist I will stomp my feet and deny your request + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + $user = Factory::getApplication()->getIdentity() + ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + // The user must be a registered user, not a guest + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Save flags used onAfterDelete + $this->deleteFlags[$record->id] = [ + 'default' => $record->default, + 'numRecords' => $this->getNumRecords($record->user_id), + 'user_id' => $record->user_id, + 'method' => $record->method, + ]; + + if (\is_null($pk)) { + $pk = [$this->_tbl_key => $this->id]; + } elseif (!\is_array($pk)) { + $pk = [$this->_tbl_key => $pk]; + } + + $isDeleted = parent::delete($pk); + + if ($isDeleted) { + $this->afterDelete($pk); + } + + return $isDeleted; + } + + /** + * Decrypt the possibly encrypted options + * + * @return void + * @since 4.2.0 + */ + private function decryptOptions(): void + { + // Try with modern decryption + $decrypted = @json_decode($this->encryptService->decrypt($this->options ?? ''), true); + + if (is_string($decrypted)) { + $decrypted = @json_decode($decrypted, true); + } + + // Fall back to legacy decryption + if (!is_array($decrypted)) { + $decrypted = @json_decode($this->encryptService->decrypt($this->options ?? '', true), true); + + if (is_string($decrypted)) { + $decrypted = @json_decode($decrypted, true); + } + } + + $this->options = $decrypted ?: []; + } + + /** + * If this record is set to be the default, unset the default flag from the other records for the same user. + * + * @return void + * @since 4.2.0 + */ + private function switchDefaultRecord(): void + { + if (!$this->default) { + return; + } + + /** + * This record is marked as default, therefore we need to unset the default flag from all other records for this + * user. + */ + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__user_mfa')) + ->set($db->quoteName('default') . ' = 0') + ->where($db->quoteName('user_id') . ' = :user_id') + ->where($db->quoteName('id') . ' != :id') + ->bind(':user_id', $this->user_id, ParameterType::INTEGER) + ->bind(':id', $this->id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + } + + /** + * Regenerate backup code is the flag is set. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + private function generateBackupCodes(): void + { + /** @var MVCFactoryInterface $factory */ + $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); + + /** @var BackupcodesModel $backupCodes */ + $backupCodes = $factory->createModel('Backupcodes', 'Administrator'); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($this->user_id); + $backupCodes->regenerateBackupCodes($user); + } + + /** + * Runs after successfully deleting a record + * + * @param int|array $pk The promary key of the deleted record + * + * @return void + * @since 4.2.0 + */ + private function afterDelete($pk): void + { + if (is_array($pk)) { + $pk = $pk[$this->_tbl_key] ?? array_shift($pk); + } + + if (!isset($this->deleteFlags[$pk])) { + return; + } + + if (($this->deleteFlags[$pk]['numRecords'] <= 2) && ($this->deleteFlags[$pk]['method'] != 'backupcodes')) { + /** + * This was the second to last MFA record in the database (the last one is the `backupcodes`). Therefore, we + * need to delete the remaining entry and go away. We don't trigger this if the Method we are deleting was + * the `backupcodes` because we might just be regenerating the backup codes. + */ + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + unset($this->deleteFlags[$pk]); + + return; + } + + // This was the default record. Promote the next available record to default. + if ($this->deleteFlags[$pk]['default']) { + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->where($db->quoteName('method') . ' != ' . $db->quote('backupcodes')) + ->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER); + $ids = $db->setQuery($query)->loadColumn(); + + if (empty($ids)) { + return; + } + + $id = array_shift($ids); + $query = $db->getQuery(true) + ->update($db->quoteName('#__user_mfa')) + ->set($db->quoteName('default') . ' = 1') + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + } + } + + /** + * Get the number of MFA records for a give user ID + * + * @param int $userId The user ID to check + * + * @return integer + * + * @since 4.2.0 + */ + private function getNumRecords(int $userId): int + { + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $userId, ParameterType::INTEGER); + $numOldRecords = $db->setQuery($query)->loadResult(); + + return (int) $numOldRecords; + } +} diff --git a/code/administrator/components/com_users/src/Table/NoteTable.php b/code/administrator/components/com_users/src/Table/NoteTable.php index b7b22b26..7a3a3ce6 100644 --- a/code/administrator/components/com_users/src/Table/NoteTable.php +++ b/code/administrator/components/com_users/src/Table/NoteTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_users.note'; - parent::__construct('#__user_notes', 'id', $db); - - $this->setColumnAlias('published', 'state'); - } - - /** - * Overloaded store method for the notes table. - * - * @param boolean $updateNulls Toggle whether null values should be updated. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate()->toSql(); - $userId = Factory::getUser()->get('id'); - - if (!((int) $this->review_time)) - { - $this->review_time = null; - } - - if ($this->id) - { - // Existing item - $this->modified_time = $date; - $this->modified_user_id = $userId; - } - else - { - // New record. - $this->created_time = $date; - $this->created_user_id = $userId; - $this->modified_time = $date; - $this->modified_user_id = $userId; - } - - // Attempt to store the data. - return parent::store($updateNulls); - } - - /** - * Method to perform sanity checks on the Table instance properties to ensure they are safe to store in the database. - * - * @return boolean True if the instance is sane and able to be stored in the database. - * - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (empty($this->modified_time)) - { - $this->modified_time = $this->created_time; - } - - if (empty($this->modified_user_id)) - { - $this->modified_user_id = $this->created_user_id; - } - - return true; - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Constructor + * + * @param DatabaseDriver $db Database object + * + * @since 2.5 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_users.note'; + parent::__construct('#__user_notes', 'id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Overloaded store method for the notes table. + * + * @param boolean $updateNulls Toggle whether null values should be updated. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate()->toSql(); + $userId = Factory::getUser()->get('id'); + + if (!((int) $this->review_time)) { + $this->review_time = null; + } + + if ($this->id) { + // Existing item + $this->modified_time = $date; + $this->modified_user_id = $userId; + } else { + // New record. + $this->created_time = $date; + $this->created_user_id = $userId; + $this->modified_time = $date; + $this->modified_user_id = $userId; + } + + // Attempt to store the data. + return parent::store($updateNulls); + } + + /** + * Method to perform sanity checks on the Table instance properties to ensure they are safe to store in the database. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (empty($this->modified_time)) { + $this->modified_time = $this->created_time; + } + + if (empty($this->modified_user_id)) { + $this->modified_user_id = $this->created_user_id; + } + + return true; + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/code/administrator/components/com_users/src/View/Captive/HtmlView.php b/code/administrator/components/com_users/src/View/Captive/HtmlView.php new file mode 100644 index 00000000..be8d5fc3 --- /dev/null +++ b/code/administrator/components/com_users/src/View/Captive/HtmlView.php @@ -0,0 +1,223 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\View\Captive; + +use Exception; +use Joomla\CMS\Event\MultiFactor\BeforeDisplayMethods; +use Joomla\CMS\Event\MultiFactor\NotifyActionLog; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Toolbar\Button\BasicButton; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; +use Joomla\Component\Users\Administrator\Model\BackupcodesModel; +use Joomla\Component\Users\Administrator\Model\CaptiveModel; +use Joomla\Component\Users\Administrator\View\SiteTemplateTrait; +use stdClass; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * View for Multi-factor Authentication captive page + * + * @since 4.2.0 + */ +class HtmlView extends BaseHtmlView +{ + use SiteTemplateTrait; + + /** + * The MFA Method records for the current user which correspond to enabled plugins + * + * @var array + * @since 4.2.0 + */ + public $records = []; + + /** + * The currently selected MFA Method record against which we'll be authenticating + * + * @var null|stdClass + * @since 4.2.0 + */ + public $record = null; + + /** + * The Captive MFA page's rendering options + * + * @var array|null + * @since 4.2.0 + */ + public $renderOptions = null; + + /** + * The title to display at the top of the page + * + * @var string + * @since 4.2.0 + */ + public $title = ''; + + /** + * Is this an administrator page? + * + * @var boolean + * @since 4.2.0 + */ + public $isAdmin = false; + + /** + * Does the currently selected Method allow authenticating against all of its records? + * + * @var boolean + * @since 4.2.0 + */ + public $allowEntryBatching = false; + + /** + * All enabled MFA Methods (plugins) + * + * @var array + * @since 4.2.0 + */ + public $mfaMethods; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void A string if successful, otherwise an Error object. + * + * @throws Exception + * @since 4.2.0 + */ + public function display($tpl = null) + { + $this->setSiteTemplateStyle(); + + $app = Factory::getApplication(); + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + PluginHelper::importPlugin('multifactorauth'); + $event = new BeforeDisplayMethods($user); + $app->getDispatcher()->dispatch($event->getName(), $event); + + /** @var CaptiveModel $model */ + $model = $this->getModel(); + + // Load data from the model + $this->isAdmin = $app->isClient('administrator'); + $this->records = $this->get('records'); + $this->record = $this->get('record'); + $this->mfaMethods = MfaHelper::getMfaMethods(); + + if (!empty($this->records)) { + /** @var BackupcodesModel $codesModel */ + $codesModel = $this->getModel('Backupcodes'); + $backupCodesRecord = $codesModel->getBackupCodesRecord(); + + if (!is_null($backupCodesRecord)) { + $backupCodesRecord->title = Text::_('COM_USERS_USER_BACKUPCODES'); + $this->records[] = $backupCodesRecord; + } + } + + // If we only have one record there's no point asking the user to select a MFA Method + if (empty($this->record) && !empty($this->records)) { + // Default to the first record + $this->record = reset($this->records); + + // If we have multiple records try to make this record the default + if (count($this->records) > 1) { + foreach ($this->records as $record) { + if ($record->default) { + $this->record = $record; + + break; + } + } + } + } + + // Set the correct layout based on the availability of a MFA record + $this->setLayout('default'); + + // If we have no record selected or explicitly asked to run the 'select' task use the correct layout + if (is_null($this->record) || ($model->getState('task') == 'select')) { + $this->setLayout('select'); + } + + switch ($this->getLayout()) { + case 'select': + $this->allowEntryBatching = 1; + + $event = new NotifyActionLog('onComUsersCaptiveShowSelect', []); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + break; + + case 'default': + default: + $this->renderOptions = $model->loadCaptiveRenderOptions($this->record); + $this->allowEntryBatching = $this->renderOptions['allowEntryBatching'] ?? 0; + + $event = new NotifyActionLog( + 'onComUsersCaptiveShowCaptive', + [ + $this->escape($this->record->title), + ] + ); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + break; + } + + // Which title should I use for the page? + $this->title = $this->get('PageTitle'); + + // Back-end: always show a title in the 'title' module position, not in the page body + if ($this->isAdmin) { + ToolbarHelper::title(Text::_('COM_USERS_USER_MULTIFACTOR_AUTH'), 'users user-lock'); + $this->title = ''; + } + + if ($this->isAdmin && $this->getLayout() === 'default') { + $bar = Toolbar::getInstance(); + $button = (new BasicButton('user-mfa-submit')) + ->text($this->renderOptions['submit_text']) + ->icon($this->renderOptions['submit_icon']); + $bar->appendButton($button); + + $button = (new BasicButton('user-mfa-logout')) + ->text('COM_USERS_MFA_LOGOUT') + ->buttonClass('btn btn-danger') + ->icon('icon icon-lock'); + $bar->appendButton($button); + + if (count($this->records) > 1) { + $arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + $button = (new BasicButton('user-mfa-choose-another')) + ->text('COM_USERS_MFA_USE_DIFFERENT_METHOD') + ->icon('icon-' . $arrow); + $bar->appendButton($button); + } + } + + // Display the view + parent::display($tpl); + } +} diff --git a/code/administrator/components/com_users/src/View/Debuggroup/HtmlView.php b/code/administrator/components/com_users/src/View/Debuggroup/HtmlView.php index c3cdf1b2..93acf884 100644 --- a/code/administrator/components/com_users/src/View/Debuggroup/HtmlView.php +++ b/code/administrator/components/com_users/src/View/Debuggroup/HtmlView.php @@ -1,4 +1,5 @@ authorise('core.manage', 'com_users')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $this->actions = $this->get('DebugActions'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->group = $this->get('Group'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_GROUP_TITLE', $this->group->id, $this->escape($this->group->title)), 'users groups'); - ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Permissions_for_Group'); - } + /** + * List of component actions + * + * @var array + */ + protected $actions; + + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * The id and title for the user group. + * + * @var \stdClass + * @since 4.0.0 + */ + protected $group; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + // Access check. + if (!$this->getCurrentUser()->authorise('core.manage', 'com_users')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $this->actions = $this->get('DebugActions'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->group = $this->get('Group'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_GROUP_TITLE', $this->group->id, $this->escape($this->group->title)), 'users groups'); + ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Permissions_for_Group'); + } } diff --git a/code/administrator/components/com_users/src/View/Debuguser/HtmlView.php b/code/administrator/components/com_users/src/View/Debuguser/HtmlView.php index ab211bc3..bf14e7ce 100644 --- a/code/administrator/components/com_users/src/View/Debuguser/HtmlView.php +++ b/code/administrator/components/com_users/src/View/Debuguser/HtmlView.php @@ -1,4 +1,5 @@ authorise('core.manage', 'com_users')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $this->actions = $this->get('DebugActions'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->user = $this->get('User'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_USER_TITLE', $this->user->id, $this->escape($this->user->name)), 'users user'); - ToolbarHelper::cancel('user.cancel', 'JTOOLBAR_CLOSE'); - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Permissions_for_User'); - } + /** + * List of component actions + * + * @var array + */ + protected $actions; + + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * The user object of the user being debugged. + * + * @var User + */ + protected $user; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + // Access check. + if (!$this->getCurrentUser()->authorise('core.manage', 'com_users')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $this->actions = $this->get('DebugActions'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->user = $this->get('User'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_USER_TITLE', $this->user->id, $this->escape($this->user->name)), 'users user'); + ToolbarHelper::cancel('user.cancel', 'JTOOLBAR_CLOSE'); + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Permissions_for_User'); + } } diff --git a/code/administrator/components/com_users/src/View/Group/HtmlView.php b/code/administrator/components/com_users/src/View/Group/HtmlView.php index ab9e05db..b35a797c 100644 --- a/code/administrator/components/com_users/src/View/Group/HtmlView.php +++ b/code/administrator/components/com_users/src/View/Group/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_GROUP_TITLE' : 'COM_USERS_VIEW_EDIT_GROUP_TITLE'), 'users-cog groups-add'); - - $toolbarButtons = []; - - if ($canDo->get('core.edit') || $canDo->get('core.create')) - { - ToolbarHelper::apply('group.apply'); - $toolbarButtons[] = ['save', 'group.save']; - } - - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'group.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'group.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('group.cancel'); - } - else - { - ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Users:_New_or_Edit_Group'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $item; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_GROUP_TITLE' : 'COM_USERS_VIEW_EDIT_GROUP_TITLE'), 'users-cog groups-add'); + + $toolbarButtons = []; + + if ($canDo->get('core.edit') || $canDo->get('core.create')) { + ToolbarHelper::apply('group.apply'); + $toolbarButtons[] = ['save', 'group.save']; + } + + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'group.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'group.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('group.cancel'); + } else { + ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Users:_New_or_Edit_Group'); + } } diff --git a/code/administrator/components/com_users/src/View/Groups/HtmlView.php b/code/administrator/components/com_users/src/View/Groups/HtmlView.php index fe17b424..cf7ce9c0 100644 --- a/code/administrator/components/com_users/src/View/Groups/HtmlView.php +++ b/code/administrator/components/com_users/src/View/Groups/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::_('COM_USERS_VIEW_GROUPS_TITLE'), 'users-cog groups'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('group.add'); - } - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'groups.delete', 'JTOOLBAR_DELETE'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Users:_Groups'); - } + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::_('COM_USERS_VIEW_GROUPS_TITLE'), 'users-cog groups'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('group.add'); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'groups.delete', 'JTOOLBAR_DELETE'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Users:_Groups'); + } } diff --git a/code/administrator/components/com_users/src/View/Level/HtmlView.php b/code/administrator/components/com_users/src/View/Level/HtmlView.php index 8d53c2c7..9dfec7c3 100644 --- a/code/administrator/components/com_users/src/View/Level/HtmlView.php +++ b/code/administrator/components/com_users/src/View/Level/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_LEVEL_TITLE' : 'COM_USERS_VIEW_EDIT_LEVEL_TITLE'), 'user-lock levels-add'); - - $toolbarButtons = []; - - if ($canDo->get('core.edit') || $canDo->get('core.create')) - { - ToolbarHelper::apply('level.apply'); - $toolbarButtons[] = ['save', 'level.save']; - } - - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'level.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'level.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('level.cancel'); - } - else - { - ToolbarHelper::cancel('level.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Users:_Edit_Viewing_Access_Level'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $item; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_LEVEL_TITLE' : 'COM_USERS_VIEW_EDIT_LEVEL_TITLE'), 'user-lock levels-add'); + + $toolbarButtons = []; + + if ($canDo->get('core.edit') || $canDo->get('core.create')) { + ToolbarHelper::apply('level.apply'); + $toolbarButtons[] = ['save', 'level.save']; + } + + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'level.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'level.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('level.cancel'); + } else { + ToolbarHelper::cancel('level.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Users:_Edit_Viewing_Access_Level'); + } } diff --git a/code/administrator/components/com_users/src/View/Levels/HtmlView.php b/code/administrator/components/com_users/src/View/Levels/HtmlView.php index 6c4429c6..9707b4e5 100644 --- a/code/administrator/components/com_users/src/View/Levels/HtmlView.php +++ b/code/administrator/components/com_users/src/View/Levels/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::_('COM_USERS_VIEW_LEVELS_TITLE'), 'user-lock levels'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('level.add'); - } - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'level.delete', 'JTOOLBAR_DELETE'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Users:_Viewing_Access_Levels'); - } + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::_('COM_USERS_VIEW_LEVELS_TITLE'), 'user-lock levels'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('level.add'); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'level.delete', 'JTOOLBAR_DELETE'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Users:_Viewing_Access_Levels'); + } } diff --git a/code/administrator/components/com_users/src/View/Mail/HtmlView.php b/code/administrator/components/com_users/src/View/Mail/HtmlView.php index beb275ee..2b4d2f9a 100644 --- a/code/administrator/components/com_users/src/View/Mail/HtmlView.php +++ b/code/administrator/components/com_users/src/View/Mail/HtmlView.php @@ -1,4 +1,5 @@ get('massmailoff', 0) == 1) - { - Factory::getApplication()->redirect(Route::_('index.php', false)); - } + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws \Exception + */ + public function display($tpl = null) + { + // Redirect to admin index if mass mailer disabled in conf + if (Factory::getApplication()->get('massmailoff', 0) == 1) { + Factory::getApplication()->redirect(Route::_('index.php', false)); + } - // Get data from the model - $this->form = $this->get('Form'); + // Get data from the model + $this->form = $this->get('Form'); - $this->addToolbar(); - parent::display($tpl); - } + $this->addToolbar(); + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); - ToolbarHelper::title(Text::_('COM_USERS_MASS_MAIL'), 'users massmail'); - ToolbarHelper::custom('mail.send', 'envelope', '', 'COM_USERS_TOOLBAR_MAIL_SEND_MAIL', false); - ToolbarHelper::cancel('mail.cancel'); - ToolbarHelper::divider(); - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - ToolbarHelper::help('Mass_Mail_Users'); - } + ToolbarHelper::title(Text::_('COM_USERS_MASS_MAIL'), 'users massmail'); + ToolbarHelper::custom('mail.send', 'envelope', '', 'COM_USERS_TOOLBAR_MAIL_SEND_MAIL', false); + ToolbarHelper::cancel('mail.cancel'); + ToolbarHelper::divider(); + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + ToolbarHelper::help('Mass_Mail_Users'); + } } diff --git a/code/administrator/components/com_users/src/View/Method/HtmlView.php b/code/administrator/components/com_users/src/View/Method/HtmlView.php new file mode 100644 index 00000000..2e6bf763 --- /dev/null +++ b/code/administrator/components/com_users/src/View/Method/HtmlView.php @@ -0,0 +1,221 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\View\Method; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Toolbar\Button\BasicButton; +use Joomla\CMS\Toolbar\Button\LinkButton; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; +use Joomla\CMS\User\User; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Users\Administrator\Model\MethodModel; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * View for Multi-factor Authentication method add/edit page + * + * @since 4.2.0 + */ +class HtmlView extends BaseHtmlView +{ + /** + * Is this an administrator page? + * + * @var boolean + * @since 4.2.0 + */ + public $isAdmin = false; + + /** + * The editor page render options + * + * @var array + * @since 4.2.0 + */ + public $renderOptions = []; + + /** + * The MFA Method record being edited + * + * @var object + * @since 4.2.0 + */ + public $record = null; + + /** + * The title text for this page + * + * @var string + * @since 4.2.0 + */ + public $title = ''; + + /** + * The return URL to use for all links and forms + * + * @var string + * @since 4.2.0 + */ + public $returnURL = null; + + /** + * The user object used to display this page + * + * @var User + * @since 4.2.0 + */ + public $user = null; + + /** + * The backup codes for the current user. Only applies when the backup codes record is being "edited" + * + * @var array + * @since 4.2.0 + */ + public $backupCodes = []; + + /** + * Am I editing an existing Method? If it's false then I'm adding a new Method. + * + * @var boolean + * @since 4.2.0 + */ + public $isEditExisting = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws \Exception + * @see \JViewLegacy::loadTemplate() + * @since 4.2.0 + */ + public function display($tpl = null): void + { + $app = Factory::getApplication(); + + if (empty($this->user)) { + $this->user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MethodModel $model */ + $model = $this->getModel(); + $this->setLayout('edit'); + $this->renderOptions = $model->getRenderOptions($this->user); + $this->record = $model->getRecord($this->user); + $this->title = $model->getPageTitle(); + $this->isAdmin = $app->isClient('administrator'); + + // Backup codes are a special case, rendered with a special layout + if ($this->record->method == 'backupcodes') { + $this->setLayout('backupcodes'); + + $backupCodes = $this->record->options; + + if (!is_array($backupCodes)) { + $backupCodes = []; + } + + $backupCodes = array_filter( + $backupCodes, + function ($x) { + return !empty($x); + } + ); + + if (count($backupCodes) % 2 != 0) { + $backupCodes[] = ''; + } + + /** + * The call to array_merge resets the array indices. This is necessary since array_filter kept the indices, + * meaning our elements are completely out of order. + */ + $this->backupCodes = array_merge($backupCodes); + } + + // Set up the isEditExisting property. + $this->isEditExisting = !empty($this->record->id); + + // Back-end: always show a title in the 'title' module position, not in the page body + if ($this->isAdmin) { + ToolbarHelper::title($this->title, 'users user-lock'); + + $helpUrl = $this->renderOptions['help_url']; + + if (!empty($helpUrl)) { + ToolbarHelper::help('', false, $helpUrl); + } + + $this->title = ''; + } + + $returnUrl = empty($this->returnURL) ? '' : base64_decode($this->returnURL); + $returnUrl = $returnUrl ?: Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id); + + if ($this->isAdmin && $this->getLayout() === 'edit') { + $bar = Toolbar::getInstance(); + $button = (new BasicButton('user-mfa-edit-save')) + ->text($this->renderOptions['submit_text']) + ->icon($this->renderOptions['submit_icon']) + ->onclick('document.getElementById(\'user-mfa-edit-save\').click()'); + + if ($this->renderOptions['show_submit'] || $this->isEditExisting) { + $bar->appendButton($button); + } + + $button = (new LinkButton('user-mfa-edit-cancel')) + ->text('JCANCEL') + ->buttonClass('btn btn-danger') + ->icon('icon-cancel-2') + ->url($returnUrl); + $bar->appendButton($button); + } elseif ($this->isAdmin && $this->getLayout() === 'backupcodes') { + $bar = Toolbar::getInstance(); + + $arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + $button = (new LinkButton('user-mfa-edit-cancel')) + ->text('JTOOLBAR_BACK') + ->icon('icon-' . $arrow) + ->url($returnUrl); + $bar->appendButton($button); + + $button = (new LinkButton('user-mfa-edit-cancel')) + ->text('COM_USERS_MFA_BACKUPCODES_RESET') + ->buttonClass('btn btn-danger') + ->icon('icon-refresh') + ->url( + Route::_( + sprintf( + "index.php?option=com_users&task=method.regenerateBackupCodes&user_id=%s&%s=1&returnurl=%s", + $this->user->id, + Factory::getApplication()->getFormToken(), + base64_encode($returnUrl) + ) + ) + ); + $bar->appendButton($button); + } + + // Display the view + parent::display($tpl); + } +} diff --git a/code/administrator/components/com_users/src/View/Methods/HtmlView.php b/code/administrator/components/com_users/src/View/Methods/HtmlView.php new file mode 100644 index 00000000..b33daebb --- /dev/null +++ b/code/administrator/components/com_users/src/View/Methods/HtmlView.php @@ -0,0 +1,195 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\View\Methods; + +use Joomla\CMS\Event\MultiFactor\NotifyActionLog; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Toolbar\ToolbarHelper; +use Joomla\CMS\User\User; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Component\Users\Administrator\DataShape\MethodDescriptor; +use Joomla\Component\Users\Administrator\Model\BackupcodesModel; +use Joomla\Component\Users\Administrator\Model\MethodsModel; +use Joomla\Component\Users\Administrator\View\SiteTemplateTrait; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * View for Multi-factor Authentication methods list page + * + * @since 4.2.0 + */ +class HtmlView extends BaseHtmlView +{ + use SiteTemplateTrait; + + /** + * Is this an administrator page? + * + * @var boolean + * @since 4.2.0 + */ + public $isAdmin = false; + + /** + * The MFA Methods available for this user + * + * @var array + * @since 4.2.0 + */ + public $methods = []; + + /** + * The return URL to use for all links and forms + * + * @var string + * @since 4.2.0 + */ + public $returnURL = null; + + /** + * Are there any active MFA Methods at all? + * + * @var boolean + * @since 4.2.0 + */ + public $mfaActive = false; + + /** + * Which Method has the default record? + * + * @var string + * @since 4.2.0 + */ + public $defaultMethod = ''; + + /** + * The user object used to display this page + * + * @var User + * @since 4.2.0 + */ + public $user = null; + + /** + * Is this page part of the mandatory Multi-factor Authentication setup? + * + * @var boolean + * @since 4.2.0 + */ + public $isMandatoryMFASetup = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws \Exception + * @see \JViewLegacy::loadTemplate() + * @since 4.2.0 + */ + public function display($tpl = null): void + { + $this->setSiteTemplateStyle(); + + $app = Factory::getApplication(); + + if (empty($this->user)) { + $this->user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MethodsModel $model */ + $model = $this->getModel(); + + if ($this->getLayout() !== 'firsttime') { + $this->setLayout('default'); + } + + $this->methods = $model->getMethods($this->user); + $this->isAdmin = $app->isClient('administrator'); + $activeRecords = 0; + + foreach ($this->methods as $methodName => $method) { + $methodActiveRecords = count($method['active']); + + if (!$methodActiveRecords) { + continue; + } + + $activeRecords += $methodActiveRecords; + $this->mfaActive = true; + + foreach ($method['active'] as $record) { + if ($record->default) { + $this->defaultMethod = $methodName; + + break; + } + } + } + + // If there are no backup codes yet we should create new ones + /** @var BackupcodesModel $model */ + $model = $this->getModel('backupcodes'); + $backupCodes = $model->getBackupCodes($this->user); + + if ($activeRecords && empty($backupCodes)) { + $model->regenerateBackupCodes($this->user); + } + + $backupCodesRecord = $model->getBackupCodesRecord($this->user); + + if (!is_null($backupCodesRecord)) { + $this->methods = array_merge( + [ + 'backupcodes' => new MethodDescriptor( + [ + 'name' => 'backupcodes', + 'display' => Text::_('COM_USERS_USER_BACKUPCODES'), + 'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'), + 'image' => 'media/com_users/images/emergency.svg', + 'canDisable' => false, + 'active' => [$backupCodesRecord], + ] + ) + ], + $this->methods + ); + } + + $this->isMandatoryMFASetup = $activeRecords === 0 && $app->getSession()->get('com_users.mandatory_mfa_setup', 0) === 1; + + // Back-end: always show a title in the 'title' module position, not in the page body + if ($this->isAdmin) { + ToolbarHelper::title(Text::_('COM_USERS_MFA_LIST_PAGE_HEAD'), 'users user-lock'); + + if (Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_users')) { + ToolbarHelper::back('JTOOLBAR_BACK', Route::_('index.php?option=com_users')); + } + } + + // Display the view + parent::display($tpl); + + $event = new NotifyActionLog('onComUsersViewMethodsAfterDisplay', [$this]); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + Text::script('JGLOBAL_CONFIRM_DELETE'); + } +} diff --git a/code/administrator/components/com_users/src/View/Note/HtmlView.php b/code/administrator/components/com_users/src/View/Note/HtmlView.php index 72b878f3..04971ed5 100644 --- a/code/administrator/components/com_users/src/View/Note/HtmlView.php +++ b/code/administrator/components/com_users/src/View/Note/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - $this->addToolbar(); - } - - /** - * Display the toolbar. - * - * @return void - * - * @since 2.5 - * @throws \Exception - */ - protected function addToolbar() - { - $input = Factory::getApplication()->input; - $input->set('hidemainmenu', 1); - - $user = Factory::getUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_users', 'category', $this->item->catid); - - ToolbarHelper::title(Text::_('COM_USERS_NOTES'), 'users user'); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if (!$checkedOut && ($canDo->get('core.edit') || count($user->getAuthorisedCategories('com_users', 'core.create')))) - { - ToolbarHelper::apply('note.apply'); - $toolbarButtons[] = ['save', 'note.save']; - } - - if (!$checkedOut && count($user->getAuthorisedCategories('com_users', 'core.create'))) - { - $toolbarButtons[] = ['save2new', 'note.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && (count($user->getAuthorisedCategories('com_users', 'core.create')) > 0)) - { - $toolbarButtons[] = ['save2copy', 'note.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('note.cancel'); - } - else - { - ToolbarHelper::cancel('note.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) - { - ToolbarHelper::versions('com_users.note', $this->item->id); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('User_Notes:_New_or_Edit'); - } + /** + * The edit form. + * + * @var \Joomla\CMS\Form\Form + * + * @since 2.5 + */ + protected $form; + + /** + * The item data. + * + * @var object + * @since 2.5 + */ + protected $item; + + /** + * The model state. + * + * @var CMSObject + * @since 2.5 + */ + protected $state; + + /** + * Override the display method for the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 2.5 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise view variables. + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + $this->addToolbar(); + } + + /** + * Display the toolbar. + * + * @return void + * + * @since 2.5 + * @throws \Exception + */ + protected function addToolbar() + { + $input = Factory::getApplication()->input; + $input->set('hidemainmenu', 1); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_users', 'category', $this->item->catid); + + ToolbarHelper::title(Text::_('COM_USERS_NOTES'), 'users user'); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if (!$checkedOut && ($canDo->get('core.edit') || count($user->getAuthorisedCategories('com_users', 'core.create')))) { + ToolbarHelper::apply('note.apply'); + $toolbarButtons[] = ['save', 'note.save']; + } + + if (!$checkedOut && count($user->getAuthorisedCategories('com_users', 'core.create'))) { + $toolbarButtons[] = ['save2new', 'note.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && (count($user->getAuthorisedCategories('com_users', 'core.create')) > 0)) { + $toolbarButtons[] = ['save2copy', 'note.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('note.cancel'); + } else { + ToolbarHelper::cancel('note.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) { + ToolbarHelper::versions('com_users.note', $this->item->id); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('User_Notes:_New_or_Edit'); + } } diff --git a/code/administrator/components/com_users/src/View/Notes/HtmlView.php b/code/administrator/components/com_users/src/View/Notes/HtmlView.php index a3c7c490..19f7a1e9 100644 --- a/code/administrator/components/com_users/src/View/Notes/HtmlView.php +++ b/code/administrator/components/com_users/src/View/Notes/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->user = $this->get('User'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Turn parameters into registry objects - foreach ($this->items as $item) - { - $item->cparams = new Registry($item->category_params); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Display the toolbar. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users', 'category', $this->state->get('filter.category_id')); - - ToolbarHelper::title(Text::_('COM_USERS_VIEW_NOTES_TITLE'), 'users user'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('note.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('notes.publish')->listCheck(true); - $childBar->unpublish('notes.unpublish')->listCheck(true); - $childBar->archive('notes.archive')->listCheck(true); - $childBar->checkin('notes.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') != -2 && $canDo->get('core.edit.state')) - { - $childBar->trash('notes.trash'); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('notes.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_users'); - } - - $toolbar->help('User_Notes'); - } + /** + * A list of user note objects. + * + * @var array + * @since 2.5 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 2.5 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 2.5 + */ + protected $state; + + /** + * The model state. + * + * @var User + * @since 2.5 + */ + protected $user; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Override the display method for the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Initialise view variables. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->user = $this->get('User'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Turn parameters into registry objects + foreach ($this->items as $item) { + $item->cparams = new Registry($item->category_params); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Display the toolbar. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users', 'category', $this->state->get('filter.category_id')); + + ToolbarHelper::title(Text::_('COM_USERS_VIEW_NOTES_TITLE'), 'users user'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('note.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('notes.publish')->listCheck(true); + $childBar->unpublish('notes.unpublish')->listCheck(true); + $childBar->archive('notes.archive')->listCheck(true); + $childBar->checkin('notes.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') != -2 && $canDo->get('core.edit.state')) { + $childBar->trash('notes.trash'); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('notes.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_users'); + } + + $toolbar->help('User_Notes'); + } } diff --git a/code/administrator/components/com_users/src/View/SiteTemplateTrait.php b/code/administrator/components/com_users/src/View/SiteTemplateTrait.php new file mode 100644 index 00000000..68fbf23f --- /dev/null +++ b/code/administrator/components/com_users/src/View/SiteTemplateTrait.php @@ -0,0 +1,68 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Administrator\View; + +use Exception; +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use ReflectionException; +use ReflectionObject; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Dynamically modify the frontend template when showing a MFA captive page. + * + * @since 4.2.0 + */ +trait SiteTemplateTrait +{ + /** + * Set a specific site template style in the frontend application + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + private function setSiteTemplateStyle(): void + { + $app = Factory::getApplication(); + $templateStyle = (int) ComponentHelper::getParams('com_users')->get('captive_template', ''); + + if (empty($templateStyle) || !$app->isClient('site')) { + return; + } + + $itemId = $app->input->get('Itemid'); + + if (!empty($itemId)) { + return; + } + + $app->input->set('templateStyle', $templateStyle); + + try { + $refApp = new ReflectionObject($app); + $refTemplate = $refApp->getProperty('template'); + $refTemplate->setAccessible(true); + $refTemplate->setValue($app, null); + } catch (ReflectionException $e) { + return; + } + + $template = $app->getTemplate(true); + + $app->set('theme', $template->template); + $app->set('themeParams', $template->params); + } +} diff --git a/code/administrator/components/com_users/src/View/User/HtmlView.php b/code/administrator/components/com_users/src/View/User/HtmlView.php index d2e40a73..b8415f85 100644 --- a/code/administrator/components/com_users/src/View/User/HtmlView.php +++ b/code/administrator/components/com_users/src/View/User/HtmlView.php @@ -1,4 +1,5 @@ item = $this->get('Item')) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_NOT_EXIST'), 'error'); - $app->redirect('index.php?option=com_users&view=users'); - } - - $this->form = $this->get('Form'); - $this->state = $this->get('State'); - $this->tfaform = $this->get('Twofactorform'); - $this->otpConfig = $this->get('otpConfig'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Prevent user from modifying own group(s) - $user = Factory::getApplication()->getIdentity(); - - if ((int) $user->id != (int) $this->item->id || $user->authorise('core.admin')) - { - $this->grouplist = $this->get('Groups'); - $this->groups = $this->get('AssignedGroups'); - } - - $this->form->setValue('password', null); - $this->form->setValue('password2', null); - - parent::display($tpl); - $this->addToolbar(); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = Factory::getApplication()->getIdentity(); - $canDo = ContentHelper::getActions('com_users'); - $isNew = ($this->item->id == 0); - $isProfile = $this->item->id == $user->id; - - ToolbarHelper::title( - Text::_( - $isNew ? 'COM_USERS_VIEW_NEW_USER_TITLE' : ($isProfile ? 'COM_USERS_VIEW_EDIT_PROFILE_TITLE' : 'COM_USERS_VIEW_EDIT_USER_TITLE') - ), - 'user ' . ($isNew ? 'user-add' : ($isProfile ? 'user-profile' : 'user-edit')) - ); - - $toolbarButtons = []; - - if ($canDo->get('core.edit') || $canDo->get('core.create') || $isProfile) - { - ToolbarHelper::apply('user.apply'); - $toolbarButtons[] = ['save', 'user.save']; - } - - if ($canDo->get('core.create') && $canDo->get('core.manage')) - { - $toolbarButtons[] = ['save2new', 'user.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('user.cancel'); - } - else - { - ToolbarHelper::cancel('user.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Users:_Edit_Profile'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * Gets the available groups + * + * @var array + */ + protected $grouplist; + + /** + * The groups this user is assigned to + * + * @var array + * @since 1.6 + */ + protected $groups; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The Multi-factor Authentication configuration interface for the user. + * + * @var string|null + * @since 4.2.0 + */ + protected $mfaConfigurationUI; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + */ + public function display($tpl = null) + { + // If no item found, dont show the edit screen, redirect with message + if (false === $this->item = $this->get('Item')) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_NOT_EXIST'), 'error'); + $app->redirect('index.php?option=com_users&view=users'); + } + + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Prevent user from modifying own group(s) + $user = Factory::getApplication()->getIdentity(); + + if ((int) $user->id != (int) $this->item->id || $user->authorise('core.admin')) { + $this->grouplist = $this->get('Groups'); + $this->groups = $this->get('AssignedGroups'); + } + + $this->form->setValue('password', null); + $this->form->setValue('password2', null); + + /** @var User $userBeingEdited */ + $userBeingEdited = Factory::getContainer() + ->get(UserFactoryInterface::class) + ->loadUserById($this->item->id); + + if ($this->item->id > 0 && (int) $userBeingEdited->id == (int) $this->item->id) { + try { + $this->mfaConfigurationUI = Mfa::canShowConfigurationInterface($userBeingEdited) + ? Mfa::getConfigurationInterface($userBeingEdited) + : ''; + } catch (\Exception $e) { + // In case something goes really wrong with the plugins; prevents hard breaks. + $this->mfaConfigurationUI = null; + } + } + + parent::display($tpl); + + $this->addToolbar(); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = Factory::getApplication()->getIdentity(); + $canDo = ContentHelper::getActions('com_users'); + $isNew = ($this->item->id == 0); + $isProfile = $this->item->id == $user->id; + + ToolbarHelper::title( + Text::_( + $isNew ? 'COM_USERS_VIEW_NEW_USER_TITLE' : ($isProfile ? 'COM_USERS_VIEW_EDIT_PROFILE_TITLE' : 'COM_USERS_VIEW_EDIT_USER_TITLE') + ), + 'user ' . ($isNew ? 'user-add' : ($isProfile ? 'user-profile' : 'user-edit')) + ); + + $toolbarButtons = []; + + if ($canDo->get('core.edit') || $canDo->get('core.create') || $isProfile) { + ToolbarHelper::apply('user.apply'); + $toolbarButtons[] = ['save', 'user.save']; + } + + if ($canDo->get('core.create') && $canDo->get('core.manage')) { + $toolbarButtons[] = ['save2new', 'user.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('user.cancel'); + } else { + ToolbarHelper::cancel('user.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Users:_Edit_Profile'); + } } diff --git a/code/administrator/components/com_users/src/View/Users/HtmlView.php b/code/administrator/components/com_users/src/View/Users/HtmlView.php index 97c253f9..a139d9d9 100644 --- a/code/administrator/components/com_users/src/View/Users/HtmlView.php +++ b/code/administrator/components/com_users/src/View/Users/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->canDo = ContentHelper::getActions('com_users'); - $this->db = Factory::getDbo(); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = $this->canDo; - $user = Factory::getUser(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_USERS_VIEW_USERS_TITLE'), 'users user'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('user.add'); - } - - if ($canDo->get('core.edit.state') || $canDo->get('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('users.activate', 'COM_USERS_TOOLBAR_ACTIVATE', true); - $childBar->unpublish('users.block', 'COM_USERS_TOOLBAR_BLOCK', true); - $childBar->standardButton('unblock') - ->text('COM_USERS_TOOLBAR_UNBLOCK') - ->task('users.unblock') - ->listCheck(true); - - // Add a batch button - if ($user->authorise('core.create', 'com_users') - && $user->authorise('core.edit', 'com_users') - && $user->authorise('core.edit.state', 'com_users')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - - if ($canDo->get('core.delete')) - { - $childBar->delete('users.delete') - ->text('JTOOLBAR_DELETE') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_users'); - } - - $toolbar->help('Users'); - } + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * A Form instance with filter fields. + * + * @var \Joomla\CMS\Form\Form + * + * @since 3.6.3 + */ + public $filterForm; + + /** + * An array with active filters. + * + * @var array + * @since 3.6.3 + */ + public $activeFilters; + + /** + * An ACL object to verify user rights. + * + * @var CMSObject + * @since 3.6.3 + */ + protected $canDo; + + /** + * An instance of DatabaseDriver. + * + * @var DatabaseDriver + * @since 3.6.3 + * + * @deprecated 5.0 Will be removed without replacement + */ + protected $db; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->canDo = ContentHelper::getActions('com_users'); + $this->db = Factory::getDbo(); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = $this->canDo; + $user = $this->getCurrentUser(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_USERS_VIEW_USERS_TITLE'), 'users user'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('user.add'); + } + + if ($canDo->get('core.edit.state') || $canDo->get('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('users.activate', 'COM_USERS_TOOLBAR_ACTIVATE', true); + $childBar->unpublish('users.block', 'COM_USERS_TOOLBAR_BLOCK', true); + $childBar->standardButton('unblock') + ->text('COM_USERS_TOOLBAR_UNBLOCK') + ->task('users.unblock') + ->listCheck(true); + + // Add a batch button + if ( + $user->authorise('core.create', 'com_users') + && $user->authorise('core.edit', 'com_users') + && $user->authorise('core.edit.state', 'com_users') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + + if ($canDo->get('core.delete')) { + $childBar->delete('users.delete') + ->text('JTOOLBAR_DELETE') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_users'); + } + + $toolbar->help('Users'); + } } diff --git a/code/administrator/components/com_users/tmpl/captive/default.php b/code/administrator/components/com_users/tmpl/captive/default.php new file mode 100644 index 00000000..7aef9700 --- /dev/null +++ b/code/administrator/components/com_users/tmpl/captive/default.php @@ -0,0 +1,133 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +use Joomla\CMS\Factory; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Administrator\Model\CaptiveModel; +use Joomla\Component\Users\Administrator\View\Captive\HtmlView; +use Joomla\Utilities\ArrayHelper; + +/** + * @var HtmlView $this View object + * @var CaptiveModel $model The model + */ +$model = $this->getModel(); + +$this->document->getWebAssetManager() + ->useScript('com_users.two-factor-focus'); + +?> +
+

+ title)) : ?> + title ?> – + + allowEntryBatching) : ?> + escape($this->record->title) ?> + + escape($this->getModel()->translateMethodName($this->record->method)) ?> + + title)) : ?> + + + renderOptions['help_url'])) : ?> + + + + + + + +

+ + renderOptions['pre_message']) : ?> +
+ renderOptions['pre_message'] ?> +
+ + +
+ + +
+ renderOptions['field_type'] == 'custom') : ?> + renderOptions['html']; ?> + +
+ renderOptions['label']) : ?> + + + $this->renderOptions['input_type'], + 'name' => 'code', + 'value' => '', + 'placeholder' => $this->renderOptions['placeholder'] ?? null, + 'id' => 'users-mfa-code', + 'class' => 'form-control' + ], + $this->renderOptions['input_attributes'] + ); + + if (strpos($attributes['class'], 'form-control') === false) { + $attributes['class'] .= ' form-control'; + } + ?> + > +
+
+ +
+
+ + + + + + + + records) > 1) : ?> + + + + +
+
+
+ + renderOptions['post_message']) : ?> +
+ renderOptions['post_message'] ?> +
+ + +
diff --git a/code/administrator/components/com_users/tmpl/captive/select.php b/code/administrator/components/com_users/tmpl/captive/select.php new file mode 100644 index 00000000..8f63b112 --- /dev/null +++ b/code/administrator/components/com_users/tmpl/captive/select.php @@ -0,0 +1,78 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Prevent direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Uri\Uri; +use Joomla\Component\Users\Administrator\View\Captive\HtmlView; + +/** @var HtmlView $this */ + +$shownMethods = []; + +?> +
+

+ +

+
+

+ +

+
+ +
+ records as $record) : + if (!array_key_exists($record->method, $this->mfaMethods) && ($record->method != 'backupcodes')) { + continue; + } + + $allowEntryBatching = isset($this->mfaMethods[$record->method]) ? $this->mfaMethods[$record->method]['allowEntryBatching'] : false; + + if ($this->allowEntryBatching) { + if ($allowEntryBatching && in_array($record->method, $shownMethods)) { + continue; + } + $shownMethods[] = $record->method; + } + + $methodName = $this->getModel()->translateMethodName($record->method); + ?> + + <?php echo $this->escape(strip_tags($record->title)) ?> + allowEntryBatching || !$allowEntryBatching) : ?> + + method === 'backupcodes') : ?> + title ?> + + escape($record->title) ?> + + + + + + + + + + + + + + + +
+
diff --git a/code/administrator/components/com_users/tmpl/debuggroup/default.php b/code/administrator/components/com_users/tmpl/debuggroup/default.php index ac658cdd..b76b0928 100644 --- a/code/administrator/components/com_users/tmpl/debuggroup/default.php +++ b/code/administrator/components/com_users/tmpl/debuggroup/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
-
- $this)); ?> -
- - - - - - - actions as $key => $action) : ?> - - - - - - - - items as $i => $item) : ?> - - - - actions as $action) : ?> - checks[$name]; - if ($check === true) : - $class = 'text-success icon-check'; - $button = 'btn-success'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); - elseif ($check === false) : - $class = 'text-danger icon-times'; - $button = 'btn-danger'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); - elseif ($check === null) : - $class = 'text-danger icon-minus-circle'; - $button = 'btn-warning'; - $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); - else : - $class = ''; - $button = ''; - $text = ''; - endif; - ?> - - - - - - - -
- , - , - -
- - - - - - - - - -
- escape(Text::_($item->title)); ?> - - $item->level + 1)) . $this->escape($item->name); ?> - - - - - lft; ?> - - rgt; ?> - - id; ?> -
-
-    -    -   -
+
+ $this)); ?> +
+ + + + + + + actions as $key => $action) : ?> + + + + + + + + items as $i => $item) : ?> + + + + actions as $action) : ?> + checks[$name]; + if ($check === true) : + $class = 'text-success icon-check'; + $button = 'btn-success'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); + elseif ($check === false) : + $class = 'text-danger icon-times'; + $button = 'btn-danger'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); + elseif ($check === null) : + $class = 'text-danger icon-minus-circle'; + $button = 'btn-warning'; + $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); + else : + $class = ''; + $button = ''; + $text = ''; + endif; + ?> + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ escape(Text::_($item->title)); ?> + + $item->level + 1)) . $this->escape($item->name); ?> + + + + + lft; ?> + - rgt; ?> + + id; ?> +
+
+    +    +   +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> -
- - - -
+
+ + + +
diff --git a/code/administrator/components/com_users/tmpl/debuguser/default.php b/code/administrator/components/com_users/tmpl/debuguser/default.php index 1933de9b..159af56b 100644 --- a/code/administrator/components/com_users/tmpl/debuguser/default.php +++ b/code/administrator/components/com_users/tmpl/debuguser/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); + +$loginActions = []; +$actions = []; + +// Split the actions table +foreach ($this->actions as $action) : + $name = $action[0]; + if (in_array($name, ['core.login.site', 'core.login.admin', 'core.login.offline', 'core.login.api', 'core.admin'])) : + $loginActions[] = $action; + else : + $actions[] = $action; + endif; +endforeach; ?>
-
- $this)); ?> -
- - - - - - - actions as $key => $action) : ?> - - - - - - - - items as $i => $item) : ?> - - - - actions as $action) : ?> - checks[$name]; - if ($check === true) : - $class = 'text-success icon-check'; - $button = 'btn-success'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); - elseif ($check === false) : - $class = 'text-danger icon-times'; - $button = 'btn-danger'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); - elseif ($check === null) : - $class = 'text-danger icon-minus-circle'; - $button = 'btn-warning'; - $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); - else : - $class = ''; - $button = ''; - $text = ''; - endif; - ?> - - - - - - - -
- , - , - -
- - - - - - - - - -
- escape(Text::_($item->title)); ?> - - $item->level + 1)) . $this->escape($item->name); ?> - - - - - lft; ?> - - rgt; ?> - - id; ?> -
-
-    -    - -
+
+ $this)); ?> +
+ items[0]->checks[$name]; + if ($check === true) : + $class = 'text-success icon-check'; + $button = 'btn-success'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); + elseif ($check === false) : + $class = 'text-danger icon-times'; + $button = 'btn-danger'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); + elseif ($check === null) : + $class = 'text-danger icon-minus-circle'; + $button = 'btn-warning'; + $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); + else : + $class = ''; + $button = ''; + $text = ''; + endif; + ?> +
+ + + +
+ +
+ + + + + + + + $action) : ?> + + + + + + + + items as $i => $item) :?> + + + + + checks[$name]; + if ($check === true) : + $class = 'text-success icon-check'; + $button = 'btn-success'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); + elseif ($check === false) : + $class = 'text-danger icon-times'; + $button = 'btn-danger'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); + elseif ($check === null) : + $class = 'text-danger icon-minus-circle'; + $button = 'btn-warning'; + $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); + else : + $class = ''; + $button = ''; + $text = ''; + endif; + ?> + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ escape(Text::_($item->title)); ?> + + $item->level + 1)) . $this->escape($item->name); ?> + + + + + lft; ?> + - rgt; ?> + + id; ?> +
+ +
+    +    + +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> -
- - - -
+ + + +
diff --git a/code/administrator/components/com_users/tmpl/group/edit.php b/code/administrator/components/com_users/tmpl/group/edit.php index 2bcbb242..d4c95a55 100644 --- a/code/administrator/components/com_users/tmpl/group/edit.php +++ b/code/administrator/components/com_users/tmpl/group/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $this->useCoreUI = true; ?>
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - -
- form->renderField('title'); ?> - form->renderField('parent_id'); ?> -
- - ignore_fieldsets = array('group_details'); ?> - - + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + +
+ form->renderField('title'); ?> + form->renderField('parent_id'); ?> +
+ + ignore_fieldsets = array('group_details'); ?> + + - - + +
diff --git a/code/administrator/components/com_users/tmpl/groups/default.php b/code/administrator/components/com_users/tmpl/groups/default.php index fa183349..36d4d22e 100644 --- a/code/administrator/components/com_users/tmpl/groups/default.php +++ b/code/administrator/components/com_users/tmpl/groups/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect') - ->useScript('com_users.admin-users-groups'); +$wa->useScript('com_users.admin-users-groups') + ->useScript('multiselect') + ->useScript('table.columns'); ?>
-
-
-
- $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_users'); - $canEdit = $user->authorise('core.edit', 'com_users'); +
+
+
+ $this, 'options' => array('filterButton' => false))); ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - -
+ + + + + + + + + + + + + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_users'); + $canEdit = $user->authorise('core.edit', 'com_users'); - // If this group is super admin and this user is not super admin, $canEdit is false - if (!$user->authorise('core.admin') && Access::checkGroup($item->id, 'core.admin')) - { - $canEdit = false; - } - $canChange = $user->authorise('core.edit.state', 'com_users'); - ?> - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + +
- - id, false, 'cid', 'cb', $item->title); ?> - - - $item->level + 1)); ?> - - - escape($item->title); ?> - - escape($item->title); ?> - - - - - - - - - count_enabled; ?> - - - - - count_disabled; ?> - - - - id; ?> -
+ // If this group is super admin and this user is not super admin, $canEdit is false + if (!$user->authorise('core.admin') && Access::checkGroup($item->id, 'core.admin')) { + $canEdit = false; + } + $canChange = $user->authorise('core.edit.state', 'com_users'); + ?> + + + + id, false, 'cid', 'cb', $item->title); ?> + + + + $item->level + 1)); ?> + + + escape($item->title); ?> + + escape($item->title); ?> + + + + + + + + + + + count_enabled; ?> + + + + + + count_disabled; ?> + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
-
-
+ + + + + +
diff --git a/code/administrator/components/com_users/tmpl/level/edit.php b/code/administrator/components/com_users/tmpl/level/edit.php index 389362f3..e4985cc6 100644 --- a/code/administrator/components/com_users/tmpl/level/edit.php +++ b/code/administrator/components/com_users/tmpl/level/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
- -
-
- form->getLabel('title'); ?> -
-
- form->getInput('title'); ?> -
-
-
- - - -
- -
- item->rules, true); ?> -
-
- - - - - - + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+ +
+
+ form->getLabel('title'); ?> +
+
+ form->getInput('title'); ?> +
+
+
+ + + +
+ +
+ item->rules, true); ?> +
+
+ + + + + +
diff --git a/code/administrator/components/com_users/tmpl/levels/default.php b/code/administrator/components/com_users/tmpl/levels/default.php index 2aa16ad3..83e260fb 100644 --- a/code/administrator/components/com_users/tmpl/levels/default.php +++ b/code/administrator/components/com_users/tmpl/levels/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'a.ordering'; -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_users&task=levels.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_users&task=levels.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
-
-
- $this, 'options' => array('filterButton' => false))); ?> +
+
+
+ $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - class="js-draggable" data-url="" data-direction=""> - items); ?> - items as $i => $item) : - $ordering = ($listOrder == 'a.ordering'); - $canCreate = $user->authorise('core.create', 'com_users'); - $canEdit = $user->authorise('core.edit', 'com_users'); - $canChange = $user->authorise('core.edit.state', 'com_users'); + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - -
+ + + + + + + + + + + class="js-draggable" data-url="" data-direction=""> + items); ?> + items as $i => $item) : + $ordering = ($listOrder == 'a.ordering'); + $canCreate = $user->authorise('core.create', 'com_users'); + $canEdit = $user->authorise('core.edit', 'com_users'); + $canChange = $user->authorise('core.edit.state', 'com_users'); - // Decode level groups - $groups = json_decode($item->rules); + // Decode level groups + $groups = json_decode($item->rules); - // If this group is super admin and this user is not super admin, $canEdit is false - if (!Factory::getUser()->authorise('core.admin') && Access::checkGroup($groups[0], 'core.admin')) - { - $canEdit = false; - $canChange = false; - } - ?> - - - - - - - - - -
+ , + , + +
+ + + + + + + + + +
- - id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - - escape($item->title); ?> - - escape($item->title); ?> - - - rules); ?> - - id; ?> -
+ // If this group is super admin and this user is not super admin, $canEdit is false + if (!Factory::getUser()->authorise('core.admin') && $groups && Access::checkGroup($groups[0], 'core.admin')) { + $canEdit = false; + $canChange = false; + } + ?> + + + + id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + + + + escape($item->title); ?> + + escape($item->title); ?> + + + + rules); ?> + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + +
+
+
diff --git a/code/administrator/components/com_users/tmpl/mail/default.php b/code/administrator/components/com_users/tmpl/mail/default.php index 6a5eae89..209d5700 100644 --- a/code/administrator/components/com_users/tmpl/mail/default.php +++ b/code/administrator/components/com_users/tmpl/mail/default.php @@ -1,4 +1,5 @@
-
-
-
-
- form->getLabel('subject'); ?> - - get('mailSubjectPrefix'))) : ?> - get('mailSubjectPrefix'); ?> - - form->getInput('subject'); ?> - -
-
- form->getLabel('message'); ?> - form->getInput('message'); ?> - get('mailBodySuffix'))) : ?> -
-
- get('mailBodySuffix'); ?> -
-
- -
-
- - -
-
-
- form->getInput('recurse'); ?> - form->getLabel('recurse'); ?> -
-
- form->getInput('mode'); ?> - form->getLabel('mode'); ?> -
-
- form->getInput('disabled'); ?> - form->getLabel('disabled'); ?> -
-
- form->getInput('bcc'); ?> - form->getLabel('bcc'); ?> -
-
- form->getLabel('group'); ?> - form->getInput('group'); ?> -
-
-
+
+
+
+
+ form->getLabel('subject'); ?> + + get('mailSubjectPrefix'))) : ?> + get('mailSubjectPrefix'); ?> + + form->getInput('subject'); ?> + +
+
+ form->getLabel('message'); ?> + form->getInput('message'); ?> + get('mailBodySuffix'))) : ?> +
+
+ get('mailBodySuffix'); ?> +
+
+ +
+
+ + +
+
+
+ form->getInput('recurse'); ?> + form->getLabel('recurse'); ?> +
+
+ form->getInput('mode'); ?> + form->getLabel('mode'); ?> +
+
+ form->getInput('disabled'); ?> + form->getLabel('disabled'); ?> +
+
+ form->getInput('bcc'); ?> + form->getLabel('bcc'); ?> +
+
+ form->getLabel('group'); ?> + form->getInput('group'); ?> +
+
+
diff --git a/code/administrator/components/com_users/tmpl/method/backupcodes.php b/code/administrator/components/com_users/tmpl/method/backupcodes.php new file mode 100644 index 00000000..56cb3078 --- /dev/null +++ b/code/administrator/components/com_users/tmpl/method/backupcodes.php @@ -0,0 +1,80 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Prevent direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Administrator\View\Method\HtmlView; + +/** @var HtmlView $this */ + +HTMLHelper::_('bootstrap.tooltip', '.hasTooltip'); + +$cancelURL = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id); + +if (!empty($this->returnURL)) { + $cancelURL = $this->escape(base64_decode($this->returnURL)); +} + +if ($this->record->method != 'backupcodes') { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); +} + +?> +

+ +

+ +

+ +

+ + + backupCodes) / 2); $i++) : ?> + + + + + +
+ backupCodes[2 * $i])) : ?> + + + backupCodes[2 * $i] ?> + + + backupCodes[1 + 2 * $i])) : ?> + + + backupCodes[1 + 2 * $i] ?> + +
+ +
+ + +
+ + diff --git a/code/administrator/components/com_users/tmpl/method/edit.php b/code/administrator/components/com_users/tmpl/method/edit.php new file mode 100644 index 00000000..bbe12af6 --- /dev/null +++ b/code/administrator/components/com_users/tmpl/method/edit.php @@ -0,0 +1,185 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Prevent direct access +defined('_JEXEC') or die; + +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Administrator\View\Method\HtmlView; +use Joomla\Utilities\ArrayHelper; + +/** @var HtmlView $this */ + +$cancelURL = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id); + +if (!empty($this->returnURL)) { + $cancelURL = $this->escape(base64_decode($this->returnURL)); +} + +$recordId = (int) $this->record->id ?? 0; +$method = $this->record->method ?? $this->getModel()->getState('method'); +$userId = (int) $this->user->id ?? 0; +$headingLevel = 2; +$hideSubmit = !$this->renderOptions['show_submit'] && !$this->isEditExisting +?> +
+
" + class="form form-horizontal" id="com-users-method-edit" method="post"> + + returnURL)) : ?> + + + + renderOptions['hidden_data'])) : ?> + renderOptions['hidden_data'] as $key => $value) : ?> + + + + + title)) : ?> + renderOptions['help_url'])) : ?> + + + + + + + + id="com-users-method-edit-head"> + title) ?> + > + + + +
+ +
+ +

+ escape(Text::_('COM_USERS_MFA_EDIT_FIELD_TITLE_DESC')) ?> +

+
+
+ +
+
+
+ record->default ? 'checked="checked"' : ''; ?> name="default"> + +
+
+
+ + renderOptions['pre_message'])) : ?> +
+ renderOptions['pre_message'] ?> +
+ + + renderOptions['tabular_data'])) : ?> +
+ renderOptions['table_heading'])) : ?> + class="h3 border-bottom mb-3"> + renderOptions['table_heading'] ?> + > + + + + renderOptions['tabular_data'] as $cell1 => $cell2) : ?> + + + + + + +
+ + + +
+
+ + + renderOptions['field_type'] == 'custom') : ?> + renderOptions['html']; ?> + +
+ renderOptions['label']) : ?> + + +
renderOptions['label'] ? '' : 'offset-sm-3' ?>> + $this->renderOptions['input_type'], + 'name' => 'code', + 'value' => $this->escape($this->renderOptions['input_value']), + 'id' => 'com-users-method-code', + 'class' => 'form-control', + 'aria-describedby' => 'com-users-method-code-help', + ], + $this->renderOptions['input_attributes'] + ); + + if (strpos($attributes['class'], 'form-control') === false) { + $attributes['class'] .= ' form-control'; + } + ?> + > +

+ escape($this->renderOptions['placeholder']) ?> +

+
+
+ +
+
+
+ + + + + + +
+
+
+ + renderOptions['post_message'])) : ?> +
+ renderOptions['post_message'] ?> +
+ +
+
diff --git a/code/administrator/components/com_users/tmpl/methods/default.php b/code/administrator/components/com_users/tmpl/methods/default.php new file mode 100644 index 00000000..af292f82 --- /dev/null +++ b/code/administrator/components/com_users/tmpl/methods/default.php @@ -0,0 +1,54 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Prevent direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Administrator\View\Methods\HtmlView; + +/** @var HtmlView $this */ +?> +
+
+
+ mfaActive ? 'ON' : 'OFF')) ?> +
+ mfaActive) : ?> +
+ + + +
+ +
+ + methods)) : ?> +
+ + +
+ isMandatoryMFASetup) : ?> +
+

+ +

+

+ +

+
+ + + setLayout('list'); + echo $this->loadTemplate(); ?> +
diff --git a/code/administrator/components/com_users/tmpl/methods/firsttime.php b/code/administrator/components/com_users/tmpl/methods/firsttime.php new file mode 100644 index 00000000..58c474bb --- /dev/null +++ b/code/administrator/components/com_users/tmpl/methods/firsttime.php @@ -0,0 +1,50 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Prevent direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Administrator\View\Methods\HtmlView; + +/** @var HtmlView $this */ + +$headingLevel = 2; +?> +
+ isAdmin) : ?> + id="com-users-methods-list-head"> + + > + +
+ class="alert-heading"> + + + > +

+ +

+ + + +
+ + setLayout('list'); + echo $this->loadTemplate(); ?> +
diff --git a/code/administrator/components/com_users/tmpl/methods/list.php b/code/administrator/components/com_users/tmpl/methods/list.php new file mode 100644 index 00000000..1fa87fbc --- /dev/null +++ b/code/administrator/components/com_users/tmpl/methods/list.php @@ -0,0 +1,144 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Prevent direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Uri\Uri; +use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; +use Joomla\Component\Users\Administrator\Model\MethodsModel; +use Joomla\Component\Users\Administrator\View\Methods\HtmlView; + +/** @var HtmlView $this */ + +HTMLHelper::_('bootstrap.tooltip', '.hasTooltip'); + +/** @var MethodsModel $model */ +$model = $this->getModel(); + +$this->document->getWebAssetManager()->useScript('com_users.two-factor-list'); + +$canAddEdit = MfaHelper::canAddEditMethod($this->user); +$canDelete = MfaHelper::canDeleteMethod($this->user); +?> +
+ methods as $methodName => $method) : + $methodClass = 'com-users-methods-list-method-name-' . htmlentities($method['name']) + . ($this->defaultMethod == $methodName ? ' com-users-methods-list-method-default' : ''); + ?> +
+
+
+ <?php echo $this->escape($method['display']) ?> +
+
+

+ + + + defaultMethod == $methodName) : ?> + + + + +

+
+
+ +
+
+ +
+ + +
+ +
+
+ + +
+ id . ($this->returnURL ? '&returnurl=' . $this->escape(urlencode($this->returnURL)) : '') . '&user_id=' . $this->user->id)) ?> +
+ + +

+ default) : ?> + + + escape(Text::_('COM_USERS_MFA_LIST_DEFAULTTAG')) ?> + + + + escape($record->title); ?> + +

+ + +
+ + formatRelative($record->created_on)) ?> + + + formatRelative($record->last_used)) ?> + +
+ +
+ + + + +
+ +
+ + + + + +
+
+ +
diff --git a/code/administrator/components/com_users/tmpl/note/edit.php b/code/administrator/components/com_users/tmpl/note/edit.php index ba80a12a..8fd2b6d2 100644 --- a/code/administrator/components/com_users/tmpl/note/edit.php +++ b/code/administrator/components/com_users/tmpl/note/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
-
-
-
-
-
- form->renderField('subject'); ?> - form->renderField('user_id'); ?> - form->renderField('catid'); ?> - form->renderField('state'); ?> - form->renderField('review_time'); ?> - form->renderField('version_note'); ?> +
+
+
+
+
+ form->renderField('subject'); ?> + form->renderField('user_id'); ?> + form->renderField('catid'); ?> + form->renderField('state'); ?> + form->renderField('review_time'); ?> + form->renderField('version_note'); ?> - - -
-
- form->renderField('body'); ?> -
-
-
-
-
+ + +
+
+ form->renderField('body'); ?> +
+
+
+
+
diff --git a/code/administrator/components/com_users/tmpl/notes/default.php b/code/administrator/components/com_users/tmpl/notes/default.php index a252b4dc..b158e7cf 100644 --- a/code/administrator/components/com_users/tmpl/notes/default.php +++ b/code/administrator/components/com_users/tmpl/notes/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -25,103 +27,103 @@ ?>
-
-
-
- $this)); ?> +
+
+
+ $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - items as $i => $item) : - $canEdit = $user->authorise('core.edit', 'com_users.category.' . $item->catid); - $canCheckin = $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_users.category.' . $item->catid) && $canCheckin; - $subject = $item->subject ?: Text::_('COM_USERS_EMPTY_SUBJECT'); - ?> - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - -
- id, false, 'cid', 'cb', $subject); ?> - - state, $i, 'notes.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'notes.', $canCheckin); ?> - - subject ?: Text::_('COM_USERS_EMPTY_SUBJECT'); ?> - - - escape($subject); ?> - - escape($subject); ?> - -
- escape($item->category_title); ?> -
-
- escape($item->user_name); ?> - - review_time !== null) : ?> - review_time, Text::_('DATE_FORMAT_LC4')); ?> - - - - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + items as $i => $item) : + $canEdit = $user->authorise('core.edit', 'com_users.category.' . $item->catid); + $canCheckin = $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_users.category.' . $item->catid) && $canCheckin; + $subject = $item->subject ?: Text::_('COM_USERS_EMPTY_SUBJECT'); + ?> + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+ id, false, 'cid', 'cb', $subject); ?> + + state, $i, 'notes.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'notes.', $canCheckin); ?> + + subject ?: Text::_('COM_USERS_EMPTY_SUBJECT'); ?> + + + escape($subject); ?> + + escape($subject); ?> + +
+ escape($item->category_title); ?> +
+
+ escape($item->user_name); ?> + + review_time !== null) : ?> + review_time, Text::_('DATE_FORMAT_LC4')); ?> + + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + -
- - - -
-
-
-
+
+ + + +
+
+
+
diff --git a/code/administrator/components/com_users/tmpl/notes/emptystate.php b/code/administrator/components/com_users/tmpl/notes/emptystate.php index 2366086b..0b963fa8 100644 --- a/code/administrator/components/com_users/tmpl/notes/emptystate.php +++ b/code/administrator/components/com_users/tmpl/notes/emptystate.php @@ -1,4 +1,5 @@ 'COM_USERS_NOTES', - 'formURL' => 'index.php?option=com_users&view=notes', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:User_Notes', - 'icon' => 'icon-users user', + 'textPrefix' => 'COM_USERS_NOTES', + 'formURL' => 'index.php?option=com_users&view=notes', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:User_Notes', + 'icon' => 'icon-users user', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_users')) -{ - $displayData['createURL'] = 'index.php?option=com_users&task=note.add'; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_users')) { + $displayData['createURL'] = 'index.php?option=com_users&task=note.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/code/administrator/components/com_users/tmpl/notes/modal.php b/code/administrator/components/com_users/tmpl/notes/modal.php index deea2d14..19b781d8 100644 --- a/code/administrator/components/com_users/tmpl/notes/modal.php +++ b/code/administrator/components/com_users/tmpl/notes/modal.php @@ -1,4 +1,5 @@
-

user->name, $this->user->id); ?>

+

user->name, $this->user->id); ?>

items)) : ?> - + -
    - items as $item) : ?> -
  • -
    - subject) : ?> -

    id, $this->escape($item->subject)); ?>

    - -

    id, Text::_('COM_USERS_EMPTY_SUBJECT')); ?>

    - -
    - -
    - created_time, Text::_('DATE_FORMAT_LC2')); ?> -
    - - cparams->get('image'); ?> - - catid && isset($category_image)) : ?> -
    - -
    - -
    - escape($item->category_title); ?> -
    - - -
    -
    - body) ? HTMLHelper::_('content.prepare', $item->body) : ''); ?> -
    -
  • - -
+
    + items as $item) : ?> +
  • +
    + subject) : ?> +

    id, $this->escape($item->subject)); ?>

    + +

    id, Text::_('COM_USERS_EMPTY_SUBJECT')); ?>

    + +
    + +
    + created_time, Text::_('DATE_FORMAT_LC2')); ?> +
    + + cparams->get('image'); ?> + + catid && isset($category_image)) : ?> +
    + +
    + +
    + escape($item->category_title); ?> +
    + + +
    +
    + body) ? HTMLHelper::_('content.prepare', $item->body) : ''); ?> +
    +
  • + +
diff --git a/code/administrator/components/com_users/tmpl/user/edit.php b/code/administrator/components/com_users/tmpl/user/edit.php index 7515e95c..826d04cd 100644 --- a/code/administrator/components/com_users/tmpl/user/edit.php +++ b/code/administrator/components/com_users/tmpl/user/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_users.two-factor-switcher'); + ->useScript('form.validate'); $input = Factory::getApplication()->input; @@ -32,88 +33,50 @@ ?>
-

form->getValue('name', null, Text::_('COM_USERS_USER_NEW_USER_TITLE')); ?>

- -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
- -
- form->renderFieldset('user_details'); ?> -
-
- - - - grouplist) : ?> - -
- -
- loadTemplate('groups'); ?> -
-
- - - - ignore_fieldsets = array('user_details'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - tfaform) && $this->item->id) : ?> - -
- -
-
- -
-
- 'Joomla.twoFactorMethodChange();', 'class' => 'form-select'), 'value', 'text', $this->otpConfig->method, 'jform_twofactor_method', false); ?> -
-
-
- tfaform as $form) : ?> - otpConfig->method ? '' : ' class="hidden"'; ?> -
> - -
- -
-
-
- -

- -

- -
- - -
- otpConfig->otep)) : ?> -
- - -
- - otpConfig->otep as $otep) : ?> -
- - - - - - - -
- - - - +

form->getValue('name', null, Text::_('COM_USERS_USER_NEW_USER_TITLE')); ?>

+ +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+ +
+ form->renderFieldset('user_details'); ?> +
+
+ + + + grouplist) : ?> + +
+ +
+ loadTemplate('groups'); ?> +
+
+ + + + ignore_fieldsets = array('user_details'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + mfaConfigurationUI)) : ?> + +
+ + mfaConfigurationUI ?> +
+ + + + +
+ + + +
diff --git a/code/administrator/components/com_users/tmpl/user/edit_groups.php b/code/administrator/components/com_users/tmpl/user/edit_groups.php index d8d97b88..2500469e 100644 --- a/code/administrator/components/com_users/tmpl/user/edit_groups.php +++ b/code/administrator/components/com_users/tmpl/user/edit_groups.php @@ -1,4 +1,5 @@ -groups, true); ?> +echo HTMLHelper::_('access.usergroups', 'jform[groups]', $this->groups, true); diff --git a/code/administrator/components/com_users/tmpl/users/default.php b/code/administrator/components/com_users/tmpl/users/default.php index 10c73a62..f006caa3 100644 --- a/code/administrator/components/com_users/tmpl/users/default.php +++ b/code/administrator/components/com_users/tmpl/users/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $loggeduser = Factory::getUser(); -$tfa = PluginHelper::isEnabled('twofactorauth'); +$mfa = PluginHelper::isEnabled('multifactorauth'); ?>
-
-
-
- $this)); - ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - - - items as $i => $item) : - $canEdit = $this->canDo->get('core.edit'); - $canChange = $loggeduser->authorise('core.edit.state', 'com_users'); +
+
+
+ $this)); + ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + + + + items as $i => $item) : + $canEdit = $this->canDo->get('core.edit'); + $canChange = $loggeduser->authorise('core.edit.state', 'com_users'); - // If this group is super admin and this user is not super admin, $canEdit is false - if ((!$loggeduser->authorise('core.admin')) && Access::check($item->id, 'core.admin')) - { - $canEdit = false; - $canChange = false; - } - ?> - - - - - - - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + +
- - id, false, 'cid', 'cb', $item->name); ?> - - -
- - - escape($item->name); ?> - - escape($item->name); ?> - -
-
- id); ?> - note_count > 0) : ?> - - - -
- note_count, $item->id); ?> - requireReset == '1') : ?> - - -
- escape($item->username); ?> - - id == $item->id; ?> - - block, $i, 'users.', !$self); ?> - - block, $i, 'users.', false); ?> - - - activation) ? 0 : 1; - echo HTMLHelper::_('jgrid.state', HTMLHelper::_('users.activateStates'), $activated, $i, 'users.', (boolean) $activated); - ?> - - - otpKey)) : ?> - - - - - - - - - group_names, "\n") > 1) : ?> - - - - group_names, false); ?> - - - - - - escape($item->email)); ?> - - lastvisitDate !== null) : ?> - lastvisitDate, Text::_('DATE_FORMAT_LC6')); ?> - - - - - registerDate, Text::_('DATE_FORMAT_LC6')); ?> - - id; ?> -
+ // If this group is super admin and this user is not super admin, $canEdit is false + if ((!$loggeduser->authorise('core.admin')) && Access::check($item->id, 'core.admin')) { + $canEdit = false; + $canChange = false; + } + ?> + + + + id, false, 'cid', 'cb', $item->name); ?> + + + +
+ + + escape($item->name); ?> + + escape($item->name); ?> + +
+
+ id); ?> + note_count > 0) : ?> + + + +
+ note_count, $item->id); ?> + requireReset == '1') : ?> + + + + + escape($item->username); ?> + + + id == $item->id; ?> + + block, $i, 'users.', !$self); ?> + + block, $i, 'users.', false); ?> + + + + activation) ? 0 : 1; + echo HTMLHelper::_('jgrid.state', HTMLHelper::_('users.activateStates'), $activated, $i, 'users.', (bool) $activated); + ?> + + + + + mfaRecords > 0 || !empty($item->otpKey)) : ?> + + + + + + + + + + + group_names, "\n") > 1) : ?> + + + + group_names, false); ?> + + + + + + + escape($item->email)); ?> + + + lastvisitDate !== null) : ?> + lastvisitDate, Text::_('DATE_FORMAT_LC6')); ?> + + + + + + registerDate, Text::_('DATE_FORMAT_LC6')); ?> + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_users') - && $loggeduser->authorise('core.edit', 'com_users') - && $loggeduser->authorise('core.edit.state', 'com_users')) : ?> - Text::_('COM_USERS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_users') + && $loggeduser->authorise('core.edit', 'com_users') + && $loggeduser->authorise('core.edit.state', 'com_users') + ) : ?> + Text::_('COM_USERS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - -
-
-
+ + + + + +
diff --git a/code/administrator/components/com_users/tmpl/users/default_batch_body.php b/code/administrator/components/com_users/tmpl/users/default_batch_body.php index b74fd0e8..1c4b4b3c 100644 --- a/code/administrator/components/com_users/tmpl/users/default_batch_body.php +++ b/code/administrator/components/com_users/tmpl/users/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\HTML\HTMLHelper; @@ -13,16 +15,16 @@ // Create the copy/move options. $options = array( - HTMLHelper::_('select.option', 'add', Text::_('COM_USERS_BATCH_ADD')), - HTMLHelper::_('select.option', 'del', Text::_('COM_USERS_BATCH_DELETE')), - HTMLHelper::_('select.option', 'set', Text::_('COM_USERS_BATCH_SET')) + HTMLHelper::_('select.option', 'add', Text::_('COM_USERS_BATCH_ADD')), + HTMLHelper::_('select.option', 'del', Text::_('COM_USERS_BATCH_DELETE')), + HTMLHelper::_('select.option', 'set', Text::_('COM_USERS_BATCH_SET')) ); // Create the reset password options. $resetOptions = array( - HTMLHelper::_('select.option', '', Text::_('COM_USERS_NO_ACTION')), - HTMLHelper::_('select.option', 'yes', Text::_('JYES')), - HTMLHelper::_('select.option', 'no', Text::_('JNO')) + HTMLHelper::_('select.option', '', Text::_('COM_USERS_NO_ACTION')), + HTMLHelper::_('select.option', 'yes', Text::_('JYES')), + HTMLHelper::_('select.option', 'no', Text::_('JNO')) ); /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -32,33 +34,33 @@ ?>
-
-
- -
- -
-
-
-
- - - - -
-
-
-
- - - - -
-
-
+
+
+ +
+ +
+
+
+
+ + + + +
+
+
+
+ + + + +
+
+
diff --git a/code/administrator/components/com_users/tmpl/users/default_batch_footer.php b/code/administrator/components/com_users/tmpl/users/default_batch_footer.php index 7f35695f..e8db0553 100644 --- a/code/administrator/components/com_users/tmpl/users/default_batch_footer.php +++ b/code/administrator/components/com_users/tmpl/users/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/code/administrator/components/com_users/tmpl/users/modal.php b/code/administrator/components/com_users/tmpl/users/modal.php index c1dae97d..787156ab 100644 --- a/code/administrator/components/com_users/tmpl/users/modal.php +++ b/code/administrator/components/com_users/tmpl/users/modal.php @@ -1,4 +1,5 @@
-
- -
-   -
- - $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - items as $item) : ?> - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - -
- - escape($item->name); ?> - - - escape($item->username); ?> - - - - - - - - - - group_names, false); ?> - - id; ?> -
+ + +
+   +
+ + $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + items as $item) : ?> + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+ + escape($item->name); ?> + + + escape($item->username); ?> + + + + + + + + + + group_names, false); ?> + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - - - -
+ + + + + + +
diff --git a/code/administrator/components/com_users/users.xml b/code/administrator/components/com_users/users.xml index c4b9b1a7..cbcef9b8 100644 --- a/code/administrator/components/com_users/users.xml +++ b/code/administrator/components/com_users/users.xml @@ -2,7 +2,7 @@ com_users Joomla! Project - April 2006 + 2006-04 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org @@ -29,6 +29,7 @@ users.xml forms helpers + postinstall services src tmpl diff --git a/code/administrator/components/com_workflow/services/provider.php b/code/administrator/components/com_workflow/services/provider.php index dbaf8e74..81471b58 100644 --- a/code/administrator/components/com_workflow/services/provider.php +++ b/code/administrator/components/com_workflow/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Workflow')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Workflow')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Workflow')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Workflow')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_workflow/src/Controller/DisplayController.php b/code/administrator/components/com_workflow/src/Controller/DisplayController.php index 7be435b3..d8bc1b6f 100644 --- a/code/administrator/components/com_workflow/src/Controller/DisplayController.php +++ b/code/administrator/components/com_workflow/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. - * - * @return BaseController|boolean This object to support chaining. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = array()) - { - $view = $this->input->get('view'); - $layout = $this->input->get('layout'); - $id = $this->input->getInt('id'); - - // Check for edit form. - if (in_array($view, ['workflow', 'stage', 'transition']) && $layout == 'edit' && !$this->checkEditId('com_workflow.edit.' . $view, $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $url = 'index.php?option=com_workflow&view=' . Inflector::pluralize($view) . '&extension=' . $this->input->getCmd('extension'); - - $this->setRedirect(Route::_($url, false)); - - return false; - } - - return parent::display(); - } + /** + * The default view. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'workflows'; + + /** + * The extension for which the workflow apply. + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return BaseController|boolean This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view'); + $layout = $this->input->get('layout'); + $id = $this->input->getInt('id'); + + // Check for edit form. + if (in_array($view, ['workflow', 'stage', 'transition']) && $layout == 'edit' && !$this->checkEditId('com_workflow.edit.' . $view, $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $url = 'index.php?option=com_workflow&view=' . Inflector::pluralize($view) . '&extension=' . $this->input->getCmd('extension'); + + $this->setRedirect(Route::_($url, false)); + + return false; + } + + return parent::display(); + } } diff --git a/code/administrator/components/com_workflow/src/Controller/StageController.php b/code/administrator/components/com_workflow/src/Controller/StageController.php index 63be6775..e4b6c003 100644 --- a/code/administrator/components/com_workflow/src/Controller/StageController.php +++ b/code/administrator/components/com_workflow/src/Controller/StageController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -16,6 +16,10 @@ use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\Input\Input; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * The stage controller * @@ -23,159 +27,151 @@ */ class StageController extends FormController { - /** - * The workflow in where the stage belongs to - * - * @var integer - * @since 4.0.0 - */ - protected $workflowId; - - /** - * The extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension or workflow id is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If workflow id is not set try to get it from input or throw an exception - if (empty($this->workflowId)) - { - $this->workflowId = $this->input->getInt('workflow_id'); - - if (empty($this->workflowId)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); - } - } - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->extension . '.workflow.' . (int) $this->workflowId); - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = isset($data[$key]) ? (int) $data[$key] : 0; - $user = $this->app->getIdentity(); - - $record = $this->getModel()->getItem($recordId); - - if (empty($record->id)) - { - return false; - } - - // Check "edit" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->extension . '.stage.' . $recordId)) - { - return true; - } - - // Check "edit own" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->extension . '.stage.' . $recordId)) - { - return !empty($record) && $record->created_by == $user->id; - } - - return false; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - - return $append; - } + /** + * The workflow in where the stage belongs to + * + * @var integer + * @since 4.0.0 + */ + protected $workflowId; + + /** + * The extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension or workflow id is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If workflow id is not set try to get it from input or throw an exception + if (empty($this->workflowId)) { + $this->workflowId = $this->input->getInt('workflow_id'); + + if (empty($this->workflowId)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); + } + } + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->extension . '.workflow.' . (int) $this->workflowId); + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = isset($data[$key]) ? (int) $data[$key] : 0; + $user = $this->app->getIdentity(); + + $record = $this->getModel()->getItem($recordId); + + if (empty($record->id)) { + return false; + } + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.stage.' . $recordId)) { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.stage.' . $recordId)) { + return !empty($record) && $record->created_by == $user->id; + } + + return false; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + + return $append; + } } diff --git a/code/administrator/components/com_workflow/src/Controller/StagesController.php b/code/administrator/components/com_workflow/src/Controller/StagesController.php index 61be18cb..651fc95e 100644 --- a/code/administrator/components/com_workflow/src/Controller/StagesController.php +++ b/code/administrator/components/com_workflow/src/Controller/StagesController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -18,6 +18,10 @@ use Joomla\Input\Input; use Joomla\Utilities\ArrayHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * The workflow stages controller * @@ -25,182 +29,170 @@ */ class StagesController extends AdminController { - /** - * The workflow in where the stage belongs to - * - * @var integer - * @since 4.0.0 - */ - protected $workflowId; - - /** - * The extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * The prefix to use with controller messages. - * - * @var string - * @since 4.0.0 - */ - protected $text_prefix = 'COM_WORKFLOW_STAGES'; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension or workflow id is set - */ - public function __construct(array $config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If workflow id is not set try to get it from input or throw an exception - if (empty($this->workflowId)) - { - $this->workflowId = $this->input->getInt('workflow_id'); - - if (empty($this->workflowId)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); - } - } - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - - $this->registerTask('unsetDefault', 'setDefault'); - } - - /** - * Proxy for getModel - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 4.0.0 - */ - public function getModel($name = 'Stage', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to set the home property for a list of items - * - * @return void - * - * @since 4.0.0 - */ - public function setDefault() - { - // Check for request forgeries - $this->checkToken(); - - // Get items to publish from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); - $data = array('setDefault' => 1, 'unsetDefault' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($data, $task, 0, 'int'); - - if (!$value) - { - $this->setMessage(Text::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&extension=' . $this->extension, false - ) - ); - - return; - } - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - $this->setMessage(Text::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); - } - elseif (count($cid) > 1) - { - $this->setMessage(Text::_('COM_WORKFLOW_TOO_MANY_STAGES'), 'error'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Make sure the item ids are integers - $id = reset($cid); - - // Publish the items. - if (!$model->setDefault($id, $value)) - { - $this->setMessage($model->getError(), 'warning'); - } - else - { - $this->setMessage(Text::_('COM_WORKFLOW_STAGE_SET_DEFAULT')); - } - } - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&extension=' . $this->extension - . '&workflow_id=' . $this->workflowId, false - ) - ); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - return '&extension=' . $this->extension . ($this->section ? '.' . $this->section : '') . '&workflow_id=' . $this->workflowId; - } + /** + * The workflow in where the stage belongs to + * + * @var integer + * @since 4.0.0 + */ + protected $workflowId; + + /** + * The extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 4.0.0 + */ + protected $text_prefix = 'COM_WORKFLOW_STAGES'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension or workflow id is set + */ + public function __construct(array $config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If workflow id is not set try to get it from input or throw an exception + if (empty($this->workflowId)) { + $this->workflowId = $this->input->getInt('workflow_id'); + + if (empty($this->workflowId)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); + } + } + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + + $this->registerTask('unsetDefault', 'setDefault'); + } + + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 4.0.0 + */ + public function getModel($name = 'Stage', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to set the home property for a list of items + * + * @return void + * + * @since 4.0.0 + */ + public function setDefault() + { + // Check for request forgeries + $this->checkToken(); + + // Get items to publish from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); + $data = array('setDefault' => 1, 'unsetDefault' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + if (!$value) { + $this->setMessage(Text::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->extension, + false + ) + ); + + return; + } + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + $this->setMessage(Text::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); + } elseif (count($cid) > 1) { + $this->setMessage(Text::_('COM_WORKFLOW_TOO_MANY_STAGES'), 'error'); + } else { + // Get the model. + $model = $this->getModel(); + + // Make sure the item ids are integers + $id = reset($cid); + + // Publish the items. + if (!$model->setDefault($id, $value)) { + $this->setMessage($model->getError(), 'warning'); + } else { + $this->setMessage(Text::_('COM_WORKFLOW_STAGE_SET_DEFAULT')); + } + } + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->extension + . '&workflow_id=' . $this->workflowId, + false + ) + ); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + return '&extension=' . $this->extension . ($this->section ? '.' . $this->section : '') . '&workflow_id=' . $this->workflowId; + } } diff --git a/code/administrator/components/com_workflow/src/Controller/TransitionController.php b/code/administrator/components/com_workflow/src/Controller/TransitionController.php index fd7b19e8..b7e5c11a 100644 --- a/code/administrator/components/com_workflow/src/Controller/TransitionController.php +++ b/code/administrator/components/com_workflow/src/Controller/TransitionController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -16,6 +16,10 @@ use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\Input\Input; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Transition controller * @@ -23,160 +27,152 @@ */ class TransitionController extends FormController { - /** - * The workflow where the transition takes place - * - * @var integer - * @since 4.0.0 - */ - protected $workflowId; - - /** - * The extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension or workflow id is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If workflow id is not set try to get it from input or throw an exception - if (empty($this->workflowId)) - { - $this->workflowId = $this->input->getInt('workflow_id'); - - if (empty($this->workflowId)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); - } - } - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->extension . '.workflow.' . (int) $this->workflowId); - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = isset($data[$key]) ? (int) $data[$key] : 0; - $user = $this->app->getIdentity(); - - $model = $this->getModel(); - - $item = $model->getItem($recordId); - - if (empty($item->id)) - { - return false; - } - - // Check "edit" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->extension . '.transition.' . $recordId)) - { - return true; - } - - // Check "edit own" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->extension . '.transition.' . $recordId)) - { - return !empty($item) && $item->created_by == $user->id; - } - - return false; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; - - return $append; - } + /** + * The workflow where the transition takes place + * + * @var integer + * @since 4.0.0 + */ + protected $workflowId; + + /** + * The extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension or workflow id is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If workflow id is not set try to get it from input or throw an exception + if (empty($this->workflowId)) { + $this->workflowId = $this->input->getInt('workflow_id'); + + if (empty($this->workflowId)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); + } + } + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->extension . '.workflow.' . (int) $this->workflowId); + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = isset($data[$key]) ? (int) $data[$key] : 0; + $user = $this->app->getIdentity(); + + $model = $this->getModel(); + + $item = $model->getItem($recordId); + + if (empty($item->id)) { + return false; + } + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.transition.' . $recordId)) { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.transition.' . $recordId)) { + return !empty($item) && $item->created_by == $user->id; + } + + return false; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; + + return $append; + } } diff --git a/code/administrator/components/com_workflow/src/Controller/TransitionsController.php b/code/administrator/components/com_workflow/src/Controller/TransitionsController.php index 31670f51..416e7e58 100644 --- a/code/administrator/components/com_workflow/src/Controller/TransitionsController.php +++ b/code/administrator/components/com_workflow/src/Controller/TransitionsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -16,6 +16,10 @@ use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\Input\Input; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Workflow Transitions controller * @@ -23,115 +27,110 @@ */ class TransitionsController extends AdminController { - /** - * The workflow where the transition takes place - * - * @var integer - * @since 4.0.0 - */ - protected $workflowId; - - /** - * The extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * The prefix to use with controller messages. - * - * @var string - * @since 4.0.0 - */ - protected $text_prefix = 'COM_WORKFLOW_TRANSITIONS'; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension or workflow id is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If workflow id is not set try to get it from input or throw an exception - if (empty($this->workflowId)) - { - $this->workflowId = $this->input->getInt('workflow_id'); - - if (empty($this->workflowId)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); - } - } - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Proxy for getModel - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 4.0.0 - */ - public function getModel($name = 'Transition', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - - $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : '') - . '&workflow_id=' . $this->workflowId; - - return $append; - } + /** + * The workflow where the transition takes place + * + * @var integer + * @since 4.0.0 + */ + protected $workflowId; + + /** + * The extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 4.0.0 + */ + protected $text_prefix = 'COM_WORKFLOW_TRANSITIONS'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension or workflow id is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If workflow id is not set try to get it from input or throw an exception + if (empty($this->workflowId)) { + $this->workflowId = $this->input->getInt('workflow_id'); + + if (empty($this->workflowId)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); + } + } + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 4.0.0 + */ + public function getModel($name = 'Transition', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + + $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : '') + . '&workflow_id=' . $this->workflowId; + + return $append; + } } diff --git a/code/administrator/components/com_workflow/src/Controller/WorkflowController.php b/code/administrator/components/com_workflow/src/Controller/WorkflowController.php index 9d4208b2..b4fe6c93 100644 --- a/code/administrator/components/com_workflow/src/Controller/WorkflowController.php +++ b/code/administrator/components/com_workflow/src/Controller/WorkflowController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -18,6 +18,10 @@ use Joomla\Database\ParameterType; use Joomla\Input\Input; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Workflow controller * @@ -25,223 +29,214 @@ */ class WorkflowController extends FormController { - /** - * The extension for which the workflows apply. - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->extension); - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = isset($data[$key]) ? (int) $data[$key] : 0; - $user = $this->app->getIdentity(); - - $record = $this->getModel()->getItem($recordId); - - if (empty($record->id)) - { - return false; - } - - // Check "edit" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->extension . '.workflow.' . $recordId)) - { - return true; - } - - // Check "edit own" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->extension . '.workflow.' . $recordId)) - { - return !empty($record) && $record->created_by == $user->id; - } - - return false; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - - return $append; - } - - /** - * Function that allows child controller access to model data - * after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 4.0.0 - */ - public function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $task = $this->getTask(); - - // The save2copy task needs to be handled slightly differently. - if ($task === 'save2copy') - { - $table = $model->getTable(); - - $key = $table->getKeyName(); - - $recordId = (int) $this->input->getInt($key); - - // @todo Moves queries out of the controller. - $db = $model->getDbo(); - $query = $db->getQuery(true); - - $query->select('*') - ->from($db->quoteName('#__workflow_stages')) - ->where($db->quoteName('workflow_id') . ' = :id') - ->bind(':id', $recordId, ParameterType::INTEGER); - - $statuses = $db->setQuery($query)->loadAssocList(); - - $smodel = $this->getModel('Stage'); - - $workflowID = (int) $model->getState($model->getName() . '.id'); - - $mapping = []; - - foreach ($statuses as $status) - { - $table = $smodel->getTable(); - - $oldID = $status['id']; - - $status['workflow_id'] = $workflowID; - $status['id'] = 0; - - unset($status['asset_id']); - - $table->save($status); - - $mapping[$oldID] = (int) $table->id; - } - - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__workflow_transitions')) - ->where($db->quoteName('workflow_id') . ' = :id') - ->bind(':id', $recordId, ParameterType::INTEGER); - - $transitions = $db->setQuery($query)->loadAssocList(); - - $tmodel = $this->getModel('Transition'); - - foreach ($transitions as $transition) - { - $table = $tmodel->getTable(); - - $transition['from_stage_id'] = $transition['from_stage_id'] != -1 ? $mapping[$transition['from_stage_id']] : -1; - $transition['to_stage_id'] = $mapping[$transition['to_stage_id']]; + /** + * The extension for which the workflows apply. + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->extension); + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = isset($data[$key]) ? (int) $data[$key] : 0; + $user = $this->app->getIdentity(); + + $record = $this->getModel()->getItem($recordId); + + if (empty($record->id)) { + return false; + } + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.workflow.' . $recordId)) { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.workflow.' . $recordId)) { + return !empty($record) && $record->created_by == $user->id; + } + + return false; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + + return $append; + } + + /** + * Function that allows child controller access to model data + * after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 4.0.0 + */ + public function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $task = $this->getTask(); + + // The save2copy task needs to be handled slightly differently. + if ($task === 'save2copy') { + $table = $model->getTable(); + + $key = $table->getKeyName(); + + $recordId = (int) $this->input->getInt($key); + + // @todo Moves queries out of the controller. + $db = $model->getDbo(); + $query = $db->getQuery(true); + + $query->select('*') + ->from($db->quoteName('#__workflow_stages')) + ->where($db->quoteName('workflow_id') . ' = :id') + ->bind(':id', $recordId, ParameterType::INTEGER); + + $statuses = $db->setQuery($query)->loadAssocList(); + + $smodel = $this->getModel('Stage'); + + $workflowID = (int) $model->getState($model->getName() . '.id'); + + $mapping = []; + + foreach ($statuses as $status) { + $table = $smodel->getTable(); + + $oldID = $status['id']; + + $status['workflow_id'] = $workflowID; + $status['id'] = 0; + + unset($status['asset_id']); + + $table->save($status); + + $mapping[$oldID] = (int) $table->id; + } + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__workflow_transitions')) + ->where($db->quoteName('workflow_id') . ' = :id') + ->bind(':id', $recordId, ParameterType::INTEGER); + + $transitions = $db->setQuery($query)->loadAssocList(); + + $tmodel = $this->getModel('Transition'); + + foreach ($transitions as $transition) { + $table = $tmodel->getTable(); + + $transition['from_stage_id'] = $transition['from_stage_id'] != -1 ? $mapping[$transition['from_stage_id']] : -1; + $transition['to_stage_id'] = $mapping[$transition['to_stage_id']]; - $transition['workflow_id'] = $workflowID; - $transition['id'] = 0; + $transition['workflow_id'] = $workflowID; + $transition['id'] = 0; - unset($transition['asset_id']); + unset($transition['asset_id']); - $table->save($transition); - } - } - } + $table->save($transition); + } + } + } } diff --git a/code/administrator/components/com_workflow/src/Controller/WorkflowsController.php b/code/administrator/components/com_workflow/src/Controller/WorkflowsController.php index 82ea1104..36a3e40d 100644 --- a/code/administrator/components/com_workflow/src/Controller/WorkflowsController.php +++ b/code/administrator/components/com_workflow/src/Controller/WorkflowsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -18,6 +18,10 @@ use Joomla\Input\Input; use Joomla\Utilities\ArrayHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Workflows controller * @@ -25,163 +29,150 @@ */ class WorkflowsController extends AdminController { - /** - * The extension for which the workflows apply. - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - - $this->registerTask('unsetDefault', 'setDefault'); - } - - /** - * Proxy for getModel - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 4.0.0 - */ - public function getModel($name = 'Workflow', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to set the home property for a list of items - * - * @return void - * - * @since 4.0.0 - */ - public function setDefault() - { - // Check for request forgeries - $this->checkToken(); - - // Get items to publish from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); - $data = array('setDefault' => 1, 'unsetDefault' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($data, $task, 0, 'int'); - - if (!$value) - { - $this->setMessage(Text::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''), false - ) - ); - - return; - } - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - $this->setMessage(Text::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); - } - elseif (count($cid) > 1) - { - $this->setMessage(Text::_('COM_WORKFLOW_TOO_MANY_WORKFLOWS'), 'error'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Make sure the item ids are integers - $id = reset($cid); - - // Publish the items. - if (!$model->setDefault($id, $value)) - { - $this->setMessage($model->getError(), 'warning'); - } - else - { - if ($value === 1) - { - $ntext = 'COM_WORKFLOW_SET_DEFAULT'; - } - else - { - $ntext = 'COM_WORKFLOW_ITEM_UNSET_DEFAULT'; - } - - $this->setMessage(Text::_($ntext, count($cid))); - } - } - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''), false - ) - ); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - return '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - } + /** + * The extension for which the workflows apply. + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + + $this->registerTask('unsetDefault', 'setDefault'); + } + + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 4.0.0 + */ + public function getModel($name = 'Workflow', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to set the home property for a list of items + * + * @return void + * + * @since 4.0.0 + */ + public function setDefault() + { + // Check for request forgeries + $this->checkToken(); + + // Get items to publish from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); + $data = array('setDefault' => 1, 'unsetDefault' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + if (!$value) { + $this->setMessage(Text::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''), + false + ) + ); + + return; + } + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + $this->setMessage(Text::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); + } elseif (count($cid) > 1) { + $this->setMessage(Text::_('COM_WORKFLOW_TOO_MANY_WORKFLOWS'), 'error'); + } else { + // Get the model. + $model = $this->getModel(); + + // Make sure the item ids are integers + $id = reset($cid); + + // Publish the items. + if (!$model->setDefault($id, $value)) { + $this->setMessage($model->getError(), 'warning'); + } else { + if ($value === 1) { + $ntext = 'COM_WORKFLOW_SET_DEFAULT'; + } else { + $ntext = 'COM_WORKFLOW_ITEM_UNSET_DEFAULT'; + } + + $this->setMessage(Text::_($ntext, count($cid))); + } + } + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''), + false + ) + ); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + return '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + } } diff --git a/code/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php b/code/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php index e34a303e..cdc140e5 100644 --- a/code/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php +++ b/code/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ getApplication()->input->getCmd('extension'); + /** + * Workflows have to check for extension permission + * + * @return void + */ + protected function checkAccess() + { + $extension = $this->getApplication()->input->getCmd('extension'); - $parts = explode('.', $extension); + $parts = explode('.', $extension); - // Check the user has permission to access this component if in the backend - if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage.workflow', $parts[0])) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + // Check the user has permission to access this component if in the backend + if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage.workflow', $parts[0])) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/code/administrator/components/com_workflow/src/Field/ComponentsWorkflowField.php b/code/administrator/components/com_workflow/src/Field/ComponentsWorkflowField.php index d68ae7f0..ee3d1ef2 100644 --- a/code/administrator/components/com_workflow/src/Field/ComponentsWorkflowField.php +++ b/code/administrator/components/com_workflow/src/Field/ComponentsWorkflowField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT a.name AS text, a.element AS value') - ->from('#__extensions as a') - ->where('a.enabled >= 1') - ->where('a.type =' . $db->quote('component')); - - $items = $db->setQuery($query)->loadObjectList(); - - $options = []; - - if (count($items)) - { - $lang = Factory::getLanguage(); - - $components = []; - - // Search for components supporting Fieldgroups - suppose that these components support fields as well - foreach ($items as &$item) - { - $availableActions = Access::getActionsFromFile( - JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', - "/access/section[@name='workflow']/" - ); - - if (!empty($availableActions)) - { - // Load language - $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; - $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) - || $lang->load($item->value . 'sys', $source); - - // Translate component name - $item->text = Text::_($item->text); - - $components[] = $item; - } - } - - if (empty($components)) - { - return []; - } - - foreach ($components as $component) - { - // Search for different contexts - $c = Factory::getApplication()->bootComponent($component->value); - - if ($c instanceof WorkflowServiceInterface) - { - $contexts = $c->getContexts(); - - foreach ($contexts as $context) - { - $newOption = new \stdClass; - $newOption->value = strtolower($component->value . '.' . $context); - $newOption->text = $component->text . ' - ' . Text::_($context); - $options[] = $newOption; - } - } - else - { - $options[] = $component; - } - } - - // Sort by name - $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $items); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'ComponentsWorkflow'; + + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + // Initialise variable. + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('DISTINCT a.name AS text, a.element AS value') + ->from('#__extensions as a') + ->where('a.enabled >= 1') + ->where('a.type =' . $db->quote('component')); + + $items = $db->setQuery($query)->loadObjectList(); + + $options = []; + + if (count($items)) { + $lang = Factory::getLanguage(); + + $components = []; + + // Search for components supporting Fieldgroups - suppose that these components support fields as well + foreach ($items as &$item) { + $availableActions = Access::getActionsFromFile( + JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', + "/access/section[@name='workflow']/" + ); + + if (!empty($availableActions)) { + // Load language + $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; + $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) + || $lang->load($item->value . 'sys', $source); + + // Translate component name + $item->text = Text::_($item->text); + + $components[] = $item; + } + } + + if (empty($components)) { + return []; + } + + foreach ($components as $component) { + // Search for different contexts + $c = Factory::getApplication()->bootComponent($component->value); + + if ($c instanceof WorkflowServiceInterface) { + $contexts = $c->getContexts(); + + foreach ($contexts as $context) { + $newOption = new \stdClass(); + $newOption->value = strtolower($component->value . '.' . $context); + $newOption->text = $component->text . ' - ' . Text::_($context); + $options[] = $newOption; + } + } else { + $options[] = $component; + } + } + + // Sort by name + $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $items); + + return $options; + } } diff --git a/code/administrator/components/com_workflow/src/Field/WorkflowcontextsField.php b/code/administrator/components/com_workflow/src/Field/WorkflowcontextsField.php index 08752825..cfbdcba1 100644 --- a/code/administrator/components/com_workflow/src/Field/WorkflowcontextsField.php +++ b/code/administrator/components/com_workflow/src/Field/WorkflowcontextsField.php @@ -1,4 +1,5 @@ getOptions()) < 2) - { - $this->layout = 'joomla.form.field.hidden'; - } + /** + * Method to get the field input markup for a generic list. + * Use the multiple attribute to enable multiselect. + * + * @return string The field input markup. + * + * @since 4.0.0 + */ + protected function getInput() + { + if (count($this->getOptions()) < 2) { + $this->layout = 'joomla.form.field.hidden'; + } - return parent::getInput(); - } + return parent::getInput(); + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 4.0.0 - */ - protected function getOptions() - { - $parts = explode('.', $this->value); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 4.0.0 + */ + protected function getOptions() + { + $parts = explode('.', $this->value); - $component = Factory::getApplication()->bootComponent($parts[0]); + $component = Factory::getApplication()->bootComponent($parts[0]); - if ($component instanceof WorkflowServiceInterface) - { - return $component->getWorkflowContexts(); - } + if ($component instanceof WorkflowServiceInterface) { + return $component->getWorkflowContexts(); + } - return []; - } + return []; + } } diff --git a/code/administrator/components/com_workflow/src/Helper/StageHelper.php b/code/administrator/components/com_workflow/src/Helper/StageHelper.php index 3348c32a..3e29a954 100644 --- a/code/administrator/components/com_workflow/src/Helper/StageHelper.php +++ b/code/administrator/components/com_workflow/src/Helper/StageHelper.php @@ -1,4 +1,5 @@ option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - $this->setState('filter.extension', $extension); - } - - /** - * Method to change the title - * - * @param integer $categoryId The id of the category. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 4.0.0 - */ - protected function generateNewTitle($categoryId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('title' => $title))) - { - $title = StringHelper::increment($title); - } - - return array($title, $alias); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function save($data) - { - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - $app = Factory::getApplication(); - $user = $app->getIdentity(); - $input = $app->input; - $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - - if (empty($data['workflow_id'])) - { - $data['workflow_id'] = $workflowID; - } - - $workflow = $this->getTable('Workflow'); - - $workflow->load($data['workflow_id']); - - $parts = explode('.', $workflow->extension); - - if (isset($data['rules']) && !$user->authorise('core.admin', $parts[0])) - { - unset($data['rules']); - } - - // Make sure we use the correct extension when editing an existing workflow - $key = $table->getKeyName(); - $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); - - if ($pk > 0) - { - $table->load($pk); - - if ((int) $table->workflow_id) - { - $data['workflow_id'] = (int) $table->workflow_id; - } - } - - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - - // Alter the title for save as copy - if ($origTable->load(['title' => $data['title']])) - { - list($title) = $this->generateNewTitle(0, '', $data['title']); - $data['title'] = $title; - } - - $data['published'] = 0; - $data['default'] = 0; - } - - return parent::save($data); - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 4.0.0 - */ - protected function canDelete($record) - { - $table = $this->getTable('Workflow', 'Administrator'); - - $table->load($record->workflow_id); - - if (empty($record->id) || $record->published != -2) - { - return false; - } - - $app = Factory::getApplication(); - $extension = $app->getUserStateFromRequest('com_workflow.stage.filter.extension', 'extension', null, 'cmd'); - - $parts = explode('.', $extension); - - $component = reset($parts); - - if (!Factory::getUser()->authorise('core.delete', $component . '.state.' . (int) $record->id) || $record->default) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); - - return false; - } - - return true; - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 4.0.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - $app = Factory::getApplication(); - $context = $this->option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - if (!\property_exists($record, 'workflow_id')) - { - $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - $record->workflow_id = $workflowID; - } - - // Check for existing workflow. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $extension . '.state.' . (int) $record->id); - } - - // Default to component settings if workflow isn't known. - return $user->authorise('core.edit.state', $extension); - } - - /** - * Abstract method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 4.0.0 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm( - 'com_workflow.state', - 'stage', - array( - 'control' => 'jform', - 'load_data' => $loadData - ) - ); - - if (empty($form)) - { - return false; - } - - $id = $data['id'] ?? $form->getValue('id'); - - $item = $this->getItem($id); - - $canEditState = $this->canEditState((object) $item); - - // Modify the form based on access controls. - if (!$canEditState || !empty($item->default)) - { - if (!$canEditState) - { - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('published', 'required', 'false'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - $form->setFieldAttribute('default', 'disabled', 'true'); - $form->setFieldAttribute('default', 'required', 'false'); - $form->setFieldAttribute('default', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState( - 'com_workflow.edit.state.data', - array() - ); - - if (empty($data)) - { - $data = $this->getItem(); - } - - return $data; - } - - /** - * Method to change the home state of one or more items. - * - * @param array $pk A list of the primary keys to change. - * @param integer $value The value of the home state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setDefault($pk, $value = 1) - { - $table = $this->getTable(); - - if ($table->load($pk)) - { - if (!$table->published) - { - $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); - - return false; - } - } - - if (empty($table->id) || !$this->canEditState($table)) - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - - return false; - } - - if ($value) - { - // Verify that the home page for this language is unique per client id - if ($table->load(array('default' => '1', 'workflow_id' => $table->workflow_id))) - { - $table->default = 0; - $table->store(); - } - } - - if ($table->load($pk)) - { - $table->default = $value; - $table->store(); - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the published state of one or more records. - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function publish(&$pks, $value = 1) - { - $table = $this->getTable(); - $pks = (array) $pks; - $app = Factory::getApplication(); - $extension = $app->getUserStateFromRequest('com_workflow.state.filter.extension', 'extension', null, 'cmd'); - - // Default item existence checks. - if ($value != 1) - { - foreach ($pks as $i => $pk) - { - if ($table->load($pk) && $table->default) - { - // Prune items that you can't change. - $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DISABLE_DEFAULT'), 'error'); - - unset($pks[$i]); - } - } - } - - return parent::publish($pks, $value); - } - - /** - * Method to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 4.0.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $extension = Factory::getApplication()->input->get('extension'); - - $parts = explode('.', $extension); - - $extension = array_shift($parts); - - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $extension); - - parent::preprocessForm($form, $data, $group); - } + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 4.0.0 + */ + public function populateState() + { + parent::populateState(); + + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + $this->setState('filter.extension', $extension); + } + + /** + * Method to change the title + * + * @param integer $categoryId The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 4.0.0 + */ + protected function generateNewTitle($categoryId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('title' => $title))) { + $title = StringHelper::increment($title); + } + + return array($title, $alias); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function save($data) + { + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $input = $app->input; + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + + if (empty($data['workflow_id'])) { + $data['workflow_id'] = $workflowID; + } + + $workflow = $this->getTable('Workflow'); + + $workflow->load($data['workflow_id']); + + $parts = explode('.', $workflow->extension); + + if (isset($data['rules']) && !$user->authorise('core.admin', $parts[0])) { + unset($data['rules']); + } + + // Make sure we use the correct extension when editing an existing workflow + $key = $table->getKeyName(); + $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); + + if ($pk > 0) { + $table->load($pk); + + if ((int) $table->workflow_id) { + $data['workflow_id'] = (int) $table->workflow_id; + } + } + + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + + // Alter the title for save as copy + if ($origTable->load(['title' => $data['title']])) { + list($title) = $this->generateNewTitle(0, '', $data['title']); + $data['title'] = $title; + } + + $data['published'] = 0; + $data['default'] = 0; + } + + return parent::save($data); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 4.0.0 + */ + protected function canDelete($record) + { + $table = $this->getTable('Workflow', 'Administrator'); + + $table->load($record->workflow_id); + + if (empty($record->id) || $record->published != -2) { + return false; + } + + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.stage.filter.extension', 'extension', null, 'cmd'); + + $parts = explode('.', $extension); + + $component = reset($parts); + + if (!Factory::getUser()->authorise('core.delete', $component . '.state.' . (int) $record->id) || $record->default) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); + + return false; + } + + return true; + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 4.0.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + if (!\property_exists($record, 'workflow_id')) { + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + $record->workflow_id = $workflowID; + } + + // Check for existing workflow. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $extension . '.state.' . (int) $record->id); + } + + // Default to component settings if workflow isn't known. + return $user->authorise('core.edit.state', $extension); + } + + /** + * Abstract method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm( + 'com_workflow.state', + 'stage', + array( + 'control' => 'jform', + 'load_data' => $loadData + ) + ); + + if (empty($form)) { + return false; + } + + $id = $data['id'] ?? $form->getValue('id'); + + $item = $this->getItem($id); + + $canEditState = $this->canEditState((object) $item); + + // Modify the form based on access controls. + if (!$canEditState || !empty($item->default)) { + if (!$canEditState) { + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('published', 'required', 'false'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + $form->setFieldAttribute('default', 'disabled', 'true'); + $form->setFieldAttribute('default', 'required', 'false'); + $form->setFieldAttribute('default', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState( + 'com_workflow.edit.state.data', + array() + ); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + /** + * Method to change the home state of one or more items. + * + * @param array $pk A list of the primary keys to change. + * @param integer $value The value of the home state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setDefault($pk, $value = 1) + { + $table = $this->getTable(); + + if ($table->load($pk)) { + if (!$table->published) { + $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } + + if (empty($table->id) || !$this->canEditState($table)) { + Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + + return false; + } + + if ($value) { + // Verify that the home page for this language is unique per client id + if ($table->load(array('default' => '1', 'workflow_id' => $table->workflow_id))) { + $table->default = 0; + $table->store(); + } + } + + if ($table->load($pk)) { + $table->default = $value; + $table->store(); + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the published state of one or more records. + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function publish(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.state.filter.extension', 'extension', null, 'cmd'); + + // Default item existence checks. + if ($value != 1) { + foreach ($pks as $i => $pk) { + if ($table->load($pk) && $table->default) { + // Prune items that you can't change. + $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DISABLE_DEFAULT'), 'error'); + + unset($pks[$i]); + } + } + } + + return parent::publish($pks, $value); + } + + /** + * Method to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $extension = Factory::getApplication()->input->get('extension'); + + $parts = explode('.', $extension); + + $extension = array_shift($parts); + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $extension); + + parent::preprocessForm($form, $data, $group); + } } diff --git a/code/administrator/components/com_workflow/src/Model/StagesModel.php b/code/administrator/components/com_workflow/src/Model/StagesModel.php index 21339272..972c7f5c 100644 --- a/code/administrator/components/com_workflow/src/Model/StagesModel.php +++ b/code/administrator/components/com_workflow/src/Model/StagesModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); - $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); - - if ($workflowID) - { - $table = $this->getTable('Workflow', 'Administrator'); - - if ($table->load($workflowID)) - { - $this->setState('active_workflow', $table->title); - } - } - - $this->setState('filter.workflow_id', $workflowID); - $this->setState('filter.extension', $extension); - - parent::populateState($ordering, $direction); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 4.0.0 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('workflow_id') . ' = ' . (int) $table->workflow_id, - ]; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\Table\Table A Table object - * - * @since 4.0.0 - */ - public function getTable($type = 'Stage', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Method to get the data that should be injected in the form. - * - * @return string The query to database. - * - * @since 4.0.0 - */ - public function getListQuery() - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query - ->select( - [ - $db->quoteName('s.id'), - $db->quoteName('s.title'), - $db->quoteName('s.ordering'), - $db->quoteName('s.default'), - $db->quoteName('s.published'), - $db->quoteName('s.checked_out'), - $db->quoteName('s.checked_out_time'), - $db->quoteName('s.description'), - $db->quoteName('uc.name', 'editor'), - ] - ) - ->from($db->quoteName('#__workflow_stages', 's')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('s.checked_out')); - - // Filter by extension - if ($workflowID = (int) $this->getState('filter.workflow_id')) - { - $query->where($db->quoteName('s.workflow_id') . ' = :id') - ->bind(':id', $workflowID, ParameterType::INTEGER); - } - - $status = (string) $this->getState('filter.published'); - - // Filter by publish state - if (is_numeric($status)) - { - $status = (int) $status; - $query->where($db->quoteName('s.published') . ' = :status') - ->bind(':status', $status, ParameterType::INTEGER); - } - elseif ($status === '') - { - $query->where($db->quoteName('s.published') . ' IN (0, 1)'); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('s.title') . ' LIKE :search1 OR ' . $db->quoteName('s.description') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 's.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Returns a workflow object - * - * @return object The workflow - * - * @since 4.0.0 - */ - public function getWorkflow() - { - $table = $this->getTable('Workflow', 'Administrator'); - - $workflowId = (int) $this->getState('filter.workflow_id'); - - if ($workflowId > 0) - { - $table->load($workflowId); - } - - return (object) $table->getProperties(); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see JController + * @since 4.0.0 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 's.id', + 'title', 's.title', + 'ordering','s.ordering', + 'published', 's.published' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = 's.ordering', $direction = 'ASC') + { + $app = Factory::getApplication(); + + $workflowID = $app->getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); + + if ($workflowID) { + $table = $this->getTable('Workflow', 'Administrator'); + + if ($table->load($workflowID)) { + $this->setState('active_workflow', $table->title); + } + } + + $this->setState('filter.workflow_id', $workflowID); + $this->setState('filter.extension', $extension); + + parent::populateState($ordering, $direction); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 4.0.0 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $table->workflow_id, + ]; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\Table\Table A Table object + * + * @since 4.0.0 + */ + public function getTable($type = 'Stage', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return string The query to database. + * + * @since 4.0.0 + */ + public function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query + ->select( + [ + $db->quoteName('s.id'), + $db->quoteName('s.title'), + $db->quoteName('s.ordering'), + $db->quoteName('s.default'), + $db->quoteName('s.published'), + $db->quoteName('s.checked_out'), + $db->quoteName('s.checked_out_time'), + $db->quoteName('s.description'), + $db->quoteName('uc.name', 'editor'), + ] + ) + ->from($db->quoteName('#__workflow_stages', 's')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('s.checked_out')); + + // Filter by extension + if ($workflowID = (int) $this->getState('filter.workflow_id')) { + $query->where($db->quoteName('s.workflow_id') . ' = :id') + ->bind(':id', $workflowID, ParameterType::INTEGER); + } + + $status = (string) $this->getState('filter.published'); + + // Filter by publish state + if (is_numeric($status)) { + $status = (int) $status; + $query->where($db->quoteName('s.published') . ' = :status') + ->bind(':status', $status, ParameterType::INTEGER); + } elseif ($status === '') { + $query->where($db->quoteName('s.published') . ' IN (0, 1)'); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('s.title') . ' LIKE :search1 OR ' . $db->quoteName('s.description') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 's.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Returns a workflow object + * + * @return object The workflow + * + * @since 4.0.0 + */ + public function getWorkflow() + { + $table = $this->getTable('Workflow', 'Administrator'); + + $workflowId = (int) $this->getState('filter.workflow_id'); + + if ($workflowId > 0) { + $table->load($workflowId); + } + + return (object) $table->getProperties(); + } } diff --git a/code/administrator/components/com_workflow/src/Model/TransitionModel.php b/code/administrator/components/com_workflow/src/Model/TransitionModel.php index 84e2e5e3..b8cec7a1 100644 --- a/code/administrator/components/com_workflow/src/Model/TransitionModel.php +++ b/code/administrator/components/com_workflow/src/Model/TransitionModel.php @@ -1,4 +1,5 @@ option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - $this->setState('filter.extension', $extension); - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 4.0.0 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - $app = Factory::getApplication(); - $extension = $app->getUserStateFromRequest('com_workflow.transition.filter.extension', 'extension', null, 'cmd'); - - return Factory::getUser()->authorise('core.delete', $extension . '.transition.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 4.0.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - $app = Factory::getApplication(); - $context = $this->option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - if (!\property_exists($record, 'workflow_id')) - { - $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - $record->workflow_id = $workflowID; - } - - // Check for existing workflow. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $extension . '.transition.' . (int) $record->id); - } - - // Default to component settings if workflow isn't known. - return $user->authorise('core.edit.state', $extension); - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return \Joomla\CMS\Object\CMSObject|boolean Object on success, false on failure. - * - * @since 4.0.0 - */ - public function getItem($pk = null) - { - $item = parent::getItem($pk); - - if (property_exists($item, 'options')) - { - $registry = new Registry($item->options); - $item->options = $registry->toArray(); - } - - return $item; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function save($data) - { - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - $app = Factory::getApplication(); - $user = $app->getIdentity(); - $input = $app->input; - - $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - - if (empty($data['workflow_id'])) - { - $data['workflow_id'] = $workflowID; - } - - $workflow = $this->getTable('Workflow'); - - $workflow->load($data['workflow_id']); - - $parts = explode('.', $workflow->extension); - - if (isset($data['rules']) && !$user->authorise('core.admin', $parts[0])) - { - unset($data['rules']); - } - - // Make sure we use the correct workflow_id when editing an existing transition - $key = $table->getKeyName(); - $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); - - if ($pk > 0) - { - $table->load($pk); - - if ((int) $table->workflow_id) - { - $data['workflow_id'] = (int) $table->workflow_id; - } - } - - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - - // Alter the title for save as copy - if ($origTable->load(['title' => $data['title']])) - { - list($title) = $this->generateNewTitle(0, '', $data['title']); - $data['title'] = $title; - } - - $data['published'] = 0; - } - - return parent::save($data); - } - - /** - * Method to change the title - * - * @param integer $categoryId The id of the category. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 4.0.0 - */ - protected function generateNewTitle($categoryId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('title' => $title))) - { - $title = StringHelper::increment($title); - } - - return array($title, $alias); - } - - /** - * Abstract method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure - * - * @since 4.0.0 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm( - 'com_workflow.transition', - 'transition', - array( - 'control' => 'jform', - 'load_data' => $loadData - ) - ); - - if (empty($form)) - { - return false; - } - - $id = $data['id'] ?? $form->getValue('id'); - - $item = $this->getItem($id); - - $canEditState = $this->canEditState((object) $item); - - // Modify the form based on access controls. - if (!$canEditState) - { - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('published', 'required', 'false'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - if (!empty($item->workflow_id)) - { - $data['workflow_id'] = (int) $item->workflow_id; - } - - if (empty($data['workflow_id'])) - { - $context = $this->option . '.' . $this->name; - - $data['workflow_id'] = (int) Factory::getApplication()->getUserStateFromRequest( - $context . '.filter.workflow_id', 'workflow_id', - 0, - 'int' - ); - } - - $where = $this->getDbo()->quoteName('workflow_id') . ' = ' . (int) $data['workflow_id']; - $where .= ' AND ' . $this->getDbo()->quoteName('published') . ' = 1'; - - $form->setFieldAttribute('from_stage_id', 'sql_where', $where); - $form->setFieldAttribute('to_stage_id', 'sql_where', $where); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState( - 'com_workflow.edit.transition.data', - array() - ); - - if (empty($data)) - { - $data = $this->getItem(); - } - - return $data; - } - - public function getWorkflow() - { - $app = Factory::getApplication(); - - $context = $this->option . '.' . $this->name; - - $workflow_id = (int) $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - - $workflow = $this->getTable('Workflow'); - - $workflow->load($workflow_id); - - return (object) $workflow->getProperties(); - } - - /** - * Trigger the form preparation for the workflow group - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @see FormField - * @since 4.0.0 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $extension = Factory::getApplication()->input->get('extension'); - - $parts = explode('.', $extension); - - $extension = array_shift($parts); - - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $extension); - - // Import the appropriate plugin group. - PluginHelper::importPlugin('workflow'); - - parent::preprocessForm($form, $data, $group); - } + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 4.0.0 + */ + public function populateState() + { + parent::populateState(); + + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + $this->setState('filter.extension', $extension); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 4.0.0 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.transition.filter.extension', 'extension', null, 'cmd'); + + return Factory::getUser()->authorise('core.delete', $extension . '.transition.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 4.0.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + if (!\property_exists($record, 'workflow_id')) { + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + $record->workflow_id = $workflowID; + } + + // Check for existing workflow. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $extension . '.transition.' . (int) $record->id); + } + + // Default to component settings if workflow isn't known. + return $user->authorise('core.edit.state', $extension); + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return \Joomla\CMS\Object\CMSObject|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getItem($pk = null) + { + $item = parent::getItem($pk); + + if (property_exists($item, 'options')) { + $registry = new Registry($item->options); + $item->options = $registry->toArray(); + } + + return $item; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function save($data) + { + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $input = $app->input; + + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + + if (empty($data['workflow_id'])) { + $data['workflow_id'] = $workflowID; + } + + $workflow = $this->getTable('Workflow'); + + $workflow->load($data['workflow_id']); + + $parts = explode('.', $workflow->extension); + + if (isset($data['rules']) && !$user->authorise('core.admin', $parts[0])) { + unset($data['rules']); + } + + // Make sure we use the correct workflow_id when editing an existing transition + $key = $table->getKeyName(); + $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); + + if ($pk > 0) { + $table->load($pk); + + if ((int) $table->workflow_id) { + $data['workflow_id'] = (int) $table->workflow_id; + } + } + + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + + // Alter the title for save as copy + if ($origTable->load(['title' => $data['title']])) { + list($title) = $this->generateNewTitle(0, '', $data['title']); + $data['title'] = $title; + } + + $data['published'] = 0; + } + + return parent::save($data); + } + + /** + * Method to change the title + * + * @param integer $categoryId The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 4.0.0 + */ + protected function generateNewTitle($categoryId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('title' => $title))) { + $title = StringHelper::increment($title); + } + + return array($title, $alias); + } + + /** + * Abstract method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm( + 'com_workflow.transition', + 'transition', + array( + 'control' => 'jform', + 'load_data' => $loadData + ) + ); + + if (empty($form)) { + return false; + } + + $id = $data['id'] ?? $form->getValue('id'); + + $item = $this->getItem($id); + + $canEditState = $this->canEditState((object) $item); + + // Modify the form based on access controls. + if (!$canEditState) { + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('published', 'required', 'false'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + if (!empty($item->workflow_id)) { + $data['workflow_id'] = (int) $item->workflow_id; + } + + if (empty($data['workflow_id'])) { + $context = $this->option . '.' . $this->name; + + $data['workflow_id'] = (int) Factory::getApplication()->getUserStateFromRequest( + $context . '.filter.workflow_id', + 'workflow_id', + 0, + 'int' + ); + } + + $where = $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $data['workflow_id']; + $where .= ' AND ' . $this->getDatabase()->quoteName('published') . ' = 1'; + + $form->setFieldAttribute('from_stage_id', 'sql_where', $where); + $form->setFieldAttribute('to_stage_id', 'sql_where', $where); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState( + 'com_workflow.edit.transition.data', + array() + ); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + public function getWorkflow() + { + $app = Factory::getApplication(); + + $context = $this->option . '.' . $this->name; + + $workflow_id = (int) $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + + $workflow = $this->getTable('Workflow'); + + $workflow->load($workflow_id); + + return (object) $workflow->getProperties(); + } + + /** + * Trigger the form preparation for the workflow group + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @see FormField + * @since 4.0.0 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $extension = Factory::getApplication()->input->get('extension'); + + $parts = explode('.', $extension); + + $extension = array_shift($parts); + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $extension); + + // Import the appropriate plugin group. + PluginHelper::importPlugin('workflow'); + + parent::preprocessForm($form, $data, $group); + } } diff --git a/code/administrator/components/com_workflow/src/Model/TransitionsModel.php b/code/administrator/components/com_workflow/src/Model/TransitionsModel.php index 02334b1d..fe582e38 100644 --- a/code/administrator/components/com_workflow/src/Model/TransitionsModel.php +++ b/code/administrator/components/com_workflow/src/Model/TransitionsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); - $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); - - if ($workflowID) - { - $table = $this->getTable('Workflow', 'Administrator'); - - if ($table->load($workflowID)) - { - $this->setState('active_workflow', $table->title); - } - } - - $this->setState('filter.workflow_id', $workflowID); - $this->setState('filter.extension', $extension); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\Table\Table A Table object - * - * @since 4.0.0 - */ - public function getTable($type = 'Transition', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 4.0.0 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('workflow_id') . ' = ' . (int) $table->workflow_id, - ]; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return string The query to database. - * - * @since 4.0.0 - */ - public function getListQuery() - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query - ->select( - [ - $db->quoteName('t.id'), - $db->quoteName('t.title'), - $db->quoteName('t.from_stage_id'), - $db->quoteName('t.to_stage_id'), - $db->quoteName('t.published'), - $db->quoteName('t.checked_out'), - $db->quoteName('t.checked_out_time'), - $db->quoteName('t.ordering'), - $db->quoteName('t.description'), - $db->quoteName('f_stage.title', 'from_stage'), - $db->quoteName('t_stage.title', 'to_stage'), - $db->quoteName('uc.name', 'editor'), - ] - ) - ->from($db->quoteName('#__workflow_transitions', 't')) - ->join('LEFT', $db->quoteName('#__workflow_stages', 'f_stage'), $db->quoteName('f_stage.id') . ' = ' . $db->quoteName('t.from_stage_id')) - ->join('LEFT', $db->quoteName('#__workflow_stages', 't_stage'), $db->quoteName('t_stage.id') . ' = ' . $db->quoteName('t.to_stage_id')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('t.checked_out')); - - // Filter by extension - if ($workflowID = (int) $this->getState('filter.workflow_id')) - { - $query->where($db->quoteName('t.workflow_id') . ' = :id') - ->bind(':id', $workflowID, ParameterType::INTEGER); - } - - $status = (string) $this->getState('filter.published'); - - // Filter by status - if (is_numeric($status)) - { - $status = (int) $status; - $query->where($db->quoteName('t.published') . ' = :status') - ->bind(':status', $status, ParameterType::INTEGER); - } - elseif ($status === '') - { - $query->where($db->quoteName('t.published') . ' IN (0, 1)'); - } - - // Filter by column from_stage_id - if ($fromStage = (int) $this->getState('filter.from_stage')) - { - $query->where($db->quoteName('from_stage_id') . ' = :fromStage') - ->bind(':fromStage', $fromStage, ParameterType::INTEGER); - } - - // Filter by column to_stage_id - if ($toStage = (int) $this->getState('filter.to_stage')) - { - $query->where($db->quoteName('to_stage_id') . ' = :toStage') - ->bind(':toStage', $toStage, ParameterType::INTEGER); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('t.title') . ' LIKE :search1 OR ' . $db->quoteName('t.description') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 't.id'); - $orderDirn = strtoupper($this->state->get('list.direction', 'ASC')); - - $query->order($db->escape($orderCol) . ' ' . ($orderDirn === 'DESC' ? 'DESC' : 'ASC')); - - return $query; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return \Joomla\CMS\Form\Form|boolean The Form object or false on error - * - * @since 4.0.0 - */ - public function getFilterForm($data = array(), $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - - $id = (int) $this->getState('filter.workflow_id'); - - if ($form) - { - $where = $this->getDbo()->quoteName('workflow_id') . ' = ' . $id . ' AND ' . $this->getDbo()->quoteName('published') . ' = 1'; - - $form->setFieldAttribute('from_stage', 'sql_where', $where, 'filter'); - $form->setFieldAttribute('to_stage', 'sql_where', $where, 'filter'); - } - - return $form; - } - - /** - * Returns a workflow object - * - * @return object The workflow - * - * @since 4.0.0 - */ - public function getWorkflow() - { - $table = $this->getTable('Workflow', 'Administrator'); - - $workflowId = (int) $this->getState('filter.workflow_id'); - - if ($workflowId > 0) - { - $table->load($workflowId); - } - - return (object) $table->getProperties(); - } - + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see JController + * @since 4.0.0 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 't.id', + 'published', 't.published', + 'ordering', 't.ordering', + 'title', 't.title', + 'from_stage', 't.from_stage_id', + 'to_stage', 't.to_stage_id' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = 't.ordering', $direction = 'ASC') + { + $app = Factory::getApplication(); + $workflowID = $app->getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); + + if ($workflowID) { + $table = $this->getTable('Workflow', 'Administrator'); + + if ($table->load($workflowID)) { + $this->setState('active_workflow', $table->title); + } + } + + $this->setState('filter.workflow_id', $workflowID); + $this->setState('filter.extension', $extension); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\Table\Table A Table object + * + * @since 4.0.0 + */ + public function getTable($type = 'Transition', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 4.0.0 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $table->workflow_id, + ]; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return string The query to database. + * + * @since 4.0.0 + */ + public function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query + ->select( + [ + $db->quoteName('t.id'), + $db->quoteName('t.title'), + $db->quoteName('t.from_stage_id'), + $db->quoteName('t.to_stage_id'), + $db->quoteName('t.published'), + $db->quoteName('t.checked_out'), + $db->quoteName('t.checked_out_time'), + $db->quoteName('t.ordering'), + $db->quoteName('t.description'), + $db->quoteName('f_stage.title', 'from_stage'), + $db->quoteName('t_stage.title', 'to_stage'), + $db->quoteName('uc.name', 'editor'), + ] + ) + ->from($db->quoteName('#__workflow_transitions', 't')) + ->join('LEFT', $db->quoteName('#__workflow_stages', 'f_stage'), $db->quoteName('f_stage.id') . ' = ' . $db->quoteName('t.from_stage_id')) + ->join('LEFT', $db->quoteName('#__workflow_stages', 't_stage'), $db->quoteName('t_stage.id') . ' = ' . $db->quoteName('t.to_stage_id')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('t.checked_out')); + + // Filter by extension + if ($workflowID = (int) $this->getState('filter.workflow_id')) { + $query->where($db->quoteName('t.workflow_id') . ' = :id') + ->bind(':id', $workflowID, ParameterType::INTEGER); + } + + $status = (string) $this->getState('filter.published'); + + // Filter by status + if (is_numeric($status)) { + $status = (int) $status; + $query->where($db->quoteName('t.published') . ' = :status') + ->bind(':status', $status, ParameterType::INTEGER); + } elseif ($status === '') { + $query->where($db->quoteName('t.published') . ' IN (0, 1)'); + } + + // Filter by column from_stage_id + if ($fromStage = (int) $this->getState('filter.from_stage')) { + $query->where($db->quoteName('from_stage_id') . ' = :fromStage') + ->bind(':fromStage', $fromStage, ParameterType::INTEGER); + } + + // Filter by column to_stage_id + if ($toStage = (int) $this->getState('filter.to_stage')) { + $query->where($db->quoteName('to_stage_id') . ' = :toStage') + ->bind(':toStage', $toStage, ParameterType::INTEGER); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('t.title') . ' LIKE :search1 OR ' . $db->quoteName('t.description') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 't.id'); + $orderDirn = strtoupper($this->state->get('list.direction', 'ASC')); + + $query->order($db->escape($orderCol) . ' ' . ($orderDirn === 'DESC' ? 'DESC' : 'ASC')); + + return $query; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \Joomla\CMS\Form\Form|boolean The Form object or false on error + * + * @since 4.0.0 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + $id = (int) $this->getState('filter.workflow_id'); + + if ($form) { + $where = $this->getDatabase()->quoteName('workflow_id') . ' = ' . $id . ' AND ' . $this->getDatabase()->quoteName('published') . ' = 1'; + + $form->setFieldAttribute('from_stage', 'sql_where', $where, 'filter'); + $form->setFieldAttribute('to_stage', 'sql_where', $where, 'filter'); + } + + return $form; + } + + /** + * Returns a workflow object + * + * @return object The workflow + * + * @since 4.0.0 + */ + public function getWorkflow() + { + $table = $this->getTable('Workflow', 'Administrator'); + + $workflowId = (int) $this->getState('filter.workflow_id'); + + if ($workflowId > 0) { + $table->load($workflowId); + } + + return (object) $table->getProperties(); + } } diff --git a/code/administrator/components/com_workflow/src/Model/WorkflowModel.php b/code/administrator/components/com_workflow/src/Model/WorkflowModel.php index dec07533..f5f77f71 100644 --- a/code/administrator/components/com_workflow/src/Model/WorkflowModel.php +++ b/code/administrator/components/com_workflow/src/Model/WorkflowModel.php @@ -1,4 +1,5 @@ option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - $this->setState('filter.extension', $extension); - } - - /** - * Method to change the title - * - * @param integer $categoryId The id of the category. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 4.0.0 - */ - protected function generateNewTitle($categoryId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('title' => $title))) - { - $title = StringHelper::increment($title); - } - - return array($title, $alias); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function save($data) - { - $table = $this->getTable(); - $app = Factory::getApplication(); - $user = $app->getIdentity(); - $input = $app->input; - $context = $this->option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - $data['extension'] = !empty($data['extension']) ? $data['extension'] : $extension; - - // Make sure we use the correct extension when editing an existing workflow - $key = $table->getKeyName(); - $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); - - if ($pk > 0) - { - $table->load($pk); - - $data['extension'] = $table->extension; - } - - if (isset($data['rules']) && !$user->authorise('core.admin', $data['extension'])) - { - unset($data['rules']); - } - - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - - // Alter the title for save as copy - if ($origTable->load(['title' => $data['title']])) - { - list($title) = $this->generateNewTitle(0, '', $data['title']); - $data['title'] = $title; - } - - // Unpublish new copy - $data['published'] = 0; - $data['default'] = 0; - } - - $result = parent::save($data); - - // Create default stage for new workflow - if ($result && $input->getCmd('task') !== 'save2copy' && $this->getState($this->getName() . '.new')) - { - $workflow_id = (int) $this->getState($this->getName() . '.id'); - - $table = $this->getTable('Stage'); - - $table->id = 0; - $table->title = 'COM_WORKFLOW_BASIC_STAGE'; - $table->description = ''; - $table->workflow_id = $workflow_id; - $table->published = 1; - $table->default = 1; - - $table->store(); - } - - return $result; - } - - /** - * Abstract method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure - * - * @since 4.0.0 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm( - 'com_workflow.workflow', - 'workflow', - array( - 'control' => 'jform', - 'load_data' => $loadData - ) - ); - - if (empty($form)) - { - return false; - } - - $id = $data['id'] ?? $form->getValue('id'); - - $item = $this->getItem($id); - - $canEditState = $this->canEditState((object) $item); - - // Modify the form based on access controls. - if (!$canEditState || !empty($item->default)) - { - if (!$canEditState) - { - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('published', 'required', 'false'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - $form->setFieldAttribute('default', 'disabled', 'true'); - $form->setFieldAttribute('default', 'required', 'false'); - $form->setFieldAttribute('default', 'filter', 'unset'); - } - - $form->setFieldAttribute('created', 'default', Factory::getDate()->format('Y-m-d H:i:s')); - $form->setFieldAttribute('modified', 'default', Factory::getDate()->format('Y-m-d H:i:s')); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState( - 'com_workflow.edit.workflow.data', - array() - ); - - if (empty($data)) - { - $data = $this->getItem(); - } - - return $data; - } - - /** - * Method to preprocess the form. - * - * @param Form $form Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 4.0.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $extension = Factory::getApplication()->input->get('extension'); - - $parts = explode('.', $extension); - - $extension = array_shift($parts); - - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $extension); - - parent::preprocessForm($form, $data, $group); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 4.0.0 - */ - protected function getReorderConditions($table) - { - return [ - $this->_db->quoteName('extension') . ' = ' . $this->_db->quote($table->extension), - ]; - } - - /** - * Method to change the default state of one item. - * - * @param array $pk A list of the primary keys to change. - * @param integer $value The value of the home state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setDefault($pk, $value = 1) - { - $table = $this->getTable(); - - if ($table->load($pk)) - { - if ($table->published !== 1) - { - $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); - - return false; - } - } - - if (empty($table->id) || !$this->canEditState($table)) - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - - return false; - } - - $date = Factory::getDate()->toSql(); - - if ($value) - { - // Unset other default item - if ($table->load( - [ - 'default' => '1', - 'extension' => $table->get('extension') - ] - )) - { - $table->default = 0; - $table->modified = $date; - $table->store(); - } - } - - if ($table->load($pk)) - { - $table->modified = $date; - $table->default = $value; - $table->store(); - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 4.0.0 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', $record->extension . '.workflow.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 4.0.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - // Check for existing workflow. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $record->extension . '.workflow.' . (int) $record->id); - } - - // Default to component settings if workflow isn't known. - return $user->authorise('core.edit.state', $record->extension); - } - - /** - * Method to change the published state of one or more records. - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function publish(&$pks, $value = 1) - { - $table = $this->getTable(); - $pks = (array) $pks; - - $date = Factory::getDate()->toSql(); - - // Default workflow item check. - foreach ($pks as $i => $pk) - { - if ($table->load($pk) && $value != 1 && $table->default) - { - // Prune items that you can't change. - Factory::getApplication()->enqueueMessage(Text::_('COM_WORKFLOW_UNPUBLISH_DEFAULT_ERROR'), 'error'); - unset($pks[$i]); - break; - } - } - - // Clean the cache. - $this->cleanCache(); - - // Ensure that previous checks don't empty the array. - if (empty($pks)) - { - return true; - } - - $table->load($pk); - $table->modified = $date; - $table->store(); - - return parent::publish($pks, $value); - } + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 4.0.0 + */ + public function populateState() + { + parent::populateState(); + + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + $this->setState('filter.extension', $extension); + } + + /** + * Method to change the title + * + * @param integer $categoryId The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 4.0.0 + */ + protected function generateNewTitle($categoryId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('title' => $title))) { + $title = StringHelper::increment($title); + } + + return array($title, $alias); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function save($data) + { + $table = $this->getTable(); + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $input = $app->input; + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + $data['extension'] = !empty($data['extension']) ? $data['extension'] : $extension; + + // Make sure we use the correct extension when editing an existing workflow + $key = $table->getKeyName(); + $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); + + if ($pk > 0) { + $table->load($pk); + + $data['extension'] = $table->extension; + } + + if (isset($data['rules']) && !$user->authorise('core.admin', $data['extension'])) { + unset($data['rules']); + } + + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + + // Alter the title for save as copy + if ($origTable->load(['title' => $data['title']])) { + list($title) = $this->generateNewTitle(0, '', $data['title']); + $data['title'] = $title; + } + + // Unpublish new copy + $data['published'] = 0; + $data['default'] = 0; + } + + $result = parent::save($data); + + // Create default stage for new workflow + if ($result && $input->getCmd('task') !== 'save2copy' && $this->getState($this->getName() . '.new')) { + $workflow_id = (int) $this->getState($this->getName() . '.id'); + + $table = $this->getTable('Stage'); + + $table->id = 0; + $table->title = 'COM_WORKFLOW_BASIC_STAGE'; + $table->description = ''; + $table->workflow_id = $workflow_id; + $table->published = 1; + $table->default = 1; + + $table->store(); + } + + return $result; + } + + /** + * Abstract method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm( + 'com_workflow.workflow', + 'workflow', + array( + 'control' => 'jform', + 'load_data' => $loadData + ) + ); + + if (empty($form)) { + return false; + } + + $id = $data['id'] ?? $form->getValue('id'); + + $item = $this->getItem($id); + + $canEditState = $this->canEditState((object) $item); + + // Modify the form based on access controls. + if (!$canEditState || !empty($item->default)) { + if (!$canEditState) { + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('published', 'required', 'false'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + $form->setFieldAttribute('default', 'disabled', 'true'); + $form->setFieldAttribute('default', 'required', 'false'); + $form->setFieldAttribute('default', 'filter', 'unset'); + } + + $form->setFieldAttribute('created', 'default', Factory::getDate()->format('Y-m-d H:i:s')); + $form->setFieldAttribute('modified', 'default', Factory::getDate()->format('Y-m-d H:i:s')); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState( + 'com_workflow.edit.workflow.data', + array() + ); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + /** + * Method to preprocess the form. + * + * @param Form $form Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $extension = Factory::getApplication()->input->get('extension'); + + $parts = explode('.', $extension); + + $extension = array_shift($parts); + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $extension); + + parent::preprocessForm($form, $data, $group); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 4.0.0 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('extension') . ' = ' . $db->quote($table->extension), + ]; + } + + /** + * Method to change the default state of one item. + * + * @param array $pk A list of the primary keys to change. + * @param integer $value The value of the home state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setDefault($pk, $value = 1) + { + $table = $this->getTable(); + + if ($table->load($pk)) { + if ($table->published !== 1) { + $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } + + if (empty($table->id) || !$this->canEditState($table)) { + Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + + return false; + } + + $date = Factory::getDate()->toSql(); + + if ($value) { + // Unset other default item + if ( + $table->load( + [ + 'default' => '1', + 'extension' => $table->get('extension') + ] + ) + ) { + $table->default = 0; + $table->modified = $date; + $table->store(); + } + } + + if ($table->load($pk)) { + $table->modified = $date; + $table->default = $value; + $table->store(); + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 4.0.0 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + return Factory::getUser()->authorise('core.delete', $record->extension . '.workflow.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 4.0.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + // Check for existing workflow. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $record->extension . '.workflow.' . (int) $record->id); + } + + // Default to component settings if workflow isn't known. + return $user->authorise('core.edit.state', $record->extension); + } + + /** + * Method to change the published state of one or more records. + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function publish(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + + $date = Factory::getDate()->toSql(); + + // Default workflow item check. + foreach ($pks as $i => $pk) { + if ($table->load($pk) && $value != 1 && $table->default) { + // Prune items that you can't change. + Factory::getApplication()->enqueueMessage(Text::_('COM_WORKFLOW_UNPUBLISH_DEFAULT_ERROR'), 'error'); + unset($pks[$i]); + break; + } + } + + // Clean the cache. + $this->cleanCache(); + + // Ensure that previous checks don't empty the array. + if (empty($pks)) { + return true; + } + + $table->load($pk); + $table->modified = $date; + $table->store(); + + return parent::publish($pks, $value); + } } diff --git a/code/administrator/components/com_workflow/src/Model/WorkflowsModel.php b/code/administrator/components/com_workflow/src/Model/WorkflowsModel.php index 92b5ebca..6c262c1f 100644 --- a/code/administrator/components/com_workflow/src/Model/WorkflowsModel.php +++ b/code/administrator/components/com_workflow/src/Model/WorkflowsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); - - $this->setState('filter.extension', $extension); - $parts = explode('.', $extension); - - // Extract the component name - $this->setState('filter.component', $parts[0]); - - // Extract the optional section name - $this->setState('filter.section', (count($parts) > 1) ? $parts[1] : null); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\Table\Table A Table object - * - * @since 4.0.0 - */ - public function getTable($type = 'Workflow', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - - if ($items) - { - $this->countItems($items); - } - - return $items; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return \Joomla\CMS\Form\Form|bool the Form object or false - * - * @since 4.0.0 - */ - public function getFilterForm($data = array(), $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - - if ($form) - { - $form->setValue('extension', null, $this->getState('filter.extension')); - } - - return $form; - } - - /** - * Add the number of transitions and states to all workflow items - * - * @param array $items The workflow items - * - * @return mixed An array of data items on success, false on failure. - * - * @since 4.0.0 - */ - protected function countItems($items) - { - $db = $this->getDbo(); - - $ids = [0]; - - foreach ($items as $item) - { - $ids[] = (int) $item->id; - - $item->count_states = 0; - $item->count_transitions = 0; - } - - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('workflow_id'), - 'COUNT(*) AS ' . $db->quoteName('count'), - ] - ) - ->from($db->quoteName('#__workflow_stages')) - ->whereIn($db->quoteName('workflow_id'), $ids) - ->where($db->quoteName('published') . ' >= 0') - ->group($db->quoteName('workflow_id')); - - $status = $db->setQuery($query)->loadObjectList('workflow_id'); - - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('workflow_id'), - 'COUNT(*) AS ' . $db->quoteName('count'), - ] - ) - ->from($db->quoteName('#__workflow_transitions')) - ->whereIn($db->quoteName('workflow_id'), $ids) - ->where($db->quoteName('published') . ' >= 0') - ->group($db->quoteName('workflow_id')); - - $transitions = $db->setQuery($query)->loadObjectList('workflow_id'); - - foreach ($items as $item) - { - if (isset($status[$item->id])) - { - $item->count_states = (int) $status[$item->id]->count; - } - - if (isset($transitions[$item->id])) - { - $item->count_transitions = (int) $transitions[$item->id]->count; - } - } - } - - /** - * Method to get the data that should be injected in the form. - * - * @return string The query to database. - * - * @since 4.0.0 - */ - public function getListQuery() - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('w.id'), - $db->quoteName('w.title'), - $db->quoteName('w.created'), - $db->quoteName('w.modified'), - $db->quoteName('w.published'), - $db->quoteName('w.checked_out'), - $db->quoteName('w.checked_out_time'), - $db->quoteName('w.ordering'), - $db->quoteName('w.default'), - $db->quoteName('w.created_by'), - $db->quoteName('w.description'), - $db->quoteName('u.name'), - $db->quoteName('uc.name', 'editor'), - ] - ) - ->from($db->quoteName('#__workflows', 'w')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('w.created_by')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('w.checked_out')); - - // Filter by extension - if ($extension = $this->getState('filter.extension')) - { - $query->where($db->quoteName('extension') . ' = :extension') - ->bind(':extension', $extension); - } - - $status = (string) $this->getState('filter.published'); - - // Filter by status - if (is_numeric($status)) - { - $status = (int) $status; - $query->where($db->quoteName('w.published') . ' = :published') - ->bind(':published', $status, ParameterType::INTEGER); - } - elseif ($status === '') - { - $query->where($db->quoteName('w.published') . ' IN (0, 1)'); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('w.title') . ' LIKE :search1 OR ' . $db->quoteName('w.description') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'w.ordering'); - $orderDirn = strtoupper($this->state->get('list.direction', 'ASC')); - - $query->order($db->escape($orderCol) . ' ' . ($orderDirn === 'DESC' ? 'DESC' : 'ASC')); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see JController + * @since 4.0.0 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'w.id', + 'title', 'w.title', + 'published', 'w.published', + 'created_by', 'w.created_by', + 'created', 'w.created', + 'ordering', 'w.ordering', + 'modified', 'w.modified', + 'description', 'w.description' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = 'w.ordering', $direction = 'asc') + { + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); + + $this->setState('filter.extension', $extension); + $parts = explode('.', $extension); + + // Extract the component name + $this->setState('filter.component', $parts[0]); + + // Extract the optional section name + $this->setState('filter.section', (count($parts) > 1) ? $parts[1] : null); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\Table\Table A Table object + * + * @since 4.0.0 + */ + public function getTable($type = 'Workflow', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + if ($items) { + $this->countItems($items); + } + + return $items; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \Joomla\CMS\Form\Form|bool the Form object or false + * + * @since 4.0.0 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + if ($form) { + $form->setValue('extension', null, $this->getState('filter.extension')); + } + + return $form; + } + + /** + * Add the number of transitions and states to all workflow items + * + * @param array $items The workflow items + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + protected function countItems($items) + { + $db = $this->getDatabase(); + + $ids = [0]; + + foreach ($items as $item) { + $ids[] = (int) $item->id; + + $item->count_states = 0; + $item->count_transitions = 0; + } + + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('workflow_id'), + 'COUNT(*) AS ' . $db->quoteName('count'), + ] + ) + ->from($db->quoteName('#__workflow_stages')) + ->whereIn($db->quoteName('workflow_id'), $ids) + ->where($db->quoteName('published') . ' >= 0') + ->group($db->quoteName('workflow_id')); + + $status = $db->setQuery($query)->loadObjectList('workflow_id'); + + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('workflow_id'), + 'COUNT(*) AS ' . $db->quoteName('count'), + ] + ) + ->from($db->quoteName('#__workflow_transitions')) + ->whereIn($db->quoteName('workflow_id'), $ids) + ->where($db->quoteName('published') . ' >= 0') + ->group($db->quoteName('workflow_id')); + + $transitions = $db->setQuery($query)->loadObjectList('workflow_id'); + + foreach ($items as $item) { + if (isset($status[$item->id])) { + $item->count_states = (int) $status[$item->id]->count; + } + + if (isset($transitions[$item->id])) { + $item->count_transitions = (int) $transitions[$item->id]->count; + } + } + } + + /** + * Method to get the data that should be injected in the form. + * + * @return string The query to database. + * + * @since 4.0.0 + */ + public function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('w.id'), + $db->quoteName('w.title'), + $db->quoteName('w.created'), + $db->quoteName('w.modified'), + $db->quoteName('w.published'), + $db->quoteName('w.checked_out'), + $db->quoteName('w.checked_out_time'), + $db->quoteName('w.ordering'), + $db->quoteName('w.default'), + $db->quoteName('w.created_by'), + $db->quoteName('w.description'), + $db->quoteName('u.name'), + $db->quoteName('uc.name', 'editor'), + ] + ) + ->from($db->quoteName('#__workflows', 'w')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('w.created_by')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('w.checked_out')); + + // Filter by extension + if ($extension = $this->getState('filter.extension')) { + $query->where($db->quoteName('extension') . ' = :extension') + ->bind(':extension', $extension); + } + + $status = (string) $this->getState('filter.published'); + + // Filter by status + if (is_numeric($status)) { + $status = (int) $status; + $query->where($db->quoteName('w.published') . ' = :published') + ->bind(':published', $status, ParameterType::INTEGER); + } elseif ($status === '') { + $query->where($db->quoteName('w.published') . ' IN (0, 1)'); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('w.title') . ' LIKE :search1 OR ' . $db->quoteName('w.description') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'w.ordering'); + $orderDirn = strtoupper($this->state->get('list.direction', 'ASC')); + + $query->order($db->escape($orderCol) . ' ' . ($orderDirn === 'DESC' ? 'DESC' : 'ASC')); + + return $query; + } } diff --git a/code/administrator/components/com_workflow/src/Table/StageTable.php b/code/administrator/components/com_workflow/src/Table/StageTable.php index 382e0422..2a788626 100644 --- a/code/administrator/components/com_workflow/src/Table/StageTable.php +++ b/code/administrator/components/com_workflow/src/Table/StageTable.php @@ -1,4 +1,5 @@ getDbo(); - $app = Factory::getApplication(); - $pk = (int) $pk; - - $query = $db->getQuery(true) - ->select($db->quoteName('default')) - ->from($db->quoteName('#__workflow_stages')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - $isDefault = $db->setQuery($query)->loadResult(); - - if ($isDefault) - { - $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DELETE_IS_DEFAULT'), 'error'); - - return false; - } - - try - { - $query = $db->getQuery(true) - ->delete($db->quoteName('#__workflow_transitions')) - ->where( - [ - $db->quoteName('to_stage_id') . ' = :idTo', - $db->quoteName('from_stage_id') . ' = :idFrom', - ], - 'OR' - ) - ->bind([':idTo', ':idFrom'], $pk, ParameterType::INTEGER); - - $db->setQuery($query)->execute(); - - return parent::delete($pk); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage(Text::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $e->getMessage()), 'error'); - } - - return false; - } - - /** - * Overloaded check function - * - * @return boolean True on success - * - * @see Table::check() - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (trim($this->title) === '') - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_STATE')); - - return false; - } - - if (!empty($this->default)) - { - if ((int) $this->published !== 1) - { - $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); - - return false; - } - } - else - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query - ->select($db->quoteName('id')) - ->from($db->quoteName('#__workflow_stages')) - ->where( - [ - $db->quoteName('workflow_id') . ' = :id', - $db->quoteName('default') . ' = 1', - ] - ) - ->bind(':id', $this->workflow_id, ParameterType::INTEGER); - - $id = $db->setQuery($query)->loadResult(); - - // If there is no default stage => set the current to default to recover - if (empty($id)) - { - $this->default = '1'; - } - // This stage is the default, but someone has tried to disable it => not allowed - elseif ($id === $this->id) - { - $this->setError(Text::_('COM_WORKFLOW_DISABLE_DEFAULT')); - - return false; - } - } - - return true; - } - - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - $table = new StageTable($this->getDbo()); - - if ($this->default == '1') - { - // Verify that the default is unique for this workflow - if ($table->load(array('default' => '1', 'workflow_id' => (int) $this->workflow_id))) - { - $table->default = 0; - $table->store(); - } - } - - return parent::store($updateNulls); - } - - /** - * Method to bind an associative array or object to the Table instance. - * This method only binds properties that are publicly accessible and optionally - * takes an array of properties to ignore when binding. - * - * @param array|object $src An associative array or object to bind to the Table instance. - * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success. - * - * @since 4.0.0 - * @throws \InvalidArgumentException - */ - public function bind($src, $ignore = array()) - { - // Bind the rules. - if (isset($src['rules']) && \is_array($src['rules'])) - { - $rules = new Rules($src['rules']); - $this->setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetName() - { - $k = $this->_tbl_key; - $workflow = new WorkflowTable($this->getDbo()); - $workflow->load($this->workflow_id); - - $parts = explode('.', $workflow->extension); - - $extension = array_shift($parts); - - return $extension . '.stage.' . (int) $this->$k; - } - - /** - * Method to return the title to use for the asset table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Get the parent asset id for the record - * - * @param Table|null $table A Table object for the asset parent. - * @param integer|null $id The id for the asset - * - * @return integer The id of the asset's parent - * - * @since 4.0.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); - - $workflow = new WorkflowTable($this->getDbo()); - $workflow->load($this->workflow_id); - - $parts = explode('.', $workflow->extension); - - $extension = array_shift($parts); - - $name = $extension . '.workflow.' . (int) $workflow->id; - - $asset->loadByName($name); - $assetId = $asset->id; - - return !empty($assetId) ? $assetId : parent::_getAssetParentId($table, $id); - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * @param DatabaseDriver $db Database connector object + * + * @since 4.0.0 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__workflow_stages', 'id', $db); + } + + /** + * Deletes workflow with transition and stages. + * + * @param int $pk Extension ids to delete. + * + * @return boolean True on success. + * + * @since 4.0.0 + * + * @throws \UnexpectedValueException + */ + public function delete($pk = null) + { + $db = $this->getDbo(); + $app = Factory::getApplication(); + $pk = (int) $pk; + + $query = $db->getQuery(true) + ->select($db->quoteName('default')) + ->from($db->quoteName('#__workflow_stages')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + $isDefault = $db->setQuery($query)->loadResult(); + + if ($isDefault) { + $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DELETE_IS_DEFAULT'), 'error'); + + return false; + } + + try { + $query = $db->getQuery(true) + ->delete($db->quoteName('#__workflow_transitions')) + ->where( + [ + $db->quoteName('to_stage_id') . ' = :idTo', + $db->quoteName('from_stage_id') . ' = :idFrom', + ], + 'OR' + ) + ->bind([':idTo', ':idFrom'], $pk, ParameterType::INTEGER); + + $db->setQuery($query)->execute(); + + return parent::delete($pk); + } catch (\RuntimeException $e) { + $app->enqueueMessage(Text::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $e->getMessage()), 'error'); + } + + return false; + } + + /** + * Overloaded check function + * + * @return boolean True on success + * + * @see Table::check() + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (trim($this->title) === '') { + $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_STATE')); + + return false; + } + + if (!empty($this->default)) { + if ((int) $this->published !== 1) { + $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } else { + $db = $this->getDbo(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName('id')) + ->from($db->quoteName('#__workflow_stages')) + ->where( + [ + $db->quoteName('workflow_id') . ' = :id', + $db->quoteName('default') . ' = 1', + ] + ) + ->bind(':id', $this->workflow_id, ParameterType::INTEGER); + + $id = $db->setQuery($query)->loadResult(); + + // If there is no default stage => set the current to default to recover + if (empty($id)) { + $this->default = '1'; + } elseif ($id === $this->id) { + // This stage is the default, but someone has tried to disable it => not allowed + $this->setError(Text::_('COM_WORKFLOW_DISABLE_DEFAULT')); + + return false; + } + } + + return true; + } + + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + $table = new StageTable($this->getDbo()); + + if ($this->default == '1') { + // Verify that the default is unique for this workflow + if ($table->load(array('default' => '1', 'workflow_id' => (int) $this->workflow_id))) { + $table->default = 0; + $table->store(); + } + } + + return parent::store($updateNulls); + } + + /** + * Method to bind an associative array or object to the Table instance. + * This method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param array|object $src An associative array or object to bind to the Table instance. + * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = array()) + { + // Bind the rules. + if (isset($src['rules']) && \is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + $parts = explode('.', $workflow->extension); + + $extension = array_shift($parts); + + return $extension . '.stage.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Get the parent asset id for the record + * + * @param Table|null $table A Table object for the asset parent. + * @param integer|null $id The id for the asset + * + * @return integer The id of the asset's parent + * + * @since 4.0.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); + + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + $parts = explode('.', $workflow->extension); + + $extension = array_shift($parts); + + $name = $extension . '.workflow.' . (int) $workflow->id; + + $asset->loadByName($name); + $assetId = $asset->id; + + return !empty($assetId) ? $assetId : parent::_getAssetParentId($table, $id); + } } diff --git a/code/administrator/components/com_workflow/src/Table/TransitionTable.php b/code/administrator/components/com_workflow/src/Table/TransitionTable.php index 95b5b2de..9eed8d5b 100644 --- a/code/administrator/components/com_workflow/src/Table/TransitionTable.php +++ b/code/administrator/components/com_workflow/src/Table/TransitionTable.php @@ -1,4 +1,5 @@ setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetName() - { - $k = $this->_tbl_key; - $workflow = new WorkflowTable($this->getDbo()); - $workflow->load($this->workflow_id); - - $parts = explode('.', $workflow->extension); - - $extension = array_shift($parts); - - return $extension . '.transition.' . (int) $this->$k; - } - - /** - * Method to return the title to use for the asset table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Get the parent asset id for the record - * - * @param Table $table A Table object for the asset parent. - * @param integer $id The id for the asset - * - * @return integer The id of the asset's parent - * - * @since 4.0.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); - - $workflow = new WorkflowTable($this->getDbo()); - $workflow->load($this->workflow_id); - - $parts = explode('.', $workflow->extension); - - $extension = array_shift($parts); - - $name = $extension . '.workflow.' . (int) $workflow->id; - - $asset->loadByName($name); - $assetId = $asset->id; - - return !empty($assetId) ? $assetId : parent::_getAssetParentId($table, $id); - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * An array of key names to be json encoded in the bind function + * + * @var array + * + * @since 4.0.0 + */ + protected $_jsonEncode = [ + 'options' + ]; + + /** + * @param DatabaseDriver $db Database connector object + * + * @since 4.0.0 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__workflow_transitions', 'id', $db); + } + + /** + * Method to bind an associative array or object to the Table instance. + * This method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param array|object $src An associative array or object to bind to the Table instance. + * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = array()) + { + // Bind the rules. + if (isset($src['rules']) && \is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + $parts = explode('.', $workflow->extension); + + $extension = array_shift($parts); + + return $extension . '.transition.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Get the parent asset id for the record + * + * @param Table $table A Table object for the asset parent. + * @param integer $id The id for the asset + * + * @return integer The id of the asset's parent + * + * @since 4.0.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); + + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + $parts = explode('.', $workflow->extension); + + $extension = array_shift($parts); + + $name = $extension . '.workflow.' . (int) $workflow->id; + + $asset->loadByName($name); + $assetId = $asset->id; + + return !empty($assetId) ? $assetId : parent::_getAssetParentId($table, $id); + } } diff --git a/code/administrator/components/com_workflow/src/Table/WorkflowTable.php b/code/administrator/components/com_workflow/src/Table/WorkflowTable.php index a9f7267b..fc2178b9 100644 --- a/code/administrator/components/com_workflow/src/Table/WorkflowTable.php +++ b/code/administrator/components/com_workflow/src/Table/WorkflowTable.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Table; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Table; use Joomla\CMS\Access\Rules; use Joomla\CMS\Factory; @@ -17,6 +17,10 @@ use Joomla\Database\DatabaseDriver; use Joomla\Database\ParameterType; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Workflow table * @@ -24,317 +28,290 @@ */ class WorkflowTable extends Table { - /** - * Indicates that columns fully support the NULL value in the database - * - * @var boolean - * - * @since 4.0.0 - */ - protected $_supportNullValue = true; - - /** - * @param DatabaseDriver $db Database connector object - * - * @since 4.0.0 - */ - public function __construct(DatabaseDriver $db) - { - $this->typeAlias = '{extension}.workflow'; - - parent::__construct('#__workflows', 'id', $db); - } - - /** - * Deletes workflow with transition and states. - * - * @param int $pk Extension ids to delete. - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception on ACL error - */ - public function delete($pk = null) - { - $db = $this->getDbo(); - $app = Factory::getApplication(); - $pk = (int) $pk; - - // Gets the workflow information that is going to be deleted. - $query = $db->getQuery(true) - ->select($db->quoteName('default')) - ->from($db->quoteName('#__workflows')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - $isDefault = $db->setQuery($query)->loadResult(); - - if ($isDefault) - { - $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DELETE_DEFAULT'), 'error'); - - return false; - } - - // Delete the workflow states, then transitions from all tables. - try - { - $query = $db->getQuery(true) - ->delete($db->quoteName('#__workflow_stages')) - ->where($db->quoteName('workflow_id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - $db->setQuery($query)->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__workflow_transitions')) - ->where($db->quoteName('workflow_id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - $db->setQuery($query)->execute(); - - return parent::delete($pk); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage(Text::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $e->getMessage()), 'error'); - - return false; - } - } - - /** - * Overloaded check function - * - * @return boolean True on success - * - * @see Table::check() - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (trim($this->title) === '') - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_WORKFLOW')); - - return false; - } - - if (!empty($this->default)) - { - if ((int) $this->published !== 1) - { - $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); - - return false; - } - } - else - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query - ->select($db->quoteName('id')) - ->from($db->quoteName('#__workflows')) - ->where($db->quoteName('default') . ' = 1'); - - $id = $db->setQuery($query)->loadResult(); - - // If there is no default workflow => set the current to default to recover - if (empty($id)) - { - $this->default = '1'; - } - // This workflow is the default, but someone has tried to disable it => not allowed - elseif ($id === $this->id) - { - $this->setError(Text::_('COM_WORKFLOW_DISABLE_DEFAULT')); - - return false; - } - } - - return true; - } - - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - $table = new WorkflowTable($this->getDbo()); - - if ($this->id) - { - // Existing item - $this->modified_by = $user->id; - $this->modified = $date->toSql(); - } - else - { - $this->modified_by = 0; - } - - if (!(int) $this->created) - { - $this->created = $date->toSql(); - } - - if (empty($this->created_by)) - { - $this->created_by = $user->id; - } - - if (!(int) $this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - - if ((int) $this->default === 1) - { - // Verify that the default is unique for this workflow - if ($table->load( - [ - 'default' => '1', - 'extension' => $this->extension - ] - )) - { - $table->default = 0; - $table->store(); - } - } - - return parent::store($updateNulls); - } - - /** - * Method to bind an associative array or object to the Table instance. - * This method only binds properties that are publicly accessible and optionally - * takes an array of properties to ignore when binding. - * - * @param array|object $src An associative array or object to bind to the Table instance. - * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success. - * - * @since 4.0.0 - * @throws \InvalidArgumentException - */ - public function bind($src, $ignore = array()) - { - // Bind the rules. - if (isset($src['rules']) && \is_array($src['rules'])) - { - $rules = new Rules($src['rules']); - $this->setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetName() - { - $k = $this->_tbl_key; - - $parts = explode('.', $this->extension); - - $extension = array_shift($parts); - - return $extension . '.workflow.' . (int) $this->$k; - } - - /** - * Method to return the title to use for the asset table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Get the parent asset id for the record - * - * @param Table $table A Table object for the asset parent. - * @param integer $id The id for the asset - * - * @return integer The id of the asset's parent - * - * @since 4.0.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $assetId = null; - - $parts = explode('.', $this->extension); - - $extension = array_shift($parts); - - // Build the query to get the asset id for the parent category. - $query = $this->getDbo()->getQuery(true) - ->select($this->getDbo()->quoteName('id')) - ->from($this->getDbo()->quoteName('#__assets')) - ->where($this->getDbo()->quoteName('name') . ' = :extension') - ->bind(':extension', $extension); - - // Get the asset id from the database. - $this->getDbo()->setQuery($query); - - if ($result = $this->getDbo()->loadResult()) - { - $assetId = (int) $result; - } - - // Return the asset id. - if ($assetId) - { - return $assetId; - } - else - { - return parent::_getAssetParentId($table, $id); - } - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * @param DatabaseDriver $db Database connector object + * + * @since 4.0.0 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = '{extension}.workflow'; + + parent::__construct('#__workflows', 'id', $db); + } + + /** + * Deletes workflow with transition and states. + * + * @param int $pk Extension ids to delete. + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception on ACL error + */ + public function delete($pk = null) + { + $db = $this->getDbo(); + $app = Factory::getApplication(); + $pk = (int) $pk; + + // Gets the workflow information that is going to be deleted. + $query = $db->getQuery(true) + ->select($db->quoteName('default')) + ->from($db->quoteName('#__workflows')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + $isDefault = $db->setQuery($query)->loadResult(); + + if ($isDefault) { + $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DELETE_DEFAULT'), 'error'); + + return false; + } + + // Delete the workflow states, then transitions from all tables. + try { + $query = $db->getQuery(true) + ->delete($db->quoteName('#__workflow_stages')) + ->where($db->quoteName('workflow_id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + $db->setQuery($query)->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__workflow_transitions')) + ->where($db->quoteName('workflow_id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + $db->setQuery($query)->execute(); + + return parent::delete($pk); + } catch (\RuntimeException $e) { + $app->enqueueMessage(Text::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $e->getMessage()), 'error'); + + return false; + } + } + + /** + * Overloaded check function + * + * @return boolean True on success + * + * @see Table::check() + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (trim($this->title) === '') { + $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_WORKFLOW')); + + return false; + } + + if (!empty($this->default)) { + if ((int) $this->published !== 1) { + $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } else { + $db = $this->getDbo(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName('id')) + ->from($db->quoteName('#__workflows')) + ->where($db->quoteName('default') . ' = 1'); + + $id = $db->setQuery($query)->loadResult(); + + // If there is no default workflow => set the current to default to recover + if (empty($id)) { + $this->default = '1'; + } elseif ($id === $this->id) { + // This workflow is the default, but someone has tried to disable it => not allowed + $this->setError(Text::_('COM_WORKFLOW_DISABLE_DEFAULT')); + + return false; + } + } + + return true; + } + + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + $table = new WorkflowTable($this->getDbo()); + + if ($this->id) { + // Existing item + $this->modified_by = $user->id; + $this->modified = $date->toSql(); + } else { + $this->modified_by = 0; + } + + if (!(int) $this->created) { + $this->created = $date->toSql(); + } + + if (empty($this->created_by)) { + $this->created_by = $user->id; + } + + if (!(int) $this->modified) { + $this->modified = $this->created; + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + + if ((int) $this->default === 1) { + // Verify that the default is unique for this workflow + if ( + $table->load( + [ + 'default' => '1', + 'extension' => $this->extension + ] + ) + ) { + $table->default = 0; + $table->store(); + } + } + + return parent::store($updateNulls); + } + + /** + * Method to bind an associative array or object to the Table instance. + * This method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param array|object $src An associative array or object to bind to the Table instance. + * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = array()) + { + // Bind the rules. + if (isset($src['rules']) && \is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + + $parts = explode('.', $this->extension); + + $extension = array_shift($parts); + + return $extension . '.workflow.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Get the parent asset id for the record + * + * @param Table $table A Table object for the asset parent. + * @param integer $id The id for the asset + * + * @return integer The id of the asset's parent + * + * @since 4.0.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $assetId = null; + + $parts = explode('.', $this->extension); + + $extension = array_shift($parts); + + // Build the query to get the asset id for the parent category. + $query = $this->getDbo()->getQuery(true) + ->select($this->getDbo()->quoteName('id')) + ->from($this->getDbo()->quoteName('#__assets')) + ->where($this->getDbo()->quoteName('name') . ' = :extension') + ->bind(':extension', $extension); + + // Get the asset id from the database. + $this->getDbo()->setQuery($query); + + if ($result = $this->getDbo()->loadResult()) { + $assetId = (int) $result; + } + + // Return the asset id. + if ($assetId) { + return $assetId; + } else { + return parent::_getAssetParentId($table, $id); + } + } } diff --git a/code/administrator/components/com_workflow/src/View/Stage/HtmlView.php b/code/administrator/components/com_workflow/src/View/Stage/HtmlView.php index 1bff632d..9670a37a 100644 --- a/code/administrator/components/com_workflow/src/View/Stage/HtmlView.php +++ b/code/administrator/components/com_workflow/src/View/Stage/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\View\Stage; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\View\Stage; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; @@ -17,6 +17,10 @@ use Joomla\CMS\Toolbar\ToolbarHelper; use Joomla\Component\Workflow\Administrator\Helper\StageHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * View class to add or edit a stage of a workflow * @@ -24,155 +28,147 @@ */ class HtmlView extends BaseHtmlView { - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * From object to generate fields - * - * @var \Joomla\CMS\Form\Form - * - * @since 4.0.0 - */ - protected $form; - - /** - * Items array - * - * @var object - * @since 4.0.0 - */ - protected $item; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display item view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - // Get the Data - $this->state = $this->get('State'); - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $extension = $this->state->get('filter.extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - // Set the toolbar - $this->addToolbar(); - - // Display the template - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = Factory::getUser(); - $userId = $user->id; - $isNew = empty($this->item->id); - - $canDo = StageHelper::getActions($this->extension, 'stage', $this->item->id); - - ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_STAGE_ADD') : Text::_('COM_WORKFLOW_STAGE_EDIT'), 'address'); - - $toolbarButtons = []; - - if ($isNew) - { - // For new records, check the create permission. - if ($canDo->get('core.create')) - { - ToolbarHelper::apply('stage.apply'); - $toolbarButtons = [['save', 'stage.save'], ['save2new', 'stage.save2new']]; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'stage.cancel' - ); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - if ($itemEditable) - { - ToolbarHelper::apply('stage.apply'); - $toolbarButtons = [['save', 'stage.save']]; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'stage.save2new']; - $toolbarButtons[] = ['save2copy', 'stage.save2copy']; - } - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'stage.cancel', - 'JTOOLBAR_CLOSE' - ); - } - - ToolbarHelper::divider(); - } + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * From object to generate fields + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + protected $form; + + /** + * Items array + * + * @var object + * @since 4.0.0 + */ + protected $item; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display item view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + // Get the Data + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $extension = $this->state->get('filter.extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + // Set the toolbar + $this->addToolbar(); + + // Display the template + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = empty($this->item->id); + + $canDo = StageHelper::getActions($this->extension, 'stage', $this->item->id); + + ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_STAGE_ADD') : Text::_('COM_WORKFLOW_STAGE_EDIT'), 'address'); + + $toolbarButtons = []; + + if ($isNew) { + // For new records, check the create permission. + if ($canDo->get('core.create')) { + ToolbarHelper::apply('stage.apply'); + $toolbarButtons = [['save', 'stage.save'], ['save2new', 'stage.save2new']]; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'stage.cancel' + ); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + if ($itemEditable) { + ToolbarHelper::apply('stage.apply'); + $toolbarButtons = [['save', 'stage.save']]; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'stage.save2new']; + $toolbarButtons[] = ['save2copy', 'stage.save2copy']; + } + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'stage.cancel', + 'JTOOLBAR_CLOSE' + ); + } + + ToolbarHelper::divider(); + } } diff --git a/code/administrator/components/com_workflow/src/View/Stages/HtmlView.php b/code/administrator/components/com_workflow/src/View/Stages/HtmlView.php index 91393fe5..114b409f 100644 --- a/code/administrator/components/com_workflow/src/View/Stages/HtmlView.php +++ b/code/administrator/components/com_workflow/src/View/Stages/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\View\Stages; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\View\Stages; use Joomla\CMS\Factory; use Joomla\CMS\Helper\ContentHelper; @@ -20,6 +20,10 @@ use Joomla\CMS\Toolbar\ToolbarHelper; use Joomla\CMS\Workflow\Workflow; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Stages view class for the Workflow package. * @@ -27,198 +31,190 @@ */ class HtmlView extends BaseHtmlView { - /** - * An array of stages - * - * @var array - * @since 4.0.0 - */ - protected $stages; - - /** - * The model stage - * - * @var object - * @since 4.0.0 - */ - protected $stage; - - /** - * The HTML for displaying sidebar - * - * @var string - * @since 4.0.0 - */ - protected $sidebar; - - /** - * The pagination object - * - * @var \Joomla\CMS\Pagination\Pagination - * - * @since 4.0.0 - */ - protected $pagination; - - /** - * Form object for search filters - * - * @var \Joomla\CMS\Form\Form - * - * @since 4.0.0 - */ - public $filterForm; - - /** - * The active search filters - * - * @var array - * @since 4.0.0 - */ - public $activeFilters; - - /** - * The current workflow - * - * @var object - * @since 4.0.0 - */ - protected $workflow; - - /** - * The ID of current workflow - * - * @var integer - * @since 4.0.0 - */ - protected $workflowID; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display the view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $this->state = $this->get('State'); - $this->stages = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->workflow = $this->get('Workflow'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->workflowID = $this->workflow->id; - - $parts = explode('.', $this->workflow->extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (!empty($this->stages)) - { - $extension = Factory::getApplication()->input->getCmd('extension'); - $workflow = new Workflow($extension); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); - - $user = Factory::getUser(); - - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::sprintf('COM_WORKFLOW_STAGES_LIST', Text::_($this->state->get('active_workflow', ''))), 'address contact'); - - $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - - ToolbarHelper::link( - Route::_('index.php?option=com_workflow&view=workflows&extension=' . $this->escape($this->workflow->extension)), - 'JTOOLBAR_BACK', - $arrow - ); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('stage.add'); - } - - if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('stages.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $childBar->unpublish('stages.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - $childBar->makeDefault('stages.setDefault', 'COM_WORKFLOW_TOOLBAR_DEFAULT'); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('stages.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') !== '-2') - { - $childBar->trash('stages.trash'); - } - } - - if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) - { - $toolbar->delete('stages.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - $toolbar->help('Stages_List:_Basic_Workflow'); - } + /** + * An array of stages + * + * @var array + * @since 4.0.0 + */ + protected $stages; + + /** + * The model stage + * + * @var object + * @since 4.0.0 + */ + protected $stage; + + /** + * The HTML for displaying sidebar + * + * @var string + * @since 4.0.0 + */ + protected $sidebar; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 4.0.0 + */ + protected $pagination; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * The current workflow + * + * @var object + * @since 4.0.0 + */ + protected $workflow; + + /** + * The ID of current workflow + * + * @var integer + * @since 4.0.0 + */ + protected $workflowID; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->stages = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->workflow = $this->get('Workflow'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->workflowID = $this->workflow->id; + + $parts = explode('.', $this->workflow->extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (!empty($this->stages)) { + $extension = Factory::getApplication()->input->getCmd('extension'); + $workflow = new Workflow($extension); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); + + $user = $this->getCurrentUser(); + + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::sprintf('COM_WORKFLOW_STAGES_LIST', Text::_($this->state->get('active_workflow', ''))), 'address contact'); + + $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + + ToolbarHelper::link( + Route::_('index.php?option=com_workflow&view=workflows&extension=' . $this->escape($this->workflow->extension)), + 'JTOOLBAR_BACK', + $arrow + ); + + if ($canDo->get('core.create')) { + $toolbar->addNew('stage.add'); + } + + if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('stages.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $childBar->unpublish('stages.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + $childBar->makeDefault('stages.setDefault', 'COM_WORKFLOW_TOOLBAR_DEFAULT'); + + if ($canDo->get('core.admin')) { + $childBar->checkin('stages.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') !== '-2') { + $childBar->trash('stages.trash'); + } + } + + if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) { + $toolbar->delete('stages.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + $toolbar->help('Stages_List:_Basic_Workflow'); + } } diff --git a/code/administrator/components/com_workflow/src/View/Transition/HtmlView.php b/code/administrator/components/com_workflow/src/View/Transition/HtmlView.php index 69810da1..ce230196 100644 --- a/code/administrator/components/com_workflow/src/View/Transition/HtmlView.php +++ b/code/administrator/components/com_workflow/src/View/Transition/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\View\Transition; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\View\Transition; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; @@ -17,6 +17,10 @@ use Joomla\CMS\Toolbar\ToolbarHelper; use Joomla\Component\Workflow\Administrator\Helper\StageHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * View class to add or edit a transition of a workflow * @@ -24,194 +28,183 @@ */ class HtmlView extends BaseHtmlView { - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * Form object to generate fields - * - * @var \Joomla\CMS\Form\Form - * - * @since 4.0.0 - */ - protected $form; - - /** - * Items array - * - * @var object - * @since 4.0.0 - */ - protected $item; - - /** - * That is object of Application - * - * @var \Joomla\CMS\Application\CMSApplication - * @since 4.0.0 - */ - protected $app; - - /** - * The application input object. - * - * @var \Joomla\CMS\Input\Input - * @since 4.0.0 - */ - protected $input; - - /** - * The ID of current workflow - * - * @var integer - * @since 4.0.0 - */ - protected $workflowID; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display item view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $this->app = Factory::getApplication(); - $this->input = $this->app->input; - - // Get the Data - $this->state = $this->get('State'); - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $extension = $this->state->get('filter.extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - // Get the ID of workflow - $this->workflowID = $this->input->getCmd("workflow_id"); - - // Set the toolbar - $this->addToolbar(); - - // Display the template - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = Factory::getUser(); - $userId = $user->id; - $isNew = empty($this->item->id); - - $canDo = StageHelper::getActions($this->extension, 'transition', $this->item->id); - - ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_TRANSITION_ADD') : Text::_('COM_WORKFLOW_TRANSITION_EDIT'), 'address'); - - $toolbarButtons = []; - - $canCreate = $canDo->get('core.create'); - - if ($isNew) - { - // For new records, check the create permission. - if ($canCreate) - { - ToolbarHelper::apply('transition.apply'); - $toolbarButtons = [['save', 'transition.save'], ['save2new', 'transition.save2new']]; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'transition.cancel' - ); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - if ($itemEditable) - { - ToolbarHelper::apply('transition.apply'); - $toolbarButtons[] = ['save', 'transition.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canCreate) - { - $toolbarButtons[] = ['save2new', 'transition.save2new']; - $toolbarButtons[] = ['save2copy', 'transition.save2copy']; - } - } - - if (count($toolbarButtons) > 1) - { - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - } - else - { - ToolbarHelper::save('transition.save'); - } - - ToolbarHelper::cancel( - 'transition.cancel', - 'JTOOLBAR_CLOSE' - ); - } - - ToolbarHelper::divider(); - } + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * Form object to generate fields + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + protected $form; + + /** + * Items array + * + * @var object + * @since 4.0.0 + */ + protected $item; + + /** + * That is object of Application + * + * @var \Joomla\CMS\Application\CMSApplication + * @since 4.0.0 + */ + protected $app; + + /** + * The application input object. + * + * @var \Joomla\CMS\Input\Input + * @since 4.0.0 + */ + protected $input; + + /** + * The ID of current workflow + * + * @var integer + * @since 4.0.0 + */ + protected $workflowID; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display item view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->app = Factory::getApplication(); + $this->input = $this->app->input; + + // Get the Data + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $extension = $this->state->get('filter.extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + // Get the ID of workflow + $this->workflowID = $this->input->getCmd("workflow_id"); + + // Set the toolbar + $this->addToolbar(); + + // Display the template + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = empty($this->item->id); + + $canDo = StageHelper::getActions($this->extension, 'transition', $this->item->id); + + ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_TRANSITION_ADD') : Text::_('COM_WORKFLOW_TRANSITION_EDIT'), 'address'); + + $toolbarButtons = []; + + $canCreate = $canDo->get('core.create'); + + if ($isNew) { + // For new records, check the create permission. + if ($canCreate) { + ToolbarHelper::apply('transition.apply'); + $toolbarButtons = [['save', 'transition.save'], ['save2new', 'transition.save2new']]; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'transition.cancel' + ); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + if ($itemEditable) { + ToolbarHelper::apply('transition.apply'); + $toolbarButtons[] = ['save', 'transition.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canCreate) { + $toolbarButtons[] = ['save2new', 'transition.save2new']; + $toolbarButtons[] = ['save2copy', 'transition.save2copy']; + } + } + + if (count($toolbarButtons) > 1) { + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + } else { + ToolbarHelper::save('transition.save'); + } + + ToolbarHelper::cancel( + 'transition.cancel', + 'JTOOLBAR_CLOSE' + ); + } + + ToolbarHelper::divider(); + } } diff --git a/code/administrator/components/com_workflow/src/View/Transitions/HtmlView.php b/code/administrator/components/com_workflow/src/View/Transitions/HtmlView.php index d2b484fa..96b9af27 100644 --- a/code/administrator/components/com_workflow/src/View/Transitions/HtmlView.php +++ b/code/administrator/components/com_workflow/src/View/Transitions/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\View\Transitions; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\View\Transitions; use Joomla\CMS\Factory; use Joomla\CMS\Helper\ContentHelper; @@ -19,6 +19,10 @@ use Joomla\CMS\Toolbar\Toolbar; use Joomla\CMS\Toolbar\ToolbarHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Transitions view class for the Workflow package. * @@ -26,191 +30,184 @@ */ class HtmlView extends BaseHtmlView { - /** - * An array of transitions - * - * @var array - * @since 4.0.0 - */ - protected $transitions; - - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * The HTML for displaying sidebar - * - * @var string - * @since 4.0.0 - */ - protected $sidebar; - - /** - * The pagination object - * - * @var \Joomla\CMS\Pagination\Pagination - * - * @since 4.0.0 - */ - protected $pagination; - - /** - * Form object for search filters - * - * @var \Joomla\CMS\Form\Form - * - * @since 4.0.0 - */ - public $filterForm; - - /** - * The active search filters - * - * @var array - * @since 4.0.0 - */ - public $activeFilters; - - /** - * The current workflow - * - * @var object - * @since 4.0.0 - */ - protected $workflow; - - /** - * The ID of current workflow - * - * @var integer - * @since 4.0.0 - */ - protected $workflowID; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display the view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $this->state = $this->get('State'); - $this->transitions = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->workflow = $this->get('Workflow'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->workflowID = $this->workflow->id; - - $parts = explode('.', $this->workflow->extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); - - $user = Factory::getUser(); - - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::sprintf('COM_WORKFLOW_TRANSITIONS_LIST', Text::_($this->state->get('active_workflow'))), 'address contact'); - - $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - - ToolbarHelper::link( - Route::_('index.php?option=com_workflow&view=workflows&extension=' . $this->escape($this->workflow->extension)), - 'JTOOLBAR_BACK', - $arrow - ); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('transition.add'); - } - - if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('transitions.publish', 'JTOOLBAR_ENABLE'); - $childBar->unpublish('transitions.unpublish', 'JTOOLBAR_DISABLE'); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('transitions.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') !== '-2') - { - $childBar->trash('transitions.trash'); - } - } - - if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) - { - $toolbar->delete('transitions.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - $toolbar->help('Transitions_List:_Basic_Workflow'); - } + /** + * An array of transitions + * + * @var array + * @since 4.0.0 + */ + protected $transitions; + + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * The HTML for displaying sidebar + * + * @var string + * @since 4.0.0 + */ + protected $sidebar; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 4.0.0 + */ + protected $pagination; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * The current workflow + * + * @var object + * @since 4.0.0 + */ + protected $workflow; + + /** + * The ID of current workflow + * + * @var integer + * @since 4.0.0 + */ + protected $workflowID; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->transitions = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->workflow = $this->get('Workflow'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->workflowID = $this->workflow->id; + + $parts = explode('.', $this->workflow->extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); + + $user = $this->getCurrentUser(); + + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::sprintf('COM_WORKFLOW_TRANSITIONS_LIST', Text::_($this->state->get('active_workflow'))), 'address contact'); + + $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + + ToolbarHelper::link( + Route::_('index.php?option=com_workflow&view=workflows&extension=' . $this->escape($this->workflow->extension)), + 'JTOOLBAR_BACK', + $arrow + ); + + if ($canDo->get('core.create')) { + $toolbar->addNew('transition.add'); + } + + if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('transitions.publish', 'JTOOLBAR_ENABLE'); + $childBar->unpublish('transitions.unpublish', 'JTOOLBAR_DISABLE'); + + if ($canDo->get('core.admin')) { + $childBar->checkin('transitions.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') !== '-2') { + $childBar->trash('transitions.trash'); + } + } + + if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) { + $toolbar->delete('transitions.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + $toolbar->help('Transitions_List:_Basic_Workflow'); + } } diff --git a/code/administrator/components/com_workflow/src/View/Workflow/HtmlView.php b/code/administrator/components/com_workflow/src/View/Workflow/HtmlView.php index 5777eaf4..05a6581d 100644 --- a/code/administrator/components/com_workflow/src/View/Workflow/HtmlView.php +++ b/code/administrator/components/com_workflow/src/View/Workflow/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\View\Workflow; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\View\Workflow; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; @@ -17,6 +17,10 @@ use Joomla\CMS\Toolbar\ToolbarHelper; use Joomla\Component\Workflow\Administrator\Helper\WorkflowHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * View class to add or edit a workflow * @@ -24,158 +28,150 @@ */ class HtmlView extends BaseHtmlView { - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * The Form object - * - * @var \Joomla\CMS\Form\Form - */ - protected $form; - - /** - * The active item - * - * @var object - */ - protected $item; - - /** - * The ID of current workflow - * - * @var integer - * @since 4.0.0 - */ - protected $workflowID; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display item view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - // Get the Data - $this->state = $this->get('State'); - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $extension = $this->state->get('filter.extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - // Set the toolbar - $this->addToolbar(); - - // Display the template - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = Factory::getUser(); - $userId = $user->id; - $isNew = empty($this->item->id); - - $canDo = WorkflowHelper::getActions($this->extension, 'workflow', $this->item->id); - - ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_WORKFLOWS_ADD') : Text::_('COM_WORKFLOW_WORKFLOWS_EDIT'), 'address'); - - $toolbarButtons = []; - - if ($isNew) - { - // For new records, check the create permission. - if ($canDo->get('core.create')) - { - ToolbarHelper::apply('workflow.apply'); - $toolbarButtons = [['save', 'workflow.save'], ['save2new', 'workflow.save2new']]; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'workflow.cancel' - ); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - if ($itemEditable) - { - ToolbarHelper::apply('workflow.apply'); - $toolbarButtons = [['save', 'workflow.save']]; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'workflow.save2new']; - $toolbarButtons[] = ['save2copy', 'workflow.save2copy']; - } - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'workflow.cancel', - 'JTOOLBAR_CLOSE' - ); - } - } + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The ID of current workflow + * + * @var integer + * @since 4.0.0 + */ + protected $workflowID; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display item view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + // Get the Data + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $extension = $this->state->get('filter.extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + // Set the toolbar + $this->addToolbar(); + + // Display the template + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = empty($this->item->id); + + $canDo = WorkflowHelper::getActions($this->extension, 'workflow', $this->item->id); + + ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_WORKFLOWS_ADD') : Text::_('COM_WORKFLOW_WORKFLOWS_EDIT'), 'address'); + + $toolbarButtons = []; + + if ($isNew) { + // For new records, check the create permission. + if ($canDo->get('core.create')) { + ToolbarHelper::apply('workflow.apply'); + $toolbarButtons = [['save', 'workflow.save'], ['save2new', 'workflow.save2new']]; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'workflow.cancel' + ); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + if ($itemEditable) { + ToolbarHelper::apply('workflow.apply'); + $toolbarButtons = [['save', 'workflow.save']]; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'workflow.save2new']; + $toolbarButtons[] = ['save2copy', 'workflow.save2copy']; + } + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'workflow.cancel', + 'JTOOLBAR_CLOSE' + ); + } + } } diff --git a/code/administrator/components/com_workflow/src/View/Workflows/HtmlView.php b/code/administrator/components/com_workflow/src/View/Workflows/HtmlView.php index c7732cce..b3fd4061 100644 --- a/code/administrator/components/com_workflow/src/View/Workflows/HtmlView.php +++ b/code/administrator/components/com_workflow/src/View/Workflows/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\View\Workflows; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\View\Workflows; use Joomla\CMS\Factory; use Joomla\CMS\Helper\ContentHelper; @@ -18,6 +18,10 @@ use Joomla\CMS\Toolbar\Toolbar; use Joomla\CMS\Toolbar\ToolbarHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Workflows view class for the Workflow package. * @@ -25,171 +29,163 @@ */ class HtmlView extends BaseHtmlView { - /** - * An array of workflows - * - * @var array - * @since 4.0.0 - */ - protected $workflows; - - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * The pagination object - * - * @var \Joomla\CMS\Pagination\Pagination - * @since 4.0.0 - */ - protected $pagination; - - /** - * The HTML for displaying sidebar - * - * @var string - * @since 4.0.0 - */ - protected $sidebar; - - /** - * Form object for search filters - * - * @var \Joomla\CMS\Form\Form - * @since 4.0.0 - */ - public $filterForm; - - /** - * The active search filters - * - * @var array - * @since 4.0.0 - */ - public $activeFilters; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display the view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $this->state = $this->get('State'); - $this->workflows = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $extension = $this->state->get('filter.extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions($this->extension, $this->section); - - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_WORKFLOW_WORKFLOWS_LIST'), 'file-alt contact'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('workflow.add'); - } - - if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('workflows.publish', 'JTOOLBAR_ENABLE'); - $childBar->unpublish('workflows.unpublish', 'JTOOLBAR_DISABLE'); - $childBar->makeDefault('workflows.setDefault', 'COM_WORKFLOW_TOOLBAR_DEFAULT'); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('workflows.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - $childBar->trash('workflows.trash'); - } - } - - if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) - { - $toolbar->delete('workflows.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences($this->extension); - } - - $toolbar->help('Workflows_List'); - } + /** + * An array of workflows + * + * @var array + * @since 4.0.0 + */ + protected $workflows; + + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 4.0.0 + */ + protected $pagination; + + /** + * The HTML for displaying sidebar + * + * @var string + * @since 4.0.0 + */ + protected $sidebar; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->workflows = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $extension = $this->state->get('filter.extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions($this->extension, $this->section); + + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_WORKFLOW_WORKFLOWS_LIST'), 'file-alt contact'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('workflow.add'); + } + + if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('workflows.publish', 'JTOOLBAR_ENABLE'); + $childBar->unpublish('workflows.unpublish', 'JTOOLBAR_DISABLE'); + $childBar->makeDefault('workflows.setDefault', 'COM_WORKFLOW_TOOLBAR_DEFAULT'); + + if ($canDo->get('core.admin')) { + $childBar->checkin('workflows.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + $childBar->trash('workflows.trash'); + } + } + + if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) { + $toolbar->delete('workflows.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences($this->extension); + } + + $toolbar->help('Workflows_List'); + } } diff --git a/code/administrator/components/com_workflow/tmpl/stage/edit.php b/code/administrator/components/com_workflow/tmpl/stage/edit.php index b6f53c27..24a89fec 100644 --- a/code/administrator/components/com_workflow/tmpl/stage/edit.php +++ b/code/administrator/components/com_workflow/tmpl/stage/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $user = $app->getIdentity(); @@ -35,54 +36,54 @@
- + - - item->id != 0) : ?> -
-
-
-
- -
-
- -
-
-
-
- + + item->id != 0) : ?> +
+
+
+
+ +
+
+ +
+
+
+
+ -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
- form->renderField('description'); ?> -
-
-
- form->renderField('published'); ?> - form->renderField('default'); ?> -
-
-
- + +
+
+ form->renderField('description'); ?> +
+
+
+ form->renderField('published'); ?> + form->renderField('default'); ?> +
+
+
+ - authorise('core.admin', $this->extension)) : ?> - -
- - form->getInput('rules'); ?> -
- - + authorise('core.admin', $this->extension)) : ?> + +
+ + form->getInput('rules'); ?> +
+ + - + - form->getInput('workflow_id'); ?> - - -
+ form->getInput('workflow_id'); ?> + + +
diff --git a/code/administrator/components/com_workflow/tmpl/stages/default.php b/code/administrator/components/com_workflow/tmpl/stages/default.php index da725512..d7de9de7 100644 --- a/code/administrator/components/com_workflow/tmpl/stages/default.php +++ b/code/administrator/components/com_workflow/tmpl/stages/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $user = Factory::getUser(); $userId = $user->id; @@ -29,128 +32,128 @@ $saveOrder = ($listOrder == 's.ordering'); -if ($saveOrder) -{ - $saveOrderingUrl = 'index.php?option=com_workflow&task=stages.saveOrderAjax&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->extension) . '&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder) { + $saveOrderingUrl = 'index.php?option=com_workflow&task=stages.saveOrderAjax&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->extension) . '&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
- sidebar)) : ?> -
- sidebar; ?> -
- -
-
- $this)); - ?> - stages)) : ?> -
- - -
- - - - - - - - - - - - - - - stages as $i => $item): - $edit = Route::_('index.php?option=com_workflow&task=stage.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->extension); +
+ sidebar)) : ?> +
+ sidebar; ?> +
+ +
+
+ $this)); + ?> + stages)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - -
+ + + + + + + + + + + + + stages as $i => $item) : + $edit = Route::_('index.php?option=com_workflow&task=stage.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->extension); - $canEdit = $user->authorise('core.edit', $this->extension . '.stage.' . $item->id); - $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', $this->extension . '.stage.' . $item->id) && $canCheckin; + $canEdit = $user->authorise('core.edit', $this->extension . '.stage.' . $item->id); + $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', $this->extension . '.stage.' . $item->id) && $canCheckin; - ?> - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + +
- id, false, 'cid', 'cb', Text::_($item->title)); ?> - - - - - - - - - - published, $i, 'stages.', $canChange); ?> - - default, $i, 'stages.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'stages.', $canCheckin); ?> - - - - escape(Text::_($item->title)); ?> - -
escape(Text::_($item->description)); ?>
- - escape(Text::_($item->title)); ?> -
escape(Text::_($item->description)); ?>
- -
- id; ?> -
- - pagination->getListFooter(); ?> + ?> + + + id, false, 'cid', 'cb', Text::_($item->title)); ?> + + + + + + + + + + + + published, $i, 'stages.', $canChange); ?> + + + default, $i, 'stages.', $canChange); ?> + + + checked_out) : ?> + editor, $item->checked_out_time, 'stages.', $canCheckin); ?> + + + + escape(Text::_($item->title)); ?> + +
escape(Text::_($item->description)); ?>
+ + escape(Text::_($item->title)); ?> +
escape(Text::_($item->description)); ?>
+ + + + id; ?> + + + + + + + pagination->getListFooter(); ?> - - - - - - -
-
-
+ + + + + + + + +
diff --git a/code/administrator/components/com_workflow/tmpl/transition/edit.php b/code/administrator/components/com_workflow/tmpl/transition/edit.php index aaefb2ff..ee1f49c0 100644 --- a/code/administrator/components/com_workflow/tmpl/transition/edit.php +++ b/code/administrator/components/com_workflow/tmpl/transition/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $user = $app->getIdentity(); @@ -34,38 +35,37 @@ ?>
- -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
-
- form->renderField('from_stage_id'); ?> - form->renderField('to_stage_id'); ?> - form->renderField('description'); ?> -
-
- -
-
- + +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> - + +
+
+ form->renderField('from_stage_id'); ?> + form->renderField('to_stage_id'); ?> + form->renderField('description'); ?> +
+
+ +
+
+ - authorise('core.admin', $this->extension)) : ?> + - -
- - form->getInput('rules'); ?> -
- - + authorise('core.admin', $this->extension)) : ?> + +
+ + form->getInput('rules'); ?> +
+ + - -
- form->getInput('workflow_id'); ?> - - + +
+ form->getInput('workflow_id'); ?> + +
diff --git a/code/administrator/components/com_workflow/tmpl/transitions/default.php b/code/administrator/components/com_workflow/tmpl/transitions/default.php index 225da1b5..97dee3f5 100644 --- a/code/administrator/components/com_workflow/tmpl/transitions/default.php +++ b/code/administrator/components/com_workflow/tmpl/transitions/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); -$user = Factory::getUser(); +$user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); @@ -28,136 +31,136 @@ $saveOrder = ($listOrder == 't.ordering'); -if ($saveOrder) -{ - $saveOrderingUrl = 'index.php?option=com_workflow&task=transitions.saveOrderAjax&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->workflow->extension) . '&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder) { + $saveOrderingUrl = 'index.php?option=com_workflow&task=transitions.saveOrderAjax&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->workflow->extension) . '&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
- sidebar)) : ?> -
- sidebar; ?> -
- -
-
- $this)); - ?> - transitions)) : ?> -
- - -
- - - - - - - - - - - - - - - - transitions as $i => $item): - $edit = Route::_('index.php?option=com_workflow&task=transition.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->workflow->extension)); +
+ sidebar)) : ?> +
+ sidebar; ?> +
+ +
+
+ $this)); + ?> + transitions)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - -
+ + + + + + + + + + + + + + transitions as $i => $item) : + $edit = Route::_('index.php?option=com_workflow&task=transition.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->workflow->extension)); - $canEdit = $user->authorise('core.edit', $this->extension . '.transition.' . $item->id); - $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $user->id || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', $this->extension . '.transition.' . $item->id) && $canCheckin; - ?> - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + +
- id, false, 'cid', 'cb', Text::_($item->title)); ?> - - - - - - - - - - published, $i, 'transitions.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'transitions.', $canCheckin); ?> - - - - escape(Text::_($item->title)); ?> - -
escape(Text::_($item->description)); ?>
- - escape(Text::_($item->title)); ?> -
escape(Text::_($item->description)); ?>
- -
- from_stage_id < 0): ?> - - - escape(Text::_($item->from_stage)); ?> - - - escape(Text::_($item->to_stage)); ?> - - id; ?> -
- - pagination->getListFooter(); ?> - - - - - - -
-
-
+ $canEdit = $user->authorise('core.edit', $this->extension . '.transition.' . $item->id); + $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $user->id || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', $this->extension . '.transition.' . $item->id) && $canCheckin; + ?> + + + id, false, 'cid', 'cb', Text::_($item->title)); ?> + + + + + + + + + + + + published, $i, 'transitions.', $canChange); ?> + + + checked_out) : ?> + editor, $item->checked_out_time, 'transitions.', $canCheckin); ?> + + + + escape(Text::_($item->title)); ?> + +
escape(Text::_($item->description)); ?>
+ + escape(Text::_($item->title)); ?> +
escape(Text::_($item->description)); ?>
+ + + + from_stage_id < 0) : ?> + + + escape(Text::_($item->from_stage)); ?> + + + + escape(Text::_($item->to_stage)); ?> + + + id; ?> + + + + + + + pagination->getListFooter(); ?> + + + + + + + + +
diff --git a/code/administrator/components/com_workflow/tmpl/workflow/edit.php b/code/administrator/components/com_workflow/tmpl/workflow/edit.php index 32725ab1..05505604 100644 --- a/code/administrator/components/com_workflow/tmpl/workflow/edit.php +++ b/code/administrator/components/com_workflow/tmpl/workflow/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $user = $app->getIdentity(); @@ -34,57 +35,56 @@
- - - - item->id != 0) : ?> -
-
-
-
- -
-
- -
-
-
-
- + -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + item->id != 0) : ?> +
+
+
+
+ +
+
+ +
+
+
+
+ - -
-
-
- form->renderField('description'); ?> -
-
-
-
- form->renderField('published'); ?> - form->renderField('default'); ?> -
-
-
- +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> - authorise('core.admin', $this->extension)) : ?> + +
+
+
+ form->renderField('description'); ?> +
+
+
+
+ form->renderField('published'); ?> + form->renderField('default'); ?> +
+
+
+ - -
- - form->getInput('rules'); ?> -
- + authorise('core.admin', $this->extension)) : ?> + +
+ + form->getInput('rules'); ?> +
+ - + - -
- form->getInput('extension'); ?> - - + +
+ form->getInput('extension'); ?> + +
diff --git a/code/administrator/components/com_workflow/tmpl/workflows/default.php b/code/administrator/components/com_workflow/tmpl/workflows/default.php index e8ccd81b..5da642bb 100644 --- a/code/administrator/components/com_workflow/tmpl/workflows/default.php +++ b/code/administrator/components/com_workflow/tmpl/workflows/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); -$wa->useScript('multiselect'); +$wa->useScript('table.columns') + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); @@ -28,15 +31,13 @@ $orderingColumn = 'created'; $saveOrderingUrl = ''; -if (strpos($listOrder, 'modified') !== false) -{ - $orderingColumn = 'modified'; +if (strpos($listOrder, 'modified') !== false) { + $orderingColumn = 'modified'; } -if ($saveOrder) -{ - $saveOrderingUrl = 'index.php?option=com_workflow&task=workflows.saveOrderAjax&tmpl=component&extension=' . $this->escape($this->extension) . '&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder) { + $saveOrderingUrl = 'index.php?option=com_workflow&task=workflows.saveOrderAjax&tmpl=component&extension=' . $this->escape($this->extension) . '&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $extension = $this->escape($this->state->get('filter.extension')); @@ -45,144 +46,147 @@ $userId = $user->id; ?>
-
- sidebar)) : ?> -
- sidebar; ?> -
- -
-
- $this, 'options' => array('selectorFieldName' => 'extension'))); - ?> - workflows)) : ?> -
- - -
- - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="false"> - workflows as $i => $item): - $states = Route::_('index.php?option=com_workflow&view=stages&workflow_id=' . $item->id . '&extension=' . $extension); - $transitions = Route::_('index.php?option=com_workflow&view=transitions&workflow_id=' . $item->id . '&extension=' . $extension); - $edit = Route::_('index.php?option=com_workflow&task=workflow.edit&id=' . $item->id . '&extension=' . $extension); +
+ sidebar)) : ?> +
+ sidebar; ?> +
+ +
+
+ $this, 'options' => array('selectorFieldName' => 'extension'))); + ?> + workflows)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + workflows as $i => $item) : + $states = Route::_('index.php?option=com_workflow&view=stages&workflow_id=' . $item->id . '&extension=' . $extension); + $transitions = Route::_('index.php?option=com_workflow&view=transitions&workflow_id=' . $item->id . '&extension=' . $extension); + $edit = Route::_('index.php?option=com_workflow&task=workflow.edit&id=' . $item->id . '&extension=' . $extension); - $canEdit = $user->authorise('core.edit', $extension . '.workflow.' . $item->id); - $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', $extension . '.workflow.' . $item->id) && $item->created_by == $userId; - $canChange = $user->authorise('core.edit.state', $extension . '.workflow.' . $item->id) && $canCheckin; - ?> - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + +
- id, false, 'cid', 'cb', Text::_($item->title)); ?> - - - - - - - - - - published, $i, 'workflows.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'workflows.', $canCheckin); ?> - - - - escape(Text::_($item->title)); ?> - -
description; ?>
- - escape(Text::_($item->title)); ?> -
description; ?>
- -
- default, $i, 'workflows.', $canChange); ?> - - - count_states; ?> - - - - - count_transitions; ?> - - - - id; ?> -
- - pagination->getListFooter(); ?> + $canEdit = $user->authorise('core.edit', $extension . '.workflow.' . $item->id); + $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', $extension . '.workflow.' . $item->id) && $item->created_by == $userId; + $canChange = $user->authorise('core.edit.state', $extension . '.workflow.' . $item->id) && $canCheckin; + ?> + + + id, false, 'cid', 'cb', Text::_($item->title)); ?> + + + + + + + + + + + + published, $i, 'workflows.', $canChange); ?> + + + checked_out) : ?> + editor, $item->checked_out_time, 'workflows.', $canCheckin); ?> + + + + escape(Text::_($item->title)); ?> + +
description; ?>
+ + escape(Text::_($item->title)); ?> +
description; ?>
+ + + + default, $i, 'workflows.', $canChange); ?> + + + + count_states; ?> + + + + + + count_transitions; ?> + + + + + id; ?> + + + + + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + + + +
diff --git a/code/administrator/components/com_workflow/workflow.xml b/code/administrator/components/com_workflow/workflow.xml index 8fc303ec..c8484948 100644 --- a/code/administrator/components/com_workflow/workflow.xml +++ b/code/administrator/components/com_workflow/workflow.xml @@ -2,7 +2,7 @@ com_workflow Joomla! Project - June 2017 + 2017-06 (C) 2018 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/components/com_wrapper/services/provider.php b/code/administrator/components/com_wrapper/services/provider.php index 0b52fecb..cf2b5135 100644 --- a/code/administrator/components/com_wrapper/services/provider.php +++ b/code/administrator/components/com_wrapper/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Wrapper')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Wrapper')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Wrapper')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new WrapperComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Wrapper')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Wrapper')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Wrapper')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new WrapperComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/code/administrator/components/com_wrapper/src/Extension/WrapperComponent.php b/code/administrator/components/com_wrapper/src/Extension/WrapperComponent.php index 5479cbbf..bba3214d 100644 --- a/code/administrator/components/com_wrapper/src/Extension/WrapperComponent.php +++ b/code/administrator/components/com_wrapper/src/Extension/WrapperComponent.php @@ -1,4 +1,5 @@ com_wrapper Joomla! Project - April 2006 + 2006-04 (C) 2007 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt diff --git a/code/administrator/help/en-GB/toc.json b/code/administrator/help/en-GB/toc.json index e44e38a2..9b69c962 100644 --- a/code/administrator/help/en-GB/toc.json +++ b/code/administrator/help/en-GB/toc.json @@ -1 +1 @@ -{"Articles":"ARTICLES","Articles:_Categories":"ARTICLES:_CATEGORIES","Articles:_Edit":"ARTICLES:_EDIT","Articles:_Featured":"ARTICLES:_FEATURED","Articles:_New_or_Edit_Category":"ARTICLES:_NEW_OR_EDIT_CATEGORY","Banners":"BANNERS","Banners:_Categories":"BANNERS:_CATEGORIES","Banners:_Clients":"BANNERS:_CLIENTS","Banners:_Edit":"BANNERS:_EDIT","Banners:_New_or_Edit_Category":"BANNERS:_NEW_OR_EDIT_CATEGORY","Banners:_New_or_Edit_Client":"BANNERS:_NEW_OR_EDIT_CLIENT","Banners:_Tracks":"BANNERS:_TRACKS","Component:_Field_Groups":"COMPONENT:_FIELD_GROUPS","Component:_Fields":"COMPONENT:_FIELDS","Component:_New_or_Edit_Field":"COMPONENT:_NEW_OR_EDIT_FIELD","Component:_New_or_Edit_Field_Group":"COMPONENT:_NEW_OR_EDIT_FIELD_GROUP","Contacts":"CONTACTS","Contacts:_Categories":"CONTACTS:_CATEGORIES","Contacts:_New_or_Edit":"CONTACTS:_NEW_OR_EDIT","Contacts:_New_or_Edit_Category":"CONTACTS:_NEW_OR_EDIT_CATEGORY","Extensions:_Discover":"EXTENSIONS:_DISCOVER","Extensions:_Install":"EXTENSIONS:_INSTALL","Extensions:_Languages":"EXTENSIONS:_LANGUAGES","Extensions:_Manage":"EXTENSIONS:_MANAGE","Extensions:_Update":"EXTENSIONS:_UPDATE","Information:_Database":"INFORMATION:_DATABASE","Information:_Warnings":"INFORMATION:_WARNINGS","Joomla_Update":"JOOMLA_UPDATE","Languages:_Content":"LANGUAGES:_CONTENT","Languages:_Edit_Content_Language":"LANGUAGES:_EDIT_CONTENT_LANGUAGE","Languages:_Edit_Override":"LANGUAGES:_EDIT_OVERRIDE","Languages:_Installed":"LANGUAGES:_INSTALLED","Languages:_Overrides":"LANGUAGES:_OVERRIDES","Mail_Template:_Edit":"MAIL_TEMPLATE:_EDIT","Mail_Templates":"MAIL_TEMPLATES","Maintenance:_Clear_Cache":"MAINTENANCE:_CLEAR_CACHE","Maintenance:_Global_Check-in":"MAINTENANCE:_GLOBAL_CHECK-IN","Mass_Mail_Users":"MASS_MAIL_USERS","Media":"MEDIA","Menu_Item:_New_Item":"MENU_ITEM:_NEW_ITEM","Menus":"MENUS","Menus:_Edit":"MENUS:_EDIT","Menus:_Items":"MENUS:_ITEMS","Modules":"MODULES","Multilingual_Associations":"MULTILINGUAL_ASSOCIATIONS","Multilingual_Associations:_Edit":"MULTILINGUAL_ASSOCIATIONS:_EDIT","News_Feeds":"NEWS_FEEDS","News_Feeds:_Categories":"NEWS_FEEDS:_CATEGORIES","News_Feeds:_New_or_Edit":"NEWS_FEEDS:_NEW_OR_EDIT","News_Feeds:_New_or_Edit_Category":"NEWS_FEEDS:_NEW_OR_EDIT_CATEGORY","Permissions_for_User":"PERMISSIONS_FOR_USER","Plugins":"PLUGINS","Plugins:_Name_of_Plugin":"PLUGINS:_NAME_OF_PLUGIN","Post-installation_Messages_for_Joomla_CMS":"POST-INSTALLATION_MESSAGES_FOR_JOOMLA_CMS","Privacy_Dashboard":"PRIVACY_DASHBOARD","Privacy:_Consents":"PRIVACY:_CONSENTS","Privacy:_Extension_Capabilities":"PRIVACY:_EXTENSION_CAPABILITIES","Privacy:_Information_Requests":"PRIVACY:_INFORMATION_REQUESTS","Privacy:_New_Information_Request":"PRIVACY:_NEW_INFORMATION_REQUEST","Privacy:_Review_Information_Request":"PRIVACY:_REVIEW_INFORMATION_REQUEST","Private_Messages":"PRIVATE_MESSAGES","Private_Messages:_Read":"PRIVATE_MESSAGES:_READ","Private_Messages:_Write":"PRIVATE_MESSAGES:_WRITE","Redirects:_Links":"REDIRECTS:_LINKS","Redirects:_New_or_Edit":"REDIRECTS:_NEW_OR_EDIT","Site_Global_Configuration":"SITE_GLOBAL_CONFIGURATION","Site_System_Information":"SITE_SYSTEM_INFORMATION","Smart_Search:_Content_Maps":"SMART_SEARCH:_CONTENT_MAPS","Smart_Search:_Indexed_Content":"SMART_SEARCH:_INDEXED_CONTENT","Smart_Search:_New_or_Edit_Filter":"SMART_SEARCH:_NEW_OR_EDIT_FILTER","Smart_Search:_Search_Filters":"SMART_SEARCH:_SEARCH_FILTERS","Start_Here":"START_HERE","Tags":"TAGS","Tags:_New_or_Edit":"TAGS:_NEW_OR_EDIT","Templates:_Customise":"TEMPLATES:_CUSTOMISE","Templates:_Customise_Source":"TEMPLATES:_CUSTOMISE_SOURCE","Templates:_Edit_Style":"TEMPLATES:_EDIT_STYLE","Templates:_Styles":"TEMPLATES:_STYLES","Templates:_Templates":"TEMPLATES:_TEMPLATES","User_Actions_Log":"USER_ACTIONS_LOG","User_Notes":"USER_NOTES","User_Notes:_New_or_Edit":"USER_NOTES:_NEW_OR_EDIT","Users":"USERS","Users:_Edit_Profile":"USERS:_EDIT_PROFILE","Users:_Edit_Viewing_Access_Level":"USERS:_EDIT_VIEWING_ACCESS_LEVEL","Users:_Groups":"USERS:_GROUPS","Users:_New_or_Edit_Group":"USERS:_NEW_OR_EDIT_GROUP","Users:_Viewing_Access_Levels":"USERS:_VIEWING_ACCESS_LEVELS"} \ No newline at end of file +{"Articles":"ARTICLES","Articles:_Categories":"ARTICLES:_CATEGORIES","Articles:_Edit":"ARTICLES:_EDIT","Articles:_Featured":"ARTICLES:_FEATURED","Articles:_New_or_Edit_Category":"ARTICLES:_NEW_OR_EDIT_CATEGORY","Banners":"BANNERS","Banners:_Categories":"BANNERS:_CATEGORIES","Banners:_Clients":"BANNERS:_CLIENTS","Banners:_Edit":"BANNERS:_EDIT","Banners:_New_or_Edit_Category":"BANNERS:_NEW_OR_EDIT_CATEGORY","Banners:_New_or_Edit_Client":"BANNERS:_NEW_OR_EDIT_CLIENT","Banners:_Tracks":"BANNERS:_TRACKS","Component:_Field_Groups":"COMPONENT:_FIELD_GROUPS","Component:_Fields":"COMPONENT:_FIELDS","Component:_New_or_Edit_Field":"COMPONENT:_NEW_OR_EDIT_FIELD","Component:_New_or_Edit_Field_Group":"COMPONENT:_NEW_OR_EDIT_FIELD_GROUP","Contacts":"CONTACTS","Contacts:_Categories":"CONTACTS:_CATEGORIES","Contacts:_New_or_Edit":"CONTACTS:_NEW_OR_EDIT","Contacts:_New_or_Edit_Category":"CONTACTS:_NEW_OR_EDIT_CATEGORY","Extensions:_Discover":"EXTENSIONS:_DISCOVER","Extensions:_Install":"EXTENSIONS:_INSTALL","Extensions:_Languages":"EXTENSIONS:_LANGUAGES","Extensions:_Manage":"EXTENSIONS:_MANAGE","Extensions:_Update":"EXTENSIONS:_UPDATE","Information:_Database":"INFORMATION:_DATABASE","Information:_Warnings":"INFORMATION:_WARNINGS","Languages:_Content":"LANGUAGES:_CONTENT","Languages:_Edit_Content_Language":"LANGUAGES:_EDIT_CONTENT_LANGUAGE","Languages:_Edit_Override":"LANGUAGES:_EDIT_OVERRIDE","Languages:_Installed":"LANGUAGES:_INSTALLED","Languages:_Overrides":"LANGUAGES:_OVERRIDES","Mail_Template:_Edit":"MAIL_TEMPLATE:_EDIT","Mail_Templates":"MAIL_TEMPLATES","Maintenance:_Clear_Cache":"MAINTENANCE:_CLEAR_CACHE","Maintenance:_Global_Check-in":"MAINTENANCE:_GLOBAL_CHECK-IN","Mass_Mail_Users":"MASS_MAIL_USERS","Media":"MEDIA","Menu_Item:_New_Item":"MENU_ITEM:_NEW_ITEM","Menus":"MENUS","Menus:_Edit":"MENUS:_EDIT","Menus:_Items":"MENUS:_ITEMS","Modules":"MODULES","Multilingual_Associations":"MULTILINGUAL_ASSOCIATIONS","Multilingual_Associations:_Edit":"MULTILINGUAL_ASSOCIATIONS:_EDIT","News_Feeds":"NEWS_FEEDS","News_Feeds:_Categories":"NEWS_FEEDS:_CATEGORIES","News_Feeds:_New_or_Edit":"NEWS_FEEDS:_NEW_OR_EDIT","News_Feeds:_New_or_Edit_Category":"NEWS_FEEDS:_NEW_OR_EDIT_CATEGORY","Permissions_for_User":"PERMISSIONS_FOR_USER","Plugins":"PLUGINS","Plugins:_Name_of_Plugin":"PLUGINS:_NAME_OF_PLUGIN","Post-installation_Messages_for_Joomla_CMS":"POST-INSTALLATION_MESSAGES_FOR_JOOMLA_CMS","Privacy_Dashboard":"PRIVACY_DASHBOARD","Privacy:_Consents":"PRIVACY:_CONSENTS","Privacy:_Extension_Capabilities":"PRIVACY:_EXTENSION_CAPABILITIES","Privacy:_Information_Requests":"PRIVACY:_INFORMATION_REQUESTS","Privacy:_New_Information_Request":"PRIVACY:_NEW_INFORMATION_REQUEST","Privacy:_Review_Information_Request":"PRIVACY:_REVIEW_INFORMATION_REQUEST","Private_Messages":"PRIVATE_MESSAGES","Private_Messages:_Read":"PRIVATE_MESSAGES:_READ","Private_Messages:_Write":"PRIVATE_MESSAGES:_WRITE","Redirects:_Links":"REDIRECTS:_LINKS","Redirects:_New_or_Edit":"REDIRECTS:_NEW_OR_EDIT","Scheduled_Tasks":"SCHEDULED_TASKS","Scheduled_Tasks:_Edit":"SCHEDULED_TASKS:_EDIT","Site_Global_Configuration":"SITE_GLOBAL_CONFIGURATION","Site_System_Information":"SITE_SYSTEM_INFORMATION","Smart_Search:_Content_Maps":"SMART_SEARCH:_CONTENT_MAPS","Smart_Search:_Indexed_Content":"SMART_SEARCH:_INDEXED_CONTENT","Smart_Search:_New_or_Edit_Filter":"SMART_SEARCH:_NEW_OR_EDIT_FILTER","Smart_Search:_Search_Filters":"SMART_SEARCH:_SEARCH_FILTERS","Start_Here":"START_HERE","Tags":"TAGS","Tags:_New_or_Edit":"TAGS:_NEW_OR_EDIT","Templates:_Customise":"TEMPLATES:_CUSTOMISE","Templates:_Customise_Source":"TEMPLATES:_CUSTOMISE_SOURCE","Templates:_Edit_Style":"TEMPLATES:_EDIT_STYLE","Templates:_Styles":"TEMPLATES:_STYLES","Templates:_Templates":"TEMPLATES:_TEMPLATES","User_Actions_Log":"USER_ACTIONS_LOG","User_Notes":"USER_NOTES","User_Notes:_New_or_Edit":"USER_NOTES:_NEW_OR_EDIT","Users":"USERS","Users:_Edit_Profile":"USERS:_EDIT_PROFILE","Users:_Edit_Viewing_Access_Level":"USERS:_EDIT_VIEWING_ACCESS_LEVEL","Users:_Groups":"USERS:_GROUPS","Users:_New_or_Edit_Group":"USERS:_NEW_OR_EDIT_GROUP","Users:_Viewing_Access_Levels":"USERS:_VIEWING_ACCESS_LEVELS"} \ No newline at end of file diff --git a/code/administrator/includes/app.php b/code/administrator/includes/app.php index ecdaf99e..2f9ed924 100644 --- a/code/administrator/includes/app.php +++ b/code/administrator/includes/app.php @@ -1,4 +1,5 @@ alias('session.web', 'session.web.administrator') - ->alias('session', 'session.web.administrator') - ->alias('JSession', 'session.web.administrator') - ->alias(\Joomla\CMS\Session\Session::class, 'session.web.administrator') - ->alias(\Joomla\Session\Session::class, 'session.web.administrator') - ->alias(\Joomla\Session\SessionInterface::class, 'session.web.administrator'); + ->alias('session', 'session.web.administrator') + ->alias('JSession', 'session.web.administrator') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.administrator') + ->alias(\Joomla\Session\Session::class, 'session.web.administrator') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.administrator'); // Instantiate the application. $app = $container->get(\Joomla\CMS\Application\AdministratorApplication::class); diff --git a/code/administrator/includes/defines.php b/code/administrator/includes/defines.php index e43728b0..3e8a3fe5 100644 --- a/code/administrator/includes/defines.php +++ b/code/administrator/includes/defines.php @@ -1,4 +1,5 @@ isInDevelopmentState()))) -{ - if (file_exists(JPATH_INSTALLATION . '/index.php')) - { - header('Location: ../installation/index.php'); - - exit(); - } - else - { - echo 'No configuration file found and no installation code available. Exiting...'; - - exit; - } +if ( + !file_exists(JPATH_CONFIGURATION . '/configuration.php') + || (filesize(JPATH_CONFIGURATION . '/configuration.php') < 10) + || (file_exists(JPATH_INSTALLATION . '/index.php') && (false === (new Version())->isInDevelopmentState())) +) { + if (file_exists(JPATH_INSTALLATION . '/index.php')) { + header('Location: ../installation/index.php'); + + exit(); + } else { + echo 'No configuration file found and no installation code available. Exiting...'; + + exit; + } } // Pre-Load configuration. Don't remove the Output Buffering due to BOM issues, see JCode 26026 @@ -39,65 +38,59 @@ ob_end_clean(); // System configuration. -$config = new JConfig; +$config = new JConfig(); // Set the error_reporting, and adjust a global Error Handler -switch ($config->error_reporting) -{ - case 'default': - case '-1': - - break; +switch ($config->error_reporting) { + case 'default': + case '-1': + break; - case 'none': - case '0': - error_reporting(0); + case 'none': + case '0': + error_reporting(0); - break; + break; - case 'simple': - error_reporting(E_ERROR | E_WARNING | E_PARSE); - ini_set('display_errors', 1); + case 'simple': + error_reporting(E_ERROR | E_WARNING | E_PARSE); + ini_set('display_errors', 1); - break; + break; - case 'maximum': - case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 - error_reporting(E_ALL); - ini_set('display_errors', 1); + case 'maximum': + case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 + error_reporting(E_ALL); + ini_set('display_errors', 1); - break; + break; - default: - error_reporting($config->error_reporting); - ini_set('display_errors', 1); + default: + error_reporting($config->error_reporting); + ini_set('display_errors', 1); - break; + break; } define('JDEBUG', $config->debug); // Check deprecation logging -if (empty($config->log_deprecated)) -{ - // Reset handler for E_USER_DEPRECATED - set_error_handler(null, E_USER_DEPRECATED); -} -else -{ - // Make sure handler for E_USER_DEPRECATED is registered - set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); +if (empty($config->log_deprecated)) { + // Reset handler for E_USER_DEPRECATED + set_error_handler(null, E_USER_DEPRECATED); +} else { + // Make sure handler for E_USER_DEPRECATED is registered + set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); } -if (JDEBUG || $config->error_reporting === 'maximum') -{ - // Set new Exception handler with debug enabled - $errorHandler->setExceptionHandler( - [ - new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), - 'renderException' - ] - ); +if (JDEBUG || $config->error_reporting === 'maximum') { + // Set new Exception handler with debug enabled + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException' + ] + ); } /** @@ -106,15 +99,12 @@ * We need to do this as high up the stack as we can, as the default in \Joomla\Utilities\IpHelper is to * $allowIpOverride = true which is the wrong default for a generic site NOT behind a trusted proxy/load balancer. */ -if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) -{ - // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR - IpHelper::setAllowIpOverrides(true); -} -else -{ - // We disable the allowing of IP overriding using headers by default. - IpHelper::setAllowIpOverrides(false); +if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) { + // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR + IpHelper::setAllowIpOverrides(true); +} else { + // We disable the allowing of IP overriding using headers by default. + IpHelper::setAllowIpOverrides(false); } unset($config); diff --git a/code/administrator/index.php b/code/administrator/index.php index cfec264f..aba7c599 100644 --- a/code/administrator/index.php +++ b/code/administrator/index.php @@ -1,4 +1,5 @@ If disabled, features dependent on this data will be unavailable." COM_CONFIG_FIELD_SESSION_METADATA_LABEL="Track Session Metadata" +COM_CONFIG_FIELD_SESSION_METADATA_GUEST_DESC="If enabled, additional metadata about both registered and non registered user's session will be logged to the session database table.
If disabled, only data for registered user will be logged." +COM_CONFIG_FIELD_SESSION_METADATA_GUEST_LABEL="Track Guest Session Metadata" COM_CONFIG_FIELD_SESSION_TIME_LABEL="Session Lifetime (minutes)" COM_CONFIG_FIELD_SHARED_SESSION_DESC="When enabled, a user's session is shared between the frontend and administrator sections of the site. Note that changing this value will invalidate all existing sessions on the site. This is not available when the \"Force HTTPS\" option is set to \"Administrator Only\"." COM_CONFIG_FIELD_SHARED_SESSION_LABEL="Shared Sessions" diff --git a/code/administrator/language/en-GB/com_cpanel.ini b/code/administrator/language/en-GB/com_cpanel.ini index ec0d0962..ec907be8 100644 --- a/code/administrator/language/en-GB/com_cpanel.ini +++ b/code/administrator/language/en-GB/com_cpanel.ini @@ -15,7 +15,7 @@ COM_CPANEL_MESSAGES_BODY_NOCLOSE="There are important post-installation messages COM_CPANEL_MESSAGES_BODYMORE_NOCLOSE="This information area won't appear when you have hidden all the messages." COM_CPANEL_MESSAGES_REVIEW="Read Messages" COM_CPANEL_MESSAGES_TITLE="You have post-installation messages" -COM_CPANEL_MSG_ADDNOSNIFF_BODY="

Joomla is now shipped with additional security hardenings in the default htaccess.txt and web.config.txt files. These hardenings disable the so called MIME-type sniffing feature in web browsers. The sniffing leads to specific attack vectors, where scripts in normally harmless file formats (eg images) will be executed, leading to Cross-Site-Scripting vulnerabilities.

The security team recommends to manually apply the necessary changes to existing .htaccess or web.config files, as those files can not be updated automatically.

Changes for .htaccess
Add the following lines before \"## Mod_rewrite in use.\":

<IfModule mod_headers.c>\nHeader always set X-Content-Type-Options \"nosniff\"\n</IfModule>

Changes for web.config
Add the following lines right after \"</rewrite>\":

<httpProtocol>\n  <customHeaders>\n    <add name=\"X-Content-Type-Options\" value=\"nosniff\" />\n  </customHeaders>\n</httpProtocol>
" ; Translators: Don't touch the code part in the message, Starting with ## Mod_rewrite ... +COM_CPANEL_MSG_ADDNOSNIFF_BODY="

Joomla is now shipped with additional security hardenings in the default htaccess.txt and web.config.txt files. These hardenings disable the so called MIME-type sniffing feature in web browsers. The sniffing leads to specific attack vectors, where scripts in normally harmless file formats (eg images) will be executed, leading to Cross-Site-Scripting vulnerabilities.

The security team recommends to manually apply the necessary changes to existing .htaccess or web.config files, as those files can not be updated automatically.

Changes for .htaccess
Add the following lines before \"## Mod_rewrite in use.\":

<IfModule mod_headers.c>\nHeader always set X-Content-Type-Options \"nosniff\"\n</IfModule>

Changes for web.config
Add the following lines right after \"</rewrite>\":

<httpProtocol>\n  <customHeaders>\n    <add name=\"X-Content-Type-Options\" value=\"nosniff\" />\n  </customHeaders>\n</httpProtocol>
" ; Translators: Don't touch the code part in the message, Starting with ## Mod_rewrite … COM_CPANEL_MSG_ADDNOSNIFF_TITLE=".htaccess & web.config Security Update" COM_CPANEL_MSG_HTACCESSSVG_BODY="

Since 3.9.21 Joomla is shipped with an additional security rule in the default htaccess.txt. This rule will protect users of svg files from potential Cross-Site-Scripting (XSS) vulnerabilities.
The security team recommends to manually apply the necessary changes to any existing .htaccess file, as this file can not be updated automatically.

Changes for .htaccess

<FilesMatch \"\.svg$\">\n  <IfModule mod_headers.c>\n    Header always set Content-Security-Policy \"script-src 'none'\"\n  </IfModule>\n</FilesMatch>

Currently we are not aware of a method to conditionally configure this on IIS web servers, please contact your hosting provider for further assistance.

" COM_CPANEL_MSG_HTACCESSSVG_TITLE="Additional XSS protection for the usage of SVG files" diff --git a/code/administrator/language/en-GB/com_finder.ini b/code/administrator/language/en-GB/com_finder.ini index aad338d0..2251edd0 100644 --- a/code/administrator/language/en-GB/com_finder.ini +++ b/code/administrator/language/en-GB/com_finder.ini @@ -24,7 +24,6 @@ COM_FINDER_CONFIG_LANGUAGE_DEFAULT_DESC="Set the language to be used for non-mul COM_FINDER_CONFIG_LANGUAGE_DEFAULT_LABEL="Default Language" COM_FINDER_CONFIG_LANGUAGE_DEFAULT_NONE="None" COM_FINDER_CONFIG_LINKED_IMAGE_LABEL="Linked Image" -COM_FINDER_CONFIG_MEMORY_TABLE_LIMIT_LABEL="Memory Table Limit" COM_FINDER_CONFIG_META_MULTIPLIER_LABEL="Metadata Weight Multiplier" COM_FINDER_CONFIG_MISC_MULTIPLIER_LABEL="Misc. Text Weight Multiplier" COM_FINDER_CONFIG_OPENSEARCH_LABEL="Enable OpenSearch" @@ -55,6 +54,11 @@ COM_FINDER_CONFIG_TITLE_MULTIPLIER_LABEL="Title Text Weight Multiplier" COM_FINDER_CONFIG_TUPLECOUNT_LABEL="Search for Phrases" COM_FINDER_CONFIG_TUPLECOUNT_PHRASE_DISABLED="Disabled (Improved performance)" COM_FINDER_CONFIG_TUPLECOUNT_PHRASE_ENABLED="Enabled (Improved search results)" +COM_FINDER_CONFIG_WORD_MATCH_DESC="Set how search terms are matched in the index. By default a word is matched exactly, but when a language supports compound words, this allows to match the search term to the beginning or in a random place inside of words in the index." +COM_FINDER_CONFIG_WORD_MATCH_LABEL="Word Match" +COM_FINDER_CONFIG_WORD_MATCH_OPTION_BEGIN="Match words beginning with the search term" +COM_FINDER_CONFIG_WORD_MATCH_OPTION_EXACT="Match exactly" +COM_FINDER_CONFIG_WORD_MATCH_OPTION_FUZZY="Match words containing the search term anywhere" COM_FINDER_CONFIGURATION="Smart Search: Options" COM_FINDER_CONTENT_PLUGIN="Smart Search Content Plugin" COM_FINDER_CREATE_FILTER="Create a filter." @@ -65,9 +69,8 @@ COM_FINDER_EMPTYSTATE_CONTENT="No content has been indexed or you have deleted a COM_FINDER_EMPTYSTATE_SEARCHES_CONTENT="There are no phrases used for site searching to view yet." COM_FINDER_FIELD_CREATED_BY_ALIAS_LABEL="Alias" COM_FINDER_FIELD_CREATED_BY_LABEL="Created By" -COM_FINDER_FIELDSET_INDEX_OPTIONS_DESCRIPTION="Indexing options" +COM_FINDER_FIELDSET_INDEX_OPTIONS_DESCRIPTION="These options influence how the content is indexed. After changing settings here, the index needs to be rebuilt." COM_FINDER_FIELDSET_INDEX_OPTIONS_LABEL="Index" -COM_FINDER_FIELDSET_SEARCH_OPTIONS_DESCRIPTION="Smart Search options" COM_FINDER_FIELDSET_SEARCH_OPTIONS_LABEL="Smart Search" COM_FINDER_FILTER_BRANCH_LABEL="Search by %s" COM_FINDER_FILTER_EDIT_TOOLBAR_TITLE="Smart Search: Edit Filter" @@ -136,6 +139,7 @@ COM_FINDER_INDEX_HEADING_LINK_URL_ASC="Raw URL ascending" COM_FINDER_INDEX_HEADING_LINK_URL_DESC="Raw URL descending" COM_FINDER_INDEX_NO_CONTENT="No content matches your search criteria." COM_FINDER_INDEX_NO_DATA="No content has been indexed." +COM_FINDER_INDEX_OPTIMISE_FINISHED="Optimisation finished." COM_FINDER_INDEX_PLUGIN_CONTENT_NOT_ENABLED="The Smart Search Content Plugin is disabled. Changes to content will not update the Smart Search index until the Plugin is enabled." COM_FINDER_INDEX_PLUGIN_CONTENT_NOT_ENABLED_LINK="The %s is disabled. Changes to content will not update the Smart Search index if you do not enable this plugin." COM_FINDER_INDEX_PURGE_FAILED="Failed to delete selected items." @@ -144,6 +148,8 @@ COM_FINDER_INDEX_SEARCH_DESC="Search in title, URL and last updated date." COM_FINDER_INDEX_SEARCH_LABEL="Search Indexed Content" COM_FINDER_INDEX_TABLE_CAPTION="Table of Indexed Content" COM_FINDER_INDEX_TIP="Start the indexer by pressing the button below, or in the toolbar." +COM_FINDER_INDEX_TOOLBAR_MAINTENANCE="Maintenance" +COM_FINDER_INDEX_TOOLBAR_OPTIMISE="Optimise" COM_FINDER_INDEX_TOOLBAR_PURGE="Clear Index" COM_FINDER_INDEX_TOOLBAR_TITLE="Smart Search: Indexed Content" COM_FINDER_INDEX_TYPE_FILTER="Any Type of Content" @@ -199,6 +205,7 @@ COM_FINDER_NO_ERROR_RETURNED="No error was returned. Make sure error reporting i COM_FINDER_NO_FILTERS="No filters have been created yet." COM_FINDER_NO_RESULTS="No results match your search criteria." COM_FINDER_NO_RESULTS_OR_FILTERS="No results match your search criteria or no filters have been created yet." +COM_FINDER_PREFILL_SEARCH_LABEL="Prefilled Search Term" COM_FINDER_QUERY_FILTER_TODAY="Today" COM_FINDER_QUERY_OPERATOR_AND="And" COM_FINDER_QUERY_OPERATOR_NOT="Not" @@ -211,6 +218,7 @@ COM_FINDER_SEARCH_LABEL="Search %s:" COM_FINDER_SEARCH_SEARCH_QUERY_DESC="Search in content map title." COM_FINDER_SEARCH_SEARCH_QUERY_LABEL="Search Content Maps" COM_FINDER_SEARCHES_TABLE_CAPTION="Table of Search Statistics" +COM_FINDER_SELECT_FILTER_LABEL="Filter" COM_FINDER_SELECT_SEARCH_FILTER="Select filter" COM_FINDER_STATISTICS="Statistics" COM_FINDER_STATISTICS_LINK_TYPE_COUNT="Count" diff --git a/code/administrator/language/en-GB/com_installer.ini b/code/administrator/language/en-GB/com_installer.ini index 97f31b23..245f636c 100644 --- a/code/administrator/language/en-GB/com_installer.ini +++ b/code/administrator/language/en-GB/com_installer.ini @@ -102,6 +102,8 @@ COM_INSTALLER_LANGUAGES_AVAILABLE_LANGUAGES="Available Languages" COM_INSTALLER_LANGUAGES_FILTER_SEARCH_DESC="Search in language name and language tag." COM_INSTALLER_LANGUAGES_FILTER_SEARCH_LABEL="Search Languages" COM_INSTALLER_LANGUAGES_TABLE_CAPTION="Table of Available Languages" +COM_INSTALLER_MANAGE_FILTER_PACKAGE_ID_LABEL="Package" +COM_INSTALLER_MANAGE_FILTER_PACKAGE_ID_DESC="Search for a package extension and extensions included with that package extension." COM_INSTALLER_MANAGE_FILTER_SEARCH_DESC="Search in extension name. Prefix with ID: to search for an extension ID." COM_INSTALLER_MANAGE_FILTER_SEARCH_LABEL="Search Extensions" COM_INSTALLER_MANAGE_TABLE_CAPTION="Table of Extensions" @@ -142,6 +144,7 @@ COM_INSTALLER_MSG_DISCOVER_NOEXTENSION="No extensions have been discover COM_INSTALLER_MSG_DISCOVER_NOEXTENSIONSELECTED="No extension selected." COM_INSTALLER_MSG_DISCOVER_PURGEDDISCOVEREDEXTENSIONS="Cleared discovered extensions." COM_INSTALLER_MSG_ERROR_CANT_CONNECT_TO_UPDATESERVER="Can't connect to %s" +COM_INSTALLER_MSG_ERROR_CANT_RETRIEVE_XML="Can't retrieve XML from %s" COM_INSTALLER_MSG_INSTALL_ENTER_A_URL="Please enter a URL" COM_INSTALLER_MSG_INSTALL_INVALID_URL="Invalid URL" COM_INSTALLER_MSG_INSTALL_INVALID_URL_SCHEME="Please enter a valid URL starting with http or https." @@ -280,6 +283,7 @@ COM_INSTALLER_VALUE_CORE_SELECT="- Select Extensions -" COM_INSTALLER_VALUE_CORE_YES="Core Extensions" COM_INSTALLER_VALUE_FOLDER_NONAPPLICABLE="N/A" COM_INSTALLER_VALUE_FOLDER_SELECT="- Select Folder -" +COM_INSTALLER_VALUE_PACKAGE_ID_SELECT="- Select Package -" COM_INSTALLER_VALUE_STATE_SELECT="- Select Status -" COM_INSTALLER_VALUE_SUPPORTED_EXISTS="Download Key valid" COM_INSTALLER_VALUE_SUPPORTED_MISSING="Download Key invalid" diff --git a/code/administrator/language/en-GB/com_joomlaupdate.ini b/code/administrator/language/en-GB/com_joomlaupdate.ini index 25685034..55d6c634 100644 --- a/code/administrator/language/en-GB/com_joomlaupdate.ini +++ b/code/administrator/language/en-GB/com_joomlaupdate.ini @@ -5,6 +5,8 @@ COM_JOOMLAUPDATE_CAPTIVE_HEADLINE="Confirm your credentials" COM_JOOMLAUPDATE_CHECKED_UPDATES="Checked for updates." +COM_JOOMLAUPDATE_CONFIG_BACKUPCHECK_DESC="Shows the checkbox to confirm you have taken a backup and you're ready to update in the final step before the update is actually applied." +COM_JOOMLAUPDATE_CONFIG_BACKUPCHECK_LABEL="Confirm Backup Checkbox" COM_JOOMLAUPDATE_CONFIG_CUSTOMURL_LABEL="Custom URL" COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where Joomla gets its update information from." COM_JOOMLAUPDATE_CONFIG_SOURCES_LABEL="Update Source" @@ -14,6 +16,8 @@ COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_DEFAULT="Default" COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_LABEL="Update Channel" COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="Joomla Next" COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_TESTING="Testing" +COM_JOOMLAUPDATE_CONFIG_VERSIONCHECK_DESC="Shows the checkbox in the pre–update check if any of the extensions installed on your site is potentially incompatible with the version of Joomla you are upgrading to. Note: the checkbox is displayed when upgrading to a new Joomla version family (minor or major version)." +COM_JOOMLAUPDATE_CONFIG_VERSIONCHECK_LABEL="Potentially incompatible extensions checkbox" COM_JOOMLAUPDATE_CONFIGURATION="Joomla Update: Options" COM_JOOMLAUPDATE_CONFIRM="Confirm" COM_JOOMLAUPDATE_EMPTYSTATE_APPEND="Upload and Update" @@ -27,6 +31,8 @@ COM_JOOMLAUPDATE_ERRORMODAL_HEAD_FORBIDDEN="Access forbidden" COM_JOOMLAUPDATE_ERRORMODAL_HEAD_GENERIC="An error occurred" COM_JOOMLAUPDATE_ERRORMODAL_HEAD_SERVERERROR="Server error" COM_JOOMLAUPDATE_ERRORMODAL_BTN_HELP="Get help with this error" +COM_JOOMLAUPDATE_ERRORSTATE_BTN_RETRY="Resume Update" +COM_JOOMLAUPDATE_ERRORSTATE_BTN_RESTART="Restart Update" COM_JOOMLAUPDATE_FAILED_TO_CHECK_UPDATES="Failed to check for updates." COM_JOOMLAUPDATE_MINIMUM_STABILITY_ALPHA="Alpha" COM_JOOMLAUPDATE_MINIMUM_STABILITY_BETA="Beta" @@ -59,7 +65,7 @@ COM_JOOMLAUPDATE_SELF_EMPTYSTATE_TITLE="A new version of the Joomla Update Compo COM_JOOMLAUPDATE_SYSTEM_CHECK="System Check" COM_JOOMLAUPDATE_TOOLBAR_CHECK="Check for Updates" COM_JOOMLAUPDATE_UPDATE_CHECK="Update Check" -COM_JOOMLAUPDATE_UPDATE_CONFIRM_BACKUP="I'm prepared for the update and have made a backup of the files and database." +COM_JOOMLAUPDATE_UPDATE_CONFIRM_BACKUP="I'm aware that a backup before any update is highly recommended." COM_JOOMLAUPDATE_UPDATE_EMPTYSTATE_TITLE="Update your site to \"Joomla! %s\"" COM_JOOMLAUPDATE_UPDATE_EMPTYSTATE_BUTTON_ADD="Start update" COM_JOOMLAUPDATE_UPDATE_LOG_CLEANUP="Cleaning up after installation." @@ -87,7 +93,7 @@ COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION_BREAK="Extensions marked with the update package and install it manually." +COM_JOOMLAUPDATE_VIEW_DEFAULT_PACKAGE_INFO="You can also download the update package to your computer and then use the fields below to upload and install it." COM_JOOMLAUPDATE_VIEW_DEFAULT_PACKAGE_REINSTALL="Reinstall package URL" COM_JOOMLAUPDATE_VIEW_DEFAULT_PHP_VERSION_NOT_SUPPORTED="Your PHP version is not supported" COM_JOOMLAUPDATE_VIEW_DEFAULT_PHP_VERSION_NOT_SUPPORTED_DESC="An update to Joomla %1$s was found, but your currently installed PHP version does not match the minimum requirements for Joomla %1$s." COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN="Potential Upgrade Issue." -COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_CONFIRM_MESSAGE="Are you sure you want to ignore the warnings about potentially incompatible extensions and proceed with the update?" +COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_CONFIRM_MESSAGE="Are you sure you want to acknowledge the warnings about potentially incompatible extensions and proceed with the update?" COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_DESC="This extension includes a plugin that could cause the update to fail.

To perform the Joomla update safely you should either update this extension to a version compatible with your target version of Joomla or disable the relevant plugin(s) and check again.

For more information about the relevant plugins please check the 'Live Update' tab." COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_LIST="The following plugins could cause problems during the update" COM_JOOMLAUPDATE_VIEW_DEFAULT_PREUPDATE_CHECK="Pre-Update Check for Joomla %s" @@ -154,7 +160,7 @@ COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_CUSTOM="You are on the "%s" COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_DEFAULT="You are on the "%s" update channel. Through this channel you'll receive notifications for all updates of the current Joomla release (4.x)" COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_NEXT="You are on the "%s" update channel. Through this channel you'll receive notifications for all updates of the current Joomla release (4.x) and you will also be notified when the future major release (5.x) will be available. Before upgrading to 5.x you'll need to assess its compatibility with your environment." COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_TESTING="You are on the "%s" update channel. This channel is designed for testing new releases and fixes in Joomla.
It is only intended for JBS (Joomla Bug Squad™) members and others within the Joomla community who are testing. Do not use this setting on a production site." -COM_JOOMLAUPDATE_VIEW_DEFAULT_UPLOAD_INTRO="You can use this feature to update Joomla if your server is behind a firewall or otherwise unable to contact the update servers. First download the Joomla Update Package in ZIP format from the official Joomla download page. Then use the fields below to upload and install it." +COM_JOOMLAUPDATE_VIEW_DEFAULT_UPLOAD_INTRO="You can use this feature to update Joomla if your server is behind a firewall or otherwise unable to contact the update servers. First download the Joomla Update Package in ZIP format from the official Joomla download page. Then use the fields below to upload and install it." COM_JOOMLAUPDATE_VIEW_UPDATE_BYTESEXTRACTED="Bytes extracted" COM_JOOMLAUPDATE_VIEW_UPDATE_BYTESREAD="Bytes read" COM_JOOMLAUPDATE_VIEW_UPDATE_CHECKSUM_WRONG="File Checksum Failed" diff --git a/code/administrator/language/en-GB/com_login.ini b/code/administrator/language/en-GB/com_login.ini index f9959904..cc5788e5 100644 --- a/code/administrator/language/en-GB/com_login.ini +++ b/code/administrator/language/en-GB/com_login.ini @@ -6,6 +6,5 @@ COM_LOGIN="Login" COM_LOGIN_JOOMLA_ADMINISTRATION_LOGIN="Joomla! Administration Login" COM_LOGIN_RETURN_TO_SITE_HOME_PAGE="Go to site home page" -COM_LOGIN_TWOFACTOR="For Two-Factor Authentication" COM_LOGIN_VALID="Use a valid username and password to gain access to the Administrator Backend." COM_LOGIN_XML_DESCRIPTION="This component lets users login to the site." diff --git a/code/administrator/language/en-GB/com_mails.ini b/code/administrator/language/en-GB/com_mails.ini index afefa4af..e69f5e96 100644 --- a/code/administrator/language/en-GB/com_mails.ini +++ b/code/administrator/language/en-GB/com_mails.ini @@ -16,7 +16,7 @@ COM_MAILS_FIELD_ATTACHMENTS_LABEL="Attachments" COM_MAILS_FIELD_BASIC_LABEL="Options" COM_MAILS_FIELD_BODY_LABEL="Body" COM_MAILS_FIELD_FILE_LABEL="File" -COM_MAILS_FIELD_FILENAME_LABEL="Filename" +COM_MAILS_FIELD_FILENAME_LABEL="File Name" COM_MAILS_FIELD_HTMLBODY_LABEL="HTML Body" COM_MAILS_FIELD_LANGUAGE_CODE_INVALID="Invalid Language Code" COM_MAILS_FIELD_MAIL_COPY_MAIL_LABEL="Send Copy To Email" diff --git a/code/administrator/language/en-GB/com_media.ini b/code/administrator/language/en-GB/com_media.ini index 1a000702..598ac5a0 100644 --- a/code/administrator/language/en-GB/com_media.ini +++ b/code/administrator/language/en-GB/com_media.ini @@ -9,7 +9,7 @@ COM_MEDIA_ACTION_DOWNLOAD="Download item" COM_MEDIA_ACTION_EDIT="Edit item" COM_MEDIA_ACTION_PREVIEW="Preview item" COM_MEDIA_ACTION_RENAME="Rename item" -COM_MEDIA_ACTION_SHARE="Get a sharable link" +COM_MEDIA_ACTION_SHARE="Get a shareable link" COM_MEDIA_BREADCRUMB_LABEL="Breadcrumb" COM_MEDIA_BROWSER_TABLE_CAPTION="Contents of Directory %s" COM_MEDIA_CONFIGURATION="Media: Options" @@ -33,7 +33,7 @@ COM_MEDIA_ERROR_NO_PROVIDERS="No filesystem providers have been found. Please en COM_MEDIA_ERROR_NOT_AUTHENTICATED="You are not authenticated. Please login." COM_MEDIA_ERROR_NOT_AUTHORIZED="You are not authorised" COM_MEDIA_ERROR_NOT_FOUND="File or Folder not found" -COM_MEDIA_ERROR_WARNFILETOOLARGE="This file is too large to upload." +COM_MEDIA_ERROR_WARNFILETOOLARGE="This file is too large to upload. You can change the limits for your site in the component options." COM_MEDIA_FIELD_CHECK_MIME_DESC="Use MIME Magic or Fileinfo to attempt to verify files. Try disabling this if you get invalid mime type errors." COM_MEDIA_FIELD_CHECK_MIME_LABEL="Check MIME Types" COM_MEDIA_FIELD_IGNORED_EXTENSIONS_DESC="Ignored file extensions for MIME type checking and restricted uploads." diff --git a/code/administrator/language/en-GB/com_postinstall.ini b/code/administrator/language/en-GB/com_postinstall.ini index b9472b3b..872d92be 100644 --- a/code/administrator/language/en-GB/com_postinstall.ini +++ b/code/administrator/language/en-GB/com_postinstall.ini @@ -4,7 +4,9 @@ ; Note : All ini files need to be saved as UTF-8 COM_POSTINSTALL="Post-installation Messages" +COM_POSTINSTALL_BTN_ARCHIVE="Archive" COM_POSTINSTALL_BTN_HIDE="Hide this message" +COM_POSTINSTALL_BTN_REPUBLISH="Read Again" COM_POSTINSTALL_CONFIGURATION="Post-installation Messages: Options" COM_POSTINSTALL_EMPTYSTATE_BUTTON_ADD="Reset Messages" COM_POSTINSTALL_EMPTYSTATE_CONTENT="You have read all the messages." diff --git a/code/administrator/language/en-GB/com_scheduler.ini b/code/administrator/language/en-GB/com_scheduler.ini index 40fa725d..506f0dbb 100644 --- a/code/administrator/language/en-GB/com_scheduler.ini +++ b/code/administrator/language/en-GB/com_scheduler.ini @@ -51,7 +51,7 @@ COM_SCHEDULER_FIELD_LABEL_INTERVAL_DAYS="Interval in Days" COM_SCHEDULER_FIELD_LABEL_INTERVAL_HOURS="Interval in Hours" COM_SCHEDULER_FIELD_LABEL_INTERVAL_MINUTES="Interval in Minutes" COM_SCHEDULER_FIELD_LABEL_INTERVAL_MONTHS="Interval in Months" -COM_SCHEDULER_FIELD_LABEL_LOG_FILE="Log Filename" +COM_SCHEDULER_FIELD_LABEL_LOG_FILE="Log File Name" COM_SCHEDULER_FIELD_LABEL_SHOW_ORPHANED="Show Orphaned" COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_DAYS_M="Days of Month" COM_SCHEDULER_FIELD_OPTION_INTERVAL_MATCH_DAYS_W="Days of Week" diff --git a/code/administrator/language/en-GB/com_templates.ini b/code/administrator/language/en-GB/com_templates.ini index 00979d8b..086c1046 100644 --- a/code/administrator/language/en-GB/com_templates.ini +++ b/code/administrator/language/en-GB/com_templates.ini @@ -7,6 +7,8 @@ COM_TEMPLATES="Templates" COM_TEMPLATES_ARE_YOU_SURE="Are you sure?" COM_TEMPLATES_ASSIGNED_1="Assigned to one menu item." COM_TEMPLATES_ASSIGNED_MORE="Assigned to %d menu items." +COM_TEMPLATES_BUTTON_CHECK="Check Overrides" +COM_TEMPLATES_BUTTON_CHECK_LIST_ENTRY="Mark Checked" COM_TEMPLATES_BUTTON_CLOSE_FILE="Close File" COM_TEMPLATES_BUTTON_COPY_FILE="Copy File" COM_TEMPLATES_BUTTON_COPY_TEMPLATE="Copy Template" @@ -14,7 +16,7 @@ COM_TEMPLATES_BUTTON_CREATE="Create" COM_TEMPLATES_BUTTON_CROP="Crop" COM_TEMPLATES_BUTTON_DELETE="Delete" COM_TEMPLATES_BUTTON_DELETE_FILE="Delete File" -COM_TEMPLATES_BUTTON_DELETE_LIST_ENTRY="Delete List Entry" +COM_TEMPLATES_BUTTON_DELETE_LIST_ENTRY="Remove Record" COM_TEMPLATES_BUTTON_EXTRACT_ARCHIVE="Extract Here" COM_TEMPLATES_BUTTON_FILE="New File" COM_TEMPLATES_BUTTON_FOLDERS="Manage Folders" @@ -23,6 +25,7 @@ COM_TEMPLATES_BUTTON_RENAME="Rename" COM_TEMPLATES_BUTTON_RENAME_FILE="Rename File" COM_TEMPLATES_BUTTON_RESIZE="Resize" COM_TEMPLATES_BUTTON_TEMPLATE_CHILD="Create Child Template" +COM_TEMPLATES_BUTTON_UNCHECK_LIST_ENTRY="Mark Unchecked" COM_TEMPLATES_BUTTON_UPLOAD="Upload" COM_TEMPLATES_CHECK_FILE_OWNERSHIP="Check file ownership" COM_TEMPLATES_CHILD_SUCCESS="Child template created." @@ -79,7 +82,7 @@ COM_TEMPLATES_ERROR_STYLE_REQUIRES_TITLE="The style requires a title." COM_TEMPLATES_ERROR_TEMPLATE_FOLDER_NOT_FOUND="Template folder not found." COM_TEMPLATES_ERROR_UPLOAD_INPUT="No file was found." COM_TEMPLATES_ERROR_WARNFILENAME="Invalid file name. Please correct the name of the file and upload again." -COM_TEMPLATES_ERROR_WARNFILETOOLARGE="Files larger than 2 MB can't be uploaded." +COM_TEMPLATES_ERROR_WARNFILETOOLARGE="This file is too large to upload." COM_TEMPLATES_ERROR_WARNFILETYPE="File format not supported." COM_TEMPLATES_ERROR_WARNIEXSS="Can't be uploaded. Has XSS." COM_TEMPLATES_FIELD_CLIENT_LABEL="Location" @@ -92,7 +95,7 @@ COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_FAIL="Failed to extract the archive file." COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_SUCCESS="Archive file extracted." COM_TEMPLATES_FILE_ARCHIVE_NOT_FOUND="Archive file not found." COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL="Failed to open the archive file." -COM_TEMPLATES_FILE_COMPARE_PANE="Diff between original and overridden file" +COM_TEMPLATES_FILE_COMPARE_PANE="Diff between original and override file" COM_TEMPLATES_FILE_CONTENT_PREVIEW="File Content Preview" COM_TEMPLATES_FILE_COPY_FAIL="Failed to copy the file." COM_TEMPLATES_FILE_COPY_SUCCESS="The current file was copied as %s." @@ -107,7 +110,7 @@ COM_TEMPLATES_FILE_EXISTS="File with the same name already exists." COM_TEMPLATES_FILE_INFO="File Information" COM_TEMPLATES_FILE_NAME="File Name" COM_TEMPLATES_FILE_NEW_NAME_LABEL="Copied File Name" -COM_TEMPLATES_FILE_OVERRIDE_PANE="Overridden file (editable)" +COM_TEMPLATES_FILE_OVERRIDE_PANE="Override file (editable)" COM_TEMPLATES_FILE_PERMISSIONS="The File Permissions are %s" COM_TEMPLATES_FILE_RENAME_ERROR="An error occurred renaming the file." COM_TEMPLATES_FILE_RENAME_SUCCESS="File renamed." @@ -148,8 +151,8 @@ COM_TEMPLATES_IMAGE_WIDTH="Width" COM_TEMPLATES_INVALID_FILE_NAME="Invalid file name. Please choose a file name with a-z, A-Z, 0-9, - and _." COM_TEMPLATES_INVALID_FILE_TYPE="File type not selected." COM_TEMPLATES_INVALID_FOLDER_NAME="Invalid folder name. Please choose a folder name with a-z, A-Z, 0-9, - and _." -COM_TEMPLATES_LAYOUTS_DIFFVIEW_CORE="Original File" -COM_TEMPLATES_LAYOUTS_DIFFVIEW_DIFF="Differences" +COM_TEMPLATES_LAYOUTS_DIFFVIEW_CORE="Show Original File" +COM_TEMPLATES_LAYOUTS_DIFFVIEW_DIFF="Show Differences" COM_TEMPLATES_MANAGE_FOLDERS="Manage Folders" COM_TEMPLATES_MANAGER_ADD_STYLE="Templates: Add Style" COM_TEMPLATES_MANAGER_EDIT_STYLE="Templates: Edit Style" @@ -171,9 +174,12 @@ COM_TEMPLATES_N_CONFLICT="%d Changes found" COM_TEMPLATES_N_CONFLICT_1="Change found" COM_TEMPLATES_N_ITEMS_DELETED="%d template styles deleted." COM_TEMPLATES_N_ITEMS_DELETED_1="Template style deleted." -COM_TEMPLATES_N_OVERRIDE_CHECKED="%d Updated files list entry marked as checked." -COM_TEMPLATES_N_OVERRIDE_DELETED="%d Updated files list entry deleted." -COM_TEMPLATES_N_OVERRIDE_UNCHECKED="%d Updated files list entry marked as unchecked." +COM_TEMPLATES_N_OVERRIDE_CHECKED="%d records checked." +COM_TEMPLATES_N_OVERRIDE_CHECKED_1="Record checked." +COM_TEMPLATES_N_OVERRIDE_DELETED="%d records removed." +COM_TEMPLATES_N_OVERRIDE_DELETED_1="Record removed." +COM_TEMPLATES_N_OVERRIDE_UNCHECKED="%d records unchecked." +COM_TEMPLATES_N_OVERRIDE_UNCHECKED_1="Record unchecked." COM_TEMPLATES_NEW_FILE_HEADER="Create or Upload a new file." COM_TEMPLATES_NEW_FILE_NAME="New File Name" COM_TEMPLATES_NEW_FILE_SELECT="Select a file type" @@ -187,9 +193,10 @@ COM_TEMPLATES_OVERRIDE_CREATED="Override created in " COM_TEMPLATES_OVERRIDE_CREATED_DATE="Added to the list" COM_TEMPLATES_OVERRIDE_FAILED="Failed to create override." COM_TEMPLATES_OVERRIDE_MODIFIED_DATE="Last change via Update" +COM_TEMPLATES_OVERRIDE_NOT_UPTODATE="The originals of the template override files listed below has been updated. This list aids you in reviewing those changes and marking them as checked as you progress." COM_TEMPLATES_OVERRIDE_SOURCE="Update Source" COM_TEMPLATES_OVERRIDE_TEMPLATE_FILE="Template File" -COM_TEMPLATES_OVERRIDE_UPTODATE="Overridden files are up to date. Nothing has been changed in the last extension or Joomla update." +COM_TEMPLATES_OVERRIDE_UPTODATE="Override files are up to date. Nothing has been changed in the last extension or Joomla update." COM_TEMPLATES_OVERRIDES="Override Files" COM_TEMPLATES_OVERRIDES_COMPONENTS="Components" COM_TEMPLATES_OVERRIDES_LAYOUTS="Layouts" diff --git a/code/administrator/language/en-GB/com_users.ini b/code/administrator/language/en-GB/com_users.ini index 2dfa4f44..8e483573 100644 --- a/code/administrator/language/en-GB/com_users.ini +++ b/code/administrator/language/en-GB/com_users.ini @@ -22,6 +22,10 @@ COM_USERS_BATCH_SET="Move To Group" COM_USERS_CATEGORIES_TITLE="User Notes: Categories" COM_USERS_CATEGORY_HEADING="Category" COM_USERS_CONFIGURATION="Users: Options" +COM_USERS_CONFIG_ALLOWED_POSITIONS_BACKEND_DESC="When displaying the backend Multi-factor Authentication page all modules will be hidden except those in the positions selected here. Please note that modules in the title position are always shown: this is required to show the backend page icon and title." +COM_USERS_CONFIG_ALLOWED_POSITIONS_BACKEND_LABEL="Allowed backend module positions" +COM_USERS_CONFIG_ALLOWED_POSITIONS_FRONTEND_DESC="When displaying the frontend Multi-factor Authentication page all modules will be hidden except those in the positions selected here." +COM_USERS_CONFIG_ALLOWED_POSITIONS_FRONTEND_LABEL="Allowed frontend module positions" COM_USERS_CONFIG_DOMAIN_OPTIONS="Email Domain Options" COM_USERS_CONFIG_FIELD_ALLOWREGISTRATION_LABEL="Allow User Registration" COM_USERS_CONFIG_FIELD_CAPTCHA_LABEL="Captcha" @@ -34,12 +38,6 @@ COM_USERS_CONFIG_FIELD_DOMAIN_RULE_DESC="Select whether to allow or disallow the COM_USERS_CONFIG_FIELD_DOMAIN_RULE_LABEL="Rule" COM_USERS_CONFIG_FIELD_DOMAIN_RULE_OPTION_ALLOW="Allow" COM_USERS_CONFIG_FIELD_DOMAIN_RULE_OPTION_DISALLOW="Disallow" -COM_USERS_CONFIG_FIELD_ENFORCE_2FA_FIELD_ADMIN="Admin (Backend)" -COM_USERS_CONFIG_FIELD_ENFORCE_2FA_FIELD_BOTH="Both" -COM_USERS_CONFIG_FIELD_ENFORCE_2FA_FIELD_DESC="You must enable at least one Two Factor Authentication plugin." -COM_USERS_CONFIG_FIELD_ENFORCE_2FA_FIELD_LABEL="Enforce Two Factor Authentication" -COM_USERS_CONFIG_FIELD_ENFORCE_2FA_FIELD_SITE="Site (Frontend)" -COM_USERS_CONFIG_FIELD_ENFORCE_2FA_GROUPS_LABEL="Enforce Two Factor Authentication for Usergroups" COM_USERS_CONFIG_FIELD_FRONTEND_LANG_LABEL="Frontend Language" COM_USERS_CONFIG_FIELD_FRONTEND_RESET_COUNT_LABEL="Maximum Reset Count" COM_USERS_CONFIG_FIELD_FRONTEND_RESET_TIME_LABEL="Reset Time (hours)" @@ -59,10 +57,29 @@ COM_USERS_CONFIG_FIELD_SUBJECT_PREFIX_LABEL="Subject Prefix" COM_USERS_CONFIG_FIELD_USERACTIVATION_LABEL="New User Account Activation" COM_USERS_CONFIG_FIELD_USERACTIVATION_OPTION_ADMINACTIVATION="Administrator" COM_USERS_CONFIG_FIELD_USERACTIVATION_OPTION_SELFACTIVATION="Self" +COM_USERS_CONFIG_FORCEMFAUSERGROUPS_DESC="Any user who belongs in any of the selected user groups will be required to enable Multi-factor Authentication before being able to use the site." +COM_USERS_CONFIG_FORCEMFAUSERGROUPS_LABEL="Enforce Multi-factor Authentication" +COM_USERS_CONFIG_FRONTEND_CAPTIVE_TEMPLATE_DESC="Choose the frontend template style to use in the Multi-factor Authentication page. Select “- Use Default -” to use the default site template style." +COM_USERS_CONFIG_FRONTEND_CAPTIVE_TEMPLATE_LABEL="Frontend template style" +COM_USERS_CONFIG_FRONTEND_SHOW_TITLE_DESC="Should I display a title in the frontend Multi-factor Authentication verification page? Please note that the title is always displayed in the backend. If you need to change the title please override the language key COM_USERS_USER_MULTIFACTOR_AUTH using the System, Manage, Language Overrides page of the site's backend." +COM_USERS_CONFIG_FRONTEND_SHOW_TITLE_LABEL="Show title in frontend" COM_USERS_CONFIG_IMPORT_FAILED="An error was encountered while importing the configuration: %s." COM_USERS_CONFIG_INTEGRATION_SETTINGS_DESC="These settings determine how the Users Component will integrate with other extensions." +COM_USERS_CONFIG_LBL_NOGROUP="( no group )" +COM_USERS_CONFIG_MFAONSILENT_DESC="Should the user have to go through Multi-factor Authentication after a silent user login? Silent logins are those which do not require a username and password e.g. the Remember Me feature, WebAuthn etc." +COM_USERS_CONFIG_MFAONSILENT_LABEL="Multi-factor Authentication after silent login" +COM_USERS_CONFIG_MULTIFACTORAUTH_SETTINGS_DESC="Configure how Multi-factor Authentication works in Joomla." +COM_USERS_CONFIG_MULTIFACTORAUTH_SETTINGS_LABEL="Multi-factor Authentication" +COM_USERS_CONFIG_NEVERMFAUSERGROUPS_DESC="Any user who belongs in any of the selected user groups will be exempt from Multi-factor Authentication. Even if they have set up Multi-factor Authentication methods they will not be asked to use them when they are logging in, nor will they be able to view them, remove them, or change their configuration." +COM_USERS_CONFIG_NEVERMFAUSERGROUPS_LABEL="Disable Multi-factor Authentication" COM_USERS_CONFIG_PASSWORD_OPTIONS="Password Options" +COM_USERS_CONFIG_REDIRECTONLOGIN_DESC="If the user has not yet set up Multi-factor Authentication and this option is enabled they will be redirected to the Multi-factor Authentication setup page or the custom URL you set up below. This is meant to be a simple way to to let your users know that Multi-factor Authentication is an option on your site." +COM_USERS_CONFIG_REDIRECTONLOGIN_LABEL="Onboard new users" +COM_USERS_CONFIG_REDIRECTURL_DESC="If it's not empty redirects to this URL instead of the Multi-factor Authentication setup page when the option above is enabled. WARNING: This must be a URL inside your site. You cannot log in to an external link or to a different subdomain." +COM_USERS_CONFIG_REDIRECTURL_LABEL="Custom redirection URL" COM_USERS_CONFIG_SAVE_FAILED="An error was encountered while saving the configuration: %s." +COM_USERS_CONFIG_SILENTRESPONSES_DESC="For experts. A comma–separated list of Joomla authentication response types which are considered silent logins. The default is cookie (the Remember Me feature) and passwordless (WebAuthn)." +COM_USERS_CONFIG_SILENTRESPONSES_LABEL="Silent login authentication response types (for experts)" COM_USERS_CONFIG_USER_OPTIONS="User Options" COM_USERS_COUNT_DISABLED_USERS="Blocked Users" COM_USERS_COUNT_ENABLED_USERS="Enabled Users" @@ -83,7 +100,8 @@ COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER="A non-Super User can't perform batch ope COM_USERS_ERROR_INVALID_GROUP="Invalid Group" COM_USERS_ERROR_LEVELS_NOLEVELS_SELECTED="No View Permission Level(s) selected." COM_USERS_ERROR_NO_ADDITIONS="The selected user(s) are already assigned to the selected group." -COM_USERS_ERROR_SECRET_CODE_WITHOUT_TFA="You have entered a Secret Code but two factor authentication is not enabled in your user account. If you want to use a secret code to secure your login please edit your user profile and enable two factor authentication." +COM_USERS_ERROR_NOT_IN_GROUP="The selected user(s) are not in the selected group." +COM_USERS_ERROR_ONLY_ONE_GROUP="A user must belong to at least one group." COM_USERS_ERROR_VIEW_LEVEL_IN_USE="You can't delete the view access level '%d:%s' because it is being used by content." COM_USERS_FIELDS_USER_FIELDS_TITLE="Users: Fields" COM_USERS_FIELDS_USER_FIELD_ADD_TITLE="Users: New Field" @@ -116,12 +134,14 @@ COM_USERS_FILTER_ACTIVE="- Select Active State -" COM_USERS_FILTER_NOTES="Show notes list" COM_USERS_FILTER_STATE="- Select State -" COM_USERS_FILTER_USERGROUP="- Select Group -" +COM_USERS_FILTER_MFA="- Multi-factor Authentication -" COM_USERS_GROUPS_CONFIRM_DELETE="Are you sure you wish to delete groups that have users?" COM_USERS_GROUPS_NO_ITEM_SELECTED="No User Groups selected." COM_USERS_GROUPS_N_ITEMS_DELETED="%d User Groups deleted." COM_USERS_GROUPS_N_ITEMS_DELETED_1="User Group deleted." COM_USERS_GROUPS_TABLE_CAPTION="Table of User Groups" COM_USERS_GROUP_FIELD_PARENT_LABEL="Group Parent" +COM_USERS_GROUP_FIELD_PARENT_SELECT="- Select Parent Group -" COM_USERS_GROUP_FIELD_TITLE_LABEL="Group Title" COM_USERS_GROUP_FORM_EDIT="Edit Group" COM_USERS_GROUP_FORM_NEW="New Group" @@ -156,6 +176,7 @@ COM_USERS_HEADING_LEVEL_NAME_DESC="Level Name descending" COM_USERS_HEADING_LFT="LFT" COM_USERS_HEADING_LFT_ASC="LFT ascending" COM_USERS_HEADING_LFT_DESC="LFT descending" +COM_USERS_HEADING_MFA="Multi-factor Authentication" COM_USERS_HEADING_NAME="Name" COM_USERS_HEADING_REGISTRATION_DATE="Registered" COM_USERS_HEADING_REGISTRATION_DATE_ASC="Registration date ascending" @@ -166,13 +187,13 @@ COM_USERS_HEADING_REVIEW_DESC="Review Date descending" COM_USERS_HEADING_SUBJECT="Subject" COM_USERS_HEADING_SUBJECT_ASC="Subject ascending" COM_USERS_HEADING_SUBJECT_DESC="Subject descending" -COM_USERS_HEADING_TFA="Two Factor" COM_USERS_HEADING_USER="User" COM_USERS_HEADING_USERNAME_ASC="Username ascending" COM_USERS_HEADING_USERNAME_DESC="Username descending" COM_USERS_HEADING_USERS_IN_GROUP="Users in group" COM_USERS_HEADING_USER_ASC="User ascending" COM_USERS_HEADING_USER_DESC="User descending" +COM_USERS_LBL_SELECT_INSTRUCTIONS="Please select how you would like to verify your login to this site." COM_USERS_LEVELS_N_ITEMS_DELETED="%d View Permission Levels deleted." COM_USERS_LEVELS_N_ITEMS_DELETED_1="View Permission Level deleted." COM_USERS_LEVELS_TABLE_CAPTION="Table of Viewing Access Levels" @@ -229,9 +250,45 @@ COM_USERS_MASSMAIL_MAIL_BODY="{BODY} {BODYSUFFIX}" COM_USERS_MASSMAIL_MAIL_SUBJECT="{SUBJECTPREFIX} {SUBJECT}" COM_USERS_MASS_MAIL="Mass Mail Users" COM_USERS_MASS_MAIL_DESC="Mass Mail options." +COM_USERS_MFA_ACTIVE="Uses Multi-factor Authentication" +COM_USERS_MFA_ADD_AUTHENTICATOR_OF_TYPE="Add a new %s" +COM_USERS_MFA_ADD_PAGE_HEAD="Add a Multi-factor Authentication Method" +COM_USERS_MFA_BACKUPCODES_PRINT_PROMPT="Backup Codes let you log into the site if your regular Multi-factor Authentication method does not work or you no longer have access to it. Each code can be used only once." +COM_USERS_MFA_BACKUPCODES_PRINT_PROMPT_HEAD="Print these codes and keep them in your wallet." +COM_USERS_MFA_BACKUPCODES_RESET="Regenerate Backup Codes" +COM_USERS_MFA_BACKUPCODES_RESET_INFO="Use the “Regenerate Backup Codes” button on the toolbar to generate a new set of Backup Codes. We recommend that you do this if you think your Backup Codes are compromised, e.g. someone got hold of a printout with them, or if you are running low on available Backup Codes." +COM_USERS_MFA_EDIT_FIELD_DEFAULT="Make this the default Multi-factor Authentication method" +COM_USERS_MFA_EDIT_FIELD_TITLE="Title" +COM_USERS_MFA_EDIT_FIELD_TITLE_DESC="You and the site administrators will see this name in the list of available Multi-factor Authentication methods for your user account. Please do not include any sensitive or personally identifiable information." +COM_USERS_MFA_EDIT_PAGE_HEAD="Modify a Multi-factor Authentication method" +COM_USERS_MFA_FIRSTTIME_INSTRUCTIONS_HEAD="Use Multi-factor Authentication for added security" +COM_USERS_MFA_FIRSTTIME_INSTRUCTIONS_WHATITDOES="Here's how it works. Add a Multi-factor Authentication method below. From now on, every time you log into the site you will be asked to use this method to complete the login. Even if someone steals your username and password they won't have access to your account on this site." +COM_USERS_MFA_FIRSTTIME_NOTINTERESTED="Don't show this again" +COM_USERS_MFA_FIRSTTIME_PAGE_HEAD="Set up your Multi-factor Authentication" +COM_USERS_MFA_INVALID_CODE="Multi-factor Authentication failed. Please try again." +COM_USERS_MFA_INVALID_METHOD="Invalid Multi-factor Authentication method." +COM_USERS_MFA_LBL_CREATEDON="Added: %s" +COM_USERS_MFA_LBL_DATE_FORMAT_PAST="F d, Y" +COM_USERS_MFA_LBL_DATE_FORMAT_TODAY="H:i" +COM_USERS_MFA_LBL_DATE_FORMAT_YESTERDAY="H:i" +COM_USERS_MFA_LBL_LASTUSED="Last used: %s" +COM_USERS_MFA_LBL_PAST="%s" +COM_USERS_MFA_LBL_TODAY="Today, %s" +COM_USERS_MFA_LBL_YESTERDAY="Yesterday, %s" +COM_USERS_MFA_LIST_DEFAULTTAG="Default" +COM_USERS_MFA_LIST_INSTRUCTIONS="Add at least one Multi-factor Authentication method. Every time you log into the site you will be asked to provide it." +COM_USERS_MFA_LIST_PAGE_HEAD="Your Multi-factor Authentication options" +COM_USERS_MFA_LIST_REMOVEALL="Turn Off" +COM_USERS_MFA_LIST_STATUS_OFF="Multi-factor Authentication is not enabled." +COM_USERS_MFA_LIST_STATUS_ON="Multi-factor Authentication is enabled." +COM_USERS_MFA_LOGOUT="Log Out" +COM_USERS_MFA_MANDATORY_NOTICE_BODY="Please enable a Multi-factor Authentication method for your user account. You will not be able to proceed using the site until you do so." +COM_USERS_MFA_MANDATORY_NOTICE_HEAD="Multi-factor Authentication is mandatory for your user account" +COM_USERS_MFA_NOTACTIVE="Does not use Multi-factor Authentication" +COM_USERS_MFA_SELECT_PAGE_HEAD="Select a Multi-factor Authentication method" +COM_USERS_MFA_USE_DIFFERENT_METHOD="Select a different method" +COM_USERS_MFA_VALIDATE="Validate" COM_USERS_NEW_NOTE="New Note" -COM_USERS_NOTE_FORM_EDIT="Edit Note" -COM_USERS_NOTE_FORM_NEW="New Note" COM_USERS_NOTES="User Notes: New/Edit" COM_USERS_NOTES_EMPTYSTATE_BUTTON_ADD="Add your first note" COM_USERS_NOTES_EMPTYSTATE_CONTENT="User Notes can be used to store a range of information about each user on your site." @@ -250,6 +307,8 @@ COM_USERS_NOTES_N_ITEMS_TRASHED_1="User Note trashed." COM_USERS_NOTES_N_ITEMS_UNPUBLISHED="%d User Notes unpublished." COM_USERS_NOTES_N_ITEMS_UNPUBLISHED_1="User Note unpublished." COM_USERS_NOTES_TABLE_CAPTION="Table of User Notes" +COM_USERS_NOTE_FORM_EDIT="Edit Note" +COM_USERS_NOTE_FORM_NEW="New Note" COM_USERS_NOTE_N_SUBJECT="#%d %s" COM_USERS_NO_ACTION="No Action" COM_USERS_NO_LEVELS_SELECTED="No Viewing Access Levels selected." @@ -293,6 +352,9 @@ COM_USERS_OPTION_SELECT_COMPONENT="- Select Component -" COM_USERS_OPTION_SELECT_LEVEL_END="- Select End Level -" COM_USERS_OPTION_SELECT_LEVEL_START="- Select Start Level -" COM_USERS_PASSWORD_RESET_REQUIRED="Password Reset Required" +COM_USERS_POSTINSTALL_MULTIFACTORAUTH_ACTION="Enable the new Multi-factor Authentication plugins" +COM_USERS_POSTINSTALL_MULTIFACTORAUTH_BODY="

Joomla! comes with a drastically improved Multi-factor Authentication experience to help you secure the logins of your users.

Unlike the Two Factor Authentication feature in previous versions of Joomla, users no longer have to enter a Security Code with their username and password. The Multi-factor Authentication happens in a separate step after logging into the site. Until they complete their Multi-factor Authentication validation users cannot navigate to other pages or use the site. This makes Multi-factor Authentication phishing–resistant. It also allows for interactive validation methods like WebAuthn (including integration with Windows Hello, Apple TouchID / FaceID and Android Biometric Screen Lock), or sending 6-digit authentication codes by email. Both of these interactive, convenient methods are now available as plugins shipped with Joomla! itself.

" +COM_USERS_POSTINSTALL_MULTIFACTORAUTH_TITLE="Improved Multi-factor Authentication" COM_USERS_REQUIRE_PASSWORD_RESET="Require Password Reset" COM_USERS_REVIEW_HEADING="Review Date" COM_USERS_SEARCH_ACCESS_LEVELS="Search Viewing Access Levels" @@ -313,8 +375,6 @@ COM_USERS_SUBMENU_LEVELS="Viewing Access Levels" COM_USERS_SUBMENU_NOTES="User Notes" COM_USERS_SUBMENU_NOTE_CATEGORIES="User Note Categories" COM_USERS_SUBMENU_USERS="Users" -COM_USERS_TFA_ACTIVE="Uses Two Factor Authentication" -COM_USERS_TFA_NOTACTIVE="Does not use Two Factor Authentication" COM_USERS_TOOLBAR_ACTIVATE="Activate" COM_USERS_TOOLBAR_BLOCK="Block" COM_USERS_TOOLBAR_MAIL_SEND_MAIL="Send Email" @@ -333,6 +393,10 @@ COM_USERS_USERS_N_ITEMS_DELETED="%d users deleted." COM_USERS_USERS_N_ITEMS_DELETED_1="User deleted." COM_USERS_USERS_TABLE_CAPTION="Table of Users" COM_USERS_USER_ACCOUNT_DETAILS="Account Details" +COM_USERS_USER_BACKUPCODE="Backup Code" +COM_USERS_USER_BACKUPCODES="Backup Codes" +COM_USERS_USER_BACKUPCODES_CAPTIVE_PROMPT="If you do not have access to your usual Multi-factor Authentication method use any of your Backup Codes in the field below. Please remember that this emergency backup code cannot be reused." +COM_USERS_USER_BACKUPCODES_DESC="Lets you access the site if all other Multi-factor Authentication methods you have set up fail." COM_USERS_USER_BATCH_FAILED="An error was encountered while performing the batch operation: %s." COM_USERS_USER_BATCH_SUCCESS="Batch operation completed." COM_USERS_USER_FIELD_BACKEND_LANGUAGE_LABEL="Backend Language" @@ -353,19 +417,15 @@ COM_USERS_USER_FIELD_REQUIRERESET_LABEL="Require Password Reset" COM_USERS_USER_FIELD_RESETCOUNT_LABEL="Password Reset Count" COM_USERS_USER_FIELD_SENDEMAIL_LABEL="Receive System Emails" COM_USERS_USER_FIELD_TIMEZONE_LABEL="Time Zone" -COM_USERS_USER_FIELD_TWOFACTOR_LABEL="Authentication Method" COM_USERS_USER_FIELD_USERNAME_LABEL="Login Name (Username)" COM_USERS_USER_FORM_EDIT="Edit User" COM_USERS_USER_FORM_NEW="New User" COM_USERS_USER_GROUPS_HAVING_ACCESS="User Groups With Viewing Access" COM_USERS_USER_HEADING="User" +COM_USERS_USER_MULTIFACTOR_AUTH="Multi-factor Authentication" COM_USERS_USER_NEW_USER_TITLE="New User Details" -COM_USERS_USER_OTEPS="One time emergency passwords" -COM_USERS_USER_OTEPS_DESC="If you do not have access to your two factor authentication device you can use any of the following passwords instead of a regular security code. Each one of these emergency passwords is immediately destroyed upon use. We recommend printing these passwords out and keeping the printout in a safe and accessible location, eg your wallet or a safety deposit box." -COM_USERS_USER_OTEPS_WAIT_DESC="There are no emergency one time passwords generated in your account. The passwords will be generated automatically and displayed here as soon as you activate two factor authentication." COM_USERS_USER_SAVE_FAILED="An error was encountered while saving the member: %s." COM_USERS_USER_SAVE_SUCCESS="User saved." -COM_USERS_USER_TWO_FACTOR_AUTH="Two Factor Authentication" COM_USERS_VIEW_DEBUG_GROUP_TITLE="Permissions for Group #%d, %s" COM_USERS_VIEW_DEBUG_USER_TITLE="Permissions for User #%d, %s" COM_USERS_VIEW_EDIT_GROUP_TITLE="Users: Edit Group" @@ -385,3 +445,22 @@ JLIB_RULES_SETTING_NOTES_COM_USERS="Changes apply to this component only.
English (en-GB) en-GB - 4.1.5 - June 2022 + 4.2.3 + 2022-09 Joomla! Project admin@joomla.org www.joomla.org diff --git a/code/administrator/language/en-GB/joomla.ini b/code/administrator/language/en-GB/joomla.ini index ed2b52fd..30bb05fb 100644 --- a/code/administrator/language/en-GB/joomla.ini +++ b/code/administrator/language/en-GB/joomla.ini @@ -89,6 +89,7 @@ JHELP="Help" JINLINEHELP="Toggle Inline Help" JHIDE="Hide" JHIDEPASSWORD="Hide Password" +JHOMEDASHBOARD="Home Dashboard" JINVALID_TOKEN="The most recent request was denied because it had an invalid security token. Please refresh the page and try again." JINVALID_TOKEN_NOTICE="The security token did not match. The request was aborted to prevent any security breach. Please try again." JLOGIN="Log in" @@ -206,7 +207,9 @@ JFIELD_ALT_LAYOUT_LABEL="Layout" JFIELD_ALT_MODULE_LAYOUT_DESC="Use a layout from the supplied module or overrides in the templates." JFIELD_ALT_PAGE_TITLE_DESC="An optional alternative page title to set that will change the TITLE tag in the HTML output." JFIELD_ALT_PAGE_TITLE_LABEL="Alternative Page Title" +; Deprecated, will be removed with 5.0 JFIELD_ASSET_ID_DESC="Asset ID" +; Deprecated, will be removed with 5.0 JFIELD_ASSET_ID_LABEL="Asset ID" JFIELD_BASIC_LOGIN_DESCRIPTION_LABEL="Login Description Text" JFIELD_BASIC_LOGIN_DESCRIPTION_SHOW_LABEL="Login Description" @@ -349,7 +352,7 @@ JGLOBAL_AUTH_FAIL="Authentication failed" JGLOBAL_AUTH_FAILED="Failed to authenticate: %s" JGLOBAL_AUTH_INCORRECT="Incorrect username/password" JGLOBAL_AUTH_INVALID_PASS="Username and password do not match or you do not have an account yet." -JGLOBAL_AUTH_INVALID_SECRETKEY="The two factor authentication Secret Key is invalid." +JGLOBAL_AUTH_INVALID_SECRETKEY="The Multi-factor Authentication Secret Key is invalid." JGLOBAL_AUTH_NO_REDIRECT="Could not redirect to server: %s" JGLOBAL_AUTH_NO_USER="Username and password do not match or you do not have an account yet." JGLOBAL_AUTH_NOT_CONNECT="Unable to connect to authentication service." @@ -387,6 +390,7 @@ JGLOBAL_CHOOSE_COMPONENT_DESC="Choose a component from the list." JGLOBAL_CHOOSE_COMPONENT_LABEL="Choose a component" JGLOBAL_CLICK_TO_SORT_THIS_COLUMN="Select to sort by this column" JGLOBAL_CLICK_TO_TOGGLE_STATE="Select icon to toggle state." +JGLOBAL_COLUMNS="Columns" JGLOBAL_CONFIRM_DELETE="Are you sure you want to delete? Confirming will permanently delete the selected item(s)!" JGLOBAL_COPY="(copy)" JGLOBAL_CREATED="Created" @@ -485,7 +489,7 @@ JGLOBAL_ISFREESOFTWARE="%s is free software released under the English (en-GB) - 4.1.5 - June 2022 + 4.2.3 + 2022-09 Joomla! Project admin@joomla.org www.joomla.org diff --git a/code/administrator/language/en-GB/lib_joomla.ini b/code/administrator/language/en-GB/lib_joomla.ini index 3b9134ff..4c6f31ee 100644 --- a/code/administrator/language/en-GB/lib_joomla.ini +++ b/code/administrator/language/en-GB/lib_joomla.ini @@ -347,7 +347,7 @@ JLIB_FORM_VALUE_CACHE_APCU="APC User Cache" JLIB_FORM_VALUE_CACHE_FILE="File" JLIB_FORM_VALUE_CACHE_MEMCACHED="Memcached (Experimental)" JLIB_FORM_VALUE_CACHE_REDIS="Redis" -JLIB_FORM_VALUE_CACHE_WINCACHE="Windows Cache" +JLIB_FORM_VALUE_CACHE_WINCACHE="Windows Cache (deprecated)" JLIB_FORM_VALUE_FROM_TEMPLATE="From Template" JLIB_FORM_VALUE_INHERITED="Inherited" JLIB_FORM_VALUE_SESSION_APCU="APC User Cache" @@ -355,7 +355,7 @@ JLIB_FORM_VALUE_SESSION_DATABASE="Database" JLIB_FORM_VALUE_SESSION_FILESYSTEM="Filesystem" JLIB_FORM_VALUE_SESSION_MEMCACHED="Memcached (Experimental)" JLIB_FORM_VALUE_SESSION_REDIS="Redis" -JLIB_FORM_VALUE_SESSION_WINCACHE="Windows Cache" +JLIB_FORM_VALUE_SESSION_WINCACHE="Windows Cache (deprecated)" JLIB_FORM_VALUE_TIMEZONE_UTC="Universal Time, Coordinated (UTC)" JLIB_HTML_ACCESS_MODIFY_DESC_CAPTION_ACL="ACL" JLIB_HTML_ACCESS_MODIFY_DESC_CAPTION_TABLE="Table" @@ -441,6 +441,8 @@ JLIB_HTML_FEATURED_PENDING_ITEM="Featured, but is Pending." JLIB_HTML_FEATURED_STARTED="Start: %s" JLIB_HTML_GOTO_PAGE="Go to page %s" JLIB_HTML_GOTO_POSITION="Go to %s page" +JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_TRASHED="Published but the category is trashed." +JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_UNPUBLISHED="Published but the category is not published." JLIB_HTML_MOVE_DOWN="Move Down" JLIB_HTML_MOVE_UP="Move Up" JLIB_HTML_NO_PARAMETERS_FOR_THIS_ITEM="There are no parameters for this item." @@ -677,7 +679,7 @@ JLIB_MAIL_INVALID_EMAIL_SENDER="Invalid email sender: %s" JLIB_MEDIA_ERROR_UPLOAD_INPUT="Unable to upload file." JLIB_MEDIA_ERROR_WARNFILENAME="File name must only have alphanumeric characters and no spaces." -JLIB_MEDIA_ERROR_WARNFILETOOLARGE="This file is too large to upload." +JLIB_MEDIA_ERROR_WARNFILETOOLARGE="This file is too large to upload. You can change the limits for your site in the component options." JLIB_MEDIA_ERROR_WARNFILETYPE="This file type is not supported." JLIB_MEDIA_ERROR_WARNIEXSS="Possible IE XSS Attack found." JLIB_MEDIA_ERROR_WARNINVALID_IMG="Not a valid image." @@ -756,3 +758,11 @@ JLIB_SIZE_PB="PiB" JLIB_SIZE_EB="EiB" JLIB_SIZE_ZB="ZiB" JLIB_SIZE_YB="YiB" + +; Database server technology types in human readable terms. Used in the Updater package. +JLIB_DB_SERVER_TYPE_MARIADB="MariaDB" +JLIB_DB_SERVER_TYPE_MSSQL="Microsoft SQL Server" +JLIB_DB_SERVER_TYPE_MYSQL="MySQL" +JLIB_DB_SERVER_TYPE_ORACLE="Oracle" +JLIB_DB_SERVER_TYPE_POSTGRESQL="PostgreSQL" +JLIB_DB_SERVER_TYPE_SQLITE="SQLite" diff --git a/code/administrator/language/en-GB/localise.php b/code/administrator/language/en-GB/localise.php index 221404fe..c45e911b 100644 --- a/code/administrator/language/en-GB/localise.php +++ b/code/administrator/language/en-GB/localise.php @@ -1,12 +1,19 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * en-GB localise class. @@ -15,76 +22,71 @@ */ abstract class En_GBLocalise { - /** - * Returns the potential suffixes for a specific number of items - * - * @param integer $count The number of items. - * - * @return array An array of potential suffixes. - * - * @since 1.6 - */ - public static function getPluralSuffixes($count) - { - if ($count == 0) - { - return array('0'); - } - elseif ($count == 1) - { - return array('ONE', '1'); - } - else - { - return array('OTHER', 'MORE'); - } - } + /** + * Returns the potential suffixes for a specific number of items + * + * @param integer $count The number of items. + * + * @return array An array of potential suffixes. + * + * @since 1.6 + */ + public static function getPluralSuffixes($count) + { + if ($count == 0) { + return array('0'); + } elseif ($count == 1) { + return array('ONE', '1'); + } else { + return array('OTHER', 'MORE'); + } + } - /** - * Returns the ignored search words - * - * @return array An array of ignored search words. - * - * @since 1.6 - */ - public static function getIgnoredSearchWords() - { - return array('and', 'in', 'on'); - } + /** + * Returns the ignored search words + * + * @return array An array of ignored search words. + * + * @since 1.6 + */ + public static function getIgnoredSearchWords() + { + return array('and', 'in', 'on'); + } - /** - * Returns the lower length limit of search words - * - * @return integer The lower length limit of search words. - * - * @since 1.6 - */ - public static function getLowerLimitSearchWord() - { - return 3; - } + /** + * Returns the lower length limit of search words + * + * @return integer The lower length limit of search words. + * + * @since 1.6 + */ + public static function getLowerLimitSearchWord() + { + return 3; + } - /** - * Returns the upper length limit of search words - * - * @return integer The upper length limit of search words. - * - * @since 1.6 - */ - public static function getUpperLimitSearchWord() - { - return 20; - } + /** + * Returns the upper length limit of search words + * + * @return integer The upper length limit of search words. + * + * @since 1.6 + */ + public static function getUpperLimitSearchWord() + { + return 20; + } - /** - * Returns the number of chars to display when searching - * - * @return integer The number of chars to display when searching. - * - * @since 1.6 - */ - public static function getSearchDisplayedCharactersNumber() - { - return 200; - } + /** + * Returns the number of chars to display when searching + * + * @return integer The number of chars to display when searching. + * + * @since 1.6 + */ + public static function getSearchDisplayedCharactersNumber() + { + return 200; + } } diff --git a/code/administrator/language/en-GB/plg_authentication_joomla.ini b/code/administrator/language/en-GB/plg_authentication_joomla.ini index b3206d11..9c04c7f2 100644 --- a/code/administrator/language/en-GB/plg_authentication_joomla.ini +++ b/code/administrator/language/en-GB/plg_authentication_joomla.ini @@ -4,5 +4,4 @@ ; Note : All ini files need to be saved as UTF-8 PLG_AUTHENTICATION_JOOMLA="Authentication - Joomla" -PLG_AUTHENTICATION_JOOMLA_ERR_SECRET_CODE_WITHOUT_TFA="You need to enable two factor authentication in your user profile to use the secret code field." PLG_AUTHENTICATION_JOOMLA_XML_DESCRIPTION="

Handles Joomla's default User authentication.

Warning! You must have at least one authentication plugin enabled or you will lose all access to your site.

" diff --git a/code/administrator/language/en-GB/plg_editors-xtd_readmore.ini b/code/administrator/language/en-GB/plg_editors-xtd_readmore.ini index 853d8318..6c86f2dc 100644 --- a/code/administrator/language/en-GB/plg_editors-xtd_readmore.ini +++ b/code/administrator/language/en-GB/plg_editors-xtd_readmore.ini @@ -4,6 +4,6 @@ ; Note : All ini files need to be saved as UTF-8 PLG_EDITORS-XTD_READMORE="Button - Readmore" -PLG_READMORE_ALREADY_EXISTS="There is already a Read more ... link that has been inserted. Only one link is permitted. Use {pagebreak} to split the page up further." +PLG_READMORE_ALREADY_EXISTS="There is already a Read more … link that has been inserted. Only one link is permitted. Use {pagebreak} to split the page up further." PLG_READMORE_BUTTON_READMORE="Read More" -PLG_READMORE_XML_DESCRIPTION="Enables a button which allows you to insert the Read more ... link into an Article." +PLG_READMORE_XML_DESCRIPTION="Enables a button which allows you to insert the Read more … link into an Article." diff --git a/code/administrator/language/en-GB/plg_editors-xtd_readmore.sys.ini b/code/administrator/language/en-GB/plg_editors-xtd_readmore.sys.ini index 02f85cea..3bc797a9 100644 --- a/code/administrator/language/en-GB/plg_editors-xtd_readmore.sys.ini +++ b/code/administrator/language/en-GB/plg_editors-xtd_readmore.sys.ini @@ -4,4 +4,4 @@ ; Note : All ini files need to be saved as UTF-8 PLG_EDITORS-XTD_READMORE="Button - Readmore" -PLG_READMORE_XML_DESCRIPTION="Enables a button which allows you to insert the Read more ... link into an Article." +PLG_READMORE_XML_DESCRIPTION="Enables a button which allows you to insert the Read more … link into an Article." diff --git a/code/administrator/language/en-GB/plg_installer_override.ini b/code/administrator/language/en-GB/plg_installer_override.ini index 72f3f7ef..848af8b1 100644 --- a/code/administrator/language/en-GB/plg_installer_override.ini +++ b/code/administrator/language/en-GB/plg_installer_override.ini @@ -4,6 +4,7 @@ ; Note : All ini files need to be saved as UTF-8 PLG_INSTALLER_OVERRIDE="Installer - Override" -PLG_INSTALLER_OVERRIDE_N_FILE_UPDATED="%d Overridden files have changed." -PLG_INSTALLER_OVERRIDE_N_FILE_UPDATED_1="Overridden file has changed." +PLG_INSTALLER_OVERRIDE_N_FILE_UPDATED="%1$s files that you have an override for in your template have been updated and you should
review those changes." +PLG_INSTALLER_OVERRIDE_N_FILE_UPDATED_1="A file that you have an override for in your template has been updated and you should review those changes." + PLG_INSTALLER_OVERRIDE_PLUGIN_XML_DESCRIPTION="This plugin enables notifications and handling of overrides after an update in case of changes." diff --git a/code/administrator/language/en-GB/plg_installer_packageinstaller.ini b/code/administrator/language/en-GB/plg_installer_packageinstaller.ini index 0aca99f0..b8c5d50b 100644 --- a/code/administrator/language/en-GB/plg_installer_packageinstaller.ini +++ b/code/administrator/language/en-GB/plg_installer_packageinstaller.ini @@ -5,8 +5,8 @@ PLG_INSTALLER_PACKAGEINSTALLER_DRAG_FILE_HERE="Drag and drop file here to upload." PLG_INSTALLER_PACKAGEINSTALLER_EXTENSION_PACKAGE_FILE="Extension package file" -PLG_INSTALLER_PACKAGEINSTALLER_INSTALLING="Installing ..." -PLG_INSTALLER_PACKAGEINSTALLER_NO_PACKAGE="Please select a package to upload" +PLG_INSTALLER_PACKAGEINSTALLER_INSTALLING="Installing …" +PLG_INSTALLER_PACKAGEINSTALLER_NO_PACKAGE="Please select a package to upload." PLG_INSTALLER_PACKAGEINSTALLER_PLUGIN_XML_DESCRIPTION="This plugin allows you to install extensions from your local computer." PLG_INSTALLER_PACKAGEINSTALLER_SELECT_FILE="Or browse for file" PLG_INSTALLER_PACKAGEINSTALLER_UPLOAD_AND_INSTALL="Upload & Install" @@ -14,4 +14,4 @@ PLG_INSTALLER_PACKAGEINSTALLER_UPLOAD_ERROR_EMPTY="Error: Server returns empty r PLG_INSTALLER_PACKAGEINSTALLER_UPLOAD_ERROR_UNKNOWN="Error: Unknown error or invalid JSON output." PLG_INSTALLER_PACKAGEINSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install Joomla Extension" PLG_INSTALLER_PACKAGEINSTALLER_UPLOAD_PACKAGE_FILE="Upload Package File" -PLG_INSTALLER_PACKAGEINSTALLER_UPLOADING="Uploading ..." +PLG_INSTALLER_PACKAGEINSTALLER_UPLOADING="Uploading …" diff --git a/code/administrator/language/en-GB/plg_installer_webinstaller.ini b/code/administrator/language/en-GB/plg_installer_webinstaller.ini index e3ebf2fa..bf54a557 100644 --- a/code/administrator/language/en-GB/plg_installer_webinstaller.ini +++ b/code/administrator/language/en-GB/plg_installer_webinstaller.ini @@ -8,7 +8,7 @@ PLG_INSTALLER_WEBINSTALLER_CANNOT_INSTALL_EXTENSION_IN_PLUGIN="This extension ca PLG_INSTALLER_WEBINSTALLER_INSTALL_WEB_CONFIRM="Please confirm the installation by selecting the Install button" PLG_INSTALLER_WEBINSTALLER_INSTALL_WEB_CONFIRM_NAME="Extension Name" PLG_INSTALLER_WEBINSTALLER_INSTALL_WEB_CONFIRM_URL="Install from" -PLG_INSTALLER_WEBINSTALLER_INSTALL_WEB_LOADING="Loading ..." +PLG_INSTALLER_WEBINSTALLER_INSTALL_WEB_LOADING="Loading …" PLG_INSTALLER_WEBINSTALLER_INSTALL_WEB_LOADING_ERROR="Can't connect to the Joomla! server. Please try again later." PLG_INSTALLER_WEBINSTALLER_REDIRECT_TO_EXTERNAL_SITE_TO_INSTALL="You will be redirected to the following link to complete the registration/purchase: [SITEURL]" ; The [SITEURL] placeholder should not be translated as it is used in the JavaScript API to insert the correct URL PLG_INSTALLER_WEBINSTALLER_TAB_LABEL="Install from Web" diff --git a/code/administrator/language/en-GB/plg_multifactorauth_email.ini b/code/administrator/language/en-GB/plg_multifactorauth_email.ini new file mode 100644 index 00000000..b764291a --- /dev/null +++ b/code/administrator/language/en-GB/plg_multifactorauth_email.ini @@ -0,0 +1,26 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_MULTIFACTORAUTH_EMAIL="Multi-factor Authentication - Authentication Code by Email" +PLG_MULTIFACTORAUTH_EMAIL_CONFIG_FORCE_ENABLE_DESC="Should I automatically add the Authentication Code by Email as an option for all users? Useful to provide a fallback to users who have lost access to their main authenticator and haven't kept a copy of the backup codes at the expense of some degree of control and security." +PLG_MULTIFACTORAUTH_EMAIL_CONFIG_FORCE_ENABLE_LABEL="Force Enable" +PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_120="Two minutes (recommended)" +PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_180="Three minutes" +PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_300="Five minutes" +PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_30="Half a minute" +PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_60="One minute" +PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_DESC="A new code is generated every this many minutes. Do note that a generated code is valid for at least this much time and at most twice as much time. The higher this period is the more likely it is for the code to be brute forced, therefore the least secure your site is. A period of 2 minutes is a good trade-off between usability and security." +PLG_MULTIFACTORAUTH_EMAIL_CONFIG_TIMESTEP_LABEL="Code Generation Period" +PLG_MULTIFACTORAUTH_EMAIL_EMAIL_BODY="Multi-factor Authentication on {SITENAME}. Your authentication code is {CODE}." +PLG_MULTIFACTORAUTH_EMAIL_EMAIL_SUBJECT="Your {SITENAME} authentication code is -{CODE}-" +PLG_MULTIFACTORAUTH_EMAIL_ERR_INVALID_CODE="Invalid or expired code. Please reload the page to send yourself a new code. Make sure to enter the code within two minutes since you requested the code." +PLG_MULTIFACTORAUTH_EMAIL_LBL_DISPLAYEDAS="Code by Email" +PLG_MULTIFACTORAUTH_EMAIL_LBL_LABEL="Authentication Code" +PLG_MULTIFACTORAUTH_EMAIL_LBL_PRE_MESSAGE="You have received a six digit Multi-factor Authentication code in your email. Please enter it below." +PLG_MULTIFACTORAUTH_EMAIL_LBL_SETUP_PLACEHOLDER="Six Digit Authentication Code" +PLG_MULTIFACTORAUTH_EMAIL_LBL_SHORTINFO="Receive six digit codes by email." +PLG_MULTIFACTORAUTH_EMAIL_MAIL_MAIL_DESC="Sent to users from the Multi-factor Authentication page when using the “Authentication Code by Email” option." +PLG_MULTIFACTORAUTH_EMAIL_MAIL_MAIL_TITLE="Code sent by email" +PLG_MULTIFACTORAUTH_EMAIL_XML_DESCRIPTION="Use time limited, six digit security codes sent to you by email." diff --git a/code/administrator/language/en-GB/plg_multifactorauth_email.sys.ini b/code/administrator/language/en-GB/plg_multifactorauth_email.sys.ini new file mode 100644 index 00000000..a7713525 --- /dev/null +++ b/code/administrator/language/en-GB/plg_multifactorauth_email.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_MULTIFACTORAUTH_EMAIL="Multi-factor Authentication - Authentication Code by Email" +PLG_MULTIFACTORAUTH_EMAIL_XML_DESCRIPTION="Use time limited, six digit security codes sent to you by email." diff --git a/code/administrator/language/en-GB/plg_multifactorauth_fixed.ini b/code/administrator/language/en-GB/plg_multifactorauth_fixed.ini new file mode 100644 index 00000000..3a06f027 --- /dev/null +++ b/code/administrator/language/en-GB/plg_multifactorauth_fixed.ini @@ -0,0 +1,17 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_MULTIFACTORAUTH_FIXED="Multi-factor Authentication - Fixed Code" +PLG_MULTIFACTORAUTH_FIXED_ERR_EMPTYCODE="Your fixed code cannot be empty." +PLG_MULTIFACTORAUTH_FIXED_LBL_DEFAULTTITLE="Fixed Code" +PLG_MULTIFACTORAUTH_FIXED_LBL_DISPLAYEDAS="Fixed Code" +PLG_MULTIFACTORAUTH_FIXED_LBL_LABEL="Fixed Code" +PLG_MULTIFACTORAUTH_FIXED_LBL_PLACEHOLDER="Enter your Fixed Code" +PLG_MULTIFACTORAUTH_FIXED_LBL_POSTMESSAGE="

The messages appearing above and below the code area can be customised by overriding the language strings PLG_MULTIFACTORAUTH_FIXED_LBL_PREMESSAGE and PLG_MULTIFACTORAUTH_FIXED_LBL_POSTMESSAGE.

" +PLG_MULTIFACTORAUTH_FIXED_LBL_PREMESSAGE="

This is a demonstration Multi-factor Authentication plugin for Joomla. You need to enter the fixed code you configured when enabling the Multi-factor Authentication for this user. It effectively works as a second password.

" +PLG_MULTIFACTORAUTH_FIXED_LBL_SETUP_POSTMESSAGE="

The messages appearing above and below the setup area can be customised by overriding the language strings PLG_MULTIFACTORAUTH_FIXED_LBL_SETUP_PREMESSAGE and PLG_MULTIFACTORAUTH_FIXED_LBL_SETUP_POSTMESSAGE

" +PLG_MULTIFACTORAUTH_FIXED_LBL_SETUP_PREMESSAGE="

Enter a Fixed Code below. This Fixed Code will be required to be entered after logging in before you're able to use the site.

" +PLG_MULTIFACTORAUTH_FIXED_LBL_SHORTINFO="Choose your own preset code. For demonstration purposes only." +PLG_MULTIFACTORAUTH_FIXED_XML_DESCRIPTION="A demonstration Multi-factor Authentication plugin using a fixed code (a “second password”). Do not use on live sites, it is not secure. This plugin is only meant to be used as an example for developers interested in creating their own plugins." diff --git a/code/administrator/language/en-GB/plg_multifactorauth_fixed.sys.ini b/code/administrator/language/en-GB/plg_multifactorauth_fixed.sys.ini new file mode 100644 index 00000000..d816b7d6 --- /dev/null +++ b/code/administrator/language/en-GB/plg_multifactorauth_fixed.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_MULTIFACTORAUTH_FIXED="Multi-factor Authentication - Fixed Code" +PLG_MULTIFACTORAUTH_FIXED_XML_DESCRIPTION="A demonstration Multi-factor Authentication plugin using a fixed code (a “second password”). Do not use on live sites, it is not secure. This plugin is only meant to be used as an example for developers interested in creating their own plugins." diff --git a/code/administrator/language/en-GB/plg_multifactorauth_totp.ini b/code/administrator/language/en-GB/plg_multifactorauth_totp.ini new file mode 100644 index 00000000..55550adf --- /dev/null +++ b/code/administrator/language/en-GB/plg_multifactorauth_totp.ini @@ -0,0 +1,21 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_MULTIFACTORAUTH_TOTP="Multi-factor Authentication - Verification Code" +PLG_MULTIFACTORAUTH_TOTP_CAPTIVE_PROMPT="Please open your authenticator application or password manager and copy the six digit code for this site in the text box below, then click on the Validate button. If this code has been automatically filled in for you just click on the Validate button." +PLG_MULTIFACTORAUTH_TOTP_ERR_VALIDATIONFAILED="You did not enter a valid verification code. Please check your authenticator app setup, and make sure that the time and time zone on your device is set correctly." +PLG_MULTIFACTORAUTH_TOTP_LBL_LABEL="Enter the six digit verification code" +PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_INSTRUCTIONS="Set up your verification code (also known as an “authenticator code”) using the information below. You can use an authenticator app (such Google Authenticator, Authy, LastPass Authenticator, etc), your favourite password manager (1Password, BitWarden, Keeper, KeePassXC, Strongbox, etc) or, in some cases, your browser." +PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_PLACEHOLDER="Six Digit Code" +PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_HEADING="Authenticator app setup" +PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_KEY="Enter this key" +PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_LINK="Click this link" +PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_LINK_NOTE="Only works on supported browsers, e.g. Safari." +PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_LINK_TEXT="Set up your verification code" +PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_QR="Scan or right click / long tap this QR code" +PLG_MULTIFACTORAUTH_TOTP_LBL_SETUP_TABLE_SUBHEAD="Use one of the following alternative methods to set up the verification code in your authenticator application, password manager or browser." +PLG_MULTIFACTORAUTH_TOTP_METHOD_TITLE="Verification code" +PLG_MULTIFACTORAUTH_TOTP_SHORTINFO="Use 6-digit codes generated by an app every 30 seconds." +PLG_MULTIFACTORAUTH_TOTP_XML_DESCRIPTION="Multi-factor Authentication for your site's users using six digit verification codes generated by an authenticator app (Google Authenticator, Authy, LastPass Authenticator, etc), a password manager (1Password, BitWarden, Keeper, KeePassXC, Strongbox, etc) or, in some cases, their browser." diff --git a/code/administrator/language/en-GB/plg_multifactorauth_totp.sys.ini b/code/administrator/language/en-GB/plg_multifactorauth_totp.sys.ini new file mode 100644 index 00000000..ae3971cd --- /dev/null +++ b/code/administrator/language/en-GB/plg_multifactorauth_totp.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_MULTIFACTORAUTH_TOTP="Multi-factor Authentication - Verification Code" +PLG_MULTIFACTORAUTH_TOTP_XML_DESCRIPTION="Multi-factor Authentication for your site's users using six digit verification codes generated by an authenticator app (Google Authenticator, Authy, LastPass Authenticator, etc), a password manager (1Password, BitWarden, Keeper, KeePassXC, Strongbox, etc) or, in some cases, their browser." diff --git a/code/administrator/language/en-GB/plg_multifactorauth_webauthn.ini b/code/administrator/language/en-GB/plg_multifactorauth_webauthn.ini new file mode 100644 index 00000000..11adfe91 --- /dev/null +++ b/code/administrator/language/en-GB/plg_multifactorauth_webauthn.ini @@ -0,0 +1,23 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_MULTIFACTORAUTH_WEBAUTHN="Multi-factor Authentication - Web Authentication" +PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST="Invalid authentication request." +PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_PK="The authenticator registration has failed. The authenticator response received from the browser does not match the Public Key issued by the server. This means that someone tried to hack you or something is broken." +PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_INVALID_USER="For security reasons you are not allowed to register authenticators on behalf of another user." +PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_NO_ATTESTED_DATA="Something went wrong but no further information about the error is available at this time. Please retry registering your authenticator." +PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_CREATE_NO_PK="The server has not issued a Public Key for authenticator registration but somehow received an authenticator registration request from the browser. This means that someone tried to hack you or something is broken." +PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NOTAVAILABLE_BODY="Your browser doesn't support the WebAuthn standard. Not all browsers are compatible with WebAuthn on all devices just yet." +PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NOTAVAILABLE_HEAD="Your browser lacks support for WebAuthn" +PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NOTHTTPS_BODY="Please access the site over HTTPS to enable Multi-factor Authentication with WebAuthn." +PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NOTHTTPS_HEAD="WebAuthn is only available on HTTPS" +PLG_MULTIFACTORAUTH_WEBAUTHN_ERR_NO_STORED_CREDENTIAL="You have not configured an Authenticator yet or the Authenticator you are trying to use is ineligible." +PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_CONFIGURED="You have already configured your Authenticator. Please note that you can only modify its title from this page." +PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_DISPLAYEDAS="Web Authentication" +PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_INSTRUCTIONS="Use the “%s” button on this page to start the Web Authentication process. Then please follow the instructions given to you by your browser to complete Web Authentication with your preferred Authenticator." +PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_REGISTERKEY="Register your Authenticator" +PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_SHORTINFO="Use WebAuthn with any hardware or software security key." +PLG_MULTIFACTORAUTH_WEBAUTHN_LBL_VALIDATEKEY="Validate with your Authenticator" +PLG_MULTIFACTORAUTH_WEBAUTHN_XML_DESCRIPTION="Use W3C Web Authentication (Webauthn) as a Multi-factor Authentication method. All modern browsers support it. Most browsers offer device-specific authentication protected by a password and/or biometrics (fingerprint sensor, face scan, …)." diff --git a/code/administrator/language/en-GB/plg_multifactorauth_webauthn.sys.ini b/code/administrator/language/en-GB/plg_multifactorauth_webauthn.sys.ini new file mode 100644 index 00000000..8f6890a3 --- /dev/null +++ b/code/administrator/language/en-GB/plg_multifactorauth_webauthn.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_MULTIFACTORAUTH_WEBAUTHN="Multi-factor Authentication - Web Authentication" +PLG_MULTIFACTORAUTH_WEBAUTHN_XML_DESCRIPTION="Use W3C Web Authentication (Webauthn) as a Multi-factor Authentication method. All modern browsers support it. Most browsers offer device-specific authentication protected by a password and/or biometrics (fingerprint sensor, face scan, …)." diff --git a/code/administrator/language/en-GB/plg_multifactorauth_yubikey.ini b/code/administrator/language/en-GB/plg_multifactorauth_yubikey.ini new file mode 100644 index 00000000..50e62c51 --- /dev/null +++ b/code/administrator/language/en-GB/plg_multifactorauth_yubikey.ini @@ -0,0 +1,16 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_MULTIFACTORAUTH_YUBIKEY="Multi-factor Authentication - YubiKey" +PLG_MULTIFACTORAUTH_YUBIKEY_CAPTIVE_PROMPT="Please click in the text box below. Then insert your YubiKey into the USB port of your device and touch its golden disk or golden pad (depending on your model) to make it produce a YubiKey code. If you are on an NFC-capable phone or tablet with an NFC-enabled YubiKey you need to instead approach your YubiKey to the NFC reader area of your phone or tablet." +PLG_MULTIFACTORAUTH_YUBIKEY_CODE_LABEL="YubiKey code" +PLG_MULTIFACTORAUTH_YUBIKEY_ERR_VALIDATIONFAILED="You did not enter a valid YubiKey secret code or the YubiCloud servers are unreachable at this time." +PLG_MULTIFACTORAUTH_YUBIKEY_LBL_AFTERSETUP_INSTRUCTIONS="You have already set up your YubiKey (the one generating codes starting with %s). You can only change its title from this page." +PLG_MULTIFACTORAUTH_YUBIKEY_LBL_SETUP_INSTRUCTIONS="Please provide a code generated by your YubiKey below and then click or touch the Confirm button. The first twelve characters, which are the unique identification code for your YubiKey, will be saved." +PLG_MULTIFACTORAUTH_YUBIKEY_LBL_SETUP_LABEL="YubiKey Identification" +PLG_MULTIFACTORAUTH_YUBIKEY_LBL_SETUP_PLACEHOLDER="Enter a YubiKey code" +PLG_MULTIFACTORAUTH_YUBIKEY_METHOD_TITLE="YubiKey" +PLG_MULTIFACTORAUTH_YUBIKEY_SHORTINFO="Use YubiKey secure hardware tokens." +PLG_MULTIFACTORAUTH_YUBIKEY_XML_DESCRIPTION="Allows users on your site to use Multi-factor Authentication using a YubiKey secure hardware token. Users need their own YubiKey available from www.yubico.com. To use Multi-factor Authentication users have to edit their user profile and enable Multi-factor Authentication." diff --git a/code/administrator/language/en-GB/plg_multifactorauth_yubikey.sys.ini b/code/administrator/language/en-GB/plg_multifactorauth_yubikey.sys.ini new file mode 100644 index 00000000..7b9835a5 --- /dev/null +++ b/code/administrator/language/en-GB/plg_multifactorauth_yubikey.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2013 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_MULTIFACTORAUTH_YUBIKEY="Multi-factor Authentication - YubiKey" +PLG_MULTIFACTORAUTH_YUBIKEY_XML_DESCRIPTION="Allows users on your site to use Multi-factor Authentication using a YubiKey secure hardware token. Users need their own YubiKey available from www.yubico.com. To use Multi-factor Authentication users have to edit their user profile and enable Multi-factor Authentication." diff --git a/code/administrator/language/en-GB/plg_quickicon_extensionupdate.ini b/code/administrator/language/en-GB/plg_quickicon_extensionupdate.ini index 116f9e99..84398346 100644 --- a/code/administrator/language/en-GB/plg_quickicon_extensionupdate.ini +++ b/code/administrator/language/en-GB/plg_quickicon_extensionupdate.ini @@ -4,8 +4,8 @@ ; Note : All ini files need to be saved as UTF-8 PLG_QUICKICON_EXTENSIONUPDATE="Quick Icon - Joomla! Extensions Updates Notification" -PLG_QUICKICON_EXTENSIONUPDATE_CHECKING="Checking extensions ..." -PLG_QUICKICON_EXTENSIONUPDATE_ERROR="Unknown extensions ..." +PLG_QUICKICON_EXTENSIONUPDATE_CHECKING="Checking extensions …" +PLG_QUICKICON_EXTENSIONUPDATE_ERROR="Unknown extensions …" PLG_QUICKICON_EXTENSIONUPDATE_GROUP_DESC="The group of this plugin (this value is compared with the group value used in Quick Icons modules to inject icons)." PLG_QUICKICON_EXTENSIONUPDATE_GROUP_LABEL="Group" PLG_QUICKICON_EXTENSIONUPDATE_UPDATEFOUND="Updates are available! %s" diff --git a/code/administrator/language/en-GB/plg_quickicon_joomlaupdate.ini b/code/administrator/language/en-GB/plg_quickicon_joomlaupdate.ini index 55e9a5b7..b6de2517 100644 --- a/code/administrator/language/en-GB/plg_quickicon_joomlaupdate.ini +++ b/code/administrator/language/en-GB/plg_quickicon_joomlaupdate.ini @@ -4,8 +4,8 @@ ; Note : All ini files need to be saved as UTF-8 PLG_QUICKICON_JOOMLAUPDATE="Quick Icon - Joomla Update Notification" -PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking Joomla ..." -PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown Joomla ..." +PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking Joomla …" +PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown Joomla …" PLG_QUICKICON_JOOMLAUPDATE_GROUP_DESC="The group of this plugin (this value is compared with the group value used in Quick Icons modules to inject icons)." PLG_QUICKICON_JOOMLAUPDATE_GROUP_LABEL="Group" PLG_QUICKICON_JOOMLAUPDATE_UPDATEFOUND="%s Available - Update now!" diff --git a/code/administrator/language/en-GB/plg_quickicon_overridecheck.ini b/code/administrator/language/en-GB/plg_quickicon_overridecheck.ini index 43a79a9c..a422a94a 100644 --- a/code/administrator/language/en-GB/plg_quickicon_overridecheck.ini +++ b/code/administrator/language/en-GB/plg_quickicon_overridecheck.ini @@ -4,7 +4,7 @@ ; Note : All ini files need to be saved as UTF-8 PLG_QUICKICON_OVERRIDECHECK="Quick Icon - Joomla! Overrides Update Notification" -PLG_QUICKICON_OVERRIDECHECK_CHECKING="Checking overrides ..." +PLG_QUICKICON_OVERRIDECHECK_CHECKING="Checking overrides …" PLG_QUICKICON_OVERRIDECHECK_ERROR="Error on checking overrides." PLG_QUICKICON_OVERRIDECHECK_ERROR_ENABLE="Enable Installer - override plugin." PLG_QUICKICON_OVERRIDECHECK_GROUP_DESC="The group of this plugin (this value is compared with the group value used in Quick Icons modules to inject icons)." diff --git a/code/administrator/language/en-GB/plg_quickicon_privacycheck.ini b/code/administrator/language/en-GB/plg_quickicon_privacycheck.ini index 371d1fdb..f1d6a9b3 100644 --- a/code/administrator/language/en-GB/plg_quickicon_privacycheck.ini +++ b/code/administrator/language/en-GB/plg_quickicon_privacycheck.ini @@ -4,8 +4,8 @@ ; Note : All ini files need to be saved as UTF-8 PLG_QUICKICON_PRIVACYCHECK="Quick Icon - Joomla! Privacy Requests Notification" -PLG_QUICKICON_PRIVACYCHECK_CHECKING="Checking requests ..." -PLG_QUICKICON_PRIVACYCHECK_ERROR="Unknown requests ..." +PLG_QUICKICON_PRIVACYCHECK_CHECKING="Checking requests …" +PLG_QUICKICON_PRIVACYCHECK_ERROR="Unknown requests …" PLG_QUICKICON_PRIVACYCHECK_GROUP_DESC="The group of this plugin (this value is compared with the group value used in Quick Icons modules to inject icons)." PLG_QUICKICON_PRIVACYCHECK_GROUP_LABEL="Group" PLG_QUICKICON_PRIVACYCHECK_NOREQUEST="No urgent privacy requests." diff --git a/code/administrator/language/en-GB/plg_sampledata_blog.ini b/code/administrator/language/en-GB/plg_sampledata_blog.ini index 910ad122..f1456775 100644 --- a/code/administrator/language/en-GB/plg_sampledata_blog.ini +++ b/code/administrator/language/en-GB/plg_sampledata_blog.ini @@ -7,7 +7,7 @@ PLG_SAMPLEDATA_BLOG="Sample Data for Cassiopeia (Blog)" PLG_SAMPLEDATA_BLOG_OVERVIEW_DESC="Sample data which will set up a blog site with articles, tags, custom fields and a workflow.
If the site is multilingual, the data will be tagged to the active backend language." PLG_SAMPLEDATA_BLOG_OVERVIEW_TITLE="Blog Sample Data" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_0_FULLTEXT="" -PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_0_INTROTEXT="

This tells you a bit about this blog and the person who writes it.

When you are logged in you will be able to edit this page by selecting the edit icon.

" +PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_0_INTROTEXT="

This tells you a bit about this blog and the person who writes it.

When you are logged in you will be able to edit this page by selecting the edit icon.

" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_0_TITLE="About" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_1_FULLTEXT="" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_1_INTROTEXT="

Here are some basic tips for working on your site.

  • Joomla! has a 'front end' that you are looking at now and an 'administrator' or 'back end' which is where you do the more advanced work of creating your site such as setting up the menus and deciding what modules to show. You need to login to the administrator separately using the same user name and password that you used to login to this part of the site.
  • One of the first things you will probably want to do is change the site title and tag line and to add a logo. To do this select the Template Settings link in the menu which is visible if you log in. To change your site description, browser title, default email and other items, select Site Settings. More advanced configuration options are available in the administrator.
  • To totally change the look of your site you will probably want to install a new template. Go to System, select Install - Extensions from the list and the extension installer will open. There are many free and commercial templates available for Joomla.
  • As you have already seen, you can control who can see different parts of you site. When you work with modules and articles, setting the Access level to Registered will mean that only logged in users can see them.
  • When you create a new article or other kind of content you also can save it as Published or Unpublished. If it is Unpublished site visitors will not be able to see it but you will.
  • You can learn much more about working with Joomla from the Joomla documentation site and get help from other users at the Joomla forums. In the administrator there are help buttons on every page that provide detailed information about the functions on that page.
" @@ -36,7 +36,7 @@ PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_6_TITLE="Millions" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_7_FULLTEXT="

Uurnip greens yarrow ricebean rutabaga endive cauliflower sea lettuce kohlrabi amaranth water spinach avocado daikon napa cabbage asparagus winter purslane kale. Celery potato scallion desert raisin horseradish spinach carrot soko. Lotus root water spinach fennel kombu maize bamboo shoot green bean swiss chard seakale pumpkin onion chickpea gram corn pea. Brussels sprout coriander water chestnut gourd swiss chard wakame kohlrabi beetroot carrot watercress. Corn amaranth salsify bunya nuts nori azuki bean chickweed potato bell pepper artichoke.

" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_7_INTROTEXT="

We love Joomla to the moon and back!

Thank you to all volunteers who have contributed!

" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_7_TITLE="Love" -PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_8_FULLTEXT="

Cupcake ipsum dolor. Sit amet cotton candy ice cream sesame snaps cake marshmallow powder. Ice cream chocolate cake marshmallow halvah bonbon. Dragée carrot cake danish candy muffin brownie. Candy sugar plum ice cream chupa chups macaroon tiramisu soufflé oat cake. Topping cheesecake lollipop gummi bears icing sweet roll donut liquorice. Pie jelly-o candy donut oat cake cotton candy.

" +PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_8_FULLTEXT="

Cupcake ipsum dolor. Sit amet cotton candy ice cream sesame snaps cake marshmallow powder. Ice cream chocolate cake marshmallow halvah bonbon. Dragée carrot cake danish candy muffin brownie. Candy sugar plum ice cream chupa chups macaroon tiramisu soufflé oat cake. Topping cheesecake lollipop gummi bears icing sweet roll donut liquorice. Pie jelly-o candy donut oat cake cotton candy.

" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_8_INTROTEXT="

We proudly present Joomla Version 4!

Learn more about workflows in Joomla.

" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_8_TITLE="Joomla" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_CONTENT_ARTICLE_9_FIELD_0="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua." @@ -130,6 +130,7 @@ PLG_SAMPLEDATA_BLOG_SAMPLEDATA_MODULES_MODULE_7_TITLE="Search" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_MODULES_MODULE_8_CONTENT="

Welcome to Joomla on Cassiopeia!

You have chosen one of the most powerful CMS Systems in the world.

Cassiopeia is the multi-purpose frontend template for Joomla 4.

Typography »

" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_MODULES_MODULE_8_TITLE="Image" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_MODULES_MODULE_9_TITLE="Popular Tags" +PLG_SAMPLEDATA_BLOG_SAMPLEDATA_NEWSFEEDS_TITLE="My Blog" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_TAG_0_TITLE="Millions" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_TAG_1_TITLE="Worldwide" PLG_SAMPLEDATA_BLOG_SAMPLEDATA_TAG_2_TITLE="Love" diff --git a/code/administrator/language/en-GB/plg_system_accessibility.ini b/code/administrator/language/en-GB/plg_system_accessibility.ini index 99aa7b24..3f96fc6e 100644 --- a/code/administrator/language/en-GB/plg_system_accessibility.ini +++ b/code/administrator/language/en-GB/plg_system_accessibility.ini @@ -8,6 +8,9 @@ PLG_SYSTEM_ACCESSIBILITY_CLOSE="Close" PLG_SYSTEM_ACCESSIBILITY_CURSOR="Big Cursor" PLG_SYSTEM_ACCESSIBILITY_DECREASE_SPACING="Decrease Text Spacing" PLG_SYSTEM_ACCESSIBILITY_DECREASE_TEXT="Decrease Text Size" +PLG_SYSTEM_ACCESSIBILITY_EMOJIS="Icons" +PLG_SYSTEM_ACCESSIBILITY_EMOJIS_FALSE="Use Google Material Font" +PLG_SYSTEM_ACCESSIBILITY_EMOJIS_TRUE="Use Emojis" PLG_SYSTEM_ACCESSIBILITY_GREY="Grey Hues" PLG_SYSTEM_ACCESSIBILITY_INCREASE_SPACING="Increase Text Spacing" PLG_SYSTEM_ACCESSIBILITY_INCREASE_TEXT="Increase Text Size" diff --git a/code/administrator/language/en-GB/plg_system_shortcut.ini b/code/administrator/language/en-GB/plg_system_shortcut.ini new file mode 100644 index 00000000..044751d0 --- /dev/null +++ b/code/administrator/language/en-GB/plg_system_shortcut.ini @@ -0,0 +1,12 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_SYSTEM_SHORTCUT="System - Keyboard Shortcuts" +PLG_SYSTEM_SHORTCUT_OVERVIEW_DESC="Press J to access the shortcut mode followed by the shortcut." +PLG_SYSTEM_SHORTCUT_OVERVIEW_HINT=" J + X Keyboard Shortcuts" +PLG_SYSTEM_SHORTCUT_OVERVIEW_TITLE="Joomla Keyboard Shortcuts" +PLG_SYSTEM_SHORTCUT_TIMEOUT_DESC="Maximum time that a shortcut can be pressed after pressing J." +PLG_SYSTEM_SHORTCUT_TIMEOUT_LABEL="Timeout (in milliseconds)" +PLG_SYSTEM_SHORTCUT_XML_DESCRIPTION="

Enables keyboard shortcuts on the administrator site, which can be provided by other plugins and includes directly the following list of shortcuts:

  • J A Save
  • J S Save & Close
  • J Q Cancel
  • J N New
  • J F Search
  • J O Options
  • J H Help
  • J M Toggle Menu
  • J X Overview
  • J D Home Dashboard
" diff --git a/code/administrator/language/en-GB/plg_system_shortcut.sys.ini b/code/administrator/language/en-GB/plg_system_shortcut.sys.ini new file mode 100644 index 00000000..fcef645b --- /dev/null +++ b/code/administrator/language/en-GB/plg_system_shortcut.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt +; Note : All ini files need to be saved as UTF-8 + +PLG_SYSTEM_SHORTCUT="System - Keyboard Shortcuts" +PLG_SYSTEM_SHORTCUT_XML_DESCRIPTION="

Enables keyboard shortcuts on the administrator site, which can be provided by other plugins and includes directly the following list of shortcuts:

  • J A Save
  • J S Save & Close
  • J Q Cancel
  • J N New
  • J F Search
  • J O Options
  • J H Help
  • J M Toggle Menu
  • J X Overview
  • J D Home Dashboard
" diff --git a/code/administrator/language/en-GB/plg_system_webauthn.ini b/code/administrator/language/en-GB/plg_system_webauthn.ini index af0b5a61..c145fd0f 100644 --- a/code/administrator/language/en-GB/plg_system_webauthn.ini +++ b/code/administrator/language/en-GB/plg_system_webauthn.ini @@ -18,17 +18,22 @@ PLG_SYSTEM_WEBAUTHN_ERR_CREDENTIAL_ID_ALREADY_IN_USE="Cannot save credentials. T PLG_SYSTEM_WEBAUTHN_ERR_EMPTY_USERNAME="You need to enter your username (but NOT your password) before selecting the Web Authentication login button." PLG_SYSTEM_WEBAUTHN_ERR_INVALID_USERNAME="The specified username does not correspond to a user account that has enabled passwordless login on this site." PLG_SYSTEM_WEBAUTHN_ERR_LABEL_NOT_SAVED="Could not save the new label" +PLG_SYSTEM_WEBAUTHN_ERR_NOT_DELETED="Could not remove the authenticator" +PLG_SYSTEM_WEBAUTHN_ERR_NOUSER="No user account has been found" PLG_SYSTEM_WEBAUTHN_ERR_NO_BROWSER_SUPPORT="Sorry, your browser does not support the W3C Web Authentication standard for passwordless logins or your site is not being served over HTTPS with a valid certificate, signed by a Certificate Authority your browser trusts. You will need to log into this site using your username and password." PLG_SYSTEM_WEBAUTHN_ERR_NO_STORED_CREDENTIAL="Cannot find the stored credentials for your login authenticator." -PLG_SYSTEM_WEBAUTHN_ERR_NOT_DELETED="Could not remove the authenticator" PLG_SYSTEM_WEBAUTHN_ERR_USER_REMOVED="The user for this authenticator seems to no longer exist on this site." +PLG_SYSTEM_WEBAUTHN_ERR_XHR_INITCREATE="Cannot get the authenticator registration information from your site." +PLG_SYSTEM_WEBAUTHN_FIELD_ATTESTATION_SUPPORT_DESC="Only allow authenticators with verifiable cryptographic signatures to be used for WebAuthn logins. Strongly recommended for high security environments. Requires the system temporary directory being writeable by PHP, and the OpenSSL extension. May prevent some cheaper, non-certified authenticators from working at all. Disabling it also prevents Joomla from identifying the make and model of the authenticator you are using (no icon will be displayed next to the Authenticator Name)." +PLG_SYSTEM_WEBAUTHN_FIELD_ATTESTATION_SUPPORT_LABEL="Attestation Support" PLG_SYSTEM_WEBAUTHN_FIELD_DESC="Lets you manage passwordless login methods using the W3C Web Authentication standard. You need a supported browser and authenticator (eg Google Chrome or Firefox with a FIDO2 certified security key).

MacOS/iOS/watchOS: Touch/Face ID.
Windows: Hello (Fingerprint / Facial Recognition / PIN).
Android: Biometric screen lock.

You can find more details in the WebAuthn Passwordless Login documentation." PLG_SYSTEM_WEBAUTHN_FIELD_LABEL="W3C Web Authentication (WebAuthn) Login" PLG_SYSTEM_WEBAUTHN_FIELD_N_AUTHENTICATORS_REGISTERED="%d WebAuthn authenticators already set up: %s" PLG_SYSTEM_WEBAUTHN_FIELD_N_AUTHENTICATORS_REGISTERED_0="No WebAuthn authenticator has been set up yet" PLG_SYSTEM_WEBAUTHN_FIELD_N_AUTHENTICATORS_REGISTERED_1="One WebAuthn authenticator already set up: %2$s" PLG_SYSTEM_WEBAUTHN_HEADER="W3C Web Authentication (WebAuthn) Login" -PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR_LABEL="Authenticator added on %s" +PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR="Generic Authenticator" +PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR_LABEL="%s added on %s" PLG_SYSTEM_WEBAUTHN_LOGIN_DESC="Login without a password using the W3C Web Authentication (WebAuthn) standard in compatible browsers. You need to have already set up WebAuthn authentication in your user profile." PLG_SYSTEM_WEBAUTHN_LOGIN_LABEL="Web Authentication" PLG_SYSTEM_WEBAUTHN_MANAGE_BTN_ADD_LABEL="Add New Authenticator" diff --git a/code/administrator/language/en-GB/plg_twofactorauth_totp.ini b/code/administrator/language/en-GB/plg_twofactorauth_totp.ini index 96ccc600..7a29b31e 100644 --- a/code/administrator/language/en-GB/plg_twofactorauth_totp.ini +++ b/code/administrator/language/en-GB/plg_twofactorauth_totp.ini @@ -2,6 +2,7 @@ ; (C) 2013 Open Source Matters, Inc. ; License GNU General Public License version 2 or later; see LICENSE.txt ; Note : All ini files need to be saved as UTF-8 +; Obsolete since 4.2.0 -- The entire file must be removed in Joomla 5.0 PLG_TWOFACTORAUTH_TOTP="Two Factor Authentication - Google Authenticator" PLG_TWOFACTORAUTH_TOTP_ERR_VALIDATIONFAILED="You did not enter a valid security code. Please check your Google Authenticator setup and make sure that the time on your device matches the time on the site." diff --git a/code/administrator/language/en-GB/plg_twofactorauth_totp.sys.ini b/code/administrator/language/en-GB/plg_twofactorauth_totp.sys.ini index 7d978673..1735c300 100644 --- a/code/administrator/language/en-GB/plg_twofactorauth_totp.sys.ini +++ b/code/administrator/language/en-GB/plg_twofactorauth_totp.sys.ini @@ -2,6 +2,7 @@ ; (C) 2013 Open Source Matters, Inc. ; License GNU General Public License version 2 or later; see LICENSE.txt ; Note : All ini files need to be saved as UTF-8 +; Obsolete since 4.2.0 -- The entire file must be removed in Joomla 5.0 PLG_TWOFACTORAUTH_TOTP="Two Factor Authentication - Google Authenticator" PLG_TWOFACTORAUTH_TOTP_XML_DESCRIPTION="Allows users on your site to use two factor authentication using Google Authenticator or other compatible time-based One Time Password generators such as FreeOTP. To use two factor authentication please edit the user profile and enable two factor authentication." diff --git a/code/administrator/language/en-GB/plg_twofactorauth_yubikey.ini b/code/administrator/language/en-GB/plg_twofactorauth_yubikey.ini index 631709ab..cb6ef398 100644 --- a/code/administrator/language/en-GB/plg_twofactorauth_yubikey.ini +++ b/code/administrator/language/en-GB/plg_twofactorauth_yubikey.ini @@ -2,6 +2,7 @@ ; (C) 2013 Open Source Matters, Inc. ; License GNU General Public License version 2 or later; see LICENSE.txt ; Note : All ini files need to be saved as UTF-8 +; Obsolete since 4.2.0 -- The entire file must be removed in Joomla 5.0 PLG_TWOFACTORAUTH_TOTP_RESET_HEAD="Your YubiKey is already linked to your user account." PLG_TWOFACTORAUTH_TOTP_RESET_TEXT="If you want to unlink your YubiKey from your user account or use another YubiKey, please first disable two factor authentication and save your user profile. Then come back to this user profile page and re-activate two factor authentication with the YubiKey method." @@ -16,4 +17,4 @@ PLG_TWOFACTORAUTH_YUBIKEY_SECTION_SITE="Site (Frontend)" PLG_TWOFACTORAUTH_YUBIKEY_SECURITYCODE="Security Code" PLG_TWOFACTORAUTH_YUBIKEY_STEP1_HEAD="Set up" PLG_TWOFACTORAUTH_YUBIKEY_STEP1_TEXT="Please insert your YubiKey device into your computer's USB port. Select the Security Code field below. Then touch the gold disk on your YubiKey device for one second. Afterwards, please save your user profile. If the code generated by your YubiKey is validated by YubiCloud the Two Factor Authentication feature will be enabled and this YubiKey will be linked with your user account." -PLG_TWOFACTORAUTH_YUBIKEY_XML_DESCRIPTION="Allows users on your site to use two factor authentication using a YubiKey secure hardware token. Users need their own YubiKey available from https://www.yubico.com/. To use two factor authentication users have to edit their user profile and enable two factor authentication." +PLG_TWOFACTORAUTH_YUBIKEY_XML_DESCRIPTION="Allows users on your site to use two factor authentication using a YubiKey secure hardware token. Users need their own YubiKey available from www.yubico.com. To use two factor authentication users have to edit their user profile and enable two factor authentication." diff --git a/code/administrator/language/en-GB/plg_twofactorauth_yubikey.sys.ini b/code/administrator/language/en-GB/plg_twofactorauth_yubikey.sys.ini index 73a16d77..15c97bdf 100644 --- a/code/administrator/language/en-GB/plg_twofactorauth_yubikey.sys.ini +++ b/code/administrator/language/en-GB/plg_twofactorauth_yubikey.sys.ini @@ -2,6 +2,7 @@ ; (C) 2013 Open Source Matters, Inc. ; License GNU General Public License version 2 or later; see LICENSE.txt ; Note : All ini files need to be saved as UTF-8 +; Obsolete since 4.2.0 -- The entire file must be removed in Joomla 5.0 PLG_TWOFACTORAUTH_YUBIKEY="Two Factor Authentication - YubiKey" -PLG_TWOFACTORAUTH_YUBIKEY_XML_DESCRIPTION="Allows users on your site to use two factor authentication using a YubiKey secure hardware token. Users need their own YubiKey available from https://www.yubico.com/. To use two factor authentication users have to edit their user profile and enable two factor authentication." +PLG_TWOFACTORAUTH_YUBIKEY_XML_DESCRIPTION="Allows users on your site to use two factor authentication using a YubiKey secure hardware token. Users need their own YubiKey available from www.yubico.com. To use two factor authentication users have to edit their user profile and enable two factor authentication." diff --git a/code/administrator/language/en-GB/plg_user_profile.ini b/code/administrator/language/en-GB/plg_user_profile.ini index a2c2a57c..02860f42 100644 --- a/code/administrator/language/en-GB/plg_user_profile.ini +++ b/code/administrator/language/en-GB/plg_user_profile.ini @@ -12,6 +12,8 @@ PLG_USER_PROFILE_FIELD_ADDRESS1_LABEL="Address 1" PLG_USER_PROFILE_FIELD_ADDRESS2_LABEL="Address 2" PLG_USER_PROFILE_FIELD_CITY_LABEL="City" PLG_USER_PROFILE_FIELD_COUNTRY_LABEL="Country" +PLG_USER_PROFILE_FIELD_DOB_DESCRIPTION="Year-Month-Day, eg 2019-01-27." ; Adapt to the format you entered in the 'DATE_FORMAT_CALENDAR_DATE' +PLG_USER_PROFILE_FIELD_DOB_HINT="YYYY-MM-DD" ; Adapt to the format you entered in the 'DATE_FORMAT_CALENDAR_DATE' PLG_USER_PROFILE_FIELD_DOB_LABEL="Date of Birth" PLG_USER_PROFILE_FIELD_FAVORITE_BOOK_LABEL="Favourite Book" PLG_USER_PROFILE_FIELD_NAME_PROFILE_REQUIRE_USER="User profile fields for profile edit form" @@ -20,12 +22,11 @@ PLG_USER_PROFILE_FIELD_PHONE_LABEL="Phone" PLG_USER_PROFILE_FIELD_POSTAL_CODE_LABEL="Postal/ZIP Code" PLG_USER_PROFILE_FIELD_REGION_LABEL="Region" PLG_USER_PROFILE_FIELD_TOS_ARTICLE_LABEL="Select TOS Article" -PLG_USER_PROFILE_FIELD_TOS_DESC_SITE="Please read the Terms of Service. You will not be able to register if you do not agree with them." +PLG_USER_PROFILE_FIELD_TOS_DESC_SITE="Please accept the Terms of Service. You will not be able to register if you do not agree with them." PLG_USER_PROFILE_FIELD_TOS_LABEL="Terms of Service" PLG_USER_PROFILE_FIELD_WEB_SITE_LABEL="Website" PLG_USER_PROFILE_FILL_FIELD_DESC_SITE="If required, please fill this field." PLG_USER_PROFILE_OPTION_AGREE="Agree" PLG_USER_PROFILE_OPTION_DO_NOT_AGREE="I do not agree" PLG_USER_PROFILE_SLIDER_LABEL="User Profile" -PLG_USER_PROFILE_SPACER_DOB="The date of birth entered should use the format Year-Month-Day, eg 2019-01-27." ; Adapt to the format you entered in the 'DATE_FORMAT_CALENDAR_DATE' PLG_USER_PROFILE_XML_DESCRIPTION="User Profile Plugin" diff --git a/code/administrator/language/en-GB/plg_workflow_notification.ini b/code/administrator/language/en-GB/plg_workflow_notification.ini index 599caccd..f6ee4e71 100644 --- a/code/administrator/language/en-GB/plg_workflow_notification.ini +++ b/code/administrator/language/en-GB/plg_workflow_notification.ini @@ -11,6 +11,7 @@ PLG_WORKFLOW_NOTIFICATION_ADDTEXT_DESC="This text will be sent: Title [title], c PLG_WORKFLOW_NOTIFICATION_ADDTEXT_LABEL="Additional Message Text" PLG_WORKFLOW_NOTIFICATION_ON_TRANSITION_MSG="Title: %1$s. Transition \"%2$s\" performed by %3$s. New state: %4$s." PLG_WORKFLOW_NOTIFICATION_ON_TRANSITION_SUBJECT="The status of \"%s\" has been changed" +PLG_WORKFLOW_NOTIFICATION_NO_RECEIVER="No notifications sent as there are no users to send this message to." PLG_WORKFLOW_NOTIFICATION_NO_TITLE="Unknown title" PLG_WORKFLOW_NOTIFICATION_RECEIVERS_LABEL="Users" PLG_WORKFLOW_NOTIFICATION_SENDMAIL_LABEL="Send Notification" diff --git a/code/administrator/manifests/files/joomla.xml b/code/administrator/manifests/files/joomla.xml index 5817c5eb..817c8c94 100644 --- a/code/administrator/manifests/files/joomla.xml +++ b/code/administrator/manifests/files/joomla.xml @@ -6,8 +6,8 @@ www.joomla.org (C) 2019 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt - 4.1.5 - June 2022 + 4.2.3 + 2022-09 FILES_JOOMLA_XML_DESCRIPTION administrator/components/com_admin/script.php diff --git a/code/administrator/manifests/libraries/joomla.xml b/code/administrator/manifests/libraries/joomla.xml index 6fa2967f..df8f8030 100644 --- a/code/administrator/manifests/libraries/joomla.xml +++ b/code/administrator/manifests/libraries/joomla.xml @@ -4,7 +4,7 @@ joomla 13.1 LIB_JOOMLA_XML_DESCRIPTION - 2008 + 2008-01 (C) 2008 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt Joomla! Project diff --git a/code/administrator/manifests/libraries/phpass.xml b/code/administrator/manifests/libraries/phpass.xml index f865d11d..0c2950fa 100644 --- a/code/administrator/manifests/libraries/phpass.xml +++ b/code/administrator/manifests/libraries/phpass.xml @@ -3,7 +3,7 @@ lib_phpass phpass LIB_PHPASS_XML_DESCRIPTION - 2004-2006 + 2004-01 Solar Designer solar@openwall.com https://www.openwall.com/phpass/ diff --git a/code/administrator/manifests/packages/pkg_en-GB.xml b/code/administrator/manifests/packages/pkg_en-GB.xml index 2ea0cab7..5f685c8f 100644 --- a/code/administrator/manifests/packages/pkg_en-GB.xml +++ b/code/administrator/manifests/packages/pkg_en-GB.xml @@ -2,8 +2,8 @@ English (en-GB) Language Pack en-GB - 4.1.5.1 - June 2022 + 4.2.3.1 + 2022-09 Joomla! Project admin@joomla.org www.joomla.org diff --git a/code/administrator/modules/mod_custom/mod_custom.php b/code/administrator/modules/mod_custom/mod_custom.php index 190a310d..05476b1e 100644 --- a/code/administrator/modules/mod_custom/mod_custom.php +++ b/code/administrator/modules/mod_custom/mod_custom.php @@ -1,4 +1,5 @@ def('prepare_content', 1)) -{ - PluginHelper::importPlugin('content'); - $module->content = HTMLHelper::_('content.prepare', $module->content, '', 'mod_custom.content'); +if ($params->def('prepare_content', 1)) { + PluginHelper::importPlugin('content'); + $module->content = HTMLHelper::_('content.prepare', $module->content, '', 'mod_custom.content'); } // Replace 'images/' to '../images/' when using an image from /images in backend. diff --git a/code/administrator/modules/mod_custom/mod_custom.xml b/code/administrator/modules/mod_custom/mod_custom.xml index 1eb2bd8c..a88ba271 100644 --- a/code/administrator/modules/mod_custom/mod_custom.xml +++ b/code/administrator/modules/mod_custom/mod_custom.xml @@ -2,7 +2,7 @@ mod_custom Joomla! Project - July 2004 + 2004-07 (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_custom/tmpl/default.php b/code/administrator/modules/mod_custom/tmpl/default.php index dea6f852..c9262884 100644 --- a/code/administrator/modules/mod_custom/tmpl/default.php +++ b/code/administrator/modules/mod_custom/tmpl/default.php @@ -1,4 +1,5 @@
- content; ?> + content; ?>
diff --git a/code/administrator/modules/mod_feed/mod_feed.php b/code/administrator/modules/mod_feed/mod_feed.php index 7c5175ed..3a3c9d76 100644 --- a/code/administrator/modules/mod_feed/mod_feed.php +++ b/code/administrator/modules/mod_feed/mod_feed.php @@ -1,4 +1,5 @@ mod_feed Joomla! Project - July 2005 + 2005-07 (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_feed/src/Helper/FeedHelper.php b/code/administrator/modules/mod_feed/src/Helper/FeedHelper.php index 986edbe3..d7b1766d 100644 --- a/code/administrator/modules/mod_feed/src/Helper/FeedHelper.php +++ b/code/administrator/modules/mod_feed/src/Helper/FeedHelper.php @@ -1,4 +1,5 @@ get('rssurl', ''); - - // Get RSS parsed object - try - { - $feed = new FeedFactory; - $rssDoc = $feed->getFeed($rssurl); - } - catch (\Exception $e) - { - return Text::_('MOD_FEED_ERR_FEED_NOT_RETRIEVED'); - } - - if (empty($rssDoc)) - { - return Text::_('MOD_FEED_ERR_FEED_NOT_RETRIEVED'); - } - - return $rssDoc; - } + /** + * Method to load a feed. + * + * @param \Joomla\Registry\Registry $params The parameters object. + * + * @return \Joomla\CMS\Feed\Feed|string Return a JFeedReader object or a string message if error. + * + * @since 1.5 + */ + public static function getFeed($params) + { + // Module params + $rssurl = $params->get('rssurl', ''); + + // Get RSS parsed object + try { + $feed = new FeedFactory(); + $rssDoc = $feed->getFeed($rssurl); + } catch (\Exception $e) { + return Text::_('MOD_FEED_ERR_FEED_NOT_RETRIEVED'); + } + + if (empty($rssDoc)) { + return Text::_('MOD_FEED_ERR_FEED_NOT_RETRIEVED'); + } + + return $rssDoc; + } } diff --git a/code/administrator/modules/mod_feed/tmpl/default.php b/code/administrator/modules/mod_feed/tmpl/default.php index 6adc4e1c..4846fceb 100644 --- a/code/administrator/modules/mod_feed/tmpl/default.php +++ b/code/administrator/modules/mod_feed/tmpl/default.php @@ -1,4 +1,5 @@ ' . Text::_('MOD_FEED_ERR_NO_URL') . ''; - - return; -} +if (empty($rssurl)) { + echo '
' . Text::_('MOD_FEED_ERR_NO_URL') . '
'; -if (!empty($feed) && is_string($feed)) -{ - echo $feed; + return; } -else -{ - $lang = $app->getLanguage(); - $myrtl = $params->get('rssrtl', 0); - $direction = ' '; - if ($lang->isRtl() && $myrtl == 0) - { - $direction = ' redirect-rtl'; - } - // Feed description - elseif ($lang->isRtl() && $myrtl == 1) - { - $direction = ' redirect-ltr'; - } - elseif ($lang->isRtl() && $myrtl == 2) - { - $direction = ' redirect-rtl'; - } - elseif ($myrtl == 0) - { - $direction = ' redirect-ltr'; - } - elseif ($myrtl == 1) - { - $direction = ' redirect-ltr'; - } - elseif ($myrtl == 2) - { - $direction = ' redirect-rtl'; - } +if (!empty($feed) && is_string($feed)) { + echo $feed; +} else { + $lang = $app->getLanguage(); + $myrtl = $params->get('rssrtl', 0); + $direction = ' '; - if ($feed != false) : - ?> -
- isRtl() && $myrtl == 0) { + $direction = ' redirect-rtl'; + } elseif ($lang->isRtl() && $myrtl == 1) { + // Feed description + $direction = ' redirect-ltr'; + } elseif ($lang->isRtl() && $myrtl == 2) { + $direction = ' redirect-rtl'; + } elseif ($myrtl == 0) { + $direction = ' redirect-ltr'; + } elseif ($myrtl == 1) { + $direction = ' redirect-ltr'; + } elseif ($myrtl == 2) { + $direction = ' redirect-rtl'; + } - // Feed title - if (!is_null($feed->title) && $params->get('rsstitle', 1)) : ?> -

- - title; ?> -

- get('rssdate', 1)) : ?> -

- publishedDate, Text::_('DATE_FORMAT_LC3')); ?> -

- + if ($feed != false) : + ?> +
+ - get('rssdesc', 1)) : ?> - description; ?> - + // Feed title + if (!is_null($feed->title) && $params->get('rsstitle', 1)) : ?> +

+ + title; ?> +

+ get('rssdate', 1)) : ?> +

+ publishedDate, Text::_('DATE_FORMAT_LC3')); ?> +

+ - - get('rssimage', 1) && $feed->image) : ?> - <?php echo $feed->image->title; ?> - + + get('rssdesc', 1)) : ?> + description; ?> + + + get('rssimage', 1) && $feed->image) : ?> + <?php echo $feed->image->title; ?> + - - -
    - get('rssitems', 3); $i++) : - if (!$feed->offsetExists($i)) : - break; - endif; - $uri = $feed[$i]->uri || !$feed[$i]->isPermaLink ? trim($feed[$i]->uri) : trim($feed[$i]->guid); - $uri = !$uri || stripos($uri, 'http') !== 0 ? $rssurl : $uri; - $text = $feed[$i]->content !== '' ? trim($feed[$i]->content) : ''; - ?> -
  • - - - - - + + +
      + get('rssitems', 3); $i++) : + if (!$feed->offsetExists($i)) : + break; + endif; + $uri = $feed[$i]->uri || !$feed[$i]->isPermaLink ? trim($feed[$i]->uri) : trim($feed[$i]->guid); + $uri = !$uri || stripos($uri, 'http') !== 0 ? $rssurl : $uri; + $text = $feed[$i]->content !== '' ? trim($feed[$i]->content) : ''; + ?> +
    • + + + + + - get('rssitemdate', 0)) : ?> -
      - publishedDate, Text::_('DATE_FORMAT_LC3')); ?> -
      - + get('rssitemdate', 0)) : ?> +
      + publishedDate, Text::_('DATE_FORMAT_LC3')); ?> +
      + - get('rssitemdesc', 1) && $text !== '') : ?> -
      - get('word_count', 0), true, false); - echo str_replace(''', "'", $text); - ?> -
      - -
    • - -
    - -
- get('rssitemdesc', 1) && $text !== '') : ?> +
+ get('word_count', 0), true, false); + echo str_replace(''', "'", $text); + ?> +
+ + + + + +
+ mod_frontend Joomla! Project - July 2019 + 2019-07 (C) 2019 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_frontend/tmpl/default.php b/code/administrator/modules/mod_frontend/tmpl/default.php index ff76b39a..fcb126f3 100644 --- a/code/administrator/modules/mod_frontend/tmpl/default.php +++ b/code/administrator/modules/mod_frontend/tmpl/default.php @@ -1,4 +1,5 @@ -
- -
-
- -
+ title="" + target="_blank"> +
+ +
+
+ +
diff --git a/code/administrator/modules/mod_latest/mod_latest.php b/code/administrator/modules/mod_latest/mod_latest.php index 28ed0c24..287f2e7b 100644 --- a/code/administrator/modules/mod_latest/mod_latest.php +++ b/code/administrator/modules/mod_latest/mod_latest.php @@ -1,4 +1,5 @@ get('workflow_enabled'); -if ($workflow_enabled) -{ - $app->getLanguage()->load('com_workflow'); +if ($workflow_enabled) { + $app->getLanguage()->load('com_workflow'); } -if ($params->get('automatic_title', 0)) -{ - $module->title = LatestHelper::getTitle($params); +if ($params->get('automatic_title', 0)) { + $module->title = LatestHelper::getTitle($params); } -if (count($list)) -{ - require ModuleHelper::getLayoutPath('mod_latest', $params->get('layout', 'default')); -} -else -{ - $app->getLanguage()->load('com_content'); - - echo LayoutHelper::render('joomla.content.emptystate_module', [ - 'textPrefix' => 'COM_CONTENT', - 'icon' => 'icon-copy', - ] - ); +if (count($list)) { + require ModuleHelper::getLayoutPath('mod_latest', $params->get('layout', 'default')); +} else { + $app->getLanguage()->load('com_content'); + + echo LayoutHelper::render('joomla.content.emptystate_module', [ + 'textPrefix' => 'COM_CONTENT', + 'icon' => 'icon-copy', + ]); } diff --git a/code/administrator/modules/mod_latest/mod_latest.xml b/code/administrator/modules/mod_latest/mod_latest.xml index 6726ca7e..0431e8f4 100644 --- a/code/administrator/modules/mod_latest/mod_latest.xml +++ b/code/administrator/modules/mod_latest/mod_latest.xml @@ -2,7 +2,7 @@ mod_latest Joomla! Project - July 2004 + 2004-07 (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_latest/src/Helper/LatestHelper.php b/code/administrator/modules/mod_latest/src/Helper/LatestHelper.php index 5353913a..bcd93fdf 100644 --- a/code/administrator/modules/mod_latest/src/Helper/LatestHelper.php +++ b/code/administrator/modules/mod_latest/src/Helper/LatestHelper.php @@ -1,4 +1,5 @@ setState('list.select', 'a.id, a.title, a.checked_out, a.checked_out_time, ' . - ' a.access, a.created, a.created_by, a.created_by_alias, a.featured, a.state, a.publish_up, a.publish_down' - ); - - // Set Ordering filter - switch ($params->get('ordering', 'c_dsc')) - { - case 'm_dsc': - $model->setState('list.ordering', 'a.modified DESC, a.created'); - $model->setState('list.direction', 'DESC'); - break; - - case 'c_dsc': - default: - $model->setState('list.ordering', 'a.created'); - $model->setState('list.direction', 'DESC'); - break; - } - - // Set Category Filter - $categoryId = $params->get('catid', null); - - if (is_numeric($categoryId)) - { - $model->setState('filter.category_id', $categoryId); - } - - // Set User Filter. - $userId = $user->get('id'); - - switch ($params->get('user_id', '0')) - { - case 'by_me': - $model->setState('filter.author_id', $userId); - break; - - case 'not_me': - $model->setState('filter.author_id', $userId); - $model->setState('filter.author_id.include', false); - break; - } - - // Set the Start and Limit - $model->setState('list.start', 0); - $model->setState('list.limit', $params->get('count', 5)); - - $items = $model->getItems(); - - if ($error = $model->getError()) - { - throw new \Exception($error, 500); - } - - // Set the links - foreach ($items as &$item) - { - $item->link = ''; - - if ($user->authorise('core.edit', 'com_content.article.' . $item->id) - || ($user->authorise('core.edit.own', 'com_content.article.' . $item->id) && ($userId === $item->created_by))) - { - $item->link = Route::_('index.php?option=com_content&task=article.edit&id=' . $item->id); - } - } - - return $items; - } - - /** - * Get the alternate title for the module. - * - * @param \Joomla\Registry\Registry $params The module parameters. - * - * @return string The alternate title for the module. - */ - public static function getTitle($params) - { - $who = $params->get('user_id', 0); - $catid = (int) $params->get('catid', null); - $type = $params->get('ordering') === 'c_dsc' ? '_CREATED' : '_MODIFIED'; - $title = ''; - - if ($catid) - { - $category = Categories::getInstance('Content')->get($catid); - $title = Text::_('MOD_POPULAR_UNEXISTING'); - - if ($category) - { - $title = $category->title; - } - } - - return Text::plural( - 'MOD_LATEST_TITLE' . $type . ($catid ? '_CATEGORY' : '') . ($who != '0' ? "_$who" : ''), - (int) $params->get('count', 5), - $title - ); - } + /** + * Get a list of articles. + * + * @param Registry &$params The module parameters. + * @param ArticlesModel $model The model. + * + * @return mixed An array of articles, or false on error. + */ + public static function getList(Registry &$params, ArticlesModel $model) + { + $user = Factory::getUser(); + + // Set List SELECT + $model->setState('list.select', 'a.id, a.title, a.checked_out, a.checked_out_time, ' . + ' a.access, a.created, a.created_by, a.created_by_alias, a.featured, a.state, a.publish_up, a.publish_down'); + + // Set Ordering filter + switch ($params->get('ordering', 'c_dsc')) { + case 'm_dsc': + $model->setState('list.ordering', 'a.modified DESC, a.created'); + $model->setState('list.direction', 'DESC'); + break; + + case 'c_dsc': + default: + $model->setState('list.ordering', 'a.created'); + $model->setState('list.direction', 'DESC'); + break; + } + + // Set Category Filter + $categoryId = $params->get('catid', null); + + if (is_numeric($categoryId)) { + $model->setState('filter.category_id', $categoryId); + } + + // Set User Filter. + $userId = $user->get('id'); + + switch ($params->get('user_id', '0')) { + case 'by_me': + $model->setState('filter.author_id', $userId); + break; + + case 'not_me': + $model->setState('filter.author_id', $userId); + $model->setState('filter.author_id.include', false); + break; + } + + // Set the Start and Limit + $model->setState('list.start', 0); + $model->setState('list.limit', $params->get('count', 5)); + + $items = $model->getItems(); + + if ($error = $model->getError()) { + throw new \Exception($error, 500); + } + + // Set the links + foreach ($items as &$item) { + $item->link = ''; + + if ( + $user->authorise('core.edit', 'com_content.article.' . $item->id) + || ($user->authorise('core.edit.own', 'com_content.article.' . $item->id) && ($userId === $item->created_by)) + ) { + $item->link = Route::_('index.php?option=com_content&task=article.edit&id=' . $item->id); + } + } + + return $items; + } + + /** + * Get the alternate title for the module. + * + * @param \Joomla\Registry\Registry $params The module parameters. + * + * @return string The alternate title for the module. + */ + public static function getTitle($params) + { + $who = $params->get('user_id', 0); + $catid = (int) $params->get('catid', null); + $type = $params->get('ordering') === 'c_dsc' ? '_CREATED' : '_MODIFIED'; + $title = ''; + + if ($catid) { + $category = Categories::getInstance('Content')->get($catid); + $title = Text::_('MOD_POPULAR_UNEXISTING'); + + if ($category) { + $title = $category->title; + } + } + + return Text::plural( + 'MOD_LATEST_TITLE' . $type . ($catid ? '_CATEGORY' : '') . ($who != '0' ? "_$who" : ''), + (int) $params->get('count', 5), + $title + ); + } } diff --git a/code/administrator/modules/mod_latest/tmpl/default.php b/code/administrator/modules/mod_latest/tmpl/default.php index 0a317f74..f77973ad 100644 --- a/code/administrator/modules/mod_latest/tmpl/default.php +++ b/code/administrator/modules/mod_latest/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - - - - $item) : ?> - - - - - - - - - - - - - - - + + + + + + + + + + + + + + $item) : ?> + + + + + + + + + + + + + + +
title; ?>
- checked_out) : ?> - editor, $item->checked_out_time, $module->id); ?> - - link) : ?> - - title, ENT_QUOTES, 'UTF-8'); ?> - - - title, ENT_QUOTES, 'UTF-8'); ?> - - - stage_title); ?> - - author_name; ?> - - created, Text::_('DATE_FORMAT_LC4')); ?> -
- -
title; ?>
+ checked_out) : ?> + editor, $item->checked_out_time, $module->id); ?> + + link) : ?> + + title, ENT_QUOTES, 'UTF-8'); ?> + + + title, ENT_QUOTES, 'UTF-8'); ?> + + + stage_title); ?> + + author_name; ?> + + created, Text::_('DATE_FORMAT_LC4')); ?> +
+ +
diff --git a/code/administrator/modules/mod_latestactions/mod_latestactions.php b/code/administrator/modules/mod_latestactions/mod_latestactions.php index 3b60fff2..91e0a7f3 100644 --- a/code/administrator/modules/mod_latestactions/mod_latestactions.php +++ b/code/administrator/modules/mod_latestactions/mod_latestactions.php @@ -1,4 +1,5 @@ getIdentity()->authorise('core.admin')) -{ - return; +if (!$app->getIdentity()->authorise('core.admin')) { + return; } $list = LatestActionsHelper::getList($params); -if ($params->get('automatic_title', 0)) -{ - $module->title = LatestActionsHelper::getTitle($params); +if ($params->get('automatic_title', 0)) { + $module->title = LatestActionsHelper::getTitle($params); } require ModuleHelper::getLayoutPath('mod_latestactions', $params->get('layout', 'default')); diff --git a/code/administrator/modules/mod_latestactions/mod_latestactions.xml b/code/administrator/modules/mod_latestactions/mod_latestactions.xml index 2578420d..66d859a4 100644 --- a/code/administrator/modules/mod_latestactions/mod_latestactions.xml +++ b/code/administrator/modules/mod_latestactions/mod_latestactions.xml @@ -2,7 +2,7 @@ mod_latestactions Joomla! Project - May 2018 + 2018-05 (C) 2018 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php b/code/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php index 61aae97b..0c4785ba 100644 --- a/code/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php +++ b/code/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php @@ -1,4 +1,5 @@ bootComponent('com_actionlogs')->getMVCFactory() - ->createModel('Actionlogs', 'Administrator', ['ignore_request' => true]); + /** + * Get a list of articles. + * + * @param Registry &$params The module parameters. + * + * @return mixed An array of action logs, or false on error. + * + * @since 3.9.1 + * + * @throws \Exception + */ + public static function getList(&$params) + { + /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogsModel $model */ + $model = Factory::getApplication()->bootComponent('com_actionlogs')->getMVCFactory() + ->createModel('Actionlogs', 'Administrator', ['ignore_request' => true]); - // Set the Start and Limit - $model->setState('list.start', 0); - $model->setState('list.limit', $params->get('count', 5)); - $model->setState('list.ordering', 'a.id'); - $model->setState('list.direction', 'DESC'); + // Set the Start and Limit + $model->setState('list.start', 0); + $model->setState('list.limit', $params->get('count', 5)); + $model->setState('list.ordering', 'a.id'); + $model->setState('list.direction', 'DESC'); - $rows = $model->getItems(); + $rows = $model->getItems(); - // Load all actionlog plugins language files - ActionlogsHelper::loadActionLogPluginsLanguage(); + // Load all actionlog plugins language files + ActionlogsHelper::loadActionLogPluginsLanguage(); - foreach ($rows as $row) - { - $row->message = ActionlogsHelper::getHumanReadableLogMessage($row); - } + foreach ($rows as $row) { + $row->message = ActionlogsHelper::getHumanReadableLogMessage($row); + } - return $rows; - } + return $rows; + } - /** - * Get the alternate title for the module - * - * @param Registry $params The module parameters. - * - * @return string The alternate title for the module. - * - * @since 3.9.1 - */ - public static function getTitle($params) - { - return Text::plural('MOD_LATESTACTIONS_TITLE', $params->get('count', 5)); - } + /** + * Get the alternate title for the module + * + * @param Registry $params The module parameters. + * + * @return string The alternate title for the module. + * + * @since 3.9.1 + */ + public static function getTitle($params) + { + return Text::plural('MOD_LATESTACTIONS_TITLE', $params->get('count', 5)); + } } diff --git a/code/administrator/modules/mod_latestactions/tmpl/default.php b/code/administrator/modules/mod_latestactions/tmpl/default.php index 0d27c285..a36b6fc5 100644 --- a/code/administrator/modules/mod_latestactions/tmpl/default.php +++ b/code/administrator/modules/mod_latestactions/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - $item) : ?> - - - - - - - - - - - + + + + + + + + + + $item) : ?> + + + + + + + + + + +
title; ?>
- message; ?> - - log_date); ?> -
- -
title; ?>
+ message; ?> + + log_date); ?> +
+ +
diff --git a/code/administrator/modules/mod_logged/mod_logged.php b/code/administrator/modules/mod_logged/mod_logged.php index e6467845..27a210d8 100644 --- a/code/administrator/modules/mod_logged/mod_logged.php +++ b/code/administrator/modules/mod_logged/mod_logged.php @@ -1,4 +1,5 @@ get('automatic_title', 0)) -{ - $module->title = LoggedHelper::getTitle($params); +if ($params->get('automatic_title', 0)) { + $module->title = LoggedHelper::getTitle($params); } // Check if session metadata tracking is enabled -if ($app->get('session_metadata', true)) -{ - $users = LoggedHelper::getList($params, $app, Factory::getContainer()->get(DatabaseInterface::class)); +if ($app->get('session_metadata', true)) { + $users = LoggedHelper::getList($params, $app, Factory::getContainer()->get(DatabaseInterface::class)); - require ModuleHelper::getLayoutPath('mod_logged', $params->get('layout', 'default')); -} -else -{ - require ModuleHelper::getLayoutPath('mod_logged', 'disabled'); + require ModuleHelper::getLayoutPath('mod_logged', $params->get('layout', 'default')); +} else { + require ModuleHelper::getLayoutPath('mod_logged', 'disabled'); } diff --git a/code/administrator/modules/mod_logged/mod_logged.xml b/code/administrator/modules/mod_logged/mod_logged.xml index 8aca611e..ca893c9c 100644 --- a/code/administrator/modules/mod_logged/mod_logged.xml +++ b/code/administrator/modules/mod_logged/mod_logged.xml @@ -2,7 +2,7 @@ mod_logged Joomla! Project - January 2005 + 2005-01 (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_logged/src/Helper/LoggedHelper.php b/code/administrator/modules/mod_logged/src/Helper/LoggedHelper.php index 490f2b5e..34f612c0 100644 --- a/code/administrator/modules/mod_logged/src/Helper/LoggedHelper.php +++ b/code/administrator/modules/mod_logged/src/Helper/LoggedHelper.php @@ -1,4 +1,5 @@ getIdentity(); - $query = $db->getQuery(true) - ->select('s.time, s.client_id, u.id, u.name, u.username') - ->from('#__session AS s') - ->join('LEFT', '#__users AS u ON s.userid = u.id') - ->where('s.guest = 0') - ->setLimit($params->get('count', 5), 0); + /** + * Get a list of logged users. + * + * @param Registry $params The module parameters + * @param CMSApplication $app The application + * @param DatabaseInterface $db The database + * + * @return mixed An array of users, or false on error. + * + * @throws \RuntimeException + */ + public static function getList(Registry $params, CMSApplication $app, DatabaseInterface $db) + { + $user = $app->getIdentity(); + $query = $db->getQuery(true) + ->select('s.time, s.client_id, u.id, u.name, u.username') + ->from('#__session AS s') + ->join('LEFT', '#__users AS u ON s.userid = u.id') + ->where('s.guest = 0') + ->setLimit($params->get('count', 5), 0); - $db->setQuery($query); + $db->setQuery($query); - try - { - $results = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - throw $e; - } + try { + $results = $db->loadObjectList(); + } catch (\RuntimeException $e) { + throw $e; + } - foreach ($results as $k => $result) - { - $results[$k]->logoutLink = ''; + foreach ($results as $k => $result) { + $results[$k]->logoutLink = ''; - if ($user->authorise('core.manage', 'com_users')) - { - $results[$k]->editLink = Route::_('index.php?option=com_users&task=user.edit&id=' . $result->id); - $results[$k]->logoutLink = Route::_( - 'index.php?option=com_login&task=logout&uid=' . $result->id . '&' . Session::getFormToken() . '=1' - ); - } + if ($user->authorise('core.manage', 'com_users')) { + $results[$k]->editLink = Route::_('index.php?option=com_users&task=user.edit&id=' . $result->id); + $results[$k]->logoutLink = Route::_( + 'index.php?option=com_login&task=logout&uid=' . $result->id . '&' . Session::getFormToken() . '=1' + ); + } - if ($params->get('name', 1) == 0) - { - $results[$k]->name = $results[$k]->username; - } - } + if ($params->get('name', 1) == 0) { + $results[$k]->name = $results[$k]->username; + } + } - return $results; - } + return $results; + } - /** - * Get the alternate title for the module - * - * @param \Joomla\Registry\Registry $params The module parameters. - * - * @return string The alternate title for the module. - */ - public static function getTitle($params) - { - return Text::plural('MOD_LOGGED_TITLE', $params->get('count', 5)); - } + /** + * Get the alternate title for the module + * + * @param \Joomla\Registry\Registry $params The module parameters. + * + * @return string The alternate title for the module. + */ + public static function getTitle($params) + { + return Text::plural('MOD_LOGGED_TITLE', $params->get('count', 5)); + } } diff --git a/code/administrator/modules/mod_logged/tmpl/default.php b/code/administrator/modules/mod_logged/tmpl/default.php index 27726f16..9e878efa 100644 --- a/code/administrator/modules/mod_logged/tmpl/default.php +++ b/code/administrator/modules/mod_logged/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + +
title; ?>
- get('name', 1) == 0) : ?> - - - - -
- editLink)) : ?> - - name, ENT_QUOTES, 'UTF-8'); ?> - - - name, ENT_QUOTES, 'UTF-8'); ?> - - - client_id === null) : ?> - - - client_id) : ?> - - -
- - -
- -
- time, Text::_('DATE_FORMAT_LC5')); ?> -
title; ?>
+ get('name', 1) == 0) : ?> + + + + +
+ editLink)) : ?> + + name, ENT_QUOTES, 'UTF-8'); ?> + + + name, ENT_QUOTES, 'UTF-8'); ?> + + + client_id === null) : ?> + + + client_id) : ?> + + +
+ + +
+ +
+ time, Text::_('DATE_FORMAT_LC5')); ?> +
diff --git a/code/administrator/modules/mod_logged/tmpl/disabled.php b/code/administrator/modules/mod_logged/tmpl/disabled.php index fade4218..676696d6 100644 --- a/code/administrator/modules/mod_logged/tmpl/disabled.php +++ b/code/administrator/modules/mod_logged/tmpl/disabled.php @@ -1,4 +1,5 @@
-

- - -

+

+ + +

diff --git a/code/administrator/modules/mod_login/mod_login.php b/code/administrator/modules/mod_login/mod_login.php index 5667db51..25e009c3 100644 --- a/code/administrator/modules/mod_login/mod_login.php +++ b/code/administrator/modules/mod_login/mod_login.php @@ -1,4 +1,5 @@ mod_login Joomla! Project - March 2005 + 2005-03 (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_login/src/Helper/LoginHelper.php b/code/administrator/modules/mod_login/src/Helper/LoginHelper.php index 0dcea31a..86ed5f6a 100644 --- a/code/administrator/modules/mod_login/src/Helper/LoginHelper.php +++ b/code/administrator/modules/mod_login/src/Helper/LoginHelper.php @@ -1,4 +1,5 @@ getLanguage()->isRtl()) - { - foreach ($languages as &$language) - { - $language['text'] = $language['text'] . '‎'; - } - } + // Fix wrongly set parentheses in RTL languages + if (Factory::getApplication()->getLanguage()->isRtl()) { + foreach ($languages as &$language) { + $language['text'] = $language['text'] . '‎'; + } + } - array_unshift($languages, HTMLHelper::_('select.option', '', Text::_('JDEFAULTLANGUAGE'))); + array_unshift($languages, HTMLHelper::_('select.option', '', Text::_('JDEFAULTLANGUAGE'))); - return HTMLHelper::_('select.genericlist', $languages, 'lang', 'class="form-select"', 'value', 'text', null); - } + return HTMLHelper::_('select.genericlist', $languages, 'lang', 'class="form-select"', 'value', 'text', null); + } - /** - * Get the redirect URI after login. - * - * @return string - */ - public static function getReturnUri() - { - $uri = Uri::getInstance(); - $return = 'index.php' . $uri->toString(array('query')); + /** + * Get the redirect URI after login. + * + * @return string + */ + public static function getReturnUri() + { + $uri = Uri::getInstance(); + $return = 'index.php' . $uri->toString(array('query')); - if ($return != 'index.php?option=com_login') - { - return base64_encode($return); - } - else - { - return base64_encode('index.php'); - } - } + if ($return != 'index.php?option=com_login') { + return base64_encode($return); + } else { + return base64_encode('index.php'); + } + } } diff --git a/code/administrator/modules/mod_login/tmpl/default.php b/code/administrator/modules/mod_login/tmpl/default.php index f2df1883..7a9234d9 100644 --- a/code/administrator/modules/mod_login/tmpl/default.php +++ b/code/administrator/modules/mod_login/tmpl/default.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('field.passwordview') - ->registerAndUseScript('mod_login.admin', 'mod_login/admin-login.min.js', [], ['defer' => true], ['core', 'form.validate']); + ->useScript('field.passwordview') + ->registerAndUseScript('mod_login.admin', 'mod_login/admin-login.min.js', [], ['defer' => true], ['core', 'form.validate']); Text::script('JSHOWPASSWORD'); Text::script('JHIDEPASSWORD'); ?>
-
- -
- -
- - -
-
-
- -
+
+ +
+ +
- - + +
+
+
+ +
-
-
+ + -
- 1): ?> -
- -
+
+
- -
-
- - -
- - -
- - -
- -
- -
- -
- - - - -
-
+
+ +
+ + +
+ + +
+ +
+ +
+ +
+ + + + +
+
-
- '_blank', - 'rel' => 'noopener nofollow', - 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGIN_CREDENTIALS')) - ] - ); ?> -
+
+ '_blank', + 'rel' => 'noopener nofollow', + 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGIN_CREDENTIALS')) + ] + ); ?> +
diff --git a/code/administrator/modules/mod_loginsupport/mod_loginsupport.php b/code/administrator/modules/mod_loginsupport/mod_loginsupport.php index 8bd2a0b2..77be8159 100644 --- a/code/administrator/modules/mod_loginsupport/mod_loginsupport.php +++ b/code/administrator/modules/mod_loginsupport/mod_loginsupport.php @@ -1,4 +1,5 @@ get('automatic_title')) -{ - $module->title = Text::_('MOD_LOGINSUPPORT_TITLE'); +if ($params->get('automatic_title')) { + $module->title = Text::_('MOD_LOGINSUPPORT_TITLE'); } require ModuleHelper::getLayoutPath('mod_loginsupport', $params->get('layout', 'default')); diff --git a/code/administrator/modules/mod_loginsupport/mod_loginsupport.xml b/code/administrator/modules/mod_loginsupport/mod_loginsupport.xml index cac87888..ac242f97 100644 --- a/code/administrator/modules/mod_loginsupport/mod_loginsupport.xml +++ b/code/administrator/modules/mod_loginsupport/mod_loginsupport.xml @@ -2,7 +2,7 @@ mod_loginsupport Joomla! Project - June 2019 + 2019-06 (C) 2019 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_loginsupport/tmpl/default.php b/code/administrator/modules/mod_loginsupport/tmpl/default.php index a01598db..78443fed 100644 --- a/code/administrator/modules/mod_loginsupport/tmpl/default.php +++ b/code/administrator/modules/mod_loginsupport/tmpl/default.php @@ -1,4 +1,5 @@
-

-
    -
  • - get('forum_url'), - Text::_('MOD_LOGINSUPPORT_FORUM'), - [ - 'target' => '_blank', - 'rel' => 'nofollow noopener', - 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_FORUM')) - ] - ); ?> -
  • -
  • - get('documentation_url'), - Text::_('MOD_LOGINSUPPORT_DOCUMENTATION'), - [ - 'target' => '_blank', - 'rel' => 'nofollow noopener', - 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_DOCUMENTATION')) - ] - ); ?> -
  • -
  • - get('news_url'), - Text::_('MOD_LOGINSUPPORT_NEWS'), - [ - 'target' => '_blank', - 'rel' => 'nofollow noopener', - 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_NEWS')) - ] - ); ?> -
  • -
+

+
    +
  • + get('forum_url'), + Text::_('MOD_LOGINSUPPORT_FORUM'), + [ + 'target' => '_blank', + 'rel' => 'nofollow noopener', + 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_FORUM')) + ] + ); ?> +
  • +
  • + get('documentation_url'), + Text::_('MOD_LOGINSUPPORT_DOCUMENTATION'), + [ + 'target' => '_blank', + 'rel' => 'nofollow noopener', + 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_DOCUMENTATION')) + ] + ); ?> +
  • +
  • + get('news_url'), + Text::_('MOD_LOGINSUPPORT_NEWS'), + [ + 'target' => '_blank', + 'rel' => 'nofollow noopener', + 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_NEWS')) + ] + ); ?> +
  • +
diff --git a/code/administrator/modules/mod_menu/mod_menu.php b/code/administrator/modules/mod_menu/mod_menu.php index 5c5dbbf1..484dd7e2 100644 --- a/code/administrator/modules/mod_menu/mod_menu.php +++ b/code/administrator/modules/mod_menu/mod_menu.php @@ -1,4 +1,5 @@ mod_menu Joomla! Project - March 2006 + 2006-03 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_menu/src/Menu/CssMenu.php b/code/administrator/modules/mod_menu/src/Menu/CssMenu.php index 67fe3f31..83b26945 100644 --- a/code/administrator/modules/mod_menu/src/Menu/CssMenu.php +++ b/code/administrator/modules/mod_menu/src/Menu/CssMenu.php @@ -1,4 +1,5 @@ application = $application; - $this->root = new AdministratorMenuItem; - } - - /** - * Populate the menu items in the menu tree object - * - * @param Registry $params Menu configuration parameters - * @param bool $enabled Whether the menu should be enabled or disabled - * - * @return AdministratorMenuItem Root node of the menu tree - * - * @since 3.7.0 - */ - public function load($params, $enabled) - { - $this->params = $params; - $this->enabled = $enabled; - $menutype = $this->params->get('menutype', '*'); - - if ($menutype === '*') - { - $name = $this->params->get('preset', 'default'); - $this->root = MenusHelper::loadPreset($name); - } - else - { - $this->root = MenusHelper::getMenuItems($menutype, true); - - // Can we access everything important with this menu? Create a recovery menu! - if ($this->enabled - && $this->params->get('check', 1) - && $this->check($this->root, $this->params)) - { - $this->params->set('recovery', true); - - // In recovery mode, load the preset inside a special root node. - $this->root = new AdministratorMenuItem(['level' => 0]); - $heading = new AdministratorMenuItem(['title' => 'MOD_MENU_RECOVERY_MENU_ROOT', 'type' => 'heading']); - $this->root->addChild($heading); - - MenusHelper::loadPreset('default', true, $heading); - - $this->preprocess($this->root); - - $this->root->addChild(new AdministratorMenuItem(['type' => 'separator'])); - - // Add link to exit recovery mode - $uri = clone Uri::getInstance(); - $uri->setVar('recover_menu', 0); - - $this->root->addChild(new AdministratorMenuItem(['title' => 'MOD_MENU_RECOVERY_EXIT', 'type' => 'url', 'link' => $uri->toString()])); - - return $this->root; - } - } - - $this->preprocess($this->root); - - return $this->root; - } - - /** - * Method to render a given level of a menu using provided layout file - * - * @param string $layoutFile The layout file to be used to render - * @param AdministratorMenuItem $node Node to render the children of - * - * @return void - * - * @since 3.8.0 - */ - public function renderSubmenu($layoutFile, $node) - { - if (is_file($layoutFile)) - { - $children = $node->getChildren(); - - foreach ($children as $current) - { - $current->level = $node->level + 1; - - // This sets the scope to this object for the layout file and also isolates other `include`s - require $layoutFile; - } - } - } - - /** - * Check the flat list of menu items for important links - * - * @param AdministratorMenuItem $node The menu items array - * @param Registry $params Module options - * - * @return boolean Whether to show recovery menu - * - * @since 3.8.0 - */ - protected function check($node, Registry $params) - { - $me = $this->application->getIdentity(); - $authMenus = $me->authorise('core.manage', 'com_menus'); - $authModules = $me->authorise('core.manage', 'com_modules'); - - if (!$authMenus && !$authModules) - { - return false; - } - - $items = $node->getChildren(true); - $types = array_column($items, 'type'); - $elements = array_column($items, 'element'); - $rMenu = $authMenus && !\in_array('com_menus', $elements); - $rModule = $authModules && !\in_array('com_modules', $elements); - $rContainer = !\in_array('container', $types); - - if ($rMenu || $rModule || $rContainer) - { - $recovery = $this->application->getUserStateFromRequest('mod_menu.recovery', 'recover_menu', 0, 'int'); - - if ($recovery) - { - return true; - } - - $missing = array(); - - if ($rMenu) - { - $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_MENU_MANAGER'); - } - - if ($rModule) - { - $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_MODULE_MANAGER'); - } - - if ($rContainer) - { - $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_COMPONENTS_CONTAINER'); - } - - $uri = clone Uri::getInstance(); - $uri->setVar('recover_menu', 1); - - $table = Table::getInstance('MenuType'); - $menutype = $params->get('menutype'); - - $table->load(array('menutype' => $menutype)); - - $menutype = $table->get('title', $menutype); - $message = Text::sprintf('MOD_MENU_IMPORTANT_ITEMS_INACCESSIBLE_LIST_WARNING', $menutype, implode(', ', $missing), $uri); - - $this->application->enqueueMessage($message, 'warning'); - } - - return false; - } - - /** - * Filter and perform other preparatory tasks for loaded menu items based on access rights and module configurations for display - * - * @param AdministratorMenuItem $parent A menu item to process - * - * @return array - * - * @since 3.8.0 - */ - protected function preprocess($parent) - { - $user = $this->application->getIdentity(); - $language = $this->application->getLanguage(); - - $noSeparator = true; - $children = $parent->getChildren(); - - /** - * Trigger onPreprocessMenuItems for the current level of backend menu items. - * $children is an array of AdministratorMenuItem objects. A plugin can traverse the whole tree, - * but new nodes will only be run through this method if their parents have not been processed yet. - */ - $this->application->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.module', $children, $this->params, $this->enabled)); - - foreach ($children as $item) - { - $itemParams = $item->getParams(); - - // Exclude item with menu item option set to exclude from menu modules - if ($itemParams->get('menu_show', 1) == 0) - { - $parent->removeChild($item); - continue; - } - - $item->scope = $item->scope ?? 'default'; - $item->icon = $item->icon ?? ''; - - // Whether this scope can be displayed. Applies only to preset items. Db driven items should use un/published state. - if (($item->scope === 'help' && $this->params->get('showhelp', 1) == 0) || ($item->scope === 'edit' && !$this->params->get('shownew', 1))) - { - $parent->removeChild($item); - continue; - } - - if (substr($item->link, 0, 8) === 'special:') - { - $special = substr($item->link, 8); - - if ($special === 'language-forum') - { - $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; - } - elseif ($special === 'custom-forum') - { - $item->link = $this->params->get('forum_url'); - } - } - - $uri = new Uri($item->link); - $query = $uri->getQuery(true); - - /** - * If component is passed in the link via option variable, we set $item->element to this value for further - * processing. It is needed for links from menu items of third party extensions link to Joomla! core - * components like com_categories, com_fields... - */ - if ($option = $uri->getVar('option')) - { - $item->element = $option; - } - - // Exclude item if is not enabled - if ($item->element && !ComponentHelper::isEnabled($item->element)) - { - $parent->removeChild($item); - continue; - } - - /* - * Multilingual Associations if the site is not set as multilingual and/or Associations is not enabled in - * the Language Filter plugin - */ - - if ($item->element === 'com_associations' && !Associations::isEnabled()) - { - $parent->removeChild($item); - continue; - } - - // Exclude Mass Mail if disabled in global configuration - if ($item->scope === 'massmail' && ($this->application->get('mailonline', 1) == 0 || $this->application->get('massmailoff', 0) == 1)) - { - $parent->removeChild($item); - continue; - } - - // Exclude item if the component is not authorised - $assetName = $item->element; - - if ($item->element === 'com_categories') - { - $assetName = $query['extension'] ?? 'com_content'; - } - elseif ($item->element === 'com_fields') - { - // Only display Fields menus when enabled in the component - $createFields = null; - - if (isset($query['context'])) - { - $createFields = ComponentHelper::getParams(strstr($query['context'], '.', true))->get('custom_fields_enable', 1); - } - - if (!$createFields) - { - $parent->removeChild($item); - continue; - } - - list($assetName) = isset($query['context']) ? explode('.', $query['context'], 2) : array('com_fields'); - } - elseif ($item->element === 'com_cpanel' && $item->link === 'index.php') - { - continue; - } - elseif ($item->link === 'index.php?option=com_cpanel&view=help' - || $item->link === 'index.php?option=com_cpanel&view=cpanel&dashboard=help') - { - if ($this->params->get('showhelp', 1)) - { - continue; - } - - // Exclude help menu item if set such in mod_menu - $parent->removeChild($item); - continue; - } - elseif ($item->element === 'com_workflow') - { - // Only display Workflow menus when enabled in the component - $workflow = null; - - if (isset($query['extension'])) - { - $parts = explode('.', $query['extension']); - - $workflow = ComponentHelper::getParams($parts[0])->get('workflow_enabled') && $user->authorise('core.manage.workflow', $parts[0]); - } - - if (!$workflow) - { - $parent->removeChild($item); - continue; - } - - list($assetName) = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); - } - // Special case for components which only allow super user access - elseif (\in_array($item->element, array('com_config', 'com_privacy', 'com_actionlogs'), true) && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - elseif ($item->element === 'com_joomlaupdate' && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - elseif (($item->link === 'index.php?option=com_installer&view=install' || $item->link === 'index.php?option=com_installer&view=languages') - && !$user->authorise('core.admin')) - { - continue; - } - elseif ($item->element === 'com_admin') - { - if (isset($query['view']) && $query['view'] === 'sysinfo' && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - } - elseif ($item->link === 'index.php?option=com_messages&view=messages' && !$user->authorise('core.manage', 'com_users')) - { - $parent->removeChild($item); - continue; - } - - if ($assetName && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $assetName)) - { - $parent->removeChild($item); - continue; - } - - // Exclude if link is invalid - if (is_null($item->link) || !\in_array($item->type, array('separator', 'heading', 'container')) && trim($item->link) === '') - { - $parent->removeChild($item); - continue; - } - - // Process any children if exists - if ($item->hasChildren()) - { - $this->preprocess($item); - } - - // Populate automatic children for container items - if ($item->type === 'container') - { - $exclude = (array) $itemParams->get('hideitems') ?: array(); - $components = MenusHelper::getMenuItems('main', false, $exclude); - - // We are adding the nodes first to preprocess them, then sort them and add them again. - foreach ($components->getChildren() as $c) - { - $item->addChild($c); - } - - $this->preprocess($item); - $children = ArrayHelper::sortObjects($item->getChildren(), 'text', 1, false, true); - - foreach ($children as $c) - { - $item->addChild($c); - } - } - - // Exclude if there are no child items under heading or container - if (\in_array($item->type, array('heading', 'container')) && !$item->hasChildren() && empty($item->components)) - { - $parent->removeChild($item); - continue; - } - - // Remove repeated and edge positioned separators, It is important to put this check at the end of any logical filtering. - if ($item->type === 'separator') - { - if ($noSeparator) - { - $parent->removeChild($item); - continue; - } - - $noSeparator = true; - } - else - { - $noSeparator = false; - } - - // Ok we passed everything, load language at last only - if ($item->element) - { - $language->load($item->element . '.sys', JPATH_ADMINISTRATOR) || - $language->load($item->element . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->element); - } - - if ($item->type === 'separator' && $itemParams->get('text_separator') == 0) - { - $item->title = ''; - } - - $item->text = Text::_($item->title); - } - - // If last one was a separator remove it too. - $last = end($parent->getChildren()); - - if ($last && $last->type === 'separator' && $last->getSibling(false) && $last->getSibling(false)->type === 'separator') - { - $parent->removeChild($last); - } - } - - /** - * Method to get the CSS class name for an icon identifier or create one if - * a custom image path is passed as the identifier - * - * @param AdministratorMenuItem $node Node to get icon data from - * - * @return string CSS class name - * - * @since 3.8.0 - */ - public function getIconClass($node) - { - $identifier = $node->class; - - // Top level is special - if (trim($identifier) == '') - { - return null; - } - - // We were passed a class name - if (substr($identifier, 0, 6) == 'class:') - { - $class = substr($identifier, 6); - } - // We were passed background icon url. Build the CSS class for the icon - else - { - if ($identifier == null) - { - return null; - } - - $class = preg_replace('#\.[^.]*$#', '', basename($identifier)); - $class = preg_replace('#\.\.[^A-Za-z0-9\.\_\- ]#', '', $class); - } - - $html = 'icon-' . $class . ' icon-fw'; - - return $html; - } - - /** - * Create unique identifier - * - * @return string - * - * @since 4.0.0 - */ - public function getCounter() - { - $this->counter++; - - return $this->counter; - } + /** + * The root of the menu + * + * @var AdministratorMenuItem + * + * @since 4.0.0 + */ + protected $root; + + /** + * An array of AdministratorMenuItem nodes + * + * @var AdministratorMenuItem[] + * + * @since 4.0.0 + */ + protected $nodes = []; + + /** + * The module options + * + * @var Registry + * + * @since 3.8.0 + */ + protected $params; + + /** + * The menu bar state + * + * @var boolean + * + * @since 3.8.0 + */ + protected $enabled; + + /** + * The application + * + * @var boolean + * + * @since 4.0.0 + */ + protected $application; + + /** + * A counter for unique IDs + * + * @var integer + * + * @since 4.0.0 + */ + protected $counter = 0; + + /** + * CssMenu constructor. + * + * @param CMSApplication $application The application + * + * @since 4.0.0 + */ + public function __construct(CMSApplication $application) + { + $this->application = $application; + $this->root = new AdministratorMenuItem(); + } + + /** + * Populate the menu items in the menu tree object + * + * @param Registry $params Menu configuration parameters + * @param bool $enabled Whether the menu should be enabled or disabled + * + * @return AdministratorMenuItem Root node of the menu tree + * + * @since 3.7.0 + */ + public function load($params, $enabled) + { + $this->params = $params; + $this->enabled = $enabled; + $menutype = $this->params->get('menutype', '*'); + + if ($menutype === '*') { + $name = $this->params->get('preset', 'default'); + $this->root = MenusHelper::loadPreset($name); + } else { + $this->root = MenusHelper::getMenuItems($menutype, true); + + // Can we access everything important with this menu? Create a recovery menu! + if ( + $this->enabled + && $this->params->get('check', 1) + && $this->check($this->root, $this->params) + ) { + $this->params->set('recovery', true); + + // In recovery mode, load the preset inside a special root node. + $this->root = new AdministratorMenuItem(['level' => 0]); + $heading = new AdministratorMenuItem(['title' => 'MOD_MENU_RECOVERY_MENU_ROOT', 'type' => 'heading']); + $this->root->addChild($heading); + + MenusHelper::loadPreset('default', true, $heading); + + $this->preprocess($this->root); + + $this->root->addChild(new AdministratorMenuItem(['type' => 'separator'])); + + // Add link to exit recovery mode + $uri = clone Uri::getInstance(); + $uri->setVar('recover_menu', 0); + + $this->root->addChild(new AdministratorMenuItem(['title' => 'MOD_MENU_RECOVERY_EXIT', 'type' => 'url', 'link' => $uri->toString()])); + + return $this->root; + } + } + + $this->preprocess($this->root); + + return $this->root; + } + + /** + * Method to render a given level of a menu using provided layout file + * + * @param string $layoutFile The layout file to be used to render + * @param AdministratorMenuItem $node Node to render the children of + * + * @return void + * + * @since 3.8.0 + */ + public function renderSubmenu($layoutFile, $node) + { + if (is_file($layoutFile)) { + $children = $node->getChildren(); + + foreach ($children as $current) { + $current->level = $node->level + 1; + + // This sets the scope to this object for the layout file and also isolates other `include`s + require $layoutFile; + } + } + } + + /** + * Check the flat list of menu items for important links + * + * @param AdministratorMenuItem $node The menu items array + * @param Registry $params Module options + * + * @return boolean Whether to show recovery menu + * + * @since 3.8.0 + */ + protected function check($node, Registry $params) + { + $me = $this->application->getIdentity(); + $authMenus = $me->authorise('core.manage', 'com_menus'); + $authModules = $me->authorise('core.manage', 'com_modules'); + + if (!$authMenus && !$authModules) { + return false; + } + + $items = $node->getChildren(true); + $types = array_column($items, 'type'); + $elements = array_column($items, 'element'); + $rMenu = $authMenus && !\in_array('com_menus', $elements); + $rModule = $authModules && !\in_array('com_modules', $elements); + $rContainer = !\in_array('container', $types); + + if ($rMenu || $rModule || $rContainer) { + $recovery = $this->application->getUserStateFromRequest('mod_menu.recovery', 'recover_menu', 0, 'int'); + + if ($recovery) { + return true; + } + + $missing = array(); + + if ($rMenu) { + $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_MENU_MANAGER'); + } + + if ($rModule) { + $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_MODULE_MANAGER'); + } + + if ($rContainer) { + $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_COMPONENTS_CONTAINER'); + } + + $uri = clone Uri::getInstance(); + $uri->setVar('recover_menu', 1); + + $table = Table::getInstance('MenuType'); + $menutype = $params->get('menutype'); + + $table->load(array('menutype' => $menutype)); + + $menutype = $table->get('title', $menutype); + $message = Text::sprintf('MOD_MENU_IMPORTANT_ITEMS_INACCESSIBLE_LIST_WARNING', $menutype, implode(', ', $missing), $uri); + + $this->application->enqueueMessage($message, 'warning'); + } + + return false; + } + + /** + * Filter and perform other preparatory tasks for loaded menu items based on access rights and module configurations for display + * + * @param AdministratorMenuItem $parent A menu item to process + * + * @return array + * + * @since 3.8.0 + */ + protected function preprocess($parent) + { + $user = $this->application->getIdentity(); + $language = $this->application->getLanguage(); + + $noSeparator = true; + $children = $parent->getChildren(); + + /** + * Trigger onPreprocessMenuItems for the current level of backend menu items. + * $children is an array of AdministratorMenuItem objects. A plugin can traverse the whole tree, + * but new nodes will only be run through this method if their parents have not been processed yet. + */ + $this->application->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.module', $children, $this->params, $this->enabled)); + + foreach ($children as $item) { + $itemParams = $item->getParams(); + + // Exclude item with menu item option set to exclude from menu modules + if ($itemParams->get('menu_show', 1) == 0) { + $parent->removeChild($item); + continue; + } + + $item->scope = $item->scope ?? 'default'; + $item->icon = $item->icon ?? ''; + + // Whether this scope can be displayed. Applies only to preset items. Db driven items should use un/published state. + if (($item->scope === 'help' && $this->params->get('showhelp', 1) == 0) || ($item->scope === 'edit' && !$this->params->get('shownew', 1))) { + $parent->removeChild($item); + continue; + } + + if (substr($item->link, 0, 8) === 'special:') { + $special = substr($item->link, 8); + + if ($special === 'language-forum') { + $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; + } elseif ($special === 'custom-forum') { + $item->link = $this->params->get('forum_url'); + } + } + + $uri = new Uri($item->link); + $query = $uri->getQuery(true); + + /** + * If component is passed in the link via option variable, we set $item->element to this value for further + * processing. It is needed for links from menu items of third party extensions link to Joomla! core + * components like com_categories, com_fields... + */ + if ($option = $uri->getVar('option')) { + $item->element = $option; + } + + // Exclude item if is not enabled + if ($item->element && !ComponentHelper::isEnabled($item->element)) { + $parent->removeChild($item); + continue; + } + + /* + * Multilingual Associations if the site is not set as multilingual and/or Associations is not enabled in + * the Language Filter plugin + */ + + if ($item->element === 'com_associations' && !Associations::isEnabled()) { + $parent->removeChild($item); + continue; + } + + // Exclude Mass Mail if disabled in global configuration + if ($item->scope === 'massmail' && ($this->application->get('mailonline', 1) == 0 || $this->application->get('massmailoff', 0) == 1)) { + $parent->removeChild($item); + continue; + } + + // Exclude item if the component is not authorised + $assetName = $item->element; + + if ($item->element === 'com_categories') { + $assetName = $query['extension'] ?? 'com_content'; + } elseif ($item->element === 'com_fields') { + // Only display Fields menus when enabled in the component + $createFields = null; + + if (isset($query['context'])) { + $createFields = ComponentHelper::getParams(strstr($query['context'], '.', true))->get('custom_fields_enable', 1); + } + + if (!$createFields) { + $parent->removeChild($item); + continue; + } + + list($assetName) = isset($query['context']) ? explode('.', $query['context'], 2) : array('com_fields'); + } elseif ($item->element === 'com_cpanel' && $item->link === 'index.php') { + continue; + } elseif ( + $item->link === 'index.php?option=com_cpanel&view=help' + || $item->link === 'index.php?option=com_cpanel&view=cpanel&dashboard=help' + ) { + if ($this->params->get('showhelp', 1)) { + continue; + } + + // Exclude help menu item if set such in mod_menu + $parent->removeChild($item); + continue; + } elseif ($item->element === 'com_workflow') { + // Only display Workflow menus when enabled in the component + $workflow = null; + + if (isset($query['extension'])) { + $parts = explode('.', $query['extension']); + + $workflow = ComponentHelper::getParams($parts[0])->get('workflow_enabled') && $user->authorise('core.manage.workflow', $parts[0]); + } + + if (!$workflow) { + $parent->removeChild($item); + continue; + } + + list($assetName) = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); + } elseif (\in_array($item->element, array('com_config', 'com_privacy', 'com_actionlogs'), true) && !$user->authorise('core.admin')) { + // Special case for components which only allow super user access + $parent->removeChild($item); + continue; + } elseif ($item->element === 'com_joomlaupdate' && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } elseif ( + ($item->link === 'index.php?option=com_installer&view=install' || $item->link === 'index.php?option=com_installer&view=languages') + && !$user->authorise('core.admin') + ) { + continue; + } elseif ($item->element === 'com_admin') { + if (isset($query['view']) && $query['view'] === 'sysinfo' && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } + } elseif ($item->link === 'index.php?option=com_messages&view=messages' && !$user->authorise('core.manage', 'com_users')) { + $parent->removeChild($item); + continue; + } + + if ($assetName && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $assetName)) { + $parent->removeChild($item); + continue; + } + + // Exclude if link is invalid + if (is_null($item->link) || !\in_array($item->type, array('separator', 'heading', 'container')) && trim($item->link) === '') { + $parent->removeChild($item); + continue; + } + + // Process any children if exists + if ($item->hasChildren()) { + $this->preprocess($item); + } + + // Populate automatic children for container items + if ($item->type === 'container') { + $exclude = (array) $itemParams->get('hideitems') ?: array(); + $components = MenusHelper::getMenuItems('main', false, $exclude); + + // We are adding the nodes first to preprocess them, then sort them and add them again. + foreach ($components->getChildren() as $c) { + $item->addChild($c); + } + + $this->preprocess($item); + $children = ArrayHelper::sortObjects($item->getChildren(), 'text', 1, false, true); + + foreach ($children as $c) { + $item->addChild($c); + } + } + + // Exclude if there are no child items under heading or container + if (\in_array($item->type, array('heading', 'container')) && !$item->hasChildren() && empty($item->components)) { + $parent->removeChild($item); + continue; + } + + // Remove repeated and edge positioned separators, It is important to put this check at the end of any logical filtering. + if ($item->type === 'separator') { + if ($noSeparator) { + $parent->removeChild($item); + continue; + } + + $noSeparator = true; + } else { + $noSeparator = false; + } + + // Ok we passed everything, load language at last only + if ($item->element) { + $language->load($item->element . '.sys', JPATH_ADMINISTRATOR) || + $language->load($item->element . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->element); + } + + if ($item->type === 'separator' && $itemParams->get('text_separator') == 0) { + $item->title = ''; + } + + $item->text = Text::_($item->title); + } + + // If last one was a separator remove it too. + $last = end($parent->getChildren()); + + if ($last && $last->type === 'separator' && $last->getSibling(false) && $last->getSibling(false)->type === 'separator') { + $parent->removeChild($last); + } + } + + /** + * Method to get the CSS class name for an icon identifier or create one if + * a custom image path is passed as the identifier + * + * @param AdministratorMenuItem $node Node to get icon data from + * + * @return string CSS class name + * + * @since 3.8.0 + */ + public function getIconClass($node) + { + $identifier = $node->class; + + // Top level is special + if (trim($identifier) == '') { + return null; + } + + // We were passed a class name + if (substr($identifier, 0, 6) == 'class:') { + $class = substr($identifier, 6); + } else { + // We were passed background icon url. Build the CSS class for the icon + if ($identifier == null) { + return null; + } + + $class = preg_replace('#\.[^.]*$#', '', basename($identifier)); + $class = preg_replace('#\.\.[^A-Za-z0-9\.\_\- ]#', '', $class); + } + + $html = 'icon-' . $class . ' icon-fw'; + + return $html; + } + + /** + * Create unique identifier + * + * @return string + * + * @since 4.0.0 + */ + public function getCounter() + { + $this->counter++; + + return $this->counter; + } } diff --git a/code/administrator/modules/mod_menu/tmpl/default.php b/code/administrator/modules/mod_menu/tmpl/default.php index 01f57feb..581f4e9f 100644 --- a/code/administrator/modules/mod_menu/tmpl/default.php +++ b/code/administrator/modules/mod_menu/tmpl/default.php @@ -1,4 +1,5 @@ getWebAssetManager(); $wa->getRegistry()->addExtensionRegistryFile('com_cpanel'); $wa->useScript('metismenujs') - ->registerAndUseScript('mod_menu.admin-menu', 'mod_menu/admin-menu.min.js', [], ['defer' => true], ['metismenujs']) - ->useScript('com_cpanel.admin-system-loader'); + ->registerAndUseScript('mod_menu.admin-menu', 'mod_menu/admin-menu.min.js', [], ['defer' => true], ['metismenujs']) + ->useScript('com_cpanel.admin-system-loader'); // Recurse through children of root node if they exist -if ($root->hasChildren()) -{ - echo '\n"; } diff --git a/code/administrator/modules/mod_menu/tmpl/default_submenu.php b/code/administrator/modules/mod_menu/tmpl/default_submenu.php index 566800a9..13230279 100644 --- a/code/administrator/modules/mod_menu/tmpl/default_submenu.php +++ b/code/administrator/modules/mod_menu/tmpl/default_submenu.php @@ -1,4 +1,5 @@ getParams(); // Build the CSS class suffix -if (!$this->enabled) -{ - $class .= ' disabled'; -} -elseif ($current->type == 'separator') -{ - $class = $current->title ? 'menuitem-group' : 'divider'; -} -elseif ($current->hasChildren()) -{ - $class .= ' parent'; +if (!$this->enabled) { + $class .= ' disabled'; +} elseif ($current->type == 'separator') { + $class = $current->title ? 'menuitem-group' : 'divider'; +} elseif ($current->hasChildren()) { + $class .= ' parent'; } -if ($current->level == 1) -{ - $class .= ' item-level-1'; -} -elseif ($current->level == 2) -{ - $class .= ' item-level-2'; -} -elseif ($current->level == 3) -{ - $class .= ' item-level-3'; +if ($current->level == 1) { + $class .= ' item-level-1'; +} elseif ($current->level == 2) { + $class .= ' item-level-2'; +} elseif ($current->level == 3) { + $class .= ' item-level-3'; } // Set the correct aria role and print the item -if ($current->type == 'separator') -{ - echo '
  • '; +if ($current->type == 'separator') { + echo '
  • '; } // Print a link if it exists @@ -67,18 +55,14 @@ $itemIconClass = ''; $itemImage = ''; -if ($current->hasChildren()) -{ - $linkClass[] = 'has-arrow'; +if ($current->hasChildren()) { + $linkClass[] = 'has-arrow'; - if ($current->level > 2) - { - $dataToggle = ' data-bs-toggle="dropdown"'; - } -} -else -{ - $linkClass[] = 'no-dropdown'; + if ($current->level > 2) { + $dataToggle = ' data-bs-toggle="dropdown"'; + } +} else { + $linkClass[] = 'no-dropdown'; } // Implode out $linkClass for rendering @@ -100,110 +84,86 @@ $iconImage = $current->icon; $homeImage = ''; -if ($iconClass === '' && $itemIconClass) -{ - $iconClass = ''; +if ($iconClass === '' && $itemIconClass) { + $iconClass = ''; } -if ($iconImage) -{ - if (substr($iconImage, 0, 6) == 'class:' && substr($iconImage, 6) == 'icon-home') - { - $iconImage = ''; - $iconImage .= '' . Text::_('JDEFAULT') . ''; - } - elseif (substr($iconImage, 0, 6) == 'image:') - { - $iconImage = ' ' . substr($iconImage, 6) . ''; - } - else - { - $iconImage = ''; - } +if ($iconImage) { + if (substr($iconImage, 0, 6) == 'class:' && substr($iconImage, 6) == 'icon-home') { + $iconImage = ''; + $iconImage .= '' . Text::_('JDEFAULT') . ''; + } elseif (substr($iconImage, 0, 6) == 'image:') { + $iconImage = ' ' . substr($iconImage, 6) . ''; + } else { + $iconImage = ''; + } } $itemImage = (empty($itemIconClass) && $itemImage) ? '  ' : ''; // If the item image is not set, the item title would not have margin. Here we add it. -if ($icon == '' && $iconClass == '' && $current->level == 1 && $current->target == '') -{ - $iconClass = ''; +if ($icon == '' && $iconClass == '' && $current->level == 1 && $current->target == '') { + $iconClass = ''; +} + +if ($link != '' && $current->target != '') { + echo '' + . $iconClass + . '' . $itemImage . Text::_($current->title) . '' . $ajax . ''; +} elseif ($link != '' && $current->type !== 'separator') { + echo '' + . $iconClass + . '' . $itemImage . Text::_($current->title) . '' . $iconImage . ''; +} elseif ($current->title != '' && $current->type !== 'separator') { + echo '' + . $iconClass + . '' . $itemImage . Text::_($current->title) . '' . $ajax . ''; +} elseif ($current->title != '' && $current->type === 'separator') { + echo '' . Text::_($current->title) . '' . $ajax; +} else { + echo '' . Text::_($current->title) . '' . $ajax; +} + +if ($currentParams->get('menu-quicktask') && (int) $this->params->get('shownew', 1) === 1) { + $params = $current->getParams(); + $user = $this->application->getIdentity(); + $link = $params->get('menu-quicktask'); + $icon = $params->get('menu-quicktask-icon', 'plus'); + $title = $params->get('menu-quicktask-title', 'MOD_MENU_QUICKTASK_NEW'); + $permission = $params->get('menu-quicktask-permission'); + $scope = $current->scope !== 'default' ? $current->scope : null; + + if (!$permission || $user->authorise($permission, $scope)) { + echo ''; + echo ''; + echo '' . Text::_($title) . ''; + echo ''; + } +} + +if (!empty($current->dashboard)) { + $titleDashboard = Text::sprintf('MOD_MENU_DASHBOARD_LINK', Text::_($current->title)); + echo '' + . '' + . '' . $titleDashboard . '' + . ''; } -if ($link != '' && $current->target != '') -{ - echo '' - . $iconClass - . '' . $itemImage . Text::_($current->title) . '' . $ajax . ''; -} -elseif ($link != '' && $current->type !== 'separator') -{ - echo '' - . $iconClass - . '' . $itemImage . Text::_($current->title) . '' . $iconImage . ''; -} -elseif ($current->title != '' && $current->type !== 'separator') -{ - echo '' - . $iconClass - . ''. $itemImage . Text::_($current->title) . '' . $ajax . ''; -} -elseif ($current->title != '' && $current->type === 'separator') -{ - echo '' . Text::_($current->title) . '' . $ajax; -} -else -{ - echo '' . Text::_($current->title) . '' . $ajax; -} +// Recurse through children if they exist +if ($this->enabled && $current->hasChildren()) { + if ($current->level > 1) { + $id = $current->id ? ' id="menu-' . strtolower($current->id) . '"' : ''; -if ($currentParams->get('menu-quicktask') && (int) $this->params->get('shownew', 1) === 1) -{ - $params = $current->getParams(); - $user = $this->application->getIdentity(); - $link = $params->get('menu-quicktask'); - $icon = $params->get('menu-quicktask-icon', 'plus'); - $title = $params->get('menu-quicktask-title', 'MOD_MENU_QUICKTASK_NEW'); - $permission = $params->get('menu-quicktask-permission'); - $scope = $current->scope !== 'default' ? $current->scope : null; - - if (!$permission || $user->authorise($permission, $scope)) - { - echo ''; - echo ''; - echo '' . Text::_($title) . ''; - echo ''; - } -} + echo '' . "\n"; + } else { + echo '
      ' . "\n"; + } -if (!empty($current->dashboard)) -{ - $titleDashboard = Text::sprintf('MOD_MENU_DASHBOARD_LINK', Text::_($current->title)); - echo '' - . '' - . '' . $titleDashboard . '' - . ''; -} + // WARNING: Do not use direct 'include' or 'require' as it is important to isolate the scope for each call + $this->renderSubmenu(__FILE__, $current); -// Recurse through children if they exist -if ($this->enabled && $current->hasChildren()) -{ - if ($current->level > 1) - { - $id = $current->id ? ' id="menu-' . strtolower($current->id) . '"' : ''; - - echo '' . "\n"; - } - else - { - echo '
        ' . "\n"; - } - - // WARNING: Do not use direct 'include' or 'require' as it is important to isolate the scope for each call - $this->renderSubmenu(__FILE__, $current); - - echo "
      \n"; + echo "
    \n"; } echo "
  • \n"; diff --git a/code/administrator/modules/mod_messages/mod_messages.php b/code/administrator/modules/mod_messages/mod_messages.php index 296f4e11..36ce8130 100644 --- a/code/administrator/modules/mod_messages/mod_messages.php +++ b/code/administrator/modules/mod_messages/mod_messages.php @@ -1,4 +1,5 @@ getIdentity()->authorise('core.login.admin') || !$app->getIdentity()->authorise('core.manage', 'com_messages')) -{ - return; +if (!$app->getIdentity()->authorise('core.login.admin') || !$app->getIdentity()->authorise('core.manage', 'com_messages')) { + return; } // Try to get the items from the messages model -try -{ - /** @var \Joomla\Component\Messages\Administrator\Model\MessagesModel $messagesModel */ - $messagesModel = $app->bootComponent('com_messages')->getMVCFactory() - ->createModel('Messages', 'Administrator', ['ignore_request' => true]); - $messagesModel->setState('filter.state', 0); - $messages = $messagesModel->getItems(); -} -catch (RuntimeException $e) -{ - $messages = []; +try { + /** @var \Joomla\Component\Messages\Administrator\Model\MessagesModel $messagesModel */ + $messagesModel = $app->bootComponent('com_messages')->getMVCFactory() + ->createModel('Messages', 'Administrator', ['ignore_request' => true]); + $messagesModel->setState('filter.state', 0); + $messages = $messagesModel->getItems(); +} catch (RuntimeException $e) { + $messages = []; - // Still render the error message from the Exception object - $app->enqueueMessage($e->getMessage(), 'error'); + // Still render the error message from the Exception object + $app->enqueueMessage($e->getMessage(), 'error'); } $countUnread = count($messages); diff --git a/code/administrator/modules/mod_messages/mod_messages.xml b/code/administrator/modules/mod_messages/mod_messages.xml index 3c2347de..361cead5 100644 --- a/code/administrator/modules/mod_messages/mod_messages.xml +++ b/code/administrator/modules/mod_messages/mod_messages.xml @@ -2,7 +2,7 @@ mod_messages Joomla! Project - July 2019 + 2019-07 (C) 2019 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_messages/tmpl/default.php b/code/administrator/modules/mod_messages/tmpl/default.php index 43da51c9..77e03cef 100644 --- a/code/administrator/modules/mod_messages/tmpl/default.php +++ b/code/administrator/modules/mod_messages/tmpl/default.php @@ -1,4 +1,5 @@ input->getBool('hidemainmenu'); -if ($hideLinks || $countUnread < 1) -{ - return; +if ($hideLinks || $countUnread < 1) { + return; } $route = 'index.php?option=com_messages&view=messages'; ?> -
    -
    - - -
    -
    -
    - -
    +
    +
    + + +
    +
    +
    + +
    diff --git a/code/administrator/modules/mod_multilangstatus/mod_multilangstatus.php b/code/administrator/modules/mod_multilangstatus/mod_multilangstatus.php index 463f03f1..959e1fb4 100644 --- a/code/administrator/modules/mod_multilangstatus/mod_multilangstatus.php +++ b/code/administrator/modules/mod_multilangstatus/mod_multilangstatus.php @@ -1,4 +1,5 @@ mod_multilangstatus Joomla! Project - September 2011 + 2011-09 (C) 2011 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_multilangstatus/tmpl/default.php b/code/administrator/modules/mod_multilangstatus/tmpl/default.php index d2a1911f..599aaf84 100644 --- a/code/administrator/modules/mod_multilangstatus/tmpl/default.php +++ b/code/administrator/modules/mod_multilangstatus/tmpl/default.php @@ -1,4 +1,5 @@ input->getBool('hidemainmenu'); -if (!$multilanguageEnabled || $hideLinks) -{ - return; +if (!$multilanguageEnabled || $hideLinks) { + return; } $modalHTML = HTMLHelper::_( - 'bootstrap.renderModal', - 'multiLangModal', - array( - 'title' => Text::_('MOD_MULTILANGSTATUS'), - 'url' => Route::_('index.php?option=com_languages&view=multilangstatus&tmpl=component'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) + 'bootstrap.renderModal', + 'multiLangModal', + array( + 'title' => Text::_('MOD_MULTILANGSTATUS'), + 'url' => Route::_('index.php?option=com_languages&view=multilangstatus&tmpl=component'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) ); $app->getDocument()->getWebAssetManager() - ->registerAndUseScript('mod_multilangstatus.admin', 'mod_multilangstatus/admin-multilangstatus.min.js', [], ['type' => 'module', 'defer' => true]); + ->registerAndUseScript('mod_multilangstatus.admin', 'mod_multilangstatus/admin-multilangstatus.min.js', [], ['type' => 'module', 'defer' => true]); ?> -
    - -
    -
    - -
    +
    + +
    +
    + +
    diff --git a/code/administrator/modules/mod_popular/mod_popular.php b/code/administrator/modules/mod_popular/mod_popular.php index 9b6d8594..6e283cfd 100644 --- a/code/administrator/modules/mod_popular/mod_popular.php +++ b/code/administrator/modules/mod_popular/mod_popular.php @@ -1,4 +1,5 @@ get('automatic_title', 0)) -{ - $module->title = PopularHelper::getTitle($params); +if ($params->get('automatic_title', 0)) { + $module->title = PopularHelper::getTitle($params); } // If recording of hits is disabled. -if (!ComponentHelper::getParams('com_content')->get('record_hits', 1)) -{ - echo LayoutHelper::render('joomla.content.emptystate_module', [ - 'title' => 'JGLOBAL_RECORD_HITS_DISABLED', - 'icon' => 'icon-minus-circle', - ] - ); - - return; +if (!ComponentHelper::getParams('com_content')->get('record_hits', 1)) { + echo LayoutHelper::render('joomla.content.emptystate_module', [ + 'title' => 'JGLOBAL_RECORD_HITS_DISABLED', + 'icon' => 'icon-minus-circle', + ]); + + return; } // If there are some articles to display. -if (count($list)) -{ - require ModuleHelper::getLayoutPath('mod_popular', $params->get('layout', 'default')); +if (count($list)) { + require ModuleHelper::getLayoutPath('mod_popular', $params->get('layout', 'default')); - return; + return; } // If there are no articles to display, show empty state. $app->getLanguage()->load('com_content'); echo LayoutHelper::render('joomla.content.emptystate_module', [ - 'textPrefix' => 'COM_CONTENT', - 'icon' => 'icon-copy', - ] -); + 'textPrefix' => 'COM_CONTENT', + 'icon' => 'icon-copy', + ]); diff --git a/code/administrator/modules/mod_popular/mod_popular.xml b/code/administrator/modules/mod_popular/mod_popular.xml index bd3df581..c289ad2d 100644 --- a/code/administrator/modules/mod_popular/mod_popular.xml +++ b/code/administrator/modules/mod_popular/mod_popular.xml @@ -2,7 +2,7 @@ mod_popular Joomla! Project - July 2004 + 2004-07 (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_popular/src/Helper/PopularHelper.php b/code/administrator/modules/mod_popular/src/Helper/PopularHelper.php index 1572b9b8..3f86b71b 100644 --- a/code/administrator/modules/mod_popular/src/Helper/PopularHelper.php +++ b/code/administrator/modules/mod_popular/src/Helper/PopularHelper.php @@ -1,4 +1,5 @@ setState('list.select', 'a.id, a.title, a.checked_out, a.checked_out_time, ' . - ' a.publish_up, a.hits' - ); - - // Set Ordering filter - $model->setState('list.ordering', 'a.hits'); - $model->setState('list.direction', 'DESC'); - - // Set Category Filter - $categoryId = $params->get('catid', null); - - if (is_numeric($categoryId)) - { - $model->setState('filter.category_id', $categoryId); - } - - // Set User Filter. - $userId = $user->get('id'); - - switch ($params->get('user_id', '0')) - { - case 'by_me': - $model->setState('filter.author_id', $userId); - break; - - case 'not_me': - $model->setState('filter.author_id', $userId); - $model->setState('filter.author_id.include', false); - break; - } - - // Set the Start and Limit - $model->setState('list.start', 0); - $model->setState('list.limit', $params->get('count', 5)); - - $items = $model->getItems(); - - if ($error = $model->getError()) - { - throw new \Exception($error, 500); - } - - // Set the links - foreach ($items as &$item) - { - $item->link = ''; - - if ($user->authorise('core.edit', 'com_content.article.' . $item->id) - || ($user->authorise('core.edit.own', 'com_content.article.' . $item->id) && ($userId === $item->created_by))) - { - $item->link = Route::_('index.php?option=com_content&task=article.edit&id=' . $item->id); - } - } - - return $items; - } - - /** - * Get the alternate title for the module - * - * @param Registry $params The module parameters. - * - * @return string The alternate title for the module. - */ - public static function getTitle($params) - { - $who = $params->get('user_id', 0); - $catid = (int) $params->get('catid', null); - $title = ''; - - if ($catid) - { - $category = Categories::getInstance('Content')->get($catid); - $title = Text::_('MOD_POPULAR_UNEXISTING'); - - if ($category) - { - $title = $category->title; - } - } - - return Text::plural( - 'MOD_POPULAR_TITLE' . ($catid ? '_CATEGORY' : '') . ($who != '0' ? "_$who" : ''), - (int) $params->get('count', 5), - $title - ); - } + /** + * Get a list of the most popular articles. + * + * @param Registry &$params The module parameters. + * @param ArticlesModel $model The model. + * + * @return mixed An array of articles, or false on error. + * + * @throws \Exception + */ + public static function getList(Registry &$params, ArticlesModel $model) + { + $user = Factory::getUser(); + + // Set List SELECT + $model->setState('list.select', 'a.id, a.title, a.checked_out, a.checked_out_time, ' . + ' a.publish_up, a.hits'); + + // Set Ordering filter + $model->setState('list.ordering', 'a.hits'); + $model->setState('list.direction', 'DESC'); + + // Set Category Filter + $categoryId = $params->get('catid', null); + + if (is_numeric($categoryId)) { + $model->setState('filter.category_id', $categoryId); + } + + // Set User Filter. + $userId = $user->get('id'); + + switch ($params->get('user_id', '0')) { + case 'by_me': + $model->setState('filter.author_id', $userId); + break; + + case 'not_me': + $model->setState('filter.author_id', $userId); + $model->setState('filter.author_id.include', false); + break; + } + + // Set the Start and Limit + $model->setState('list.start', 0); + $model->setState('list.limit', $params->get('count', 5)); + + $items = $model->getItems(); + + if ($error = $model->getError()) { + throw new \Exception($error, 500); + } + + // Set the links + foreach ($items as &$item) { + $item->link = ''; + + if ( + $user->authorise('core.edit', 'com_content.article.' . $item->id) + || ($user->authorise('core.edit.own', 'com_content.article.' . $item->id) && ($userId === $item->created_by)) + ) { + $item->link = Route::_('index.php?option=com_content&task=article.edit&id=' . $item->id); + } + } + + return $items; + } + + /** + * Get the alternate title for the module + * + * @param Registry $params The module parameters. + * + * @return string The alternate title for the module. + */ + public static function getTitle($params) + { + $who = $params->get('user_id', 0); + $catid = (int) $params->get('catid', null); + $title = ''; + + if ($catid) { + $category = Categories::getInstance('Content')->get($catid); + $title = Text::_('MOD_POPULAR_UNEXISTING'); + + if ($category) { + $title = $category->title; + } + } + + return Text::plural( + 'MOD_POPULAR_TITLE' . ($catid ? '_CATEGORY' : '') . ($who != '0' ? "_$who" : ''), + (int) $params->get('count', 5), + $title + ); + } } diff --git a/code/administrator/modules/mod_popular/tmpl/default.php b/code/administrator/modules/mod_popular/tmpl/default.php index 6992abc8..4949093a 100644 --- a/code/administrator/modules/mod_popular/tmpl/default.php +++ b/code/administrator/modules/mod_popular/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - $item) : ?> - - hits; ?> - = 10000 ? 'danger' : ($hits >= 1000 ? 'warning' : ($hits >= 100 ? 'info' : 'secondary'))); ?> - - - - - - - - - - - - + + + + + + + + + + + $item) : ?> + + hits; ?> + = 10000 ? 'danger' : ($hits >= 1000 ? 'warning' : ($hits >= 100 ? 'info' : 'secondary'))); ?> + + + + + + + + + + + +
    title; ?>
    - checked_out) : ?> - editor, $item->checked_out_time); ?> - - link) : ?> - - title, ENT_QUOTES, 'UTF-8'); ?> - - - title, ENT_QUOTES, 'UTF-8'); ?> - - - hits; ?> - - publish_up, Text::_('DATE_FORMAT_LC4')); ?> -
    - -
    title; ?>
    + checked_out) : ?> + editor, $item->checked_out_time); ?> + + link) : ?> + + title, ENT_QUOTES, 'UTF-8'); ?> + + + title, ENT_QUOTES, 'UTF-8'); ?> + + + hits; ?> + + publish_up, Text::_('DATE_FORMAT_LC4')); ?> +
    + +
    diff --git a/code/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.php b/code/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.php index 1812161d..9f9268c4 100644 --- a/code/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.php +++ b/code/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.php @@ -1,4 +1,5 @@ bootComponent('com_postinstall')->getMVCFactory() - ->createModel('Messages', 'Administrator', ['ignore_request' => true]); - $messagesCount = $messagesModel->getItemsCount(); -} -catch (RuntimeException $e) -{ - $messagesCount = 0; +try { + /** @var \Joomla\Component\Postinstall\Administrator\Model\MessagesModel $messagesModel */ + $messagesModel = $app->bootComponent('com_postinstall')->getMVCFactory() + ->createModel('Messages', 'Administrator', ['ignore_request' => true]); + $messagesCount = $messagesModel->getItemsCount(); +} catch (RuntimeException $e) { + $messagesCount = 0; - // Still render the error message from the Exception object - $app->enqueueMessage($e->getMessage(), 'error'); + // Still render the error message from the Exception object + $app->enqueueMessage($e->getMessage(), 'error'); } $joomlaFilesExtensionId = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; diff --git a/code/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.xml b/code/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.xml index 5b6de655..6dc8eaf9 100644 --- a/code/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.xml +++ b/code/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.xml @@ -2,7 +2,7 @@ mod_post_installation_messages Joomla! Project - July2019 + 2019-07 (C) 2019 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_post_installation_messages/tmpl/default.php b/code/administrator/modules/mod_post_installation_messages/tmpl/default.php index 8efb6503..f2b9cef5 100644 --- a/code/administrator/modules/mod_post_installation_messages/tmpl/default.php +++ b/code/administrator/modules/mod_post_installation_messages/tmpl/default.php @@ -1,4 +1,5 @@ input->getBool('hidemainmenu'); -if ($hideLinks || $messagesCount < 1) -{ - return; +if ($hideLinks || $messagesCount < 1) { + return; } ?> getIdentity()->authorise('core.manage', 'com_postinstall')) : ?> - -
    -
    - - -
    -
    -
    - -
    -
    + +
    +
    + + +
    +
    +
    + +
    +
    diff --git a/code/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.php b/code/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.php index a58da39b..28c4bb5e 100644 --- a/code/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.php +++ b/code/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.php @@ -1,4 +1,5 @@ getIdentity()->authorise('core.admin')) -{ - return; +if (!$app->getIdentity()->authorise('core.admin')) { + return; } // Boot component to ensure HTML helpers are loaded @@ -25,19 +25,15 @@ // Load the privacy component language file. $lang = $app->getLanguage(); $lang->load('com_privacy', JPATH_ADMINISTRATOR) - || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); + || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); $list = PrivacyDashboardHelper::getData(); -if (count($list)) -{ - require ModuleHelper::getLayoutPath('mod_privacy_dashboard', $params->get('layout', 'default')); -} -else -{ - echo LayoutHelper::render('joomla.content.emptystate_module', [ - 'textPrefix' => 'COM_PRIVACY_REQUESTS', - 'icon' => 'icon-lock', - ] - ); +if (count($list)) { + require ModuleHelper::getLayoutPath('mod_privacy_dashboard', $params->get('layout', 'default')); +} else { + echo LayoutHelper::render('joomla.content.emptystate_module', [ + 'textPrefix' => 'COM_PRIVACY_REQUESTS', + 'icon' => 'icon-lock', + ]); } diff --git a/code/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.xml b/code/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.xml index 0b8398e0..e8dd47cd 100644 --- a/code/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.xml +++ b/code/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.xml @@ -2,7 +2,7 @@ mod_privacy_dashboard Joomla! Project - June 2018 + 2018-06 (C) 2018 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_privacy_dashboard/src/Helper/PrivacyDashboardHelper.php b/code/administrator/modules/mod_privacy_dashboard/src/Helper/PrivacyDashboardHelper.php index 7a7088d6..7e61ff13 100644 --- a/code/administrator/modules/mod_privacy_dashboard/src/Helper/PrivacyDashboardHelper.php +++ b/code/administrator/modules/mod_privacy_dashboard/src/Helper/PrivacyDashboardHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select( - [ - 'COUNT(*) AS count', - $db->quoteName('status'), - $db->quoteName('request_type'), - ] - ) - ->from($db->quoteName('#__privacy_requests')) - ->group($db->quoteName('status')) - ->group($db->quoteName('request_type')); + /** + * Method to retrieve information about the site privacy requests + * + * @return array Array containing site privacy requests + * + * @since 3.9.0 + */ + public static function getData() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + 'COUNT(*) AS count', + $db->quoteName('status'), + $db->quoteName('request_type'), + ] + ) + ->from($db->quoteName('#__privacy_requests')) + ->group($db->quoteName('status')) + ->group($db->quoteName('request_type')); - $db->setQuery($query); + $db->setQuery($query); - try - { - return $db->loadObjectList(); - } - catch (ExecutionFailureException $e) - { - return []; - } - } + try { + return $db->loadObjectList(); + } catch (ExecutionFailureException $e) { + return []; + } + } } diff --git a/code/administrator/modules/mod_privacy_dashboard/tmpl/default.php b/code/administrator/modules/mod_privacy_dashboard/tmpl/default.php index 0450c0f4..c82f8205 100644 --- a/code/administrator/modules/mod_privacy_dashboard/tmpl/default.php +++ b/code/administrator/modules/mod_privacy_dashboard/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - $item) : ?> - status, array(0, 1))) : ?> - count; ?> - - count; ?> - - - - - - - - - - - - + + + + + + + + + + + $item) : ?> + status, array(0, 1))) : ?> + count; ?> + + count; ?> + + + + + + + + + + + +
    title; ?>
    - - request_type); ?> - - - status); ?> - - count; ?> -
    - -
    title; ?>
    + + request_type); ?> + + + status); ?> + + count; ?> +
    + +
    -
    -
    -
    -
    +
    +
    +
    +
    diff --git a/code/administrator/modules/mod_privacy_status/mod_privacy_status.php b/code/administrator/modules/mod_privacy_status/mod_privacy_status.php index 964b1511..afa43af9 100644 --- a/code/administrator/modules/mod_privacy_status/mod_privacy_status.php +++ b/code/administrator/modules/mod_privacy_status/mod_privacy_status.php @@ -1,4 +1,5 @@ getIdentity()->authorise('core.admin')) -{ - return; +if (!$app->getIdentity()->authorise('core.admin')) { + return; } // Boot component to ensure HTML helpers are loaded @@ -27,7 +27,7 @@ // Load the privacy component language file. $lang = $app->getLanguage(); $lang->load('com_privacy', JPATH_ADMINISTRATOR) - || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); + || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); $privacyPolicyInfo = PrivacyStatusHelper::getPrivacyPolicyInfo(); $requestFormPublished = PrivacyStatusHelper::getRequestFormPublished(); diff --git a/code/administrator/modules/mod_privacy_status/mod_privacy_status.xml b/code/administrator/modules/mod_privacy_status/mod_privacy_status.xml index 69c7a6c5..ae78cd22 100644 --- a/code/administrator/modules/mod_privacy_status/mod_privacy_status.xml +++ b/code/administrator/modules/mod_privacy_status/mod_privacy_status.xml @@ -2,7 +2,7 @@ mod_privacy_status Joomla! Project - July 2019 + 2019-07 (C) 2019 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_privacy_status/src/Helper/PrivacyStatusHelper.php b/code/administrator/modules/mod_privacy_status/src/Helper/PrivacyStatusHelper.php index 2bbeab66..b584b760 100644 --- a/code/administrator/modules/mod_privacy_status/src/Helper/PrivacyStatusHelper.php +++ b/code/administrator/modules/mod_privacy_status/src/Helper/PrivacyStatusHelper.php @@ -1,4 +1,5 @@ false, - 'articlePublished' => false, - 'editLink' => '', - ]; - - /* - * Prior to 3.9.0 it was common for a plugin such as the User - Profile plugin to define a privacy policy or - * terms of service article, therefore we will also import the user plugin group to process this event. - */ - PluginHelper::importPlugin('privacy'); - PluginHelper::importPlugin('user'); - - Factory::getApplication()->triggerEvent('onPrivacyCheckPrivacyPolicyPublished', [&$policy]); - - return $policy; - } - - /** - * Check whether there is a menu item for the request form - * - * @return array Array containing a status of whether a menu is published for the request form and its current link - * - * @since 4.0.0 - */ - public static function getRequestFormPublished() - { - $status = [ - 'exists' => false, - 'published' => false, - 'link' => '', - ]; - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id'), - $db->quoteName('published'), - $db->quoteName('language'), - ] - ) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('client_id') . ' = 0', - $db->quoteName('link') . ' = ' . $db->quote('index.php?option=com_privacy&view=request'), - ] - ) - ->setLimit(1); - $db->setQuery($query); - - $menuItem = $db->loadObject(); - - // Check if the menu item exists in database - if ($menuItem) - { - $status['exists'] = true; - - // Check if the menu item is published - if ($menuItem->published == 1) - { - $status['published'] = true; - } - - // Add language to the url if the site is multilingual - if (Multilanguage::isEnabled() && $menuItem->language && $menuItem->language !== '*') - { - $lang = '&lang=' . $menuItem->language; - } - else - { - $lang = ''; - } - } - - $linkMode = Factory::getApplication()->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - if (!$menuItem) - { - if (Multilanguage::isEnabled()) - { - // Find the Itemid of the home menu item tagged to the site default language - $params = ComponentHelper::getParams('com_languages'); - $defaultSiteLanguage = $params->get('site'); - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('client_id') . ' = 0', - $db->quoteName('home') . ' = 1', - $db->quoteName('language') . ' = :language', - ] - ) - ->bind(':language', $defaultSiteLanguage) - ->setLimit(1); - $db->setQuery($query); - - $homeId = (int) $db->loadResult(); - $itemId = $homeId ? '&Itemid=' . $homeId : ''; - } - else - { - $itemId = ''; - } - - $status['link'] = Route::link('site', 'index.php?option=com_privacy&view=request' . $itemId, true, $linkMode); - } - else - { - $status['link'] = Route::link('site', 'index.php?Itemid=' . $menuItem->id . $lang, true, $linkMode); - } - - return $status; - } - - /** - * Method to return number privacy requests older than X days. - * - * @return integer - * - * @since 4.0.0 - */ - public static function getNumberUrgentRequests() - { - // Load the parameters. - $params = ComponentHelper::getComponent('com_privacy')->getParams(); - $notify = (int) $params->get('notify', 14); - $now = Factory::getDate()->toSql(); - $period = '-' . $notify; - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select('COUNT(*)') - ->from($db->quoteName('#__privacy_requests')) - ->where( - [ - $db->quoteName('status') . ' = 1', - $query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('requested_at'), - ] - ); - $db->setQuery($query); - - return (int) $db->loadResult(); - } + /** + * Get the information about the published privacy policy + * + * @return array Array containing a status of whether a privacy policy is set and a link to the policy document for editing + * + * @since 4.0.0 + */ + public static function getPrivacyPolicyInfo() + { + $policy = [ + 'published' => false, + 'articlePublished' => false, + 'editLink' => '', + ]; + + /* + * Prior to 3.9.0 it was common for a plugin such as the User - Profile plugin to define a privacy policy or + * terms of service article, therefore we will also import the user plugin group to process this event. + */ + PluginHelper::importPlugin('privacy'); + PluginHelper::importPlugin('user'); + + Factory::getApplication()->triggerEvent('onPrivacyCheckPrivacyPolicyPublished', [&$policy]); + + return $policy; + } + + /** + * Check whether there is a menu item for the request form + * + * @return array Array containing a status of whether a menu is published for the request form and its current link + * + * @since 4.0.0 + */ + public static function getRequestFormPublished() + { + $status = [ + 'exists' => false, + 'published' => false, + 'link' => '', + ]; + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id'), + $db->quoteName('published'), + $db->quoteName('language'), + ] + ) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('client_id') . ' = 0', + $db->quoteName('link') . ' = ' . $db->quote('index.php?option=com_privacy&view=request'), + ] + ) + ->setLimit(1); + $db->setQuery($query); + + $menuItem = $db->loadObject(); + + // Check if the menu item exists in database + if ($menuItem) { + $status['exists'] = true; + + // Check if the menu item is published + if ($menuItem->published == 1) { + $status['published'] = true; + } + + // Add language to the url if the site is multilingual + if (Multilanguage::isEnabled() && $menuItem->language && $menuItem->language !== '*') { + $lang = '&lang=' . $menuItem->language; + } else { + $lang = ''; + } + } + + $linkMode = Factory::getApplication()->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + if (!$menuItem) { + if (Multilanguage::isEnabled()) { + // Find the Itemid of the home menu item tagged to the site default language + $params = ComponentHelper::getParams('com_languages'); + $defaultSiteLanguage = $params->get('site'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('client_id') . ' = 0', + $db->quoteName('home') . ' = 1', + $db->quoteName('language') . ' = :language', + ] + ) + ->bind(':language', $defaultSiteLanguage) + ->setLimit(1); + $db->setQuery($query); + + $homeId = (int) $db->loadResult(); + $itemId = $homeId ? '&Itemid=' . $homeId : ''; + } else { + $itemId = ''; + } + + $status['link'] = Route::link('site', 'index.php?option=com_privacy&view=request' . $itemId, true, $linkMode); + } else { + $status['link'] = Route::link('site', 'index.php?Itemid=' . $menuItem->id . $lang, true, $linkMode); + } + + return $status; + } + + /** + * Method to return number privacy requests older than X days. + * + * @return integer + * + * @since 4.0.0 + */ + public static function getNumberUrgentRequests() + { + // Load the parameters. + $params = ComponentHelper::getComponent('com_privacy')->getParams(); + $notify = (int) $params->get('notify', 14); + $now = Factory::getDate()->toSql(); + $period = '-' . $notify; + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select('COUNT(*)') + ->from($db->quoteName('#__privacy_requests')) + ->where( + [ + $db->quoteName('status') . ' = 1', + $query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('requested_at'), + ] + ); + $db->setQuery($query); + + return (int) $db->loadResult(); + } } diff --git a/code/administrator/modules/mod_privacy_status/tmpl/default.php b/code/administrator/modules/mod_privacy_status/tmpl/default.php index 35bc270b..5b1c3663 100644 --- a/code/administrator/modules/mod_privacy_status/tmpl/default.php +++ b/code/administrator/modules/mod_privacy_status/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    - - - - - - - - - - - - - - - - - -
    - - - - - - -
    - - - - - - - - - - - - - - - - - -
    - - - -
    - - - - - - - - - - - - -
    - - 0) : ?> - - -
    - - - - - - - - - - - - - -
    - - -
    - -
    - - - - - - - - - - - - - - - - - -
    + + + + + + + + + + + + + + + + + +
    + + + + + + +
    + + + + + + + + + + + + + + + + + +
    + + + +
    + + + + + + + + + + + + +
    + + 0) : ?> + + +
    + + + + + + + + + + + + + +
    + + +
    + +
    + + + + + + + + + + + + + + + + + +
    diff --git a/code/administrator/modules/mod_quickicon/mod_quickicon.xml b/code/administrator/modules/mod_quickicon/mod_quickicon.xml index 6b8935c4..a983ae20 100644 --- a/code/administrator/modules/mod_quickicon/mod_quickicon.xml +++ b/code/administrator/modules/mod_quickicon/mod_quickicon.xml @@ -2,7 +2,7 @@ mod_quickicon Joomla! Project - November 2005 + 2005-11 (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org @@ -11,8 +11,7 @@ MOD_QUICKICON_XML_DESCRIPTION Joomla\Module\Quickicon - mod_quickicon.php - services + services src tmpl diff --git a/code/administrator/modules/mod_quickicon/services/provider.php b/code/administrator/modules/mod_quickicon/services/provider.php index 86c39696..34f4e43c 100644 --- a/code/administrator/modules/mod_quickicon/services/provider.php +++ b/code/administrator/modules/mod_quickicon/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new ModuleDispatcherFactory('\\Joomla\\Module\\Quickicon')); - $container->registerServiceProvider(new HelperFactory('\\Joomla\\Module\\Quickicon\\Administrator\\Helper')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new ModuleDispatcherFactory('\\Joomla\\Module\\Quickicon')); + $container->registerServiceProvider(new HelperFactory('\\Joomla\\Module\\Quickicon\\Administrator\\Helper')); - $container->registerServiceProvider(new Module); - } + $container->registerServiceProvider(new Module()); + } }; diff --git a/code/administrator/modules/mod_quickicon/src/Dispatcher/Dispatcher.php b/code/administrator/modules/mod_quickicon/src/Dispatcher/Dispatcher.php index f0475627..745474ad 100644 --- a/code/administrator/modules/mod_quickicon/src/Dispatcher/Dispatcher.php +++ b/code/administrator/modules/mod_quickicon/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->bootModule('mod_quickicon', 'administrator')->getHelper('QuickIconHelper'); - $data['buttons'] = $helper->getButtons($data['params'], $this->getApplication()); + $helper = $this->app->bootModule('mod_quickicon', 'administrator')->getHelper('QuickIconHelper'); + $data['buttons'] = $helper->getButtons($data['params'], $this->getApplication()); - return $data; - } + return $data; + } } diff --git a/code/administrator/modules/mod_quickicon/src/Event/QuickIconsEvent.php b/code/administrator/modules/mod_quickicon/src/Event/QuickIconsEvent.php index 3e0da899..03dda783 100644 --- a/code/administrator/modules/mod_quickicon/src/Event/QuickIconsEvent.php +++ b/code/administrator/modules/mod_quickicon/src/Event/QuickIconsEvent.php @@ -1,4 +1,5 @@ context; - } - - /** - * Set the event context - * - * @param string $context The event context - * - * @return string - * - * @since 4.0.0 - */ - public function setContext($context) - { - $this->context = $context; - - return $context; - } + /** + * The event context + * + * @var string + * @since 4.0.0 + */ + private $context; + + /** + * Get the event context + * + * @return string + * + * @since 4.0.0 + */ + public function getContext() + { + return $this->context; + } + + /** + * Set the event context + * + * @param string $context The event context + * + * @return string + * + * @since 4.0.0 + */ + public function setContext($context) + { + $this->context = $context; + + return $context; + } } diff --git a/code/administrator/modules/mod_quickicon/src/Helper/QuickIconHelper.php b/code/administrator/modules/mod_quickicon/src/Helper/QuickIconHelper.php index e6496e28..739e8bb3 100644 --- a/code/administrator/modules/mod_quickicon/src/Helper/QuickIconHelper.php +++ b/code/administrator/modules/mod_quickicon/src/Helper/QuickIconHelper.php @@ -1,4 +1,5 @@ get('context', 'mod_quickicon'); - - if (!isset($this->buttons[$key])) - { - // Load mod_quickicon language file in case this method is called before rendering the module - $application->getLanguage()->load('mod_quickicon'); - - $this->buttons[$key] = []; - - if ($params->get('show_users')) - { - $tmp = [ - 'image' => 'icon-users', - 'link' => Route::_('index.php?option=com_users&view=users'), - 'linkadd' => Route::_('index.php?option=com_users&task=user.add'), - 'name' => 'MOD_QUICKICON_USER_MANAGER', - 'access' => array('core.manage', 'com_users', 'core.create', 'com_users'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - - if ($params->get('show_users') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_users&task=users.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_menuitems')) - { - $tmp = [ - 'image' => 'icon-list', - 'link' => Route::_('index.php?option=com_menus&view=items&menutype='), - 'linkadd' => Route::_('index.php?option=com_menus&task=item.add'), - 'name' => 'MOD_QUICKICON_MENUITEMS_MANAGER', - 'access' => array('core.manage', 'com_menus', 'core.create', 'com_menus'), - 'group' => 'MOD_QUICKICON_STRUCTURE', - ]; - - if ($params->get('show_menuitems') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_menus&task=items.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_articles')) - { - $tmp = [ - 'image' => 'icon-file-alt', - 'link' => Route::_('index.php?option=com_content&view=articles'), - 'linkadd' => Route::_('index.php?option=com_content&task=article.add'), - 'name' => 'MOD_QUICKICON_ARTICLE_MANAGER', - 'access' => array('core.manage', 'com_content', 'core.create', 'com_content'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - - if ($params->get('show_articles') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_content&task=articles.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_tags')) - { - $tmp = [ - 'image' => 'icon-tag', - 'link' => Route::_('index.php?option=com_tags&view=tags'), - 'linkadd' => Route::_('index.php?option=com_tags&task=tag.edit'), - 'name' => 'MOD_QUICKICON_TAGS_MANAGER', - 'access' => array('core.manage', 'com_tags', 'core.create', 'com_tags'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - - if ($params->get('show_tags') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_tags&task=tags.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_categories')) - { - $tmp = [ - 'image' => 'icon-folder-open', - 'link' => Route::_('index.php?option=com_categories&view=categories&extension=com_content'), - 'linkadd' => Route::_('index.php?option=com_categories&task=category.add'), - 'name' => 'MOD_QUICKICON_CATEGORY_MANAGER', - 'access' => array('core.manage', 'com_categories', 'core.create', 'com_categories'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - - if ($params->get('show_categories') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_categories&task=categories.getQuickiconContent&extension=content&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_media')) - { - $this->buttons[$key][] = [ - 'image' => 'icon-images', - 'link' => Route::_('index.php?option=com_media'), - 'name' => 'MOD_QUICKICON_MEDIA_MANAGER', - 'access' => array('core.manage', 'com_media'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - } - - if ($params->get('show_modules')) - { - $tmp = [ - 'image' => 'icon-cube', - 'link' => Route::_('index.php?option=com_modules&view=modules&client_id=0'), - 'linkadd' => Route::_('index.php?option=com_modules&view=select&client_id=0'), - 'name' => 'MOD_QUICKICON_MODULE_MANAGER', - 'access' => array('core.manage', 'com_modules'), - 'group' => 'MOD_QUICKICON_SITE' - ]; - - if ($params->get('show_modules') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_modules&task=modules.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_plugins')) - { - $tmp = [ - 'image' => 'icon-plug', - 'link' => Route::_('index.php?option=com_plugins'), - 'name' => 'MOD_QUICKICON_PLUGIN_MANAGER', - 'access' => array('core.manage', 'com_plugins'), - 'group' => 'MOD_QUICKICON_SITE' - ]; - - if ($params->get('show_plugins') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_plugins&task=plugins.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_template_styles')) - { - $this->buttons[$key][] = [ - 'image' => 'icon-paint-brush', - 'link' => Route::_('index.php?option=com_templates&view=styles&client_id=0'), - 'name' => 'MOD_QUICKICON_TEMPLATE_STYLES', - 'access' => array('core.admin', 'com_templates'), - 'group' => 'MOD_QUICKICON_SITE' - ]; - } - - if ($params->get('show_template_code')) - { - $this->buttons[$key][] = [ - 'image' => 'icon-code', - 'link' => Route::_('index.php?option=com_templates&view=templates&client_id=0'), - 'name' => 'MOD_QUICKICON_TEMPLATE_CODE', - 'access' => array('core.admin', 'com_templates'), - 'group' => 'MOD_QUICKICON_SITE' - ]; - } - - if ($params->get('show_checkin')) - { - $tmp = [ - 'image' => 'icon-unlock-alt', - 'link' => Route::_('index.php?option=com_checkin'), - 'name' => 'MOD_QUICKICON_CHECKINS', - 'access' => array('core.admin', 'com_checkin'), - 'group' => 'MOD_QUICKICON_SYSTEM' - ]; - - if ($params->get('show_checkin') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_checkin&task=getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_cache')) - { - $tmp = [ - 'image' => 'icon-cloud', - 'link' => Route::_('index.php?option=com_cache'), - 'name' => 'MOD_QUICKICON_CACHE', - 'access' => array('core.admin', 'com_cache'), - 'group' => 'MOD_QUICKICON_SYSTEM' - ]; - - if ($params->get('show_cache') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_cache&task=display.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_global')) - { - $this->buttons[$key][] = [ - 'image' => 'icon-cog', - 'link' => Route::_('index.php?option=com_config'), - 'name' => 'MOD_QUICKICON_GLOBAL_CONFIGURATION', - 'access' => array('core.manage', 'com_config', 'core.admin', 'com_config'), - 'group' => 'MOD_QUICKICON_SYSTEM', - ]; - } - - PluginHelper::importPlugin('quickicon'); - - $arrays = (array) $application->triggerEvent( - 'onGetIcons', - new QuickIconsEvent('onGetIcons', ['context' => $context]) - ); - - foreach ($arrays as $response) - { - if (!\is_array($response)) - { - continue; - } - - foreach ($response as $icon) - { - $default = array( - 'link' => null, - 'image' => null, - 'text' => null, - 'name' => null, - 'linkadd' => null, - 'access' => true, - 'class' => null, - 'group' => 'MOD_QUICKICON', - ); - - $icon = array_merge($default, $icon); - - if (!\is_null($icon['link']) && (!\is_null($icon['text']) || !\is_null($icon['name']))) - { - $this->buttons[$key][] = $icon; - } - } - } - } - - return $this->buttons[$key]; - } + /** + * Stack to hold buttons + * + * @var array[] + * @since 1.6 + */ + protected $buttons = array(); + + /** + * Helper method to return button list. + * + * This method returns the array by reference so it can be + * used to add custom buttons or remove default ones. + * + * @param Registry $params The module parameters + * @param CMSApplication $application The application + * + * @return array An array of buttons + * + * @since 1.6 + */ + public function getButtons(Registry $params, CMSApplication $application = null) + { + if ($application == null) { + $application = Factory::getApplication(); + } + + $key = (string) $params; + $context = (string) $params->get('context', 'mod_quickicon'); + + if (!isset($this->buttons[$key])) { + // Load mod_quickicon language file in case this method is called before rendering the module + $application->getLanguage()->load('mod_quickicon'); + + $this->buttons[$key] = []; + + if ($params->get('show_users')) { + $tmp = [ + 'image' => 'icon-users', + 'link' => Route::_('index.php?option=com_users&view=users'), + 'linkadd' => Route::_('index.php?option=com_users&task=user.add'), + 'name' => 'MOD_QUICKICON_USER_MANAGER', + 'access' => array('core.manage', 'com_users', 'core.create', 'com_users'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + + if ($params->get('show_users') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_users&task=users.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_menuitems')) { + $tmp = [ + 'image' => 'icon-list', + 'link' => Route::_('index.php?option=com_menus&view=items&menutype='), + 'linkadd' => Route::_('index.php?option=com_menus&task=item.add'), + 'name' => 'MOD_QUICKICON_MENUITEMS_MANAGER', + 'access' => array('core.manage', 'com_menus', 'core.create', 'com_menus'), + 'group' => 'MOD_QUICKICON_STRUCTURE', + ]; + + if ($params->get('show_menuitems') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_menus&task=items.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_articles')) { + $tmp = [ + 'image' => 'icon-file-alt', + 'link' => Route::_('index.php?option=com_content&view=articles'), + 'linkadd' => Route::_('index.php?option=com_content&task=article.add'), + 'name' => 'MOD_QUICKICON_ARTICLE_MANAGER', + 'access' => array('core.manage', 'com_content', 'core.create', 'com_content'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + + if ($params->get('show_articles') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_content&task=articles.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_tags')) { + $tmp = [ + 'image' => 'icon-tag', + 'link' => Route::_('index.php?option=com_tags&view=tags'), + 'linkadd' => Route::_('index.php?option=com_tags&task=tag.edit'), + 'name' => 'MOD_QUICKICON_TAGS_MANAGER', + 'access' => array('core.manage', 'com_tags', 'core.create', 'com_tags'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + + if ($params->get('show_tags') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_tags&task=tags.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_categories')) { + $tmp = [ + 'image' => 'icon-folder-open', + 'link' => Route::_('index.php?option=com_categories&view=categories&extension=com_content'), + 'linkadd' => Route::_('index.php?option=com_categories&task=category.add'), + 'name' => 'MOD_QUICKICON_CATEGORY_MANAGER', + 'access' => array('core.manage', 'com_categories', 'core.create', 'com_categories'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + + if ($params->get('show_categories') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_categories&task=categories.getQuickiconContent&extension=content&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_media')) { + $this->buttons[$key][] = [ + 'image' => 'icon-images', + 'link' => Route::_('index.php?option=com_media'), + 'name' => 'MOD_QUICKICON_MEDIA_MANAGER', + 'access' => array('core.manage', 'com_media'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + } + + if ($params->get('show_modules')) { + $tmp = [ + 'image' => 'icon-cube', + 'link' => Route::_('index.php?option=com_modules&view=modules&client_id=0'), + 'linkadd' => Route::_('index.php?option=com_modules&view=select&client_id=0'), + 'name' => 'MOD_QUICKICON_MODULE_MANAGER', + 'access' => array('core.manage', 'com_modules'), + 'group' => 'MOD_QUICKICON_SITE' + ]; + + if ($params->get('show_modules') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_modules&task=modules.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_plugins')) { + $tmp = [ + 'image' => 'icon-plug', + 'link' => Route::_('index.php?option=com_plugins'), + 'name' => 'MOD_QUICKICON_PLUGIN_MANAGER', + 'access' => array('core.manage', 'com_plugins'), + 'group' => 'MOD_QUICKICON_SITE' + ]; + + if ($params->get('show_plugins') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_plugins&task=plugins.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_template_styles')) { + $this->buttons[$key][] = [ + 'image' => 'icon-paint-brush', + 'link' => Route::_('index.php?option=com_templates&view=styles&client_id=0'), + 'name' => 'MOD_QUICKICON_TEMPLATE_STYLES', + 'access' => array('core.admin', 'com_templates'), + 'group' => 'MOD_QUICKICON_SITE' + ]; + } + + if ($params->get('show_template_code')) { + $this->buttons[$key][] = [ + 'image' => 'icon-code', + 'link' => Route::_('index.php?option=com_templates&view=templates&client_id=0'), + 'name' => 'MOD_QUICKICON_TEMPLATE_CODE', + 'access' => array('core.admin', 'com_templates'), + 'group' => 'MOD_QUICKICON_SITE' + ]; + } + + if ($params->get('show_checkin')) { + $tmp = [ + 'image' => 'icon-unlock-alt', + 'link' => Route::_('index.php?option=com_checkin'), + 'name' => 'MOD_QUICKICON_CHECKINS', + 'access' => array('core.admin', 'com_checkin'), + 'group' => 'MOD_QUICKICON_SYSTEM' + ]; + + if ($params->get('show_checkin') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_checkin&task=getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_cache')) { + $tmp = [ + 'image' => 'icon-cloud', + 'link' => Route::_('index.php?option=com_cache'), + 'name' => 'MOD_QUICKICON_CACHE', + 'access' => array('core.admin', 'com_cache'), + 'group' => 'MOD_QUICKICON_SYSTEM' + ]; + + if ($params->get('show_cache') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_cache&task=display.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_global')) { + $this->buttons[$key][] = [ + 'image' => 'icon-cog', + 'link' => Route::_('index.php?option=com_config'), + 'name' => 'MOD_QUICKICON_GLOBAL_CONFIGURATION', + 'access' => array('core.manage', 'com_config', 'core.admin', 'com_config'), + 'group' => 'MOD_QUICKICON_SYSTEM', + ]; + } + + PluginHelper::importPlugin('quickicon'); + + $arrays = (array) $application->triggerEvent( + 'onGetIcons', + new QuickIconsEvent('onGetIcons', ['context' => $context]) + ); + + foreach ($arrays as $response) { + if (!\is_array($response)) { + continue; + } + + foreach ($response as $icon) { + $default = array( + 'link' => null, + 'image' => null, + 'text' => null, + 'name' => null, + 'linkadd' => null, + 'access' => true, + 'class' => null, + 'group' => 'MOD_QUICKICON', + ); + + $icon = array_merge($default, $icon); + + if (!\is_null($icon['link']) && (!\is_null($icon['text']) || !\is_null($icon['name']))) { + $this->buttons[$key][] = $icon; + } + } + } + } + + return $this->buttons[$key]; + } } diff --git a/code/administrator/modules/mod_quickicon/tmpl/default.php b/code/administrator/modules/mod_quickicon/tmpl/default.php index effa3b2e..058db0db 100644 --- a/code/administrator/modules/mod_quickicon/tmpl/default.php +++ b/code/administrator/modules/mod_quickicon/tmpl/default.php @@ -1,4 +1,5 @@ - + diff --git a/code/administrator/modules/mod_sampledata/mod_sampledata.php b/code/administrator/modules/mod_sampledata/mod_sampledata.php index 7feac851..79ffb013 100644 --- a/code/administrator/modules/mod_sampledata/mod_sampledata.php +++ b/code/administrator/modules/mod_sampledata/mod_sampledata.php @@ -1,4 +1,5 @@ mod_sampledata Joomla! Project - July 2017 + 2017-07 (C) 2017 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_sampledata/src/Helper/SampledataHelper.php b/code/administrator/modules/mod_sampledata/src/Helper/SampledataHelper.php index 3d688971..62656dfe 100644 --- a/code/administrator/modules/mod_sampledata/src/Helper/SampledataHelper.php +++ b/code/administrator/modules/mod_sampledata/src/Helper/SampledataHelper.php @@ -1,4 +1,5 @@ getDispatcher() - ->dispatch( - 'onSampledataGetOverview', - AbstractEvent::create( - 'onSampledataGetOverview', - [ - 'subject' => new \stdClass, - ] - ) - ) - ->getArgument('result') ?? []; - } + return Factory::getApplication() + ->getDispatcher() + ->dispatch( + 'onSampledataGetOverview', + AbstractEvent::create( + 'onSampledataGetOverview', + [ + 'subject' => new \stdClass(), + ] + ) + ) + ->getArgument('result') ?? []; + } } diff --git a/code/administrator/modules/mod_sampledata/tmpl/default.php b/code/administrator/modules/mod_sampledata/tmpl/default.php index 6effa8da..f1159997 100644 --- a/code/administrator/modules/mod_sampledata/tmpl/default.php +++ b/code/administrator/modules/mod_sampledata/tmpl/default.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager() - ->registerAndUseScript('mod_sampledata', 'mod_sampledata/sampledata-process.js', [], ['type' => 'module'], ['core']); + ->registerAndUseScript('mod_sampledata', 'mod_sampledata/sampledata-process.js', [], ['type' => 'module'], ['core']); Text::script('MOD_SAMPLEDATA_COMPLETED'); Text::script('MOD_SAMPLEDATA_CONFIRM_START'); @@ -21,37 +22,37 @@ Text::script('MOD_SAMPLEDATA_INVALID_RESPONSE'); $app->getDocument()->addScriptOptions( - 'sample-data', - [ - 'icon' => Uri::root(true) . '/media/system/images/ajax-loader.gif', - ] + 'sample-data', + [ + 'icon' => Uri::root(true) . '/media/system/images/ajax-loader.gif', + ] ); ?> -
      - $item) : ?> -
    • -
      -
      - - title, ENT_QUOTES, 'UTF-8'); ?> -
      - -
      -

      description; ?>

      -
    • - -
    • -
      -
      -
      -
    • - -
    - - - +
      + $item) : ?> +
    • +
      +
      + + title, ENT_QUOTES, 'UTF-8'); ?> +
      + +
      +

      description; ?>

      +
    • + +
    • +
      +
      +
      +
    • + +
    + + + diff --git a/code/administrator/modules/mod_stats_admin/mod_stats_admin.php b/code/administrator/modules/mod_stats_admin/mod_stats_admin.php index 1e23913e..49e9cb3f 100644 --- a/code/administrator/modules/mod_stats_admin/mod_stats_admin.php +++ b/code/administrator/modules/mod_stats_admin/mod_stats_admin.php @@ -1,4 +1,5 @@ mod_stats_admin Joomla! Project - July 2004 + 2004-07 (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_stats_admin/src/Helper/StatsAdminHelper.php b/code/administrator/modules/mod_stats_admin/src/Helper/StatsAdminHelper.php index 931d763f..a796e2b7 100644 --- a/code/administrator/modules/mod_stats_admin/src/Helper/StatsAdminHelper.php +++ b/code/administrator/modules/mod_stats_admin/src/Helper/StatsAdminHelper.php @@ -1,4 +1,5 @@ getIdentity(); - - $rows = array(); - $query = $db->getQuery(true); - - $serverinfo = $params->get('serverinfo', 0); - $siteinfo = $params->get('siteinfo', 0); - - $i = 0; - - if ($serverinfo) - { - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_PHP'); - $rows[$i]->icon = 'cogs'; - $rows[$i]->data = PHP_VERSION; - $i++; - - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_($db->name); - $rows[$i]->icon = 'database'; - $rows[$i]->data = $db->getVersion(); - $i++; - - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_CACHING'); - $rows[$i]->icon = 'tachometer-alt'; - $rows[$i]->data = $app->get('caching') ? Text::_('JENABLED') : Text::_('JDISABLED'); - $i++; - - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_GZIP'); - $rows[$i]->icon = 'bolt'; - $rows[$i]->data = $app->get('gzip') ? Text::_('JENABLED') : Text::_('JDISABLED'); - $i++; - } - - if ($siteinfo) - { - $query->select('COUNT(id) AS count_users') - ->from('#__users'); - $db->setQuery($query); - - try - { - $users = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $users = false; - } - - $query->clear() - ->select('COUNT(id) AS count_items') - ->from('#__content') - ->where('state = 1'); - $db->setQuery($query); - - try - { - $items = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $items = false; - } - - if ($users) - { - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_USERS'); - $rows[$i]->icon = 'users'; - $rows[$i]->data = $users; - - if ($user->authorise('core.manage', 'com_users')) - { - $rows[$i]->link = Route::_('index.php?option=com_users'); - } - - $i++; - } - - if ($items) - { - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_ARTICLES'); - $rows[$i]->icon = 'file'; - $rows[$i]->data = $items; - $rows[$i]->link = Route::_('index.php?option=com_content&view=articles&filter[published]=1'); - $i++; - } - } - - // Include additional data defined by published system plugins - PluginHelper::importPlugin('system'); - - $arrays = (array) $app->triggerEvent('onGetStats', array('mod_stats_admin')); - - foreach ($arrays as $response) - { - foreach ($response as $row) - { - // We only add a row if the title and data are given - if (isset($row['title']) && isset($row['data'])) - { - $rows[$i] = new \stdClass; - $rows[$i]->title = $row['title']; - $rows[$i]->icon = $row['icon'] ?? 'info'; - $rows[$i]->data = $row['data']; - $rows[$i]->link = isset($row['link']) ? $row['link'] : null; - $i++; - } - } - } - - return $rows; - } + /** + * Method to retrieve information about the site + * + * @param Registry $params The module parameters + * @param CMSApplication $app The application + * @param DatabaseInterface $db The database + * + * @return array Array containing site information + * + * @since 3.0 + */ + public static function getStats(Registry $params, CMSApplication $app, DatabaseInterface $db) + { + $user = $app->getIdentity(); + + $rows = array(); + $query = $db->getQuery(true); + + $serverinfo = $params->get('serverinfo', 0); + $siteinfo = $params->get('siteinfo', 0); + + $i = 0; + + if ($serverinfo) { + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_PHP'); + $rows[$i]->icon = 'cogs'; + $rows[$i]->data = PHP_VERSION; + $i++; + + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_($db->name); + $rows[$i]->icon = 'database'; + $rows[$i]->data = $db->getVersion(); + $i++; + + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_CACHING'); + $rows[$i]->icon = 'tachometer-alt'; + $rows[$i]->data = $app->get('caching') ? Text::_('JENABLED') : Text::_('JDISABLED'); + $i++; + + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_GZIP'); + $rows[$i]->icon = 'bolt'; + $rows[$i]->data = $app->get('gzip') ? Text::_('JENABLED') : Text::_('JDISABLED'); + $i++; + } + + if ($siteinfo) { + $query->select('COUNT(id) AS count_users') + ->from('#__users'); + $db->setQuery($query); + + try { + $users = $db->loadResult(); + } catch (\RuntimeException $e) { + $users = false; + } + + $query->clear() + ->select('COUNT(id) AS count_items') + ->from('#__content') + ->where('state = 1'); + $db->setQuery($query); + + try { + $items = $db->loadResult(); + } catch (\RuntimeException $e) { + $items = false; + } + + if ($users) { + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_USERS'); + $rows[$i]->icon = 'users'; + $rows[$i]->data = $users; + + if ($user->authorise('core.manage', 'com_users')) { + $rows[$i]->link = Route::_('index.php?option=com_users'); + } + + $i++; + } + + if ($items) { + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_ARTICLES'); + $rows[$i]->icon = 'file'; + $rows[$i]->data = $items; + $rows[$i]->link = Route::_('index.php?option=com_content&view=articles&filter[published]=1'); + $i++; + } + } + + // Include additional data defined by published system plugins + PluginHelper::importPlugin('system'); + + $arrays = (array) $app->triggerEvent('onGetStats', array('mod_stats_admin')); + + foreach ($arrays as $response) { + foreach ($response as $row) { + // We only add a row if the title and data are given + if (isset($row['title']) && isset($row['data'])) { + $rows[$i] = new \stdClass(); + $rows[$i]->title = $row['title']; + $rows[$i]->icon = $row['icon'] ?? 'info'; + $rows[$i]->data = $row['data']; + $rows[$i]->link = isset($row['link']) ? $row['link'] : null; + $i++; + } + } + } + + return $rows; + } } diff --git a/code/administrator/modules/mod_stats_admin/tmpl/default.php b/code/administrator/modules/mod_stats_admin/tmpl/default.php index 732c2fcc..c79e483e 100644 --- a/code/administrator/modules/mod_stats_admin/tmpl/default.php +++ b/code/administrator/modules/mod_stats_admin/tmpl/default.php @@ -1,4 +1,5 @@
      - -
    • - title; ?> + +
    • + title; ?> - link)) : ?> - data; ?> - - data; ?> - -
    • - + link)) : ?> + data; ?> + + data; ?> + + +
    diff --git a/code/administrator/modules/mod_submenu/mod_submenu.php b/code/administrator/modules/mod_submenu/mod_submenu.php index 0fd9eb87..86cea5ee 100644 --- a/code/administrator/modules/mod_submenu/mod_submenu.php +++ b/code/administrator/modules/mod_submenu/mod_submenu.php @@ -1,4 +1,5 @@ get('menutype', '*'); $root = false; -if ($menutype === '*') -{ - $name = $params->get('preset', 'system'); - $root = MenusHelper::loadPreset($name); -} -else -{ - $root = MenusHelper::getMenuItems($menutype, true); +if ($menutype === '*') { + $name = $params->get('preset', 'system'); + $root = MenusHelper::loadPreset($name); +} else { + $root = MenusHelper::getMenuItems($menutype, true); } -if ($root && $root->hasChildren()) -{ - Factory::getLanguage()->load( - 'mod_menu', - JPATH_ADMINISTRATOR, - Factory::getLanguage()->getTag(), - true - ); +if ($root && $root->hasChildren()) { + Factory::getLanguage()->load( + 'mod_menu', + JPATH_ADMINISTRATOR, + Factory::getLanguage()->getTag(), + true + ); - Menu::preprocess($root); + Menu::preprocess($root); - // Render the module layout - require ModuleHelper::getLayoutPath('mod_submenu', $params->get('layout', 'default')); + // Render the module layout + require ModuleHelper::getLayoutPath('mod_submenu', $params->get('layout', 'default')); } diff --git a/code/administrator/modules/mod_submenu/mod_submenu.xml b/code/administrator/modules/mod_submenu/mod_submenu.xml index eeaa441c..7722dbc6 100644 --- a/code/administrator/modules/mod_submenu/mod_submenu.xml +++ b/code/administrator/modules/mod_submenu/mod_submenu.xml @@ -2,7 +2,7 @@ mod_submenu Joomla! Project - February 2006 + 2006-02 (C) 2006 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_submenu/src/Menu/Menu.php b/code/administrator/modules/mod_submenu/src/Menu/Menu.php index 61f7a50f..42b5e8ed 100644 --- a/code/administrator/modules/mod_submenu/src/Menu/Menu.php +++ b/code/administrator/modules/mod_submenu/src/Menu/Menu.php @@ -1,4 +1,5 @@ getIdentity(); - $children = $parent->getChildren(); - $language = Factory::getLanguage(); - - /** - * Trigger onPreprocessMenuItems for the current level of backend menu items. - * $children is an array of MenuItem objects. A plugin can traverse the whole tree, - * but new nodes will only be run through this method if their parents have not been processed yet. - */ - $app->triggerEvent('onPreprocessMenuItems', array('administrator.module.mod_submenu', $children)); - - foreach ($children as $item) - { - if (substr($item->link, 0, 8) === 'special:') - { - $special = substr($item->link, 8); - - if ($special === 'language-forum') - { - $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; - } - } - - $uri = new Uri($item->link); - $query = $uri->getQuery(true); - - /** - * This is needed to populate the element property when the component is no longer - * installed but its core menu items are left behind. - */ - if ($option = $uri->getVar('option')) - { - $item->element = $option; - } - - // Exclude item if is not enabled - if ($item->element && !ComponentHelper::isEnabled($item->element)) - { - $parent->removeChild($item); - continue; - } - - /* - * Multilingual Associations if the site is not set as multilingual and/or Associations is not enabled in - * the Language Filter plugin - */ - - if ($item->element === 'com_associations' && !Associations::isEnabled()) - { - $parent->removeChild($item); - continue; - } - - $itemParams = $item->getParams(); - - // Exclude item with menu item option set to exclude from menu modules - if ($itemParams->get('menu-permission')) - { - @list($action, $asset) = explode(';', $itemParams->get('menu-permission')); - - if (!$user->authorise($action, $asset)) - { - $parent->removeChild($item); - continue; - } - } - - // Populate automatic children for container items - if ($item->type === 'container') - { - $exclude = (array) $itemParams->get('hideitems') ?: array(); - $components = MenusHelper::getMenuItems('main', false, $exclude); - - // We are adding the nodes first to preprocess them, then sort them and add them again. - foreach ($components->getChildren() as $c) - { - if (!$c->hasChildren()) - { - $temp = clone $c; - $c->addChild($temp); - } - - $item->addChild($c); - } - - self::preprocess($item); - $children = ArrayHelper::sortObjects($item->getChildren(), 'text', 1, false, true); - - foreach ($children as $c) - { - $parent->addChild($c); - } - - $parent->removeChild($item); - continue; - } - - // Exclude Mass Mail if disabled in global configuration - if ($item->scope === 'massmail' && ($app->get('massmailoff', 0) == 1)) - { - $parent->removeChild($item); - continue; - } - - if ($item->element === 'com_fields') - { - parse_str($item->link, $query); - - // Only display Fields menus when enabled in the component - $createFields = null; - - if (isset($query['context'])) - { - $createFields = ComponentHelper::getParams(strstr($query['context'], '.', true))->get('custom_fields_enable', 1); - } - - if (!$createFields || !$user->authorise('core.manage', 'com_users')) - { - $parent->removeChild($item); - continue; - } - } - elseif ($item->element === 'com_workflow') - { - parse_str($item->link, $query); - - // Only display Workflow menus when enabled in the component - $workflow = null; - - if (isset($query['extension'])) - { - $parts = explode('.', $query['extension']); - - $workflow = ComponentHelper::getParams($parts[0])->get('workflow_enabled'); - } - - if (!$workflow) - { - $parent->removeChild($item); - continue; - } - - [$assetName] = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); - } - // Special case for components which only allow super user access - elseif (\in_array($item->element, array('com_config', 'com_privacy', 'com_actionlogs'), true) && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - elseif ($item->element === 'com_joomlaupdate' && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - elseif (($item->link === 'index.php?option=com_installer&view=install' || $item->link === 'index.php?option=com_installer&view=languages') - && !$user->authorise('core.admin')) - { - continue; - } - elseif ($item->element === 'com_admin') - { - parse_str($item->link, $query); - - if (isset($query['view']) && $query['view'] === 'sysinfo' && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - } - elseif ($item->element && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $item->element)) - { - $parent->removeChild($item); - continue; - } - elseif ($item->element === 'com_menus') - { - // Get badges for Menus containing a Home page. - $iconImage = $item->icon; - - if ($iconImage) - { - if (substr($iconImage, 0, 6) === 'class:' && substr($iconImage, 6) === 'icon-home') - { - $iconImage = ''; - $iconImage .= '' . Text::_('JDEFAULT') . ''; - } - elseif (substr($iconImage, 0, 6) === 'image:') - { - $iconImage = ' ' . substr($iconImage, 6) . ''; - } - - $item->iconImage = $iconImage; - } - } - - if ($item->hasChildren()) - { - self::preprocess($item); - } - - // Ok we passed everything, load language at last only - if ($item->element) - { - $language->load($item->element . '.sys', JPATH_ADMINISTRATOR) || - $language->load($item->element . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->element); - } - - if ($item->type === 'separator' && $item->getParams()->get('text_separator') == 0) - { - $item->title = ''; - } - - $item->text = Text::_($item->title); - } - } + /** + * Filter and perform other preparatory tasks for loaded menu items based on access rights and module configurations for display + * + * @param MenuItem $parent A menu item to process + * + * @return void + * + * @since 4.0.0 + */ + public static function preprocess($parent) + { + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $children = $parent->getChildren(); + $language = Factory::getLanguage(); + + /** + * Trigger onPreprocessMenuItems for the current level of backend menu items. + * $children is an array of MenuItem objects. A plugin can traverse the whole tree, + * but new nodes will only be run through this method if their parents have not been processed yet. + */ + $app->triggerEvent('onPreprocessMenuItems', array('administrator.module.mod_submenu', $children)); + + foreach ($children as $item) { + if (substr($item->link, 0, 8) === 'special:') { + $special = substr($item->link, 8); + + if ($special === 'language-forum') { + $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; + } + } + + $uri = new Uri($item->link); + $query = $uri->getQuery(true); + + /** + * This is needed to populate the element property when the component is no longer + * installed but its core menu items are left behind. + */ + if ($option = $uri->getVar('option')) { + $item->element = $option; + } + + // Exclude item if is not enabled + if ($item->element && !ComponentHelper::isEnabled($item->element)) { + $parent->removeChild($item); + continue; + } + + /* + * Multilingual Associations if the site is not set as multilingual and/or Associations is not enabled in + * the Language Filter plugin + */ + + if ($item->element === 'com_associations' && !Associations::isEnabled()) { + $parent->removeChild($item); + continue; + } + + $itemParams = $item->getParams(); + + // Exclude item with menu item option set to exclude from menu modules + if ($itemParams->get('menu-permission')) { + @list($action, $asset) = explode(';', $itemParams->get('menu-permission')); + + if (!$user->authorise($action, $asset)) { + $parent->removeChild($item); + continue; + } + } + + // Populate automatic children for container items + if ($item->type === 'container') { + $exclude = (array) $itemParams->get('hideitems') ?: array(); + $components = MenusHelper::getMenuItems('main', false, $exclude); + + // We are adding the nodes first to preprocess them, then sort them and add them again. + foreach ($components->getChildren() as $c) { + if (!$c->hasChildren()) { + $temp = clone $c; + $c->addChild($temp); + } + + $item->addChild($c); + } + + self::preprocess($item); + $children = ArrayHelper::sortObjects($item->getChildren(), 'text', 1, false, true); + + foreach ($children as $c) { + $parent->addChild($c); + } + + $parent->removeChild($item); + continue; + } + + // Exclude Mass Mail if disabled in global configuration + if ($item->scope === 'massmail' && ($app->get('massmailoff', 0) == 1)) { + $parent->removeChild($item); + continue; + } + + if ($item->element === 'com_fields') { + parse_str($item->link, $query); + + // Only display Fields menus when enabled in the component + $createFields = null; + + if (isset($query['context'])) { + $createFields = ComponentHelper::getParams(strstr($query['context'], '.', true))->get('custom_fields_enable', 1); + } + + if (!$createFields || !$user->authorise('core.manage', 'com_users')) { + $parent->removeChild($item); + continue; + } + } elseif ($item->element === 'com_workflow') { + parse_str($item->link, $query); + + // Only display Workflow menus when enabled in the component + $workflow = null; + + if (isset($query['extension'])) { + $parts = explode('.', $query['extension']); + + $workflow = ComponentHelper::getParams($parts[0])->get('workflow_enabled'); + } + + if (!$workflow) { + $parent->removeChild($item); + continue; + } + + [$assetName] = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); + } elseif (\in_array($item->element, array('com_config', 'com_privacy', 'com_actionlogs'), true) && !$user->authorise('core.admin')) { + // Special case for components which only allow super user access + $parent->removeChild($item); + continue; + } elseif ($item->element === 'com_joomlaupdate' && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } elseif ( + ($item->link === 'index.php?option=com_installer&view=install' || $item->link === 'index.php?option=com_installer&view=languages') + && !$user->authorise('core.admin') + ) { + continue; + } elseif ($item->element === 'com_admin') { + parse_str($item->link, $query); + + if (isset($query['view']) && $query['view'] === 'sysinfo' && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } + } elseif ($item->element && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $item->element)) { + $parent->removeChild($item); + continue; + } elseif ($item->element === 'com_menus') { + // Get badges for Menus containing a Home page. + $iconImage = $item->icon; + + if ($iconImage) { + if (substr($iconImage, 0, 6) === 'class:' && substr($iconImage, 6) === 'icon-home') { + $iconImage = ''; + $iconImage .= '' . Text::_('JDEFAULT') . ''; + } elseif (substr($iconImage, 0, 6) === 'image:') { + $iconImage = ' ' . substr($iconImage, 6) . ''; + } + + $item->iconImage = $iconImage; + } + } + + if ($item->hasChildren()) { + self::preprocess($item); + } + + // Ok we passed everything, load language at last only + if ($item->element) { + $language->load($item->element . '.sys', JPATH_ADMINISTRATOR) || + $language->load($item->element . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->element); + } + + if ($item->type === 'separator' && $item->getParams()->get('text_separator') == 0) { + $item->title = ''; + } + + $item->text = Text::_($item->title); + } + } } diff --git a/code/administrator/modules/mod_submenu/tmpl/default.php b/code/administrator/modules/mod_submenu/tmpl/default.php index 1bf99857..ceef2b42 100644 --- a/code/administrator/modules/mod_submenu/tmpl/default.php +++ b/code/administrator/modules/mod_submenu/tmpl/default.php @@ -1,4 +1,5 @@ getIdentity(); + /** @var \Joomla\CMS\Menu\MenuItem $root */ ?> getChildren() as $child) : ?> - hasChildren()) : ?> -
    -
    - img = $child->img ?? ''; + hasChildren()) : ?> +
    +
    + img = $child->img ?? ''; - if (substr($child->img, 0, 6) === 'class:') - { - $iconImage = ''; - } - elseif (substr($child->img, 0, 6) === 'image:') - { - $iconImage = ''; - } - elseif (!empty($child->img)) - { - $iconImage = ''; - } - elseif ($child->icon) - { - $iconImage = ''; - } - else - { - $iconImage = ''; - } - ?> -

    - - title); ?> -

    -
    -
    - + $sronly = Text::_($item->title) . ' - ' . $title; + ?> + + + + + + + + dashboard) : ?> + + + + + + + + + + +
    +
    + diff --git a/code/administrator/modules/mod_title/mod_title.php b/code/administrator/modules/mod_title/mod_title.php index edcbc9bf..3e1340ff 100644 --- a/code/administrator/modules/mod_title/mod_title.php +++ b/code/administrator/modules/mod_title/mod_title.php @@ -1,4 +1,5 @@ JComponentTitle)) -{ - $title = $app->JComponentTitle; +if (isset($app->JComponentTitle)) { + $title = $app->JComponentTitle; } require ModuleHelper::getLayoutPath('mod_title', $params->get('layout', 'default')); diff --git a/code/administrator/modules/mod_title/mod_title.xml b/code/administrator/modules/mod_title/mod_title.xml index 0cca7401..38ae9664 100644 --- a/code/administrator/modules/mod_title/mod_title.xml +++ b/code/administrator/modules/mod_title/mod_title.xml @@ -2,7 +2,7 @@ mod_title Joomla! Project - November 2005 + 2005-11 (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_title/tmpl/default.php b/code/administrator/modules/mod_title/tmpl/default.php index 7db10595..0c24a082 100644 --- a/code/administrator/modules/mod_title/tmpl/default.php +++ b/code/administrator/modules/mod_title/tmpl/default.php @@ -1,4 +1,5 @@
    -
    - -
    +
    + +
    diff --git a/code/administrator/modules/mod_toolbar/mod_toolbar.php b/code/administrator/modules/mod_toolbar/mod_toolbar.php index 9b7fabde..9466a9ef 100644 --- a/code/administrator/modules/mod_toolbar/mod_toolbar.php +++ b/code/administrator/modules/mod_toolbar/mod_toolbar.php @@ -1,4 +1,5 @@ mod_toolbar Joomla! Project - November 2005 + 2005-11 (C) 2005 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_toolbar/tmpl/default.php b/code/administrator/modules/mod_toolbar/tmpl/default.php index 2cd92544..d4712be7 100644 --- a/code/administrator/modules/mod_toolbar/tmpl/default.php +++ b/code/administrator/modules/mod_toolbar/tmpl/default.php @@ -1,4 +1,5 @@ mod_user Joomla! Project - July 2019 + 2019-07 (C) 2019 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org @@ -10,7 +10,7 @@ 4.0.0 MOD_USER_XML_DESCRIPTION - mod_user.php + mod_user.php tmpl diff --git a/code/administrator/modules/mod_user/tmpl/default.php b/code/administrator/modules/mod_user/tmpl/default.php index d2aa88ee..5125ce4a 100644 --- a/code/administrator/modules/mod_user/tmpl/default.php +++ b/code/administrator/modules/mod_user/tmpl/default.php @@ -1,4 +1,5 @@ input->getBool('hidemainmenu'); -if ($hideLinks) -{ - return; +if ($hideLinks) { + return; } // Load the Bootstrap Dropdown HTMLHelper::_('bootstrap.dropdown', '.dropdown-toggle'); ?> diff --git a/code/administrator/modules/mod_version/mod_version.php b/code/administrator/modules/mod_version/mod_version.php index 5ce96037..c94b138b 100644 --- a/code/administrator/modules/mod_version/mod_version.php +++ b/code/administrator/modules/mod_version/mod_version.php @@ -1,4 +1,5 @@ mod_version Joomla! Project - January 2012 + 2012-01 (C) 2012 Open Source Matters, Inc. GNU General Public License version 2 or later; see LICENSE.txt admin@joomla.org diff --git a/code/administrator/modules/mod_version/src/Helper/VersionHelper.php b/code/administrator/modules/mod_version/src/Helper/VersionHelper.php index 38d11cb4..d74cce3a 100644 --- a/code/administrator/modules/mod_version/src/Helper/VersionHelper.php +++ b/code/administrator/modules/mod_version/src/Helper/VersionHelper.php @@ -1,4 +1,5 @@ getShortVersion(); - } + return '‎' . $version->getShortVersion(); + } } diff --git a/code/administrator/modules/mod_version/tmpl/default.php b/code/administrator/modules/mod_version/tmpl/default.php index 5e77f801..fa0f143a 100644 --- a/code/administrator/modules/mod_version/tmpl/default.php +++ b/code/administrator/modules/mod_version/tmpl/default.php @@ -1,4 +1,5 @@
    - +
    diff --git a/code/administrator/templates/atum/component.php b/code/administrator/templates/atum/component.php index 2913982b..0d52a6be 100644 --- a/code/administrator/templates/atum/component.php +++ b/code/administrator/templates/atum/component.php @@ -1,4 +1,5 @@ usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '--template-bg-light') . '; --template-text-dark: ' . $this->params->get('text-dark', '--template-text-dark') . '; @@ -47,12 +48,12 @@ - - - + + + - - + + diff --git a/code/administrator/templates/atum/cpanel.php b/code/administrator/templates/atum/cpanel.php index e75d1c51..8a2b1aaf 100644 --- a/code/administrator/templates/atum/cpanel.php +++ b/code/administrator/templates/atum/cpanel.php @@ -1,4 +1,5 @@ guest) -{ - require __DIR__ . '/error_login.php'; -} -else -{ - require __DIR__ . '/error_full.php'; +if ($user->guest) { + require __DIR__ . '/error_login.php'; +} else { + require __DIR__ . '/error_full.php'; } diff --git a/code/administrator/templates/atum/error_full.php b/code/administrator/templates/atum/error_full.php index 6886a175..d2c2381f 100644 --- a/code/administrator/templates/atum/error_full.php +++ b/code/administrator/templates/atum/error_full.php @@ -1,4 +1,5 @@ params->get('logoBrandLarge') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; $logoBrandSmall = $this->params->get('logoBrandSmall') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; $logoBrandLargeAlt = empty($this->params->get('logoBrandLargeAlt')) && empty($this->params->get('emptyLogoBrandLargeAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt', ''), ENT_COMPAT, 'UTF-8') . '"'; $logoBrandSmallAlt = empty($this->params->get('logoBrandSmallAlt')) && empty($this->params->get('emptyLogoBrandSmallAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt', ''), ENT_COMPAT, 'UTF-8') . '"'; - // Get the hue value - preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); + // Get the hue value + preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); - // Enable assets - $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + // Enable assets + $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '#f0f4fb') . '; --template-text-dark: ' . $this->params->get('text-dark', '#495057') . '; @@ -66,122 +67,122 @@ }'); // Override 'template.active' asset to set correct ltr/rtl dependency -$wa->registerStyle('template.active', '', [], [], ['template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')]); + $wa->registerStyle('template.active', '', [], [], ['template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')]); // Set some meta data -$this->setMetaData('viewport', 'width=device-width, initial-scale=1'); + $this->setMetaData('viewport', 'width=device-width, initial-scale=1'); -$monochrome = (bool) $this->params->get('monochrome'); + $monochrome = (bool) $this->params->get('monochrome'); // @see administrator/templates/atum/html/layouts/status.php -$statusModules = LayoutHelper::render('status', ['modules' => 'status']); -?> + $statusModules = LayoutHelper::render('status', ['modules' => 'status']); + ?> - - - + + + - - - - -
    -
    - - - -
    -
    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -

    -
    - error->getCode(); ?> - error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> -
    - debug) : ?> -
    - renderBacktrace(); ?> - - error->getPrevious()) : ?> - - _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> - - setError($this->_error->getPrevious()); ?> - -

    -

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    - renderBacktrace(); ?> - setError($this->_error->getPrevious()); ?> - - - setError($this->error); ?> - -
    - -

    - - - -

    -
    - - countModules('bottom')) : ?> - - -
    -
    -
    - - - - - - -
    - + + + + +
    +
    + + + +
    +
    +
    +
    + +
    +
    +
    + +
    + + +
    +
    +

    +
    + error->getCode(); ?> + error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> +
    + debug) : ?> +
    + renderBacktrace(); ?> + + error->getPrevious()) : ?> + + _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> + + setError($this->_error->getPrevious()); ?> + +

    +

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    + renderBacktrace(); ?> + setError($this->_error->getPrevious()); ?> + + + setError($this->error); ?> + +
    + +

    + + + +

    +
    + + countModules('bottom')) : ?> + + +
    +
    +
    + + + + + + +
    + diff --git a/code/administrator/templates/atum/error_login.php b/code/administrator/templates/atum/error_login.php index 68144ad0..a760530c 100644 --- a/code/administrator/templates/atum/error_login.php +++ b/code/administrator/templates/atum/error_login.php @@ -1,4 +1,5 @@ params->get('logoBrandLarge') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; $loginLogo = $this->params->get('loginLogo') - ? Uri::root() . $this->params->get('loginLogo') - : Uri::root() . 'media/templates/administrator/atum/images/logos/login.svg'; + ? Uri::root() . $this->params->get('loginLogo') + : Uri::root() . 'media/templates/administrator/atum/images/logos/login.svg'; $logoBrandSmall = $this->params->get('logoBrandSmall') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; $logoBrandLargeAlt = empty($this->params->get('logoBrandLargeAlt')) && empty($this->params->get('emptyLogoBrandLargeAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt', ''), ENT_COMPAT, 'UTF-8') . '"'; $logoBrandSmallAlt = empty($this->params->get('logoBrandSmallAlt')) && empty($this->params->get('emptyLogoBrandSmallAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt', ''), ENT_COMPAT, 'UTF-8') . '"'; $loginLogoAlt = empty($this->params->get('loginLogoAlt')) && empty($this->params->get('emptyLoginLogoAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('loginLogoAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('loginLogoAlt', ''), ENT_COMPAT, 'UTF-8') . '"'; - // Get the hue value + // Get the hue value preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); // Enable assets $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '#f0f4fb') . '; --template-text-dark: ' . $this->params->get('text-dark', '#495057') . '; @@ -83,83 +84,83 @@ - - - + + + - - - - -
    -
    -
    -
    -
    -
    -
    - > -
    -

    - -
    - error->getCode(); ?> - error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> -
    - debug) : ?> -
    - renderBacktrace(); ?> - - error->getPrevious()) : ?> - - _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> - - setError($this->_error->getPrevious()); ?> - -

    -

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    - renderBacktrace(); ?> - setError($this->_error->getPrevious()); ?> - - - setError($this->error); ?> - -
    - -
    -
    -
    -
    -
    - - -
    - + + + + +
    +
    +
    +
    +
    +
    +
    + > +
    +

    + +
    + error->getCode(); ?> + error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> +
    + debug) : ?> +
    + renderBacktrace(); ?> + + error->getPrevious()) : ?> + + _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> + + setError($this->_error->getPrevious()); ?> + +

    +

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    + renderBacktrace(); ?> + setError($this->_error->getPrevious()); ?> + + + setError($this->error); ?> + +
    + +
    +
    +
    +
    +
    + + +
    + diff --git a/code/administrator/templates/atum/html/layouts/chromes/body.php b/code/administrator/templates/atum/html/layouts/chromes/body.php index b73e693f..ab4c8f31 100644 --- a/code/administrator/templates/atum/html/layouts/chromes/body.php +++ b/code/administrator/templates/atum/html/layouts/chromes/body.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } $id = $module->id; @@ -41,31 +41,31 @@ ?>
    - < class="card pt-3"> - - isRtl() ? 'start' : 'end'; ?> - - - showtitle) : ?> - < class="card-header">title; ?>> - -
    - content; ?> -
    - > + < class="card pt-3"> + + isRtl() ? 'start' : 'end'; ?> + + + showtitle) : ?> + < class="card-header">title; ?>> + +
    + content; ?> +
    + >
    diff --git a/code/administrator/templates/atum/html/layouts/chromes/header-item.php b/code/administrator/templates/atum/html/layouts/chromes/header-item.php index 7e6d4745..6beaeadf 100644 --- a/code/administrator/templates/atum/html/layouts/chromes/header-item.php +++ b/code/administrator/templates/atum/html/layouts/chromes/header-item.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } ?>
    - content; ?> + content; ?>
    diff --git a/code/administrator/templates/atum/html/layouts/chromes/title.php b/code/administrator/templates/atum/html/layouts/chromes/title.php index c4979a36..23278b57 100644 --- a/code/administrator/templates/atum/html/layouts/chromes/title.php +++ b/code/administrator/templates/atum/html/layouts/chromes/title.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } ?>
    -
    title; ?>
    +
    title; ?>
    content; ?> diff --git a/code/administrator/templates/atum/html/layouts/chromes/well.php b/code/administrator/templates/atum/html/layouts/chromes/well.php index 76d5bac5..be61e19d 100644 --- a/code/administrator/templates/atum/html/layouts/chromes/well.php +++ b/code/administrator/templates/atum/html/layouts/chromes/well.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } $id = $module->id; @@ -44,39 +44,39 @@ ?>
    - < class="card mb-3 "> - showtitle) : ?> -
    - - isRtl() ? 'start' : 'end'; ?> - - + < class="card mb-3 "> + showtitle) : ?> +
    + + isRtl() ? 'start' : 'end'; ?> + + - showtitle) : ?> - <> - - title, ENT_QUOTES, 'UTF-8'); ?> - > - -
    - -
    - content; ?> -
    - > + showtitle) : ?> + <> + + title, ENT_QUOTES, 'UTF-8'); ?> + > + +
    + +
    + content; ?> +
    + >
    diff --git a/code/administrator/templates/atum/html/layouts/status.php b/code/administrator/templates/atum/html/layouts/status.php index 41a28e95..364eec92 100644 --- a/code/administrator/templates/atum/html/layouts/status.php +++ b/code/administrator/templates/atum/html/layouts/status.php @@ -1,4 +1,5 @@ $mod) -{ - $out = $renderer->render($mod); +foreach ($modules as $key => $mod) { + $out = $renderer->render($mod); - if ($out !== '') - { - if (strpos($out, 'data-bs-toggle="modal"') !== false) - { - $dom = new \DOMDocument; - $dom->loadHTML($out); - $els = $dom->getElementsByTagName('a'); + if ($out !== '') { + if (strpos($out, 'data-bs-toggle="modal"') !== false) { + $dom = new \DOMDocument(); + $dom->loadHTML($out); + $els = $dom->getElementsByTagName('a'); - $moduleCollapsedHtml[] = $dom->saveHTML($els[0]); //$els[0]->nodeValue; - } - else - { - $moduleCollapsedHtml[] = $out; - } + $moduleCollapsedHtml[] = $dom->saveHTML($els[0]); //$els[0]->nodeValue; + } else { + $moduleCollapsedHtml[] = $out; + } - $moduleHtml[] = $out; - } + $moduleHtml[] = $out; + } } ?>
    - ' . $mod . '
    '; - } - ?> -
    - - -
    + ' . $mod . ''; + } + ?> +
    + + +
    diff --git a/code/administrator/templates/atum/index.php b/code/administrator/templates/atum/index.php index 734e0ca9..d1fff8ed 100644 --- a/code/administrator/templates/atum/index.php +++ b/code/administrator/templates/atum/index.php @@ -1,4 +1,5 @@ params->get('logoBrandLarge') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; $logoBrandSmall = $this->params->get('logoBrandSmall') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; $logoBrandLargeAlt = empty($this->params->get('logoBrandLargeAlt')) && empty($this->params->get('emptyLogoBrandLargeAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt', ''), ENT_COMPAT, 'UTF-8') . '"'; $logoBrandSmallAlt = empty($this->params->get('logoBrandSmallAlt')) && empty($this->params->get('emptyLogoBrandSmallAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt', ''), ENT_COMPAT, 'UTF-8') . '"'; // Get the hue value preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); // Enable assets $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '#f0f4fb') . '; --template-text-dark: ' . $this->params->get('text-dark', '#495057') . '; @@ -89,99 +90,99 @@ > - - - + + +
    - - - - - - - - - -
    - - - - -
    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - - -
    -
    - countModules('bottom')) : ?> - - -
    - -
    -
    + + + + + + + + + +
    + + + + +
    +
    +
    + +
    +
    +
    + +
    + + +
    +
    +
    + + +
    +
    + countModules('bottom')) : ?> + + +
    + +
    +
    diff --git a/code/administrator/templates/atum/joomla.asset.json b/code/administrator/templates/atum/joomla.asset.json index bfc09124..d3ca87af 100644 --- a/code/administrator/templates/atum/joomla.asset.json +++ b/code/administrator/templates/atum/joomla.asset.json @@ -39,6 +39,7 @@ "description": "A file where a user can add their own css.", "type": "style", "uri": "user.css", + "weight": 500, "dependencies": [ "template.active", "template.active.language" diff --git a/code/administrator/templates/atum/login.php b/code/administrator/templates/atum/login.php index 98c3bb76..3f043caa 100644 --- a/code/administrator/templates/atum/login.php +++ b/code/administrator/templates/atum/login.php @@ -1,4 +1,5 @@ params->get('logoBrandLarge') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; $loginLogo = $this->params->get('loginLogo') - ? Uri::root() . $this->params->get('loginLogo') - : Uri::root() . 'media/templates/administrator/atum/images/logos/login.svg'; + ? Uri::root() . $this->params->get('loginLogo') + : Uri::root() . 'media/templates/administrator/atum/images/logos/login.svg'; $logoBrandSmall = $this->params->get('logoBrandSmall') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; $logoBrandLargeAlt = empty($this->params->get('logoBrandLargeAlt')) && empty($this->params->get('emptyLogoBrandLargeAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt', ''), ENT_COMPAT, 'UTF-8') . '"'; $logoBrandSmallAlt = empty($this->params->get('logoBrandSmallAlt')) && empty($this->params->get('emptyLogoBrandSmallAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt', ''), ENT_COMPAT, 'UTF-8') . '"'; $loginLogoAlt = empty($this->params->get('loginLogoAlt')) && empty($this->params->get('emptyLoginLogoAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('loginLogoAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('loginLogoAlt', ''), ENT_COMPAT, 'UTF-8') . '"'; // Get the hue value preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); // Enable assets $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '#f0f4fb') . '; --template-text-dark: ' . $this->params->get('text-dark', '#495057') . '; @@ -89,62 +90,62 @@ - - - + + + - - - - - -
    -
    -
    - -
    - -
    -
    -
    - - -
    - + + + + + +
    +
    +
    + +
    + +
    +
    +
    + + +
    + diff --git a/code/administrator/templates/atum/templateDetails.xml b/code/administrator/templates/atum/templateDetails.xml index 0e3c8e04..29284585 100644 --- a/code/administrator/templates/atum/templateDetails.xml +++ b/code/administrator/templates/atum/templateDetails.xml @@ -2,7 +2,7 @@ atum 1.0 - September 2016 + 2016-09 Joomla! Project admin@joomla.org (C) 2016 Open Source Matters, Inc. diff --git a/code/administrator/templates/system/component.php b/code/administrator/templates/system/component.php index dd283dae..672f4e6f 100644 --- a/code/administrator/templates/system/component.php +++ b/code/administrator/templates/system/component.php @@ -1,4 +1,5 @@ - + - - + + diff --git a/code/administrator/templates/system/error.php b/code/administrator/templates/system/error.php index 53c7a91e..ea06bd4e 100644 --- a/code/administrator/templates/system/error.php +++ b/code/administrator/templates/system/error.php @@ -1,4 +1,5 @@ - - - + + + - - - - - - - -
    -

    error->getCode() ?> -

    -
    -

    - error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> - debug) : ?> -
    error->getFile(), ENT_QUOTES, 'UTF-8');?>:error->getLine(); ?> - -

    -

    - debug) : ?> -
    - renderBacktrace(); ?> - - error->getPrevious()) : ?> - - _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> - - setError($this->_error->getPrevious()); ?> - -

    -

    - _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> -
    _error->getFile(), ENT_QUOTES, 'UTF-8');?>:_error->getLine(); ?> -

    - renderBacktrace(); ?> - setError($this->_error->getPrevious()); ?> - - - setError($this->error); ?> - -
    - -
    + + + + + + + +
    +

    error->getCode() ?> -

    +
    +

    + error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> + debug) : ?> +
    error->getFile(), ENT_QUOTES, 'UTF-8');?>:error->getLine(); ?> + +

    +

    + debug) : ?> +
    + renderBacktrace(); ?> + + error->getPrevious()) : ?> + + _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> + + setError($this->_error->getPrevious()); ?> + +

    +

    + _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> +
    _error->getFile(), ENT_QUOTES, 'UTF-8');?>:_error->getLine(); ?> +

    + renderBacktrace(); ?> + setError($this->_error->getPrevious()); ?> + + + setError($this->error); ?> + +
    + +
    - + diff --git a/code/administrator/templates/system/index.php b/code/administrator/templates/system/index.php index 14ed2b2e..c0d47db8 100644 --- a/code/administrator/templates/system/index.php +++ b/code/administrator/templates/system/index.php @@ -1,4 +1,5 @@ getExtensionFromInput(); - $data['extension'] = $extension; - - // TODO: This is a hack to drop the extension into the global input object - to satisfy how state is built - // we should be able to improve this in the future - $this->input->set('extension', $extension); - - return $data; - } - - /** - * Method to save a record. - * - * @param integer $recordKey The primary key of the item (if exists) - * - * @return integer The record ID on success, false on failure - * - * @since 4.0.6 - */ - protected function save($recordKey = null) - { - $recordId = parent::save($recordKey); - - if (!$recordId) - { - return $recordId; - } - - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - if (empty($data['location'])) - { - return $recordId; - } - - /** @var Category $category */ - $category = $this->getModel('Category')->getTable('Category'); - $category->load((int) $recordId); - - $reference = $category->parent_id; - - if (!empty($data['location_reference'])) - { - $reference = (int) $data['location_reference']; - } - - $category->setLocation($reference, $data['location']); - $category->store(); - - return $recordId; - } - - /** - * Basic display of an item view - * - * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayItem($id = null) - { - $this->modelState->set('filter.extension', $this->getExtensionFromInput()); - - return parent::displayItem($id); - } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.extension', $this->getExtensionFromInput()); - - return parent::displayList(); - } - - /** - * Get extension from input - * - * @return string - * - * @since 4.0.0 - */ - private function getExtensionFromInput() - { - return $this->input->exists('extension') ? - $this->input->get('extension') : $this->input->post->get('extension'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'categories'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'categories'; + + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + */ + protected function preprocessSaveData(array $data): array + { + $extension = $this->getExtensionFromInput(); + $data['extension'] = $extension; + + // TODO: This is a hack to drop the extension into the global input object - to satisfy how state is built + // we should be able to improve this in the future + $this->input->set('extension', $extension); + + return $data; + } + + /** + * Method to save a record. + * + * @param integer $recordKey The primary key of the item (if exists) + * + * @return integer The record ID on success, false on failure + * + * @since 4.0.6 + */ + protected function save($recordKey = null) + { + $recordId = parent::save($recordKey); + + if (!$recordId) { + return $recordId; + } + + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + if (empty($data['location'])) { + return $recordId; + } + + /** @var Category $category */ + $category = $this->getModel('Category')->getTable('Category'); + $category->load((int) $recordId); + + $reference = $category->parent_id; + + if (!empty($data['location_reference'])) { + $reference = (int) $data['location_reference']; + } + + $category->setLocation($reference, $data['location']); + $category->store(); + + return $recordId; + } + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.extension', $this->getExtensionFromInput()); + + return parent::displayItem($id); + } + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.extension', $this->getExtensionFromInput()); + + return parent::displayList(); + } + + /** + * Get extension from input + * + * @return string + * + * @since 4.0.0 + */ + private function getExtensionFromInput() + { + return $this->input->exists('extension') ? + $this->input->get('extension') : $this->input->post->get('extension'); + } } diff --git a/code/api/components/com_categories/src/View/Categories/JsonapiView.php b/code/api/components/com_categories/src/View/Categories/JsonapiView.php index c2d244c6..25a63ba4 100644 --- a/code/api/components/com_categories/src/View/Categories/JsonapiView.php +++ b/code/api/components/com_categories/src/View/Categories/JsonapiView.php @@ -1,4 +1,5 @@ fieldsToRenderList[] = $field->name; - } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + foreach (FieldsHelper::getFields('com_content.categories') as $field) { + $this->fieldsToRenderList[] = $field->name; + } - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - foreach (FieldsHelper::getFields('com_content.categories') as $field) - { - $this->fieldsToRenderItem[] = $field->name; - } + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + foreach (FieldsHelper::getFields('com_content.categories') as $field) { + $this->fieldsToRenderItem[] = $field->name; + } - if ($item === null) - { - /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ - $model = $this->getModel(); - $item = $this->prepareItem($model->getItem()); - } + if ($item === null) { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + $item = $this->prepareItem($model->getItem()); + } - if ($item->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } - if ($item->extension != $this->getModel()->getState('filter.extension')) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->extension != $this->getModel()->getState('filter.extension')) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::displayItem($item); - } + return parent::displayItem($item); + } - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - foreach (FieldsHelper::getFields('com_content.categories', $item, true) as $field) - { - $item->{$field->name} = $field->apivalue ?? $field->rawvalue; - } + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + foreach (FieldsHelper::getFields('com_content.categories', $item, true) as $field) { + $item->{$field->name} = $field->apivalue ?? $field->rawvalue; + } - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/code/api/components/com_config/src/Controller/ApplicationController.php b/code/api/components/com_config/src/Controller/ApplicationController.php index c273f891..389c394d 100644 --- a/code/api/components/com_config/src/Controller/ApplicationController.php +++ b/code/api/components/com_config/src/Controller/ApplicationController.php @@ -1,4 +1,5 @@ app->getDocument()->getType(); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $this->default_view, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var ApplicationModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); - } - - // Push the model into the view (as default) - $view->setModel($model, true); - - $view->document = $this->app->getDocument(); - $view->displayList(); - - return $this; - } - - /** - * Method to edit an existing record. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function edit() - { - /** @var ApplicationModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); - } - - // Access check. - if (!$this->allowEdit()) - { - throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); - } - - $data = json_decode($this->input->json->getRaw(), true); - - // Complete data array if needed - $oldData = $model->getData(); - $data = array_replace($oldData, $data); - - // @todo: Not the cleanest thing ever but it works... - Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms'); - - // Must load after serving service-requests - $form = $model->getForm(); - - // Validate the posted data. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - $errors = $model->getErrors(); - $messages = []; - - for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $messages[] = "{$errors[$i]->getMessage()}"; - } - else - { - $messages[] = "{$errors[$i]}"; - } - } - - throw new InvalidParameterException(implode("\n", $messages)); - } - - if (!$model->save($validData)) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500); - } - - return $this; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'application'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'application'; + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $viewType = $this->app->getDocument()->getType(); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $this->default_view, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var ApplicationModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); + } + + // Push the model into the view (as default) + $view->setModel($model, true); + + $view->document = $this->app->getDocument(); + $view->displayList(); + + return $this; + } + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function edit() + { + /** @var ApplicationModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); + } + + // Access check. + if (!$this->allowEdit()) { + throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); + } + + $data = json_decode($this->input->json->getRaw(), true); + + // Complete data array if needed + $oldData = $model->getData(); + $data = array_replace($oldData, $data); + + // @todo: Not the cleanest thing ever but it works... + Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms'); + + // Must load after serving service-requests + $form = $model->getForm(); + + // Validate the posted data. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + $errors = $model->getErrors(); + $messages = []; + + for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $messages[] = "{$errors[$i]->getMessage()}"; + } else { + $messages[] = "{$errors[$i]}"; + } + } + + throw new InvalidParameterException(implode("\n", $messages)); + } + + if (!$model->save($validData)) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500); + } + + return $this; + } } diff --git a/code/api/components/com_config/src/Controller/ComponentController.php b/code/api/components/com_config/src/Controller/ComponentController.php index ef60de1d..2d386092 100644 --- a/code/api/components/com_config/src/Controller/ComponentController.php +++ b/code/api/components/com_config/src/Controller/ComponentController.php @@ -1,4 +1,5 @@ app->getDocument()->getType(); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $this->default_view, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var ComponentModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); - } - - // Push the model into the view (as default) - $view->setModel($model, true); - $view->set('component_name', $this->input->get('component_name')); - - $view->document = $this->app->getDocument(); - $view->displayList(); - - return $this; - } - - /** - * Method to edit an existing record. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function edit() - { - /** @var ComponentModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); - } - - // Access check. - if (!$this->allowEdit()) - { - throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); - } - - $option = $this->input->get('component_name'); - - // @todo: Not the cleanest thing ever but it works... - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/' . $option); - - // Must load after serving service-requests - $form = $model->getForm(); - - $data = json_decode($this->input->json->getRaw(), true); - - $component = ComponentHelper::getComponent($option); - $oldData = $component->getParams()->toArray(); - $data = array_replace($oldData, $data); - - // Validate the posted data. - $validData = $model->validate($form, $data); - - if ($validData === false) - { - $errors = $model->getErrors(); - $messages = []; - - for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $messages[] = "{$errors[$i]->getMessage()}"; - } - else - { - $messages[] = "{$errors[$i]}"; - } - } - - throw new InvalidParameterException(implode("\n", $messages)); - } - - // Attempt to save the configuration. - $data = [ - 'params' => $validData, - 'id' => ExtensionHelper::getExtensionRecord($option, 'component')->extension_id, - 'option' => $option, - ]; - - if (!$model->save($data)) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500); - } - - return $this; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'component'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'component'; + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $viewType = $this->app->getDocument()->getType(); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $this->default_view, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var ComponentModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); + } + + // Push the model into the view (as default) + $view->setModel($model, true); + $view->set('component_name', $this->input->get('component_name')); + + $view->document = $this->app->getDocument(); + $view->displayList(); + + return $this; + } + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function edit() + { + /** @var ComponentModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); + } + + // Access check. + if (!$this->allowEdit()) { + throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); + } + + $option = $this->input->get('component_name'); + + // @todo: Not the cleanest thing ever but it works... + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/' . $option); + + // Must load after serving service-requests + $form = $model->getForm(); + + $data = json_decode($this->input->json->getRaw(), true); + + $component = ComponentHelper::getComponent($option); + $oldData = $component->getParams()->toArray(); + $data = array_replace($oldData, $data); + + // Validate the posted data. + $validData = $model->validate($form, $data); + + if ($validData === false) { + $errors = $model->getErrors(); + $messages = []; + + for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $messages[] = "{$errors[$i]->getMessage()}"; + } else { + $messages[] = "{$errors[$i]}"; + } + } + + throw new InvalidParameterException(implode("\n", $messages)); + } + + // Attempt to save the configuration. + $data = [ + 'params' => $validData, + 'id' => ExtensionHelper::getExtensionRecord($option, 'component')->extension_id, + 'option' => $option, + ]; + + if (!$model->save($data)) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500); + } + + return $this; + } } diff --git a/code/api/components/com_config/src/View/Application/JsonapiView.php b/code/api/components/com_config/src/View/Application/JsonapiView.php index 090b2ddb..02b154df 100644 --- a/code/api/components/com_config/src/View/Application/JsonapiView.php +++ b/code/api/components/com_config/src/View/Application/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $items = []; - - foreach ($model->getData() as $key => $value) - { - $item = (object) [$key => $value]; - $items[] = $this->prepareItem($item); - } - - // Set up links for pagination - $currentUrl = Uri::getInstance(); - $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; - $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); - - $offset = $currentPageQuery['offset']; - $limit = $currentPageQuery['limit']; - $totalItemsCount = \count($items); - $totalPagesAvailable = ceil($totalItemsCount / $limit); - - $items = array_splice($items, $offset, $limit); - - $this->document->addMeta('total-pages', $totalPagesAvailable) - ->addLink('self', (string) $currentUrl); - - // Check for first and previous pages - if ($offset > 0) - { - $firstPage = clone $currentUrl; - $firstPageQuery = $currentPageQuery; - $firstPageQuery['offset'] = 0; - $firstPage->setVar('page', $firstPageQuery); - - $previousPage = clone $currentUrl; - $previousPageQuery = $currentPageQuery; - $previousOffset = $currentPageQuery['offset'] - $limit; - $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; - $previousPage->setVar('page', $previousPageQuery); - - $this->document->addLink('first', $this->queryEncode((string) $firstPage)) - ->addLink('previous', $this->queryEncode((string) $previousPage)); - } - - // Check for next and last pages - if ($offset + $limit < $totalItemsCount) - { - $nextPage = clone $currentUrl; - $nextPageQuery = $currentPageQuery; - $nextOffset = $currentPageQuery['offset'] + $limit; - $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; - $nextPage->setVar('page', $nextPageQuery); - - $lastPage = clone $currentUrl; - $lastPageQuery = $currentPageQuery; - $lastPageQuery['offset'] = ($totalPagesAvailable - 1) * $limit; - $lastPage->setVar('page', $lastPageQuery); - - $this->document->addLink('next', $this->queryEncode((string) $nextPage)) - ->addLink('last', $this->queryEncode((string) $lastPage)); - } - - $collection = (new Collection($items, new JoomlaSerializer($this->type))); - - // Set the data into the document and render it - $this->document->setData($collection); - - return $this->document->render(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - - return $item; - } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + /** @var ApplicationModel $model */ + $model = $this->getModel(); + $items = []; + + foreach ($model->getData() as $key => $value) { + $item = (object) [$key => $value]; + $items[] = $this->prepareItem($item); + } + + // Set up links for pagination + $currentUrl = Uri::getInstance(); + $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; + $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); + + $offset = $currentPageQuery['offset']; + $limit = $currentPageQuery['limit']; + $totalItemsCount = \count($items); + $totalPagesAvailable = ceil($totalItemsCount / $limit); + + $items = array_splice($items, $offset, $limit); + + $this->document->addMeta('total-pages', $totalPagesAvailable) + ->addLink('self', (string) $currentUrl); + + // Check for first and previous pages + if ($offset > 0) { + $firstPage = clone $currentUrl; + $firstPageQuery = $currentPageQuery; + $firstPageQuery['offset'] = 0; + $firstPage->setVar('page', $firstPageQuery); + + $previousPage = clone $currentUrl; + $previousPageQuery = $currentPageQuery; + $previousOffset = $currentPageQuery['offset'] - $limit; + $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; + $previousPage->setVar('page', $previousPageQuery); + + $this->document->addLink('first', $this->queryEncode((string) $firstPage)) + ->addLink('previous', $this->queryEncode((string) $previousPage)); + } + + // Check for next and last pages + if ($offset + $limit < $totalItemsCount) { + $nextPage = clone $currentUrl; + $nextPageQuery = $currentPageQuery; + $nextOffset = $currentPageQuery['offset'] + $limit; + $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; + $nextPage->setVar('page', $nextPageQuery); + + $lastPage = clone $currentUrl; + $lastPageQuery = $currentPageQuery; + $lastPageQuery['offset'] = ($totalPagesAvailable - 1) * $limit; + $lastPage->setVar('page', $lastPageQuery); + + $this->document->addLink('next', $this->queryEncode((string) $nextPage)) + ->addLink('last', $this->queryEncode((string) $lastPage)); + } + + $collection = (new Collection($items, new JoomlaSerializer($this->type))); + + // Set the data into the document and render it + $this->document->setData($collection); + + return $this->document->render(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + + return $item; + } } diff --git a/code/api/components/com_config/src/View/Component/JsonapiView.php b/code/api/components/com_config/src/View/Component/JsonapiView.php index 13818546..872da71f 100644 --- a/code/api/components/com_config/src/View/Component/JsonapiView.php +++ b/code/api/components/com_config/src/View/Component/JsonapiView.php @@ -1,4 +1,5 @@ get('component_name')); - - if ($component === null || !$component->enabled) - { - // @todo: exception component unavailable - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_INVALID_COMPONENT_NAME'), 400); - } - - $data = $component->getParams()->toObject(); - } - catch (\Exception $e) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500, $e); - } - - $items = []; - - foreach ($data as $key => $value) - { - $item = (object) [$key => $value]; - $items[] = $this->prepareItem($item); - } - - // Set up links for pagination - $currentUrl = Uri::getInstance(); - $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; - $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); - - $offset = $currentPageQuery['offset']; - $limit = $currentPageQuery['limit']; - $totalItemsCount = \count($items); - $totalPagesAvailable = ceil($totalItemsCount / $limit); - - $items = array_splice($items, $offset, $limit); - - $this->document->addMeta('total-pages', $totalPagesAvailable) - ->addLink('self', (string) $currentUrl); - - // Check for first and previous pages - if ($offset > 0) - { - $firstPage = clone $currentUrl; - $firstPageQuery = $currentPageQuery; - $firstPageQuery['offset'] = 0; - $firstPage->setVar('page', $firstPageQuery); - - $previousPage = clone $currentUrl; - $previousPageQuery = $currentPageQuery; - $previousOffset = $currentPageQuery['offset'] - $limit; - $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; - $previousPage->setVar('page', $previousPageQuery); - - $this->document->addLink('first', $this->queryEncode((string) $firstPage)) - ->addLink('previous', $this->queryEncode((string) $previousPage)); - } - - // Check for next and last pages - if ($offset + $limit < $totalItemsCount) - { - $nextPage = clone $currentUrl; - $nextPageQuery = $currentPageQuery; - $nextOffset = $currentPageQuery['offset'] + $limit; - $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; - $nextPage->setVar('page', $nextPageQuery); - - $lastPage = clone $currentUrl; - $lastPageQuery = $currentPageQuery; - $lastPageQuery['offset'] = ($totalPagesAvailable - 1) * $limit; - $lastPage->setVar('page', $lastPageQuery); - - $this->document->addLink('next', $this->queryEncode((string) $nextPage)) - ->addLink('last', $this->queryEncode((string) $lastPage)); - } - - $collection = (new Collection($items, new JoomlaSerializer($this->type))); - - // Set the data into the document and render it - $this->document->setData($collection); - - return $this->document->render(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->id = ExtensionHelper::getExtensionRecord($this->get('component_name'), 'component')->extension_id; - - return $item; - } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + try { + $component = ComponentHelper::getComponent($this->get('component_name')); + + if ($component === null || !$component->enabled) { + // @todo: exception component unavailable + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_INVALID_COMPONENT_NAME'), 400); + } + + $data = $component->getParams()->toObject(); + } catch (\Exception $e) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500, $e); + } + + $items = []; + + foreach ($data as $key => $value) { + $item = (object) [$key => $value]; + $items[] = $this->prepareItem($item); + } + + // Set up links for pagination + $currentUrl = Uri::getInstance(); + $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; + $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); + + $offset = $currentPageQuery['offset']; + $limit = $currentPageQuery['limit']; + $totalItemsCount = \count($items); + $totalPagesAvailable = ceil($totalItemsCount / $limit); + + $items = array_splice($items, $offset, $limit); + + $this->document->addMeta('total-pages', $totalPagesAvailable) + ->addLink('self', (string) $currentUrl); + + // Check for first and previous pages + if ($offset > 0) { + $firstPage = clone $currentUrl; + $firstPageQuery = $currentPageQuery; + $firstPageQuery['offset'] = 0; + $firstPage->setVar('page', $firstPageQuery); + + $previousPage = clone $currentUrl; + $previousPageQuery = $currentPageQuery; + $previousOffset = $currentPageQuery['offset'] - $limit; + $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; + $previousPage->setVar('page', $previousPageQuery); + + $this->document->addLink('first', $this->queryEncode((string) $firstPage)) + ->addLink('previous', $this->queryEncode((string) $previousPage)); + } + + // Check for next and last pages + if ($offset + $limit < $totalItemsCount) { + $nextPage = clone $currentUrl; + $nextPageQuery = $currentPageQuery; + $nextOffset = $currentPageQuery['offset'] + $limit; + $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; + $nextPage->setVar('page', $nextPageQuery); + + $lastPage = clone $currentUrl; + $lastPageQuery = $currentPageQuery; + $lastPageQuery['offset'] = ($totalPagesAvailable - 1) * $limit; + $lastPage->setVar('page', $lastPageQuery); + + $this->document->addLink('next', $this->queryEncode((string) $nextPage)) + ->addLink('last', $this->queryEncode((string) $lastPage)); + } + + $collection = (new Collection($items, new JoomlaSerializer($this->type))); + + // Set the data into the document and render it + $this->document->setData($collection); + + return $this->document->render(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = ExtensionHelper::getExtensionRecord($this->get('component_name'), 'component')->extension_id; + + return $item; + } } diff --git a/code/api/components/com_contact/src/Controller/ContactController.php b/code/api/components/com_contact/src/Controller/ContactController.php index b9805d33..46df34ea 100644 --- a/code/api/components/com_contact/src/Controller/ContactController.php +++ b/code/api/components/com_contact/src/Controller/ContactController.php @@ -1,4 +1,5 @@ name])) - { - !isset($data['com_fields']) && $data['com_fields'] = []; - - $data['com_fields'][$field->name] = $data[$field->name]; - unset($data[$field->name]); - } - } - - return $data; - } - - /** - * Submit contact form - * - * @param integer $id Leave empty if you want to retrieve data from the request - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function submitForm($id = null) - { - if ($id === null) - { - $id = $this->input->post->get('id', 0, 'int'); - } - - $modelName = Inflector::singularize($this->contentType); - - /** @var \Joomla\Component\Contact\Site\Model\ContactModel $model */ - $model = $this->getModel($modelName, 'Site'); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('filter.published', 1); - - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - $contact = $model->getItem($id); - - if ($contact->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } - - $contactParams = new Registry($contact->params); - - if (!$contactParams->get('show_email_form')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_DISPLAY_EMAIL_FORM')); - } - - // Contact plugins - PluginHelper::importPlugin('contact'); - - Form::addFormPath(JPATH_COMPONENT_SITE . '/forms'); - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \RuntimeException($model->getError(), 500); - } - - if (!$model->validate($form, $data)) - { - $errors = $model->getErrors(); - $messages = []; - - for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $messages[] = "{$errors[$i]->getMessage()}"; - } - else - { - $messages[] = "{$errors[$i]}"; - } - } - - throw new InvalidParameterException(implode("\n", $messages)); - } - - // Validation succeeded, continue with custom handlers - $results = $this->app->triggerEvent('onValidateContact', [&$contact, &$data]); - - foreach ($results as $result) - { - if ($result instanceof \Exception) - { - throw new InvalidParameterException($result->getMessage()); - } - } - - // Passed Validation: Process the contact plugins to integrate with other applications - $this->app->triggerEvent('onSubmitContact', [&$contact, &$data]); - - // Send the email - $sent = false; - - $params = ComponentHelper::getParams('com_contact'); - - if (!$params->get('custom_reply')) - { - $sent = $this->_sendEmail($data, $contact, $params->get('show_email_copy', 0)); - } - - if (!$sent) - { - throw new SendEmail('Error sending message'); - } - - return $this; - } - - /** - * Method to get a model object, loading it if required. - * - * @param array $data The data to send in the email. - * @param \stdClass $contact The user information to send the email to - * @param boolean $emailCopyToSender True to send a copy of the email to the user. - * - * @return boolean True on success sending the email, false on failure. - * - * @since 1.6.4 - */ - private function _sendEmail($data, $contact, $emailCopyToSender) - { - $app = $this->app; - - Factory::getLanguage()->load('com_contact', JPATH_SITE, $app->getLanguage()->getTag(), true); - - if ($contact->email_to == '' && $contact->user_id != 0) - { - $contact_user = User::getInstance($contact->user_id); - $contact->email_to = $contact_user->get('email'); - } - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'name' => $data['contact_name'], - 'contactname' => $contact->name, - 'email' => PunycodeHelper::emailToPunycode($data['contact_email']), - 'subject' => $data['contact_subject'], - 'body' => stripslashes($data['contact_message']), - 'url' => Uri::base(), - 'customfields' => '', - ]; - - // Load the custom fields - if (!empty($data['com_fields']) && $fields = FieldsHelper::getFields('com_contact.mail', $contact, true, $data['com_fields'])) - { - $output = FieldsHelper::render( - 'com_contact.mail', - 'fields.render', - array( - 'context' => 'com_contact.mail', - 'item' => $contact, - 'fields' => $fields, - ) - ); - - if ($output) - { - $templateData['customfields'] = $output; - } - } - - try - { - $mailer = new MailTemplate('com_contact.mail', $app->getLanguage()->getTag()); - $mailer->addRecipient($contact->email_to); - $mailer->setReplyTo($templateData['email'], $templateData['name']); - $mailer->addTemplateData($templateData); - $sent = $mailer->send(); - - // If we are supposed to copy the sender, do so. - if ($emailCopyToSender == true && !empty($data['contact_email_copy'])) - { - $mailer = new MailTemplate('com_contact.mail.copy', $app->getLanguage()->getTag()); - $mailer->addRecipient($templateData['email']); - $mailer->setReplyTo($templateData['email'], $templateData['name']); - $mailer->addTemplateData($templateData); - $sent = $mailer->send(); - } - } - catch (MailDisabledException | phpMailerException $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $sent = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $sent = false; - } - } - - return $sent; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'contacts'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'contacts'; + + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + */ + protected function preprocessSaveData(array $data): array + { + foreach (FieldsHelper::getFields('com_contact.contact') as $field) { + if (isset($data[$field->name])) { + !isset($data['com_fields']) && $data['com_fields'] = []; + + $data['com_fields'][$field->name] = $data[$field->name]; + unset($data[$field->name]); + } + } + + return $data; + } + + /** + * Submit contact form + * + * @param integer $id Leave empty if you want to retrieve data from the request + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function submitForm($id = null) + { + if ($id === null) { + $id = $this->input->post->get('id', 0, 'int'); + } + + $modelName = Inflector::singularize($this->contentType); + + /** @var \Joomla\Component\Contact\Site\Model\ContactModel $model */ + $model = $this->getModel($modelName, 'Site'); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('filter.published', 1); + + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + $contact = $model->getItem($id); + + if ($contact->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } + + $contactParams = new Registry($contact->params); + + if (!$contactParams->get('show_email_form')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_DISPLAY_EMAIL_FORM')); + } + + // Contact plugins + PluginHelper::importPlugin('contact'); + + Form::addFormPath(JPATH_COMPONENT_SITE . '/forms'); + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \RuntimeException($model->getError(), 500); + } + + if (!$model->validate($form, $data)) { + $errors = $model->getErrors(); + $messages = []; + + for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $messages[] = "{$errors[$i]->getMessage()}"; + } else { + $messages[] = "{$errors[$i]}"; + } + } + + throw new InvalidParameterException(implode("\n", $messages)); + } + + // Validation succeeded, continue with custom handlers + $results = $this->app->triggerEvent('onValidateContact', [&$contact, &$data]); + + foreach ($results as $result) { + if ($result instanceof \Exception) { + throw new InvalidParameterException($result->getMessage()); + } + } + + // Passed Validation: Process the contact plugins to integrate with other applications + $this->app->triggerEvent('onSubmitContact', [&$contact, &$data]); + + // Send the email + $sent = false; + + $params = ComponentHelper::getParams('com_contact'); + + if (!$params->get('custom_reply')) { + $sent = $this->_sendEmail($data, $contact, $params->get('show_email_copy', 0)); + } + + if (!$sent) { + throw new SendEmail('Error sending message'); + } + + return $this; + } + + /** + * Method to get a model object, loading it if required. + * + * @param array $data The data to send in the email. + * @param \stdClass $contact The user information to send the email to + * @param boolean $emailCopyToSender True to send a copy of the email to the user. + * + * @return boolean True on success sending the email, false on failure. + * + * @since 1.6.4 + */ + private function _sendEmail($data, $contact, $emailCopyToSender) + { + $app = $this->app; + + Factory::getLanguage()->load('com_contact', JPATH_SITE, $app->getLanguage()->getTag(), true); + + if ($contact->email_to == '' && $contact->user_id != 0) { + $contact_user = User::getInstance($contact->user_id); + $contact->email_to = $contact_user->get('email'); + } + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'name' => $data['contact_name'], + 'contactname' => $contact->name, + 'email' => PunycodeHelper::emailToPunycode($data['contact_email']), + 'subject' => $data['contact_subject'], + 'body' => stripslashes($data['contact_message']), + 'url' => Uri::base(), + 'customfields' => '', + ]; + + // Load the custom fields + if (!empty($data['com_fields']) && $fields = FieldsHelper::getFields('com_contact.mail', $contact, true, $data['com_fields'])) { + $output = FieldsHelper::render( + 'com_contact.mail', + 'fields.render', + array( + 'context' => 'com_contact.mail', + 'item' => $contact, + 'fields' => $fields, + ) + ); + + if ($output) { + $templateData['customfields'] = $output; + } + } + + try { + $mailer = new MailTemplate('com_contact.mail', $app->getLanguage()->getTag()); + $mailer->addRecipient($contact->email_to); + $mailer->setReplyTo($templateData['email'], $templateData['name']); + $mailer->addTemplateData($templateData); + $sent = $mailer->send(); + + // If we are supposed to copy the sender, do so. + if ($emailCopyToSender == true && !empty($data['contact_email_copy'])) { + $mailer = new MailTemplate('com_contact.mail.copy', $app->getLanguage()->getTag()); + $mailer->addRecipient($templateData['email']); + $mailer->setReplyTo($templateData['email'], $templateData['name']); + $mailer->addTemplateData($templateData); + $sent = $mailer->send(); + } + } catch (MailDisabledException | phpMailerException $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $sent = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $sent = false; + } + } + + return $sent; + } } diff --git a/code/api/components/com_contact/src/Serializer/ContactSerializer.php b/code/api/components/com_contact/src/Serializer/ContactSerializer.php index 23fe7333..c413c13c 100644 --- a/code/api/components/com_contact/src/Serializer/ContactSerializer.php +++ b/code/api/components/com_contact/src/Serializer/ContactSerializer.php @@ -1,4 +1,5 @@ type); - - foreach ($model->associations as $association) - { - $resources[] = (new Resource($association, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/contact/' . $association->id)); - } - - $collection = new Collection($resources, $serializer); - - return new Relationship($collection); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function category($model) - { - $serializer = new JoomlaSerializer('categories'); - - $resource = (new Resource($model->catid, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/categories/' . $model->catid)); - - return new Relationship($resource); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function createdBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->created_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); - - return new Relationship($resource); - } - - /** - * Build editor relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function modifiedBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->modified_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); - - return new Relationship($resource); - } - - /** - * Build contact user relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function userId($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->user_id, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->user_id)); - - return new Relationship($resource); - } + use TagApiSerializerTrait; + + /** + * Build content relationships by associations + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function languageAssociations($model) + { + $resources = []; + + // @todo: This can't be hardcoded in the future? + $serializer = new JoomlaSerializer($this->type); + + foreach ($model->associations as $association) { + $resources[] = (new Resource($association, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/contact/' . $association->id)); + } + + $collection = new Collection($resources, $serializer); + + return new Relationship($collection); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function category($model) + { + $serializer = new JoomlaSerializer('categories'); + + $resource = (new Resource($model->catid, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/categories/' . $model->catid)); + + return new Relationship($resource); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function createdBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->created_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); + + return new Relationship($resource); + } + + /** + * Build editor relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function modifiedBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->modified_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); + + return new Relationship($resource); + } + + /** + * Build contact user relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function userId($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->user_id, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->user_id)); + + return new Relationship($resource); + } } diff --git a/code/api/components/com_contact/src/View/Contacts/JsonapiView.php b/code/api/components/com_contact/src/View/Contacts/JsonapiView.php index de1091fd..59d66eaa 100644 --- a/code/api/components/com_contact/src/View/Contacts/JsonapiView.php +++ b/code/api/components/com_contact/src/View/Contacts/JsonapiView.php @@ -1,4 +1,5 @@ serializer = new ContactSerializer($config['contentType']); - } - - parent::__construct($config); - } - - /** - * Execute and display a template script. - * - * @param array|null $items Array of items - * - * @return string - * - * @since 4.0.0 - */ - public function displayList(array $items = null) - { - foreach (FieldsHelper::getFields('com_contact.contact') as $field) - { - $this->fieldsToRenderList[] = $field->name; - } - - return parent::displayList(); - } - - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - foreach (FieldsHelper::getFields('com_contact.contact') as $field) - { - $this->fieldsToRenderItem[] = $field->name; - } - - if (Multilanguage::isEnabled()) - { - $this->fieldsToRenderItem[] = 'languageAssociations'; - $this->relationship[] = 'languageAssociations'; - } - - return parent::displayItem(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - foreach (FieldsHelper::getFields('com_contact.contact', $item, true) as $field) - { - $item->{$field->name} = $field->apivalue ?? $field->rawvalue; - } - - if (Multilanguage::isEnabled() && !empty($item->associations)) - { - $associations = []; - - foreach ($item->associations as $language => $association) - { - $itemId = explode(':', $association)[0]; - - $associations[] = (object) [ - 'id' => $itemId, - 'language' => $language, - ]; - } - - $item->associations = $associations; - } - - if (!empty($item->tags->tags)) - { - $tagsIds = explode(',', $item->tags->tags); - $tagsNames = $item->tagsHelper->getTagNames($tagsIds); - - $item->tags = array_combine($tagsIds, $tagsNames); - } - else - { - $item->tags = []; - } - - if (isset($item->image)) - { - $item->image = ContentHelper::resolve($item->image); - } - - return parent::prepareItem($item); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'alias', + 'name', + 'category', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'modified_by', + 'image', + 'tags', + 'featured', + 'publish_up', + 'publish_down', + 'version', + 'hits', + 'metakey', + 'metadesc', + 'metadata', + 'con_position', + 'address', + 'suburb', + 'state', + 'country', + 'postcode', + 'telephone', + 'fax', + 'misc', + 'email_to', + 'default_con', + 'user_id', + 'access', + 'mobile', + 'webpage', + 'sortname1', + 'sortname2', + 'sortname3', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'alias', + 'name', + 'category', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'modified_by', + 'image', + 'tags', + 'user_id', + ]; + + /** + * The relationships the item has + * + * @var array + * @since 4.0.0 + */ + protected $relationship = [ + 'category', + 'created_by', + 'modified_by', + 'user_id', + 'tags', + ]; + + /** + * Constructor. + * + * @param array $config A named configuration array for object construction. + * contentType: the name (optional) of the content type to use for the serialization + * + * @since 4.0.0 + */ + public function __construct($config = []) + { + if (\array_key_exists('contentType', $config)) { + $this->serializer = new ContactSerializer($config['contentType']); + } + + parent::__construct($config); + } + + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + foreach (FieldsHelper::getFields('com_contact.contact') as $field) { + $this->fieldsToRenderList[] = $field->name; + } + + return parent::displayList(); + } + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + foreach (FieldsHelper::getFields('com_contact.contact') as $field) { + $this->fieldsToRenderItem[] = $field->name; + } + + if (Multilanguage::isEnabled()) { + $this->fieldsToRenderItem[] = 'languageAssociations'; + $this->relationship[] = 'languageAssociations'; + } + + return parent::displayItem(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + foreach (FieldsHelper::getFields('com_contact.contact', $item, true) as $field) { + $item->{$field->name} = $field->apivalue ?? $field->rawvalue; + } + + if (Multilanguage::isEnabled() && !empty($item->associations)) { + $associations = []; + + foreach ($item->associations as $language => $association) { + $itemId = explode(':', $association)[0]; + + $associations[] = (object) [ + 'id' => $itemId, + 'language' => $language, + ]; + } + + $item->associations = $associations; + } + + if (!empty($item->tags->tags)) { + $tagsIds = explode(',', $item->tags->tags); + $tagsNames = $item->tagsHelper->getTagNames($tagsIds); + + $item->tags = array_combine($tagsIds, $tagsNames); + } else { + $item->tags = []; + } + + if (isset($item->image)) { + $item->image = ContentHelper::resolve($item->image); + } + + return parent::prepareItem($item); + } } diff --git a/code/api/components/com_content/src/Controller/ArticlesController.php b/code/api/components/com_content/src/Controller/ArticlesController.php index 691a6493..1e527f60 100644 --- a/code/api/components/com_content/src/Controller/ArticlesController.php +++ b/code/api/components/com_content/src/Controller/ArticlesController.php @@ -1,4 +1,5 @@ input->get('filter', [], 'array'); - $filter = InputFilter::getInstance(); - - if (\array_key_exists('author', $apiFilterInfo)) - { - $this->modelState->set('filter.author_id', $filter->clean($apiFilterInfo['author'], 'INT')); - } - - if (\array_key_exists('category', $apiFilterInfo)) - { - $this->modelState->set('filter.category_id', $filter->clean($apiFilterInfo['category'], 'INT')); - } - - if (\array_key_exists('search', $apiFilterInfo)) - { - $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); - } - - if (\array_key_exists('state', $apiFilterInfo)) - { - $this->modelState->set('filter.published', $filter->clean($apiFilterInfo['state'], 'INT')); - } - - if (\array_key_exists('language', $apiFilterInfo)) - { - $this->modelState->set('filter.language', $filter->clean($apiFilterInfo['language'], 'STRING')); - } - - return parent::displayList(); - } - - /** - * Method to allow extended classes to manipulate the data to be saved for an extension. - * - * @param array $data An array of input data. - * - * @return array - * - * @since 4.0.0 - */ - protected function preprocessSaveData(array $data): array - { - foreach (FieldsHelper::getFields('com_content.article') as $field) - { - if (isset($data[$field->name])) - { - !isset($data['com_fields']) && $data['com_fields'] = []; - - $data['com_fields'][$field->name] = $data[$field->name]; - unset($data[$field->name]); - } - } - - return $data; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'articles'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'articles'; + + /** + * Article list view amended to add filtering of data + * + * @return static A BaseController object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); + + if (\array_key_exists('author', $apiFilterInfo)) { + $this->modelState->set('filter.author_id', $filter->clean($apiFilterInfo['author'], 'INT')); + } + + if (\array_key_exists('category', $apiFilterInfo)) { + $this->modelState->set('filter.category_id', $filter->clean($apiFilterInfo['category'], 'INT')); + } + + if (\array_key_exists('search', $apiFilterInfo)) { + $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); + } + + if (\array_key_exists('state', $apiFilterInfo)) { + $this->modelState->set('filter.published', $filter->clean($apiFilterInfo['state'], 'INT')); + } + + if (\array_key_exists('language', $apiFilterInfo)) { + $this->modelState->set('filter.language', $filter->clean($apiFilterInfo['language'], 'STRING')); + } + + $apiListInfo = $this->input->get('list', [], 'array'); + + if (array_key_exists('ordering', $apiListInfo)) { + $this->modelState->set('list.ordering', $filter->clean($apiListInfo['ordering'], 'STRING')); + } + + if (array_key_exists('direction', $apiListInfo)) { + $this->modelState->set('list.direction', $filter->clean($apiListInfo['direction'], 'STRING')); + } + + return parent::displayList(); + } + + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + */ + protected function preprocessSaveData(array $data): array + { + foreach (FieldsHelper::getFields('com_content.article') as $field) { + if (isset($data[$field->name])) { + !isset($data['com_fields']) && $data['com_fields'] = []; + + $data['com_fields'][$field->name] = $data[$field->name]; + unset($data[$field->name]); + } + } + + return $data; + } } diff --git a/code/api/components/com_content/src/Helper/ContentHelper.php b/code/api/components/com_content/src/Helper/ContentHelper.php index d7e64de5..3ade40a4 100644 --- a/code/api/components/com_content/src/Helper/ContentHelper.php +++ b/code/api/components/com_content/src/Helper/ContentHelper.php @@ -1,4 +1,5 @@ type); - - foreach ($model->associations as $association) - { - $resources[] = (new Resource($association, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/articles/' . $association->id)); - } - - $collection = new Collection($resources, $serializer); - - return new Relationship($collection); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function category($model) - { - $serializer = new JoomlaSerializer('categories'); - - $resource = (new Resource($model->catid, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/categories/' . $model->catid)); - - return new Relationship($resource); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function createdBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->created_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); - - return new Relationship($resource); - } - - /** - * Build editor relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function modifiedBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->modified_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); - - return new Relationship($resource); - } + /** + * Build content relationships by associations + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function languageAssociations($model) + { + $resources = []; + + // @todo: This can't be hardcoded in the future? + $serializer = new JoomlaSerializer($this->type); + + foreach ($model->associations as $association) { + $resources[] = (new Resource($association, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/articles/' . $association->id)); + } + + $collection = new Collection($resources, $serializer); + + return new Relationship($collection); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function category($model) + { + $serializer = new JoomlaSerializer('categories'); + + $resource = (new Resource($model->catid, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/categories/' . $model->catid)); + + return new Relationship($resource); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function createdBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->created_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); + + return new Relationship($resource); + } + + /** + * Build editor relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function modifiedBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->modified_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); + + return new Relationship($resource); + } } diff --git a/code/api/components/com_content/src/View/Articles/JsonapiView.php b/code/api/components/com_content/src/View/Articles/JsonapiView.php index 6b4170a5..62d5cc6e 100644 --- a/code/api/components/com_content/src/View/Articles/JsonapiView.php +++ b/code/api/components/com_content/src/View/Articles/JsonapiView.php @@ -1,4 +1,5 @@ serializer = new ContentSerializer($config['contentType']); - } - - parent::__construct($config); - } - - /** - * Execute and display a template script. - * - * @param array|null $items Array of items - * - * @return string - * - * @since 4.0.0 - */ - public function displayList(array $items = null) - { - foreach (FieldsHelper::getFields('com_content.article') as $field) - { - $this->fieldsToRenderList[] = $field->name; - } - - return parent::displayList(); - } - - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - $this->relationship[] = 'modified_by'; - - foreach (FieldsHelper::getFields('com_content.article') as $field) - { - $this->fieldsToRenderItem[] = $field->name; - } - - if (Multilanguage::isEnabled()) - { - $this->fieldsToRenderItem[] = 'languageAssociations'; - $this->relationship[] = 'languageAssociations'; - } - - return parent::displayItem(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->text = $item->introtext . ' ' . $item->fulltext; - - // Process the content plugins. - PluginHelper::importPlugin('content'); - Factory::getApplication()->triggerEvent('onContentPrepare', ['com_content.article', &$item, &$item->params]); - - foreach (FieldsHelper::getFields('com_content.article', $item, true) as $field) - { - $item->{$field->name} = $field->apivalue ?? $field->rawvalue; - } - - if (Multilanguage::isEnabled() && !empty($item->associations)) - { - $associations = []; - - foreach ($item->associations as $language => $association) - { - $itemId = explode(':', $association)[0]; - - $associations[] = (object) [ - 'id' => $itemId, - 'language' => $language, - ]; - } - - $item->associations = $associations; - } - - if (!empty($item->tags->tags)) - { - $tagsIds = explode(',', $item->tags->tags); - $tagsNames = $item->tagsHelper->getTagNames($tagsIds); - - $item->tags = array_combine($tagsIds, $tagsNames); - } - else - { - $item->tags = []; - } - - if (isset($item->images)) - { - $registry = new Registry($item->images); - $item->images = $registry->toArray(); - - if (!empty($item->images['image_intro'])) - { - $item->images['image_intro'] = ContentHelper::resolve($item->images['image_intro']); - } - - if (!empty($item->images['image_fulltext'])) - { - $item->images['image_fulltext'] = ContentHelper::resolve($item->images['image_fulltext']); - } - } - - return parent::prepareItem($item); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'typeAlias', + 'asset_id', + 'title', + 'text', + 'tags', + 'language', + 'state', + 'category', + 'images', + 'metakey', + 'metadesc', + 'metadata', + 'access', + 'featured', + 'alias', + 'note', + 'publish_up', + 'publish_down', + 'urls', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'modified_by', + 'hits', + 'version', + 'featured_up', + 'featured_down', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'typeAlias', + 'asset_id', + 'title', + 'text', + 'tags', + 'language', + 'state', + 'category', + 'images', + 'metakey', + 'metadesc', + 'metadata', + 'access', + 'featured', + 'alias', + 'note', + 'publish_up', + 'publish_down', + 'urls', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'hits', + 'version', + 'featured_up', + 'featured_down', + ]; + + /** + * The relationships the item has + * + * @var array + * @since 4.0.0 + */ + protected $relationship = [ + 'category', + 'created_by', + 'tags', + ]; + + /** + * Constructor. + * + * @param array $config A named configuration array for object construction. + * contentType: the name (optional) of the content type to use for the serialization + * + * @since 4.0.0 + */ + public function __construct($config = []) + { + if (\array_key_exists('contentType', $config)) { + $this->serializer = new ContentSerializer($config['contentType']); + } + + parent::__construct($config); + } + + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + foreach (FieldsHelper::getFields('com_content.article') as $field) { + $this->fieldsToRenderList[] = $field->name; + } + + return parent::displayList(); + } + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + $this->relationship[] = 'modified_by'; + + foreach (FieldsHelper::getFields('com_content.article') as $field) { + $this->fieldsToRenderItem[] = $field->name; + } + + if (Multilanguage::isEnabled()) { + $this->fieldsToRenderItem[] = 'languageAssociations'; + $this->relationship[] = 'languageAssociations'; + } + + return parent::displayItem(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->text = $item->introtext . ' ' . $item->fulltext; + + // Process the content plugins. + PluginHelper::importPlugin('content'); + Factory::getApplication()->triggerEvent('onContentPrepare', ['com_content.article', &$item, &$item->params]); + + foreach (FieldsHelper::getFields('com_content.article', $item, true) as $field) { + $item->{$field->name} = $field->apivalue ?? $field->rawvalue; + } + + if (Multilanguage::isEnabled() && !empty($item->associations)) { + $associations = []; + + foreach ($item->associations as $language => $association) { + $itemId = explode(':', $association)[0]; + + $associations[] = (object) [ + 'id' => $itemId, + 'language' => $language, + ]; + } + + $item->associations = $associations; + } + + if (!empty($item->tags->tags)) { + $tagsIds = explode(',', $item->tags->tags); + $tagsNames = $item->tagsHelper->getTagNames($tagsIds); + + $item->tags = array_combine($tagsIds, $tagsNames); + } else { + $item->tags = []; + } + + if (isset($item->images)) { + $registry = new Registry($item->images); + $item->images = $registry->toArray(); + + if (!empty($item->images['image_intro'])) { + $item->images['image_intro'] = ContentHelper::resolve($item->images['image_intro']); + } + + if (!empty($item->images['image_fulltext'])) { + $item->images['image_fulltext'] = ContentHelper::resolve($item->images['image_fulltext']); + } + } + + return parent::prepareItem($item); + } } diff --git a/code/api/components/com_contenthistory/src/Controller/HistoryController.php b/code/api/components/com_contenthistory/src/Controller/HistoryController.php index fba5a7f2..2b94d664 100644 --- a/code/api/components/com_contenthistory/src/Controller/HistoryController.php +++ b/code/api/components/com_contenthistory/src/Controller/HistoryController.php @@ -1,4 +1,5 @@ modelState->set('type_alias', $this->getTypeAliasFromInput()); - $this->modelState->set('type_id', $this->getTypeIdFromInput()); - $this->modelState->set('item_id', $this->getTypeAliasFromInput() . '.' . $this->getItemIdFromInput()); - $this->modelState->set('list.ordering', 'h.save_date'); - $this->modelState->set('list.direction', 'DESC'); - - return parent::displayList(); - } - - /** - * Method to edit an existing record. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function keep() - { - /** @var HistoryModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $recordId = $this->input->getInt('id'); - - if (!$recordId) - { - throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404); - } - - $cid = [$recordId]; - - if (!$model->keep($cid)) - { - throw new Exception\Save(Text::plural('COM_CONTENTHISTORY_N_ITEMS_KEEP_TOGGLE', \count($cid))); - } - - return $this; - } - - /** - * Get item id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getItemIdFromInput() - { - return $this->input->exists('id') ? - $this->input->get('id') : $this->input->post->get('id'); - } - - /** - * Get type id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getTypeIdFromInput() - { - return $this->input->exists('type_id') ? - $this->input->get('type_id') : $this->input->post->get('type_id'); - } - - /** - * Get type alias from input - * - * @return string - * - * @since 4.0.0 - */ - private function getTypeAliasFromInput() - { - return $this->input->exists('type_alias') ? - $this->input->get('type_alias') : $this->input->post->get('type_alias'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'history'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'history'; + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('type_alias', $this->getTypeAliasFromInput()); + $this->modelState->set('type_id', $this->getTypeIdFromInput()); + $this->modelState->set('item_id', $this->getTypeAliasFromInput() . '.' . $this->getItemIdFromInput()); + $this->modelState->set('list.ordering', 'h.save_date'); + $this->modelState->set('list.direction', 'DESC'); + + return parent::displayList(); + } + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function keep() + { + /** @var HistoryModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $recordId = $this->input->getInt('id'); + + if (!$recordId) { + throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404); + } + + $cid = [$recordId]; + + if (!$model->keep($cid)) { + throw new Exception\Save(Text::plural('COM_CONTENTHISTORY_N_ITEMS_KEEP_TOGGLE', \count($cid))); + } + + return $this; + } + + /** + * Get item id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getItemIdFromInput() + { + return $this->input->exists('id') ? + $this->input->get('id') : $this->input->post->get('id'); + } + + /** + * Get type id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getTypeIdFromInput() + { + return $this->input->exists('type_id') ? + $this->input->get('type_id') : $this->input->post->get('type_id'); + } + + /** + * Get type alias from input + * + * @return string + * + * @since 4.0.0 + */ + private function getTypeAliasFromInput() + { + return $this->input->exists('type_alias') ? + $this->input->get('type_alias') : $this->input->post->get('type_alias'); + } } diff --git a/code/api/components/com_contenthistory/src/View/History/JsonapiView.php b/code/api/components/com_contenthistory/src/View/History/JsonapiView.php index 686c049c..6e9e7e66 100644 --- a/code/api/components/com_contenthistory/src/View/History/JsonapiView.php +++ b/code/api/components/com_contenthistory/src/View/History/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->version_id; - unset($item->version_id); - - $item->version_data = (array) json_decode($item->version_data, true); - - return parent::prepareItem($item); - } + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'ucm_item_id', + 'ucm_type_id', + 'version_note', + 'save_date', + 'editor_user_id', + 'character_count', + 'sha1_hash', + 'version_data', + 'keep_forever', + 'editor', + ]; + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->version_id; + unset($item->version_id); + + $item->version_data = (array) json_decode($item->version_data, true); + + return parent::prepareItem($item); + } } diff --git a/code/api/components/com_fields/src/Controller/FieldsController.php b/code/api/components/com_fields/src/Controller/FieldsController.php index d3611102..fb0c441e 100644 --- a/code/api/components/com_fields/src/Controller/FieldsController.php +++ b/code/api/components/com_fields/src/Controller/FieldsController.php @@ -1,4 +1,5 @@ modelState->set('filter.context', $this->getContextFromInput()); + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.context', $this->getContextFromInput()); - return parent::displayItem($id); - } + return parent::displayItem($id); + } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.context', $this->getContextFromInput()); + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.context', $this->getContextFromInput()); - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Get extension from input - * - * @return string - * - * @since 4.0.0 - */ - private function getContextFromInput() - { - return $this->input->exists('context') ? - $this->input->get('context') : $this->input->post->get('context'); - } + /** + * Get extension from input + * + * @return string + * + * @since 4.0.0 + */ + private function getContextFromInput() + { + return $this->input->exists('context') ? + $this->input->get('context') : $this->input->post->get('context'); + } } diff --git a/code/api/components/com_fields/src/Controller/GroupsController.php b/code/api/components/com_fields/src/Controller/GroupsController.php index 85e42217..d851c10b 100644 --- a/code/api/components/com_fields/src/Controller/GroupsController.php +++ b/code/api/components/com_fields/src/Controller/GroupsController.php @@ -1,4 +1,5 @@ modelState->set('filter.context', $this->getContextFromInput()); + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.context', $this->getContextFromInput()); - return parent::displayItem($id); - } + return parent::displayItem($id); + } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.context', $this->getContextFromInput()); + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.context', $this->getContextFromInput()); - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Get extension from input - * - * @return string - * - * @since 4.0.0 - */ - private function getContextFromInput() - { - return $this->input->exists('context') ? - $this->input->get('context') : $this->input->post->get('context'); - } + /** + * Get extension from input + * + * @return string + * + * @since 4.0.0 + */ + private function getContextFromInput() + { + return $this->input->exists('context') ? + $this->input->get('context') : $this->input->post->get('context'); + } } diff --git a/code/api/components/com_fields/src/View/Fields/JsonapiView.php b/code/api/components/com_fields/src/View/Fields/JsonapiView.php index 06adcbcf..b522a879 100644 --- a/code/api/components/com_fields/src/View/Fields/JsonapiView.php +++ b/code/api/components/com_fields/src/View/Fields/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $item = $this->prepareItem($model->getItem()); - } + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + if ($item === null) { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + $item = $this->prepareItem($model->getItem()); + } - if ($item->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } - if ($item->context != $this->getModel()->getState('filter.context')) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->context != $this->getModel()->getState('filter.context')) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::displayItem($item); - } + return parent::displayItem($item); + } } diff --git a/code/api/components/com_fields/src/View/Groups/JsonapiView.php b/code/api/components/com_fields/src/View/Groups/JsonapiView.php index 30d6f55e..8871e48d 100644 --- a/code/api/components/com_fields/src/View/Groups/JsonapiView.php +++ b/code/api/components/com_fields/src/View/Groups/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $item = $this->prepareItem($model->getItem()); - } + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + if ($item === null) { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + $item = $this->prepareItem($model->getItem()); + } - if ($item->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } - if ($item->context != $this->getModel()->getState('filter.context')) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->context != $this->getModel()->getState('filter.context')) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::displayItem($item); - } + return parent::displayItem($item); + } } diff --git a/code/api/components/com_installer/src/Controller/ManageController.php b/code/api/components/com_installer/src/Controller/ManageController.php index c87ebc37..734c1e92 100644 --- a/code/api/components/com_installer/src/Controller/ManageController.php +++ b/code/api/components/com_installer/src/Controller/ManageController.php @@ -1,4 +1,5 @@ input->get('core', $this->input->get->get('core')); + /** + * Extension list view amended to add filtering of data + * + * @return static A BaseController object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $requestBool = $this->input->get('core', $this->input->get->get('core')); - if (!\is_null($requestBool) && $requestBool !== 'true' && $requestBool !== 'false') - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'core'); + if (!\is_null($requestBool) && $requestBool !== 'true' && $requestBool !== 'false') { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'core'); - throw new InvalidParameterException($error, 400, null, 'core'); - } + throw new InvalidParameterException($error, 400, null, 'core'); + } - if (!\is_null($requestBool)) - { - $this->modelState->set('filter.core', ($requestBool === 'true') ? '1' : '0'); - } + if (!\is_null($requestBool)) { + $this->modelState->set('filter.core', ($requestBool === 'true') ? '1' : '0'); + } - $this->modelState->set('filter.status', $this->input->get('status', $this->input->get->get('status', null, 'INT'), 'INT')); - $this->modelState->set('filter.type', $this->input->get('type', $this->input->get->get('type', null, 'STRING'), 'STRING')); + $this->modelState->set('filter.status', $this->input->get('status', $this->input->get->get('status', null, 'INT'), 'INT')); + $this->modelState->set('filter.type', $this->input->get('type', $this->input->get->get('type', null, 'STRING'), 'STRING')); - return parent::displayList(); - } + return parent::displayList(); + } } diff --git a/code/api/components/com_installer/src/View/Manage/JsonapiView.php b/code/api/components/com_installer/src/View/Manage/JsonapiView.php index 9cd75874..41b71303 100644 --- a/code/api/components/com_installer/src/View/Manage/JsonapiView.php +++ b/code/api/components/com_installer/src/View/Manage/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->extension_id; - unset($item->extension_id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->extension_id; + unset($item->extension_id); - return $item; - } + return $item; + } } diff --git a/code/api/components/com_languages/src/Controller/LanguagesController.php b/code/api/components/com_languages/src/Controller/LanguagesController.php index 5ef456a9..758789e6 100644 --- a/code/api/components/com_languages/src/Controller/LanguagesController.php +++ b/code/api/components/com_languages/src/Controller/LanguagesController.php @@ -1,4 +1,5 @@ modelState->set('filter.language', $this->getLanguageFromInput()); - $this->modelState->set('filter.client', $this->getClientFromInput()); - - return parent::displayItem($id); - } - - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.language', $this->getLanguageFromInput()); - $this->modelState->set('filter.client', $this->getClientFromInput()); - - return parent::displayList(); - } - - /** - * Method to save a record. - * - * @param integer $recordKey The primary key of the item (if exists) - * - * @return integer The record ID on success, false on failure - * - * @since 4.0.0 - */ - protected function save($recordKey = null) - { - /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ - $model = $this->getModel(Inflector::singularize($this->contentType)); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('filter.language', $this->input->post->get('lang_code')); - $model->setState('filter.client', $this->input->post->get('app')); - - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - // @todo: Not the cleanest thing ever but it works... - Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms'); - - // Validate the posted data. - $form = $model->getForm($data, false); - - if (!$form) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_FORM_CREATE')); - } - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - $errors = $model->getErrors(); - $messages = []; - - for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $messages[] = "{$errors[$i]->getMessage()}"; - } - else - { - $messages[] = "{$errors[$i]}"; - } - } - - throw new InvalidParameterException(implode("\n", $messages)); - } - - if (!isset($validData['tags'])) - { - $validData['tags'] = []; - } - - if (!$model->save($validData)) - { - throw new Exception\Save(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError())); - } - - return $validData['key']; - } - - /** - * Removes an item. - * - * @param integer $id The primary key to delete item. - * - * @return void - * - * @since 4.0.0 - */ - public function delete($id = null) - { - $id = $this->input->get('id', '', 'string'); - - $this->input->set('model', $this->contentType); - - $this->modelState->set('filter.language', $this->getLanguageFromInput()); - $this->modelState->set('filter.client', $this->getClientFromInput()); - - parent::delete($id); - } - - /** - * Get client code from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientFromInput() - { - return $this->input->exists('app') ? $this->input->get('app') : $this->input->post->get('app'); - } - - /** - * Get language code from input - * - * @return string - * - * @since 4.0.0 - */ - private function getLanguageFromInput() - { - return $this->input->exists('lang_code') ? - $this->input->get('lang_code') : $this->input->post->get('lang_code'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'overrides'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'overrides'; + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.language', $this->getLanguageFromInput()); + $this->modelState->set('filter.client', $this->getClientFromInput()); + + return parent::displayItem($id); + } + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.language', $this->getLanguageFromInput()); + $this->modelState->set('filter.client', $this->getClientFromInput()); + + return parent::displayList(); + } + + /** + * Method to save a record. + * + * @param integer $recordKey The primary key of the item (if exists) + * + * @return integer The record ID on success, false on failure + * + * @since 4.0.0 + */ + protected function save($recordKey = null) + { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(Inflector::singularize($this->contentType)); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('filter.language', $this->input->post->get('lang_code')); + $model->setState('filter.client', $this->input->post->get('app')); + + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + // @todo: Not the cleanest thing ever but it works... + Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms'); + + // Validate the posted data. + $form = $model->getForm($data, false); + + if (!$form) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_FORM_CREATE')); + } + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + $errors = $model->getErrors(); + $messages = []; + + for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $messages[] = "{$errors[$i]->getMessage()}"; + } else { + $messages[] = "{$errors[$i]}"; + } + } + + throw new InvalidParameterException(implode("\n", $messages)); + } + + if (!isset($validData['tags'])) { + $validData['tags'] = []; + } + + if (!$model->save($validData)) { + throw new Exception\Save(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError())); + } + + return $validData['key']; + } + + /** + * Removes an item. + * + * @param integer $id The primary key to delete item. + * + * @return void + * + * @since 4.0.0 + */ + public function delete($id = null) + { + $id = $this->input->get('id', '', 'string'); + + $this->input->set('model', $this->contentType); + + $this->modelState->set('filter.language', $this->getLanguageFromInput()); + $this->modelState->set('filter.client', $this->getClientFromInput()); + + parent::delete($id); + } + + /** + * Get client code from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientFromInput() + { + return $this->input->exists('app') ? $this->input->get('app') : $this->input->post->get('app'); + } + + /** + * Get language code from input + * + * @return string + * + * @since 4.0.0 + */ + private function getLanguageFromInput() + { + return $this->input->exists('lang_code') ? + $this->input->get('lang_code') : $this->input->post->get('lang_code'); + } } diff --git a/code/api/components/com_languages/src/Controller/StringsController.php b/code/api/components/com_languages/src/Controller/StringsController.php index 4bdd554f..33db2be6 100644 --- a/code/api/components/com_languages/src/Controller/StringsController.php +++ b/code/api/components/com_languages/src/Controller/StringsController.php @@ -1,4 +1,5 @@ input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - if (!isset($data['searchstring']) || !\is_string($data['searchstring'])) - { - throw new InvalidParameterException("Invalid param 'searchstring'"); - } - - if (!isset($data['searchtype']) || !\in_array($data['searchtype'], ['constant', 'value'])) - { - throw new InvalidParameterException("Invalid param 'searchtype'"); - } - - $app = Factory::getApplication(); - $app->input->set('searchstring', $data['searchstring']); - $app->input->set('searchtype', $data['searchtype']); - $app->input->set('more', 0); - - $viewType = $this->app->getDocument()->getType(); - $viewName = $this->input->get('view', $this->default_view); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var \Joomla\Component\Languages\Api\View\Strings\JsonapiView $view */ - $view = $this->getView( - $viewName, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ - $model = $this->getModel($this->contentType, '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - // Push the model into the view (as default) - $view->setModel($model, true); - - $view->document = $this->app->getDocument(); - $view->displayList(); - - return $this; - } - - /** - * Refresh cache - * - * @return static A \JControllerLegacy object to support chaining. - * - * @throws \Exception - * @since 4.0.0 - */ - public function refresh() - { - /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ - $model = $this->getModel($this->contentType, '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $result = $model->refresh(); - - if ($result instanceof \Exception) - { - throw $result; - } - - return $this; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'strings'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'strings'; + + /** + * Search by languages constants + * + * @return static A \JControllerLegacy object to support chaining. + * + * @throws InvalidParameterException + * @since 4.0.0 + */ + public function search() + { + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + if (!isset($data['searchstring']) || !\is_string($data['searchstring'])) { + throw new InvalidParameterException("Invalid param 'searchstring'"); + } + + if (!isset($data['searchtype']) || !\in_array($data['searchtype'], ['constant', 'value'])) { + throw new InvalidParameterException("Invalid param 'searchtype'"); + } + + $app = Factory::getApplication(); + $app->input->set('searchstring', $data['searchstring']); + $app->input->set('searchtype', $data['searchtype']); + $app->input->set('more', 0); + + $viewType = $this->app->getDocument()->getType(); + $viewName = $this->input->get('view', $this->default_view); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var \Joomla\Component\Languages\Api\View\Strings\JsonapiView $view */ + $view = $this->getView( + $viewName, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ + $model = $this->getModel($this->contentType, '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + // Push the model into the view (as default) + $view->setModel($model, true); + + $view->document = $this->app->getDocument(); + $view->displayList(); + + return $this; + } + + /** + * Refresh cache + * + * @return static A \JControllerLegacy object to support chaining. + * + * @throws \Exception + * @since 4.0.0 + */ + public function refresh() + { + /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ + $model = $this->getModel($this->contentType, '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $result = $model->refresh(); + + if ($result instanceof \Exception) { + throw $result; + } + + return $this; + } } diff --git a/code/api/components/com_languages/src/View/Languages/JsonapiView.php b/code/api/components/com_languages/src/View/Languages/JsonapiView.php index 803a9896..8a8da418 100644 --- a/code/api/components/com_languages/src/View/Languages/JsonapiView.php +++ b/code/api/components/com_languages/src/View/Languages/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->lang_id; - unset($item->lang->id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->lang_id; + unset($item->lang->id); - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/code/api/components/com_languages/src/View/Overrides/JsonapiView.php b/code/api/components/com_languages/src/View/Overrides/JsonapiView.php index eb23f85d..03261138 100644 --- a/code/api/components/com_languages/src/View/Overrides/JsonapiView.php +++ b/code/api/components/com_languages/src/View/Overrides/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $id = $model->getState($model->getName() . '.id'); - $item = $this->prepareItem($model->getItem($id)); + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + /** @var \Joomla\Component\Languages\Administrator\Model\OverrideModel $model */ + $model = $this->getModel(); + $id = $model->getState($model->getName() . '.id'); + $item = $this->prepareItem($model->getItem($id)); - return parent::displayItem($item); - } - /** - * Execute and display a template script. - * - * @param array|null $items Array of items - * - * @return string - * - * @since 4.0.0 - */ - public function displayList(array $items = null) - { - /** @var \Joomla\Component\Languages\Administrator\Model\OverridesModel $model */ - $model = $this->getModel(); - $items = []; + return parent::displayItem($item); + } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + /** @var \Joomla\Component\Languages\Administrator\Model\OverridesModel $model */ + $model = $this->getModel(); + $items = []; - foreach ($model->getOverrides() as $key => $override) - { - $item = (object) [ - 'key' => $key, - 'override' => $override, - ]; + foreach ($model->getOverrides() as $key => $override) { + $item = (object) [ + 'key' => $key, + 'override' => $override, + ]; - $items[] = $this->prepareItem($item); - } + $items[] = $this->prepareItem($item); + } - return parent::displayList($items); - } + return parent::displayList($items); + } - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->id = $item->key; - $item->value = $item->override; - unset($item->key); - unset($item->override); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->key; + $item->value = $item->override; + unset($item->key); + unset($item->override); - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/code/api/components/com_languages/src/View/Strings/JsonapiView.php b/code/api/components/com_languages/src/View/Strings/JsonapiView.php index 73766eec..9c14d17d 100644 --- a/code/api/components/com_languages/src/View/Strings/JsonapiView.php +++ b/code/api/components/com_languages/src/View/Strings/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $result = $model->search(); - - if ($result instanceof \Exception) - { - throw $result; - } - - $items = []; - - foreach ($result['results'] as $item) - { - $items[] = $this->prepareItem($item); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - if ($this->type === null) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CONTENT_TYPE_MISSING'), 400); - } - - $collection = (new Collection($items, new JoomlaSerializer($this->type))) - ->fields([$this->type => $this->fieldsToRenderList]); - - // Set the data into the document and render it - $this->document->setData($collection); - - return $this->document->render(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->id = $item->constant; - unset($item->constant); - - return parent::prepareItem($item); - } + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'string', + 'file', + ]; + + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ + $model = $this->getModel(); + $result = $model->search(); + + if ($result instanceof \Exception) { + throw $result; + } + + $items = []; + + foreach ($result['results'] as $item) { + $items[] = $this->prepareItem($item); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + if ($this->type === null) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CONTENT_TYPE_MISSING'), 400); + } + + $collection = (new Collection($items, new JoomlaSerializer($this->type))) + ->fields([$this->type => $this->fieldsToRenderList]); + + // Set the data into the document and render it + $this->document->setData($collection); + + return $this->document->render(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->constant; + unset($item->constant); + + return parent::prepareItem($item); + } } diff --git a/code/api/components/com_menus/src/Controller/ItemsController.php b/code/api/components/com_menus/src/Controller/ItemsController.php index 44f5da57..57fca229 100644 --- a/code/api/components/com_menus/src/Controller/ItemsController.php +++ b/code/api/components/com_menus/src/Controller/ItemsController.php @@ -1,4 +1,5 @@ modelState->set('filter.client_id', $this->getClientIdFromInput()); - - return parent::displayItem($id); - } - - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); - - return parent::displayList(); - } - - /** - * Method to add a new record. - * - * @return void - * - * @since 4.0.0 - * @throws NotAllowed - * @throws \RuntimeException - */ - public function add() - { - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - if (isset($data['menutype'])) - { - $this->input->set('menutype', $data['menutype']); - $this->input->set('com_menus.items.menutype', $data['menutype']); - } - - isset($data['type']) && $this->input->set('type', $data['type']); - isset($data['parent_id']) && $this->input->set('parent_id', $data['parent_id']); - isset($data['link']) && $this->input->set('link', $data['link']); - - $this->input->set('id', '0'); - - parent::add(); - } - - /** - * Method to edit an existing record. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function edit() - { - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - if (isset($data['menutype'])) - { - $this->input->set('menutype', $data['menutype']); - $this->input->set('com_menus.items.menutype', $data['menutype']); - } - - isset($data['type']) && $this->input->set('type', $data['type']); - isset($data['parent_id']) && $this->input->set('parent_id', $data['parent_id']); - isset($data['link']) && $this->input->set('link', $data['link']); - - return parent::edit(); - } - - /** - * Return menu items types - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function getTypes() - { - $viewType = $this->app->getDocument()->getType(); - $viewName = $this->input->get('view', $this->default_view); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $viewName, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var ListModel $model */ - $model = $this->getModel('menutypes', '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('client_id', $this->getClientIdFromInput()); - - $view->setModel($model, true); - - $view->document = $this->app->getDocument(); - - $view->displayListTypes(); - - return $this; - } - - /** - * Get client id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientIdFromInput() - { - return $this->input->exists('client_id') ? - $this->input->get('client_id') : $this->input->post->get('client_id'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'items'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'items'; + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + + return parent::displayItem($id); + } + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + + return parent::displayList(); + } + + /** + * Method to add a new record. + * + * @return void + * + * @since 4.0.0 + * @throws NotAllowed + * @throws \RuntimeException + */ + public function add() + { + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + if (isset($data['menutype'])) { + $this->input->set('menutype', $data['menutype']); + $this->input->set('com_menus.items.menutype', $data['menutype']); + } + + isset($data['type']) && $this->input->set('type', $data['type']); + isset($data['parent_id']) && $this->input->set('parent_id', $data['parent_id']); + isset($data['link']) && $this->input->set('link', $data['link']); + + $this->input->set('id', '0'); + + parent::add(); + } + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function edit() + { + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + if (isset($data['menutype'])) { + $this->input->set('menutype', $data['menutype']); + $this->input->set('com_menus.items.menutype', $data['menutype']); + } + + isset($data['type']) && $this->input->set('type', $data['type']); + isset($data['parent_id']) && $this->input->set('parent_id', $data['parent_id']); + isset($data['link']) && $this->input->set('link', $data['link']); + + return parent::edit(); + } + + /** + * Return menu items types + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function getTypes() + { + $viewType = $this->app->getDocument()->getType(); + $viewName = $this->input->get('view', $this->default_view); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $viewName, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var ListModel $model */ + $model = $this->getModel('menutypes', '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('client_id', $this->getClientIdFromInput()); + + $view->setModel($model, true); + + $view->document = $this->app->getDocument(); + + $view->displayListTypes(); + + return $this; + } + + /** + * Get client id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientIdFromInput() + { + return $this->input->exists('client_id') ? + $this->input->get('client_id') : $this->input->post->get('client_id'); + } } diff --git a/code/api/components/com_menus/src/Controller/MenusController.php b/code/api/components/com_menus/src/Controller/MenusController.php index b7424ff7..280d6ff4 100644 --- a/code/api/components/com_menus/src/Controller/MenusController.php +++ b/code/api/components/com_menus/src/Controller/MenusController.php @@ -1,4 +1,5 @@ modelState->set('filter.client_id', $this->getClientIdFromInput()); + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); - return parent::displayItem($id); - } + return parent::displayItem($id); + } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Get client id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientIdFromInput() - { - return $this->input->exists('client_id') ? - $this->input->get('client_id') : $this->input->post->get('client_id'); - } + /** + * Get client id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientIdFromInput() + { + return $this->input->exists('client_id') ? + $this->input->get('client_id') : $this->input->post->get('client_id'); + } } diff --git a/code/api/components/com_menus/src/View/Items/JsonapiView.php b/code/api/components/com_menus/src/View/Items/JsonapiView.php index 02b5b8d0..8affc8f7 100644 --- a/code/api/components/com_menus/src/View/Items/JsonapiView.php +++ b/code/api/components/com_menus/src/View/Items/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $items = []; - - foreach ($model->getTypeOptions() as $type => $data) - { - $groupItems = []; - - foreach ($data as $item) - { - $item->id = implode('/', $item->request); - $item->title = Text::_($item->title); - $item->description = Text::_($item->description); - $item->group = Text::_($type); - - $groupItems[] = $item; - } - - $items = array_merge($items, $groupItems); - } - - // Set up links for pagination - $currentUrl = Uri::getInstance(); - $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; - $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); - - $offset = $currentPageQuery['offset']; - $limit = $currentPageQuery['limit']; - $totalItemsCount = \count($items); - $totalPagesAvailable = ceil($totalItemsCount / $limit); - - $items = array_splice($items, $offset, $limit); - - $firstPage = clone $currentUrl; - $firstPageQuery = $currentPageQuery; - $firstPageQuery['offset'] = 0; - $firstPage->setVar('page', $firstPageQuery); - - $nextPage = clone $currentUrl; - $nextPageQuery = $currentPageQuery; - $nextOffset = $currentPageQuery['offset'] + $limit; - $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; - $nextPage->setVar('page', $nextPageQuery); - - $previousPage = clone $currentUrl; - $previousPageQuery = $currentPageQuery; - $previousOffset = $currentPageQuery['offset'] - $limit; - $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; - $previousPage->setVar('page', $previousPageQuery); - - $lastPage = clone $currentUrl; - $lastPageQuery = $currentPageQuery; - $lastPageQuery['offset'] = $totalPagesAvailable - $limit; - $lastPage->setVar('page', $lastPageQuery); - - $collection = (new Collection($items, new JoomlaSerializer('menutypes'))); - - // Set the data into the document and render it - $this->document->addMeta('total-pages', $totalPagesAvailable) - ->setData($collection) - ->addLink('self', (string) $currentUrl) - ->addLink('first', (string) $firstPage) - ->addLink('next', (string) $nextPage) - ->addLink('previous', (string) $previousPage) - ->addLink('last', (string) $lastPage); - - return $this->document->render(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - if (\is_string($item->params)) - { - $item->params = json_decode($item->params); - } - - return parent::prepareItem($item); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'parent_id', + 'level', + 'lft', + 'rgt', + 'alias', + 'typeAlias', + 'menutype', + 'title', + 'note', + 'path', + 'link', + 'type', + 'published', + 'component_id', + 'checked_out', + 'checked_out_time', + 'browserNav', + 'access', + 'img', + 'template_style_id', + 'params', + 'home', + 'language', + 'client_id', + 'publish_up', + 'publish_down', + 'request', + 'associations', + 'menuordering', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'menutype', + 'title', + 'alias', + 'note', + 'path', + 'link', + 'type', + 'parent_id', + 'level', + 'a.published', + 'component_id', + 'checked_out', + 'checked_out_time', + 'browserNav', + 'access', + 'img', + 'template_style_id', + 'params', + 'lft', + 'rgt', + 'home', + 'language', + 'client_id', + 'enabled', + 'publish_up', + 'publish_down', + 'published', + 'language_title', + 'language_image', + 'language_sef', + 'editor', + 'componentname', + 'access_level', + 'menutype_id', + 'menutype_title', + 'association', + 'name', + ]; + + /** + * Execute and display a list items types. + * + * @return string + * + * @since 4.0.0 + */ + public function displayListTypes() + { + /** @var \Joomla\Component\Menus\Administrator\Model\MenutypesModel $model */ + $model = $this->getModel(); + $items = []; + + foreach ($model->getTypeOptions() as $type => $data) { + $groupItems = []; + + foreach ($data as $item) { + $item->id = implode('/', $item->request); + $item->title = Text::_($item->title); + $item->description = Text::_($item->description); + $item->group = Text::_($type); + + $groupItems[] = $item; + } + + $items = array_merge($items, $groupItems); + } + + // Set up links for pagination + $currentUrl = Uri::getInstance(); + $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; + $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); + + $offset = $currentPageQuery['offset']; + $limit = $currentPageQuery['limit']; + $totalItemsCount = \count($items); + $totalPagesAvailable = ceil($totalItemsCount / $limit); + + $items = array_splice($items, $offset, $limit); + + $firstPage = clone $currentUrl; + $firstPageQuery = $currentPageQuery; + $firstPageQuery['offset'] = 0; + $firstPage->setVar('page', $firstPageQuery); + + $nextPage = clone $currentUrl; + $nextPageQuery = $currentPageQuery; + $nextOffset = $currentPageQuery['offset'] + $limit; + $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; + $nextPage->setVar('page', $nextPageQuery); + + $previousPage = clone $currentUrl; + $previousPageQuery = $currentPageQuery; + $previousOffset = $currentPageQuery['offset'] - $limit; + $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; + $previousPage->setVar('page', $previousPageQuery); + + $lastPage = clone $currentUrl; + $lastPageQuery = $currentPageQuery; + $lastPageQuery['offset'] = $totalPagesAvailable - $limit; + $lastPage->setVar('page', $lastPageQuery); + + $collection = (new Collection($items, new JoomlaSerializer('menutypes'))); + + // Set the data into the document and render it + $this->document->addMeta('total-pages', $totalPagesAvailable) + ->setData($collection) + ->addLink('self', (string) $currentUrl) + ->addLink('first', (string) $firstPage) + ->addLink('next', (string) $nextPage) + ->addLink('previous', (string) $previousPage) + ->addLink('last', (string) $lastPage); + + return $this->document->render(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + if (\is_string($item->params)) { + $item->params = json_decode($item->params); + } + + return parent::prepareItem($item); + } } diff --git a/code/api/components/com_menus/src/View/Menus/JsonapiView.php b/code/api/components/com_menus/src/View/Menus/JsonapiView.php index 0750e5a3..836256a4 100644 --- a/code/api/components/com_menus/src/View/Menus/JsonapiView.php +++ b/code/api/components/com_menus/src/View/Menus/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->message_id; - unset($item->message_id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->message_id; + unset($item->message_id); - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/code/api/components/com_modules/src/Controller/ModulesController.php b/code/api/components/com_modules/src/Controller/ModulesController.php index bb900e2b..f3947940 100644 --- a/code/api/components/com_modules/src/Controller/ModulesController.php +++ b/code/api/components/com_modules/src/Controller/ModulesController.php @@ -1,4 +1,5 @@ modelState->set('filter.client_id', $this->getClientIdFromInput()); - - return parent::displayItem($id); - } - - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); - - return parent::displayList(); - } - - /** - * Return module items types - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function getTypes() - { - $viewType = $this->app->getDocument()->getType(); - $viewName = $this->input->get('view', $this->default_view); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $viewName, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var SelectModel $model */ - $model = $this->getModel('select', '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('client_id', $this->getClientIdFromInput()); - - $view->setModel($model, true); - - $view->document = $this->app->getDocument(); - - $view->displayListTypes(); - - return $this; - } - - /** - * Get client id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientIdFromInput() - { - return $this->input->exists('client_id') ? - $this->input->get('client_id') : $this->input->post->get('client_id'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'modules'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'modules'; + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + + return parent::displayItem($id); + } + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + + return parent::displayList(); + } + + /** + * Return module items types + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function getTypes() + { + $viewType = $this->app->getDocument()->getType(); + $viewName = $this->input->get('view', $this->default_view); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $viewName, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var SelectModel $model */ + $model = $this->getModel('select', '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('client_id', $this->getClientIdFromInput()); + + $view->setModel($model, true); + + $view->document = $this->app->getDocument(); + + $view->displayListTypes(); + + return $this; + } + + /** + * Get client id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientIdFromInput() + { + return $this->input->exists('client_id') ? + $this->input->get('client_id') : $this->input->post->get('client_id'); + } } diff --git a/code/api/components/com_modules/src/View/Modules/JsonapiView.php b/code/api/components/com_modules/src/View/Modules/JsonapiView.php index 0cd3f3f6..d49d7932 100644 --- a/code/api/components/com_modules/src/View/Modules/JsonapiView.php +++ b/code/api/components/com_modules/src/View/Modules/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - - if ($item === null) - { - $item = $this->prepareItem($model->getItem()); - } - - if ($item->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } - - if ((int) $model->getState('client_id') !== $item->client_id) - { - throw new RouteNotFoundException('Item does not exist'); - } - - return parent::displayItem($item); - } - - /** - * Execute and display a list modules types. - * - * @return string - * - * @since 4.0.0 - */ - public function displayListTypes() - { - /** @var SelectModel $model */ - $model = $this->getModel(); - $items = []; - - foreach ($model->getItems() as $item) - { - $item->id = $item->extension_id; - unset($item->extension_id); - - $items[] = $item; - } - - $this->fieldsToRenderList = ['id', 'name', 'module', 'xml', 'desc']; - - return parent::displayList($items); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'typeAlias', + 'asset_id', + 'title', + 'note', + 'content', + 'ordering', + 'position', + 'checked_out', + 'checked_out_time', + 'publish_up', + 'publish_down', + 'published', + 'module', + 'access', + 'showtitle', + 'params', + 'client_id', + 'language', + 'assigned', + 'assignment', + 'xml', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'title', + 'note', + 'position', + 'module', + 'language', + 'checked_out', + 'checked_out_time', + 'published', + 'enabled', + 'access', + 'ordering', + 'publish_up', + 'publish_down', + 'language_title', + 'language_image', + 'editor', + 'access_level', + 'pages', + 'name', + ]; + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + + if ($item === null) { + $item = $this->prepareItem($model->getItem()); + } + + if ($item->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } + + if ((int) $model->getState('client_id') !== $item->client_id) { + throw new RouteNotFoundException('Item does not exist'); + } + + return parent::displayItem($item); + } + + /** + * Execute and display a list modules types. + * + * @return string + * + * @since 4.0.0 + */ + public function displayListTypes() + { + /** @var SelectModel $model */ + $model = $this->getModel(); + $items = []; + + foreach ($model->getItems() as $item) { + $item->id = $item->extension_id; + unset($item->extension_id); + + $items[] = $item; + } + + $this->fieldsToRenderList = ['id', 'name', 'module', 'xml', 'desc']; + + return parent::displayList($items); + } } diff --git a/code/api/components/com_newsfeeds/src/Controller/FeedsController.php b/code/api/components/com_newsfeeds/src/Controller/FeedsController.php index 26122525..6aa99b1a 100644 --- a/code/api/components/com_newsfeeds/src/Controller/FeedsController.php +++ b/code/api/components/com_newsfeeds/src/Controller/FeedsController.php @@ -1,4 +1,5 @@ type); - - foreach ($model->associations as $association) - { - $resources[] = (new Resource($association, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/newsfeeds/feeds/' . $association->id)); - } - - $collection = new Collection($resources, $serializer); - - return new Relationship($collection); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function category($model) - { - $serializer = new JoomlaSerializer('categories'); - - $resource = (new Resource($model->catid, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/newfeeds/categories/' . $model->catid)); - - return new Relationship($resource); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function createdBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->created_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); - - return new Relationship($resource); - } - - /** - * Build editor relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function modifiedBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->modified_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); - - return new Relationship($resource); - } + use TagApiSerializerTrait; + + /** + * Build content relationships by associations + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function languageAssociations($model) + { + $resources = []; + + // @todo: This can't be hardcoded in the future? + $serializer = new JoomlaSerializer($this->type); + + foreach ($model->associations as $association) { + $resources[] = (new Resource($association, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/newsfeeds/feeds/' . $association->id)); + } + + $collection = new Collection($resources, $serializer); + + return new Relationship($collection); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function category($model) + { + $serializer = new JoomlaSerializer('categories'); + + $resource = (new Resource($model->catid, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/newfeeds/categories/' . $model->catid)); + + return new Relationship($resource); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function createdBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->created_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); + + return new Relationship($resource); + } + + /** + * Build editor relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function modifiedBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->modified_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); + + return new Relationship($resource); + } } diff --git a/code/api/components/com_newsfeeds/src/View/Feeds/JsonapiView.php b/code/api/components/com_newsfeeds/src/View/Feeds/JsonapiView.php index 191b6427..7fca3397 100644 --- a/code/api/components/com_newsfeeds/src/View/Feeds/JsonapiView.php +++ b/code/api/components/com_newsfeeds/src/View/Feeds/JsonapiView.php @@ -1,4 +1,5 @@ serializer = new NewsfeedSerializer($config['contentType']); - } - - parent::__construct($config); - } - - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - if (Multilanguage::isEnabled()) - { - $this->fieldsToRenderItem[] = 'languageAssociations'; - $this->relationship[] = 'languageAssociations'; - } - - return parent::displayItem(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - if (Multilanguage::isEnabled() && !empty($item->associations)) - { - $associations = []; - - foreach ($item->associations as $language => $association) - { - $itemId = explode(':', $association)[0]; - - $associations[] = (object) [ - 'id' => $itemId, - 'language' => $language, - ]; - } - - $item->associations = $associations; - } - - if (!empty($item->tags->tags)) - { - $tagsIds = explode(',', $item->tags->tags); - $tagsNames = $item->tagsHelper->getTagNames($tagsIds); - - $item->tags = array_combine($tagsIds, $tagsNames); - } - else - { - $item->tags = []; - } - - return parent::prepareItem($item); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'category', + 'name', + 'alias', + 'link', + 'published', + 'numarticles', + 'cache_time', + 'checked_out', + 'checked_out_time', + 'ordering', + 'rtl', + 'access', + 'language', + 'params', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'modified_by', + 'metakey', + 'metadesc', + 'metadata', + 'publish_up', + 'publish_down', + 'description', + 'version', + 'hits', + 'images', + 'tags', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'name', + 'alias', + 'checked_out', + 'checked_out_time', + 'category', + 'numarticles', + 'cache_time', + 'created_by', + 'published', + 'access', + 'ordering', + 'language', + 'publish_up', + 'publish_down', + 'language_title', + 'language_image', + 'editor', + 'access_level', + 'category_title', + ]; + + /** + * The relationships the item has + * + * @var array + * @since 4.0.0 + */ + protected $relationship = [ + 'category', + 'created_by', + 'modified_by', + 'tags', + ]; + + /** + * Constructor. + * + * @param array $config A named configuration array for object construction. + * contentType: the name (optional) of the content type to use for the serialization + * + * @since 4.0.0 + */ + public function __construct($config = []) + { + if (\array_key_exists('contentType', $config)) { + $this->serializer = new NewsfeedSerializer($config['contentType']); + } + + parent::__construct($config); + } + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + if (Multilanguage::isEnabled()) { + $this->fieldsToRenderItem[] = 'languageAssociations'; + $this->relationship[] = 'languageAssociations'; + } + + return parent::displayItem(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + if (Multilanguage::isEnabled() && !empty($item->associations)) { + $associations = []; + + foreach ($item->associations as $language => $association) { + $itemId = explode(':', $association)[0]; + + $associations[] = (object) [ + 'id' => $itemId, + 'language' => $language, + ]; + } + + $item->associations = $associations; + } + + if (!empty($item->tags->tags)) { + $tagsIds = explode(',', $item->tags->tags); + $tagsNames = $item->tagsHelper->getTagNames($tagsIds); + + $item->tags = array_combine($tagsIds, $tagsNames); + } else { + $item->tags = []; + } + + return parent::prepareItem($item); + } } diff --git a/code/api/components/com_plugins/src/Controller/PluginsController.php b/code/api/components/com_plugins/src/Controller/PluginsController.php index 67bfb198..a402d46d 100644 --- a/code/api/components/com_plugins/src/Controller/PluginsController.php +++ b/code/api/components/com_plugins/src/Controller/PluginsController.php @@ -1,4 +1,5 @@ input->getInt('id'); - - if (!$recordId) - { - throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404); - } - - $data = json_decode($this->input->json->getRaw(), true); - - foreach ($data as $key => $value) - { - if (!\in_array($key, ['enabled', 'access', 'ordering'])) - { - throw new InvalidParameterException("Invalid parameter {$key}.", 400); - } - } - - /** @var \Joomla\Component\Plugins\Administrator\Model\PluginModel $model */ - $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $item = $model->getItem($recordId); - - if (!isset($item->extension_id)) - { - throw new RouteNotFoundException('Item does not exist'); - } - - $data['folder'] = $item->folder; - $data['element'] = $item->element; - - $this->input->set('data', $data); - - return parent::edit(); - } - - /** - * Plugin list view with filtering of data - * - * @return static A BaseController object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $apiFilterInfo = $this->input->get('filter', [], 'array'); - $filter = InputFilter::getInstance(); - - if (\array_key_exists('element', $apiFilterInfo)) - { - $this->modelState->set('filter.element', $filter->clean($apiFilterInfo['element'], 'STRING')); - } - - if (\array_key_exists('status', $apiFilterInfo)) - { - $this->modelState->set('filter.enabled', $filter->clean($apiFilterInfo['status'], 'INT')); - } - - if (\array_key_exists('search', $apiFilterInfo)) - { - $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); - } - - if (\array_key_exists('type', $apiFilterInfo)) - { - $this->modelState->set('filter.folder', $filter->clean($apiFilterInfo['type'], 'STRING')); - } - - return parent::displayList(); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'plugins'; + + /** + * The default view for the display method. + * + * @var string + * + * @since 3.0 + */ + protected $default_view = 'plugins'; + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function edit() + { + $recordId = $this->input->getInt('id'); + + if (!$recordId) { + throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404); + } + + $data = json_decode($this->input->json->getRaw(), true); + + foreach ($data as $key => $value) { + if (!\in_array($key, ['enabled', 'access', 'ordering'])) { + throw new InvalidParameterException("Invalid parameter {$key}.", 400); + } + } + + /** @var \Joomla\Component\Plugins\Administrator\Model\PluginModel $model */ + $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $item = $model->getItem($recordId); + + if (!isset($item->extension_id)) { + throw new RouteNotFoundException('Item does not exist'); + } + + $data['folder'] = $item->folder; + $data['element'] = $item->element; + + $this->input->set('data', $data); + + return parent::edit(); + } + + /** + * Plugin list view with filtering of data + * + * @return static A BaseController object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); + + if (\array_key_exists('element', $apiFilterInfo)) { + $this->modelState->set('filter.element', $filter->clean($apiFilterInfo['element'], 'STRING')); + } + + if (\array_key_exists('status', $apiFilterInfo)) { + $this->modelState->set('filter.enabled', $filter->clean($apiFilterInfo['status'], 'INT')); + } + + if (\array_key_exists('search', $apiFilterInfo)) { + $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); + } + + if (\array_key_exists('type', $apiFilterInfo)) { + $this->modelState->set('filter.folder', $filter->clean($apiFilterInfo['type'], 'STRING')); + } + + return parent::displayList(); + } } diff --git a/code/api/components/com_plugins/src/View/Plugins/JsonapiView.php b/code/api/components/com_plugins/src/View/Plugins/JsonapiView.php index c26d50b8..e07782b2 100644 --- a/code/api/components/com_plugins/src/View/Plugins/JsonapiView.php +++ b/code/api/components/com_plugins/src/View/Plugins/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->extension_id; - unset($item->extension_id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->extension_id; + unset($item->extension_id); - return $item; - } + return $item; + } } diff --git a/code/api/components/com_privacy/src/Controller/ConsentsController.php b/code/api/components/com_privacy/src/Controller/ConsentsController.php index 66949d2d..3951aa58 100644 --- a/code/api/components/com_privacy/src/Controller/ConsentsController.php +++ b/code/api/components/com_privacy/src/Controller/ConsentsController.php @@ -1,4 +1,5 @@ input->get('id', 0, 'int'); - } - - $this->input->set('model', $this->contentType); - - return parent::displayItem($id); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'consents'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'consents'; + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + if ($id === null) { + $id = $this->input->get('id', 0, 'int'); + } + + $this->input->set('model', $this->contentType); + + return parent::displayItem($id); + } } diff --git a/code/api/components/com_privacy/src/Controller/RequestsController.php b/code/api/components/com_privacy/src/Controller/RequestsController.php index 772d1b0f..e58e12d2 100644 --- a/code/api/components/com_privacy/src/Controller/RequestsController.php +++ b/code/api/components/com_privacy/src/Controller/RequestsController.php @@ -1,4 +1,5 @@ input->get('id', 0, 'int'); - } + /** + * Export request data + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function export($id = null) + { + if ($id === null) { + $id = $this->input->get('id', 0, 'int'); + } - $viewType = $this->app->getDocument()->getType(); - $viewName = $this->input->get('view', $this->default_view); - $viewLayout = $this->input->get('layout', 'default', 'string'); + $viewType = $this->app->getDocument()->getType(); + $viewName = $this->input->get('view', $this->default_view); + $viewLayout = $this->input->get('layout', 'default', 'string'); - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $viewName, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $viewName, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } - $model = $this->getModel('export'); + $model = $this->getModel('export'); - try - { - $modelName = $model->getName(); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } + try { + $modelName = $model->getName(); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } - $model->setState($modelName . '.request_id', $id); + $model->setState($modelName . '.request_id', $id); - $view->setModel($model, true); + $view->setModel($model, true); - $view->document = $this->app->getDocument(); - $view->export(); + $view->document = $this->app->getDocument(); + $view->export(); - return $this; - } + return $this; + } } diff --git a/code/api/components/com_privacy/src/View/Consents/JsonapiView.php b/code/api/components/com_privacy/src/View/Consents/JsonapiView.php index 60f8fadd..f3b34893 100644 --- a/code/api/components/com_privacy/src/View/Consents/JsonapiView.php +++ b/code/api/components/com_privacy/src/View/Consents/JsonapiView.php @@ -1,4 +1,5 @@ get('state')->get($this->getName() . '.id'); - - if ($id === null) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ITEMID_MISSING')); - } - - /** @var \Joomla\CMS\MVC\Model\ListModel $model */ - $model = $this->getModel(); - $displayItem = null; - - foreach ($model->getItems() as $item) - { - $item = $this->prepareItem($item); - - if ($item->id === $id) - { - $displayItem = $item; - break; - } - } - - if ($displayItem === null) - { - throw new RouteNotFoundException('Item does not exist'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - if ($this->type === null) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CONTENT_TYPE_MISSING')); - } - - $serializer = new JoomlaSerializer($this->type); - $element = (new Resource($displayItem, $serializer)) - ->fields([$this->type => $this->fieldsToRenderItem]); - - $this->document->setData($element); - $this->document->addLink('self', Uri::current()); - - return $this->document->render(); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'user_id', + 'state', + 'created', + 'subject', + 'body', + 'remind', + 'token', + 'username', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'user_id', + 'state', + 'created', + 'subject', + 'body', + 'remind', + 'token', + 'username', + ]; + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + $id = $this->get('state')->get($this->getName() . '.id'); + + if ($id === null) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ITEMID_MISSING')); + } + + /** @var \Joomla\CMS\MVC\Model\ListModel $model */ + $model = $this->getModel(); + $displayItem = null; + + foreach ($model->getItems() as $item) { + $item = $this->prepareItem($item); + + if ($item->id === $id) { + $displayItem = $item; + break; + } + } + + if ($displayItem === null) { + throw new RouteNotFoundException('Item does not exist'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + if ($this->type === null) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CONTENT_TYPE_MISSING')); + } + + $serializer = new JoomlaSerializer($this->type); + $element = (new Resource($displayItem, $serializer)) + ->fields([$this->type => $this->fieldsToRenderItem]); + + $this->document->setData($element); + $this->document->addLink('self', Uri::current()); + + return $this->document->render(); + } } diff --git a/code/api/components/com_privacy/src/View/Requests/JsonapiView.php b/code/api/components/com_privacy/src/View/Requests/JsonapiView.php index 65cdefc6..ead484be 100644 --- a/code/api/components/com_privacy/src/View/Requests/JsonapiView.php +++ b/code/api/components/com_privacy/src/View/Requests/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); + /** + * Execute and display a template script. + * + * @return string + * + * @since 4.0.0 + */ + public function export() + { + /** @var ExportModel $model */ + $model = $this->getModel(); - $exportData = $model->collectDataForExportRequest(); + $exportData = $model->collectDataForExportRequest(); - if ($exportData == false) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($exportData == false) { + throw new RouteNotFoundException('Item does not exist'); + } - $serializer = new JoomlaSerializer('export'); - $element = (new Resource($exportData, $serializer)); + $serializer = new JoomlaSerializer('export'); + $element = (new Resource($exportData, $serializer)); - $this->document->setData($element); - $this->document->addLink('self', Uri::current()); + $this->document->setData($element); + $this->document->addLink('self', Uri::current()); - return $this->document->render(); - } + return $this->document->render(); + } } diff --git a/code/api/components/com_redirect/src/Controller/RedirectController.php b/code/api/components/com_redirect/src/Controller/RedirectController.php index 58a1ba43..5bfe4add 100644 --- a/code/api/components/com_redirect/src/Controller/RedirectController.php +++ b/code/api/components/com_redirect/src/Controller/RedirectController.php @@ -1,4 +1,5 @@ modelState->set('client_id', $this->getClientIdFromInput()); + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('client_id', $this->getClientIdFromInput()); - return parent::displayItem($id); - } + return parent::displayItem($id); + } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('client_id', $this->getClientIdFromInput()); + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('client_id', $this->getClientIdFromInput()); - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Method to allow extended classes to manipulate the data to be saved for an extension. - * - * @param array $data An array of input data. - * - * @return array - * - * @since 4.0.0 - * @throws InvalidParameterException - */ - protected function preprocessSaveData(array $data): array - { - $data['client_id'] = $this->getClientIdFromInput(); + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + * @throws InvalidParameterException + */ + protected function preprocessSaveData(array $data): array + { + $data['client_id'] = $this->getClientIdFromInput(); - // If we are updating an item the template is a readonly property based on the ID - if ($this->input->getMethod() === 'PATCH') - { - if (\array_key_exists('template', $data)) - { - throw new InvalidParameterException('The template property cannot be modified for an existing style'); - } + // If we are updating an item the template is a readonly property based on the ID + if ($this->input->getMethod() === 'PATCH') { + if (\array_key_exists('template', $data)) { + throw new InvalidParameterException('The template property cannot be modified for an existing style'); + } - $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); - $data['template'] = $model->getItem($this->input->getInt('id'))->template; - } + $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); + $data['template'] = $model->getItem($this->input->getInt('id'))->template; + } - return $data; - } + return $data; + } - /** - * Get client id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientIdFromInput() - { - return $this->input->exists('client_id') ? $this->input->get('client_id') : $this->input->post->get('client_id'); - } + /** + * Get client id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientIdFromInput() + { + return $this->input->exists('client_id') ? $this->input->get('client_id') : $this->input->post->get('client_id'); + } } diff --git a/code/api/components/com_templates/src/View/Styles/JsonapiView.php b/code/api/components/com_templates/src/View/Styles/JsonapiView.php index 880716be..0f94a5cc 100644 --- a/code/api/components/com_templates/src/View/Styles/JsonapiView.php +++ b/code/api/components/com_templates/src/View/Styles/JsonapiView.php @@ -1,4 +1,5 @@ client_id != $this->getModel()->getState('client_id')) - { - throw new RouteNotFoundException('Item does not exist'); - } + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + if ($item->client_id != $this->getModel()->getState('client_id')) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/code/api/components/com_users/src/Controller/GroupsController.php b/code/api/components/com_users/src/Controller/GroupsController.php index 8c49ef4b..4b4fb00e 100644 --- a/code/api/components/com_users/src/Controller/GroupsController.php +++ b/code/api/components/com_users/src/Controller/GroupsController.php @@ -1,4 +1,5 @@ name])) - { - !isset($data['com_fields']) && $data['com_fields'] = []; - - $data['com_fields'][$field->name] = $data[$field->name]; - unset($data[$field->name]); - } - } - - if (isset($data['password']) && $this->task !== 'add') - { - $data['password2'] = $data['password']; - } - - return $data; - } - - /** - * User list view with filtering of data - * - * @return static A BaseController object to support chaining. - * - * @since 4.0.0 - * @throws InvalidParameterException - */ - public function displayList() - { - $apiFilterInfo = $this->input->get('filter', [], 'array'); - $filter = InputFilter::getInstance(); - - if (\array_key_exists('state', $apiFilterInfo)) - { - $this->modelState->set('filter.state', $filter->clean($apiFilterInfo['state'], 'INT')); - } - - if (\array_key_exists('active', $apiFilterInfo)) - { - $this->modelState->set('filter.active', $filter->clean($apiFilterInfo['active'], 'INT')); - } - - if (\array_key_exists('groupid', $apiFilterInfo)) - { - $this->modelState->set('filter.group_id', $filter->clean($apiFilterInfo['groupid'], 'INT')); - } - - if (\array_key_exists('search', $apiFilterInfo)) - { - $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); - } - - if (\array_key_exists('registrationDateStart', $apiFilterInfo)) - { - $registrationStartInput = $filter->clean($apiFilterInfo['registrationDateStart'], 'STRING'); - $registrationStartDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $registrationStartInput); - - if (!$registrationStartDate) - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'registrationDateStart'); - - throw new InvalidParameterException($error, 400, null, 'registrationDateStart'); - } - - $this->modelState->set('filter.registrationDateStart', $registrationStartDate); - } - - if (\array_key_exists('registrationDateEnd', $apiFilterInfo)) - { - $registrationEndInput = $filter->clean($apiFilterInfo['registrationDateEnd'], 'STRING'); - $registrationEndDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $registrationEndInput); - - if (!$registrationEndDate) - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'registrationDateEnd'); - throw new InvalidParameterException($error, 400, null, 'registrationDateEnd'); - } - - $this->modelState->set('filter.registrationDateEnd', $registrationEndDate); - } - elseif (\array_key_exists('registrationDateStart', $apiFilterInfo) - && !\array_key_exists('registrationDateEnd', $apiFilterInfo)) - { - // If no end date specified the end date is now - $this->modelState->set('filter.registrationDateEnd', new Date); - } - - if (\array_key_exists('lastVisitDateStart', $apiFilterInfo)) - { - $lastVisitStartInput = $filter->clean($apiFilterInfo['lastVisitDateStart'], 'STRING'); - $lastVisitStartDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $lastVisitStartInput); - - if (!$lastVisitStartDate) - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'lastVisitDateStart'); - throw new InvalidParameterException($error, 400, null, 'lastVisitDateStart'); - } - - $this->modelState->set('filter.lastVisitStart', $lastVisitStartDate); - } - - if (\array_key_exists('lastVisitDateEnd', $apiFilterInfo)) - { - $lastVisitEndInput = $filter->clean($apiFilterInfo['lastVisitDateEnd'], 'STRING'); - $lastVisitEndDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $lastVisitEndInput); - - if (!$lastVisitEndDate) - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'lastVisitDateEnd'); - - throw new InvalidParameterException($error, 400, null, 'lastVisitDateEnd'); - } - - $this->modelState->set('filter.lastVisitEnd', $lastVisitEndDate); - } - elseif (\array_key_exists('lastVisitDateStart', $apiFilterInfo) - && !\array_key_exists('lastVisitDateEnd', $apiFilterInfo)) - { - // If no end date specified the end date is now - $this->modelState->set('filter.lastVisitEnd', new Date); - } - - return parent::displayList(); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'users'; + + /** + * The default view for the display method. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'users'; + + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + */ + protected function preprocessSaveData(array $data): array + { + foreach (FieldsHelper::getFields('com_users.user') as $field) { + if (isset($data[$field->name])) { + !isset($data['com_fields']) && $data['com_fields'] = []; + + $data['com_fields'][$field->name] = $data[$field->name]; + unset($data[$field->name]); + } + } + + if (isset($data['password']) && $this->task !== 'add') { + $data['password2'] = $data['password']; + } + + return $data; + } + + /** + * User list view with filtering of data + * + * @return static A BaseController object to support chaining. + * + * @since 4.0.0 + * @throws InvalidParameterException + */ + public function displayList() + { + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); + + if (\array_key_exists('state', $apiFilterInfo)) { + $this->modelState->set('filter.state', $filter->clean($apiFilterInfo['state'], 'INT')); + } + + if (\array_key_exists('active', $apiFilterInfo)) { + $this->modelState->set('filter.active', $filter->clean($apiFilterInfo['active'], 'INT')); + } + + if (\array_key_exists('groupid', $apiFilterInfo)) { + $this->modelState->set('filter.group_id', $filter->clean($apiFilterInfo['groupid'], 'INT')); + } + + if (\array_key_exists('search', $apiFilterInfo)) { + $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); + } + + if (\array_key_exists('registrationDateStart', $apiFilterInfo)) { + $registrationStartInput = $filter->clean($apiFilterInfo['registrationDateStart'], 'STRING'); + $registrationStartDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $registrationStartInput); + + if (!$registrationStartDate) { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'registrationDateStart'); + + throw new InvalidParameterException($error, 400, null, 'registrationDateStart'); + } + + $this->modelState->set('filter.registrationDateStart', $registrationStartDate); + } + + if (\array_key_exists('registrationDateEnd', $apiFilterInfo)) { + $registrationEndInput = $filter->clean($apiFilterInfo['registrationDateEnd'], 'STRING'); + $registrationEndDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $registrationEndInput); + + if (!$registrationEndDate) { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'registrationDateEnd'); + throw new InvalidParameterException($error, 400, null, 'registrationDateEnd'); + } + + $this->modelState->set('filter.registrationDateEnd', $registrationEndDate); + } elseif ( + \array_key_exists('registrationDateStart', $apiFilterInfo) + && !\array_key_exists('registrationDateEnd', $apiFilterInfo) + ) { + // If no end date specified the end date is now + $this->modelState->set('filter.registrationDateEnd', new Date()); + } + + if (\array_key_exists('lastVisitDateStart', $apiFilterInfo)) { + $lastVisitStartInput = $filter->clean($apiFilterInfo['lastVisitDateStart'], 'STRING'); + $lastVisitStartDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $lastVisitStartInput); + + if (!$lastVisitStartDate) { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'lastVisitDateStart'); + throw new InvalidParameterException($error, 400, null, 'lastVisitDateStart'); + } + + $this->modelState->set('filter.lastVisitStart', $lastVisitStartDate); + } + + if (\array_key_exists('lastVisitDateEnd', $apiFilterInfo)) { + $lastVisitEndInput = $filter->clean($apiFilterInfo['lastVisitDateEnd'], 'STRING'); + $lastVisitEndDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $lastVisitEndInput); + + if (!$lastVisitEndDate) { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'lastVisitDateEnd'); + + throw new InvalidParameterException($error, 400, null, 'lastVisitDateEnd'); + } + + $this->modelState->set('filter.lastVisitEnd', $lastVisitEndDate); + } elseif ( + \array_key_exists('lastVisitDateStart', $apiFilterInfo) + && !\array_key_exists('lastVisitDateEnd', $apiFilterInfo) + ) { + // If no end date specified the end date is now + $this->modelState->set('filter.lastVisitEnd', new Date()); + } + + return parent::displayList(); + } } diff --git a/code/api/components/com_users/src/View/Groups/JsonapiView.php b/code/api/components/com_users/src/View/Groups/JsonapiView.php index 34e70606..c912642e 100644 --- a/code/api/components/com_users/src/View/Groups/JsonapiView.php +++ b/code/api/components/com_users/src/View/Groups/JsonapiView.php @@ -1,4 +1,5 @@ fieldsToRenderList[] = $field->name; - } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + foreach (FieldsHelper::getFields('com_users.user') as $field) { + $this->fieldsToRenderList[] = $field->name; + } - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - foreach (FieldsHelper::getFields('com_users.user') as $field) - { - $this->fieldsToRenderItem[] = $field->name; - } + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + foreach (FieldsHelper::getFields('com_users.user') as $field) { + $this->fieldsToRenderItem[] = $field->name; + } - return parent::displayItem(); - } + return parent::displayItem(); + } - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - if (empty($item->username)) - { - throw new RouteNotFoundException('Item does not exist'); - } + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + if (empty($item->username)) { + throw new RouteNotFoundException('Item does not exist'); + } - foreach (FieldsHelper::getFields('com_users.user', $item, true) as $field) - { - $item->{$field->name} = $field->apivalue ?? $field->rawvalue; - } + foreach (FieldsHelper::getFields('com_users.user', $item, true) as $field) { + $item->{$field->name} = $field->apivalue ?? $field->rawvalue; + } - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/code/api/includes/app.php b/code/api/includes/app.php index 50c934cd..9f10825f 100644 --- a/code/api/includes/app.php +++ b/code/api/includes/app.php @@ -1,4 +1,5 @@ alias('session', 'session.cli') - ->alias('JSession', 'session.cli') - ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); + ->alias('JSession', 'session.cli') + ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); // Instantiate the application. $app = $container->get(\Joomla\CMS\Application\ApiApplication::class); diff --git a/code/api/includes/defines.php b/code/api/includes/defines.php index 63922bd8..6148b98c 100644 --- a/code/api/includes/defines.php +++ b/code/api/includes/defines.php @@ -1,4 +1,5 @@ isInDevelopmentState()))) -{ - if (file_exists(JPATH_INSTALLATION . '/index.php')) - { - header('HTTP/1.1 500 Internal Server Error'); - echo json_encode( - array('error' => 'You must install Joomla to use the API') - ); - - exit(); - } - else - { - header('HTTP/1.1 500 Internal Server Error'); - echo json_encode( - array('error' => 'No configuration file found and no installation code available. Exiting...') - ); - - exit; - } +if ( + !file_exists(JPATH_CONFIGURATION . '/configuration.php') + || (filesize(JPATH_CONFIGURATION . '/configuration.php') < 10) + || (file_exists(JPATH_INSTALLATION . '/index.php') && (false === (new Version())->isInDevelopmentState())) +) { + if (file_exists(JPATH_INSTALLATION . '/index.php')) { + header('HTTP/1.1 500 Internal Server Error'); + echo json_encode( + array('error' => 'You must install Joomla to use the API') + ); + + exit(); + } else { + header('HTTP/1.1 500 Internal Server Error'); + echo json_encode( + array('error' => 'No configuration file found and no installation code available. Exiting...') + ); + + exit; + } } // Pre-Load configuration. Don't remove the Output Buffering due to BOM issues, see JCode 26026 @@ -45,64 +44,59 @@ ob_end_clean(); // System configuration. -$config = new JConfig; +$config = new JConfig(); // Set the error_reporting -switch ($config->error_reporting) -{ - case 'default': - case '-1': - break; +switch ($config->error_reporting) { + case 'default': + case '-1': + break; - case 'none': - case '0': - error_reporting(0); + case 'none': + case '0': + error_reporting(0); - break; + break; - case 'simple': - error_reporting(E_ERROR | E_WARNING | E_PARSE); - ini_set('display_errors', 1); + case 'simple': + error_reporting(E_ERROR | E_WARNING | E_PARSE); + ini_set('display_errors', 1); - break; + break; - case 'maximum': - case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 - error_reporting(E_ALL); - ini_set('display_errors', 1); + case 'maximum': + case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 + error_reporting(E_ALL); + ini_set('display_errors', 1); - break; + break; - default: - error_reporting($config->error_reporting); - ini_set('display_errors', 1); + default: + error_reporting($config->error_reporting); + ini_set('display_errors', 1); - break; + break; } define('JDEBUG', $config->debug); // Check deprecation logging -if (empty($config->log_deprecated)) -{ - // Reset handler for E_USER_DEPRECATED - set_error_handler(null, E_USER_DEPRECATED); -} -else -{ - // Make sure handler for E_USER_DEPRECATED is registered - set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); +if (empty($config->log_deprecated)) { + // Reset handler for E_USER_DEPRECATED + set_error_handler(null, E_USER_DEPRECATED); +} else { + // Make sure handler for E_USER_DEPRECATED is registered + set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); } -if (JDEBUG || $config->error_reporting === 'maximum') -{ - // Set new Exception handler with debug enabled - $errorHandler->setExceptionHandler( - [ - new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), - 'renderException', - ] - ); +if (JDEBUG || $config->error_reporting === 'maximum') { + // Set new Exception handler with debug enabled + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException', + ] + ); } /** @@ -111,15 +105,12 @@ * We need to do this as high up the stack as we can, as the default in \Joomla\Utilities\IpHelper is to * $allowIpOverride = true which is the wrong default for a generic site NOT behind a trusted proxy/load balancer. */ -if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) -{ - // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR - IpHelper::setAllowIpOverrides(true); -} -else -{ - // We disable the allowing of IP overriding using headers by default. - IpHelper::setAllowIpOverrides(false); +if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) { + // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR + IpHelper::setAllowIpOverrides(true); +} else { + // We disable the allowing of IP overriding using headers by default. + IpHelper::setAllowIpOverrides(false); } unset($config); diff --git a/code/api/index.php b/code/api/index.php index 0f65a633..08dac50c 100644 --- a/code/api/index.php +++ b/code/api/index.php @@ -1,4 +1,5 @@ sprintf('Joomla requires PHP version %s to run', JOOMLA_MINIMUM_PHP)) - ); +if (version_compare(PHP_VERSION, JOOMLA_MINIMUM_PHP, '<')) { + header('HTTP/1.1 500 Internal Server Error'); + echo json_encode( + array('error' => sprintf('Joomla requires PHP version %s to run', JOOMLA_MINIMUM_PHP)) + ); - return; + return; } /** diff --git a/code/api/language/en-GB/install.xml b/code/api/language/en-GB/install.xml index 608da625..0afeca87 100644 --- a/code/api/language/en-GB/install.xml +++ b/code/api/language/en-GB/install.xml @@ -2,8 +2,8 @@ English (en-GB) en-GB - 4.1.5 - June 2022 + 4.2.3 + 2022-09 Joomla! Project admin@joomla.org www.joomla.org diff --git a/code/api/language/en-GB/joomla.ini b/code/api/language/en-GB/joomla.ini index 80848154..2b0e8e70 100644 --- a/code/api/language/en-GB/joomla.ini +++ b/code/api/language/en-GB/joomla.ini @@ -205,7 +205,9 @@ JFIELD_ALT_LAYOUT_LABEL="Layout" JFIELD_ALT_MODULE_LAYOUT_DESC="Use a layout from the supplied module or overrides in the templates." JFIELD_ALT_PAGE_TITLE_DESC="An optional alternative page title to set that will change the TITLE tag in the HTML output." JFIELD_ALT_PAGE_TITLE_LABEL="Alternative Page Title" +; Deprecated, will be removed with 5.0 JFIELD_ASSET_ID_DESC="Asset ID" +; Deprecated, will be removed with 5.0 JFIELD_ASSET_ID_LABEL="Asset ID" JFIELD_BASIC_LOGIN_DESCRIPTION_LABEL="Login Description Text" JFIELD_BASIC_LOGIN_DESCRIPTION_SHOW_LABEL="Login Description" @@ -348,7 +350,7 @@ JGLOBAL_AUTH_FAIL="Authentication failed" JGLOBAL_AUTH_FAILED="Failed to authenticate: %s" JGLOBAL_AUTH_INCORRECT="Incorrect username/password" JGLOBAL_AUTH_INVALID_PASS="Username and password do not match or you do not have an account yet." -JGLOBAL_AUTH_INVALID_SECRETKEY="The two factor authentication Secret Key is invalid." +JGLOBAL_AUTH_INVALID_SECRETKEY="The Multi-factor Authentication Secret Key is invalid." JGLOBAL_AUTH_NO_REDIRECT="Could not redirect to server: %s" JGLOBAL_AUTH_NO_USER="Username and password do not match or you do not have an account yet." JGLOBAL_AUTH_NOT_CONNECT="Unable to connect to authentication service." @@ -482,7 +484,7 @@ JGLOBAL_ISFREESOFTWARE="%s is free software released under the English (en-GB) - 4.1.5 - June 2022 + 4.2.3 + 2022-09 Joomla! Project admin@joomla.org www.joomla.org diff --git a/code/cli/joomla.php b/code/cli/joomla.php index fb078e12..6b5bea7d 100644 --- a/code/cli/joomla.php +++ b/code/cli/joomla.php @@ -1,4 +1,5 @@ alias('session', 'session.cli') - ->alias('JSession', 'session.cli') - ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); + ->alias('JSession', 'session.cli') + ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); $app = \Joomla\CMS\Factory::getContainer()->get(\Joomla\Console\Application::class); \Joomla\CMS\Factory::$application = $app; diff --git a/code/components/com_ajax/ajax.php b/code/components/com_ajax/ajax.php index 1b8fcc87..20674d4b 100644 --- a/code/components/com_ajax/ajax.php +++ b/code/components/com_ajax/ajax.php @@ -1,4 +1,5 @@ get('module')) -{ - $module = $input->get('module'); - $table = Table::getInstance('extension'); - $moduleId = $table->find(array('type' => 'module', 'element' => 'mod_' . $module)); - - if ($moduleId && $table->load($moduleId) && $table->enabled) - { - $helperFile = JPATH_BASE . '/modules/mod_' . $module . '/helper.php'; - - if (strpos($module, '_')) - { - $parts = explode('_', $module); - } - elseif (strpos($module, '-')) - { - $parts = explode('-', $module); - } - - if ($parts) - { - $class = 'Mod'; - - foreach ($parts as $part) - { - $class .= ucfirst($part); - } - - $class .= 'Helper'; - } - else - { - $class = 'Mod' . ucfirst($module) . 'Helper'; - } - - $method = $input->get('method') ?: 'get'; - - $moduleInstance = $app->bootModule('mod_' . $module, $app->getName()); - - if ($moduleInstance instanceof \Joomla\CMS\Helper\HelperFactoryInterface && $helper = $moduleInstance->getHelper(substr($class, 3))) - { - $results = method_exists($helper, $method . 'Ajax') ? $helper->{$method . 'Ajax'}() : null; - } - - if ($results === null && is_file($helperFile)) - { - JLoader::register($class, $helperFile); - - if (method_exists($class, $method . 'Ajax')) - { - // Load language file for module - $basePath = JPATH_BASE; - $lang = Factory::getLanguage(); - $lang->load('mod_' . $module, $basePath) - || $lang->load('mod_' . $module, $basePath . '/modules/mod_' . $module); - - try - { - $results = call_user_func($class . '::' . $method . 'Ajax'); - } - catch (Exception $e) - { - $results = $e; - } - } - // Method does not exist - else - { - $results = new LogicException(Text::sprintf('COM_AJAX_METHOD_NOT_EXISTS', $method . 'Ajax'), 404); - } - } - // The helper file does not exist - elseif ($results === null) - { - $results = new RuntimeException(Text::sprintf('COM_AJAX_FILE_NOT_EXISTS', 'mod_' . $module . '/helper.php'), 404); - } - } - // Module is not published, you do not have access to it, or it is not assigned to the current menu item - else - { - $results = new LogicException(Text::sprintf('COM_AJAX_MODULE_NOT_ACCESSIBLE', 'mod_' . $module), 404); - } -} -/* - * Plugin support by default is based on the "Ajax" plugin group. - * An optional 'group' variable can be passed via the URL. - * - * The plugin event triggered is onAjaxFoo, where 'foo' is - * the value of the 'plugin' variable passed via the URL - * (i.e. index.php?option=com_ajax&plugin=foo) - * - */ -elseif ($input->get('plugin')) -{ - $group = $input->get('group', 'ajax'); - PluginHelper::importPlugin($group); - $plugin = ucfirst($input->get('plugin')); - - try - { - $results = Factory::getApplication()->triggerEvent('onAjax' . $plugin); - } - catch (Exception $e) - { - $results = $e; - } -} -/* - * Template support. - * - * tplFooHelper::getAjax() is called where 'foo' is the value - * of the 'template' variable passed via the URL - * (i.e. index.php?option=com_ajax&template=foo). - * - */ -elseif ($input->get('template')) -{ - $template = $input->get('template'); - $table = Table::getInstance('extension'); - $templateId = $table->find(array('type' => 'template', 'element' => $template)); - - if ($templateId && $table->load($templateId) && $table->enabled) - { - $basePath = ($table->client_id) ? JPATH_ADMINISTRATOR : JPATH_SITE; - $helperFile = $basePath . '/templates/' . $template . '/helper.php'; - - if (strpos($template, '_')) - { - $parts = explode('_', $template); - } - elseif (strpos($template, '-')) - { - $parts = explode('-', $template); - } - - if ($parts) - { - $class = 'Tpl'; - - foreach ($parts as $part) - { - $class .= ucfirst($part); - } - - $class .= 'Helper'; - } - else - { - $class = 'Tpl' . ucfirst($template) . 'Helper'; - } - - $method = $input->get('method') ?: 'get'; - - if (is_file($helperFile)) - { - JLoader::register($class, $helperFile); - - if (method_exists($class, $method . 'Ajax')) - { - // Load language file for template - $lang = Factory::getLanguage(); - $lang->load('tpl_' . $template, $basePath) - || $lang->load('tpl_' . $template, $basePath . '/templates/' . $template); - - try - { - $results = call_user_func($class . '::' . $method . 'Ajax'); - } - catch (Exception $e) - { - $results = $e; - } - } - // Method does not exist - else - { - $results = new LogicException(Text::sprintf('COM_AJAX_METHOD_NOT_EXISTS', $method . 'Ajax'), 404); - } - } - // The helper file does not exist - else - { - $results = new RuntimeException(Text::sprintf('COM_AJAX_FILE_NOT_EXISTS', 'tpl_' . $template . '/helper.php'), 404); - } - } - // Template is not assigned to the current menu item - else - { - $results = new LogicException(Text::sprintf('COM_AJAX_TEMPLATE_NOT_ACCESSIBLE', 'tpl_' . $template), 404); - } +if (!$format) { + $results = new InvalidArgumentException(Text::_('COM_AJAX_SPECIFY_FORMAT'), 404); +} elseif ($input->get('module')) { + /** + * Module support. + * + * modFooHelper::getAjax() is called where 'foo' is the value + * of the 'module' variable passed via the URL + * (i.e. index.php?option=com_ajax&module=foo). + * + */ + $module = $input->get('module'); + $table = Table::getInstance('extension'); + $moduleId = $table->find(array('type' => 'module', 'element' => 'mod_' . $module)); + + if ($moduleId && $table->load($moduleId) && $table->enabled) { + $helperFile = JPATH_BASE . '/modules/mod_' . $module . '/helper.php'; + + if (strpos($module, '_')) { + $parts = explode('_', $module); + } elseif (strpos($module, '-')) { + $parts = explode('-', $module); + } + + if ($parts) { + $class = 'Mod'; + + foreach ($parts as $part) { + $class .= ucfirst($part); + } + + $class .= 'Helper'; + } else { + $class = 'Mod' . ucfirst($module) . 'Helper'; + } + + $method = $input->get('method') ?: 'get'; + + $moduleInstance = $app->bootModule('mod_' . $module, $app->getName()); + + if ($moduleInstance instanceof \Joomla\CMS\Helper\HelperFactoryInterface && $helper = $moduleInstance->getHelper(substr($class, 3))) { + $results = method_exists($helper, $method . 'Ajax') ? $helper->{$method . 'Ajax'}() : null; + } + + if ($results === null && is_file($helperFile)) { + JLoader::register($class, $helperFile); + + if (method_exists($class, $method . 'Ajax')) { + // Load language file for module + $basePath = JPATH_BASE; + $lang = Factory::getLanguage(); + $lang->load('mod_' . $module, $basePath) + || $lang->load('mod_' . $module, $basePath . '/modules/mod_' . $module); + + try { + $results = call_user_func($class . '::' . $method . 'Ajax'); + } catch (Exception $e) { + $results = $e; + } + } else { + // Method does not exist + $results = new LogicException(Text::sprintf('COM_AJAX_METHOD_NOT_EXISTS', $method . 'Ajax'), 404); + } + } elseif ($results === null) { + // The helper file does not exist + $results = new RuntimeException(Text::sprintf('COM_AJAX_FILE_NOT_EXISTS', 'mod_' . $module . '/helper.php'), 404); + } + } else { + // Module is not published, you do not have access to it, or it is not assigned to the current menu item + $results = new LogicException(Text::sprintf('COM_AJAX_MODULE_NOT_ACCESSIBLE', 'mod_' . $module), 404); + } +} elseif ($input->get('plugin')) { + /** + * Plugin support by default is based on the "Ajax" plugin group. + * An optional 'group' variable can be passed via the URL. + * + * The plugin event triggered is onAjaxFoo, where 'foo' is + * the value of the 'plugin' variable passed via the URL + * (i.e. index.php?option=com_ajax&plugin=foo) + * + */ + $group = $input->get('group', 'ajax'); + PluginHelper::importPlugin($group); + $plugin = ucfirst($input->get('plugin')); + + try { + $results = Factory::getApplication()->triggerEvent('onAjax' . $plugin); + } catch (Exception $e) { + $results = $e; + } +} elseif ($input->get('template')) { + /** + * Template support. + * + * tplFooHelper::getAjax() is called where 'foo' is the value + * of the 'template' variable passed via the URL + * (i.e. index.php?option=com_ajax&template=foo). + * + */ + $template = $input->get('template'); + $table = Table::getInstance('extension'); + $templateId = $table->find(array('type' => 'template', 'element' => $template)); + + if ($templateId && $table->load($templateId) && $table->enabled) { + $basePath = ($table->client_id) ? JPATH_ADMINISTRATOR : JPATH_SITE; + $helperFile = $basePath . '/templates/' . $template . '/helper.php'; + + if (strpos($template, '_')) { + $parts = explode('_', $template); + } elseif (strpos($template, '-')) { + $parts = explode('-', $template); + } + + if ($parts) { + $class = 'Tpl'; + + foreach ($parts as $part) { + $class .= ucfirst($part); + } + + $class .= 'Helper'; + } else { + $class = 'Tpl' . ucfirst($template) . 'Helper'; + } + + $method = $input->get('method') ?: 'get'; + + if (is_file($helperFile)) { + JLoader::register($class, $helperFile); + + if (method_exists($class, $method . 'Ajax')) { + // Load language file for template + $lang = Factory::getLanguage(); + $lang->load('tpl_' . $template, $basePath) + || $lang->load('tpl_' . $template, $basePath . '/templates/' . $template); + + try { + $results = call_user_func($class . '::' . $method . 'Ajax'); + } catch (Exception $e) { + $results = $e; + } + } else { + // Method does not exist + $results = new LogicException(Text::sprintf('COM_AJAX_METHOD_NOT_EXISTS', $method . 'Ajax'), 404); + } + } else { + // The helper file does not exist + $results = new RuntimeException(Text::sprintf('COM_AJAX_FILE_NOT_EXISTS', 'tpl_' . $template . '/helper.php'), 404); + } + } else { + // Template is not assigned to the current menu item + $results = new LogicException(Text::sprintf('COM_AJAX_TEMPLATE_NOT_ACCESSIBLE', 'tpl_' . $template), 404); + } } // Return the results in the desired format -switch ($format) -{ - // JSONinzed - case 'json': - echo new JsonResponse($results, null, false, $input->get('ignoreMessages', true, 'bool')); - - break; - - // Handle as raw format - default: - // Output exception - if ($results instanceof Exception) - { - // Log an error - Log::add($results->getMessage(), Log::ERROR); - - // Set status header code - $app->setHeader('status', $results->getCode(), true); - - // Echo exception type and message - $out = get_class($results) . ': ' . $results->getMessage(); - } - // Output string/ null - elseif (is_scalar($results)) - { - $out = (string) $results; - } - // Output array/ object - else - { - $out = implode((array) $results); - } - - echo $out; - - break; +switch ($format) { + // JSONinzed + case 'json': + echo new JsonResponse($results, null, false, $input->get('ignoreMessages', true, 'bool')); + + break; + + // Handle as raw format + default: + // Output exception + if ($results instanceof Exception) { + // Log an error + Log::add($results->getMessage(), Log::ERROR); + + // Set status header code + $app->setHeader('status', $results->getCode(), true); + + // Echo exception type and message + $out = get_class($results) . ': ' . $results->getMessage(); + } elseif (is_scalar($results)) { + // Output string/ null + $out = (string) $results; + } else { + // Output array/ object + $out = implode((array) $results); + } + + echo $out; + + break; } diff --git a/code/components/com_banners/src/Controller/DisplayController.php b/code/components/com_banners/src/Controller/DisplayController.php index 75b3f4f6..51af41e1 100644 --- a/code/components/com_banners/src/Controller/DisplayController.php +++ b/code/components/com_banners/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->getInt('id', 0); + /** + * Method when a banner is clicked on. + * + * @return void + * + * @since 1.5 + */ + public function click() + { + $id = $this->input->getInt('id', 0); - if ($id) - { - /** @var \Joomla\Component\Banners\Site\Model\BannerModel $model */ - $model = $this->getModel('Banner', 'Site', array('ignore_request' => true)); - $model->setState('banner.id', $id); - $model->click(); - $this->setRedirect($model->getUrl()); - } - } + if ($id) { + /** @var \Joomla\Component\Banners\Site\Model\BannerModel $model */ + $model = $this->getModel('Banner', 'Site', array('ignore_request' => true)); + $model->setState('banner.id', $id); + $model->click(); + $this->setRedirect($model->getUrl()); + } + } } diff --git a/code/components/com_banners/src/Helper/BannerHelper.php b/code/components/com_banners/src/Helper/BannerHelper.php index 3476a584..562f3be9 100644 --- a/code/components/com_banners/src/Helper/BannerHelper.php +++ b/code/components/com_banners/src/Helper/BannerHelper.php @@ -1,4 +1,5 @@ getItem(); - - if (empty($item)) - { - throw new \Exception(Text::_('JERROR_PAGE_NOT_FOUND'), 404); - } - - $id = (int) $this->getState('banner.id'); - - // Update click count - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query->update($db->quoteName('#__banners')) - ->set($db->quoteName('clicks') . ' = ' . $db->quoteName('clicks') . ' + 1') - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - // Track clicks - $trackClicks = $item->track_clicks; - - if ($trackClicks < 0 && $item->cid) - { - $trackClicks = $item->client_track_clicks; - } - - if ($trackClicks < 0) - { - $config = ComponentHelper::getParams('com_banners'); - $trackClicks = $config->get('track_clicks'); - } - - if ($trackClicks > 0) - { - $trackDate = Factory::getDate()->format('Y-m-d H:00:00'); - $trackDate = Factory::getDate($trackDate)->toSql(); - - $query = $db->getQuery(true); - - $query->select($db->quoteName('count')) - ->from($db->quoteName('#__banner_tracks')) - ->where( - [ - $db->quoteName('track_type') . ' = 2', - $db->quoteName('banner_id') . ' = :id', - $db->quoteName('track_date') . ' = :trackDate', - ] - ) - ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - $count = $db->loadResult(); - - $query = $db->getQuery(true); - - if ($count) - { - // Update count - $query->update($db->quoteName('#__banner_tracks')) - ->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1') - ->where( - [ - $db->quoteName('track_type') . ' = 2', - $db->quoteName('banner_id') . ' = :id', - $db->quoteName('track_date') . ' = :trackDate', - ] - ) - ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - } - else - { - // Insert new count - $query->insert($db->quoteName('#__banner_tracks')) - ->columns( - [ - $db->quoteName('count'), - $db->quoteName('track_type'), - $db->quoteName('banner_id'), - $db->quoteName('track_date'), - ] - ) - ->values('1, 2 , :id, :trackDate') - ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } - } - } - - /** - * Get the data for a banner. - * - * @return object - * - * @since 1.6 - */ - public function &getItem() - { - if (!isset($this->_item)) - { - /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ - $cache = Factory::getCache('com_banners', 'callback'); - - $id = (int) $this->getState('banner.id'); - - // For PHP 5.3 compat we can't use $this in the lambda function below, so grab the database driver now to use it - $db = $this->getDbo(); - - $loader = function ($id) use ($db) - { - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('a.clickurl'), - $db->quoteName('a.cid'), - $db->quoteName('a.track_clicks'), - $db->quoteName('cl.track_clicks', 'client_track_clicks'), - ] - ) - ->from($db->quoteName('#__banners', 'a')) - ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) - ->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - return $db->loadObject(); - }; - - try - { - $this->_item = $cache->get($loader, array($id), md5(__METHOD__ . $id)); - } - catch (CacheExceptionInterface $e) - { - $this->_item = $loader($id); - } - } - - return $this->_item; - } - - /** - * Get the URL for a banner - * - * @return string - * - * @since 1.5 - */ - public function getUrl() - { - $item = $this->getItem(); - $url = $item->clickurl; - - // Check for links - if (!preg_match('#http[s]?://|index[2]?\.php#', $url)) - { - $url = "http://$url"; - } - - return $url; - } + /** + * Cached item object + * + * @var object + * @since 1.6 + */ + protected $_item; + + /** + * Clicks the URL, incrementing the counter + * + * @return void + * + * @since 1.5 + * @throws \Exception + */ + public function click() + { + $item = $this->getItem(); + + if (empty($item)) { + throw new \Exception(Text::_('JERROR_PAGE_NOT_FOUND'), 404); + } + + $id = (int) $this->getState('banner.id'); + + // Update click count + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->update($db->quoteName('#__banners')) + ->set($db->quoteName('clicks') . ' = ' . $db->quoteName('clicks') . ' + 1') + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } + + // Track clicks + $trackClicks = $item->track_clicks; + + if ($trackClicks < 0 && $item->cid) { + $trackClicks = $item->client_track_clicks; + } + + if ($trackClicks < 0) { + $config = ComponentHelper::getParams('com_banners'); + $trackClicks = $config->get('track_clicks'); + } + + if ($trackClicks > 0) { + $trackDate = Factory::getDate()->format('Y-m-d H:00:00'); + $trackDate = Factory::getDate($trackDate)->toSql(); + + $query = $db->getQuery(true); + + $query->select($db->quoteName('count')) + ->from($db->quoteName('#__banner_tracks')) + ->where( + [ + $db->quoteName('track_type') . ' = 2', + $db->quoteName('banner_id') . ' = :id', + $db->quoteName('track_date') . ' = :trackDate', + ] + ) + ->bind(':id', $id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } + + $count = $db->loadResult(); + + $query = $db->getQuery(true); + + if ($count) { + // Update count + $query->update($db->quoteName('#__banner_tracks')) + ->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1') + ->where( + [ + $db->quoteName('track_type') . ' = 2', + $db->quoteName('banner_id') . ' = :id', + $db->quoteName('track_date') . ' = :trackDate', + ] + ) + ->bind(':id', $id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + } else { + // Insert new count + $query->insert($db->quoteName('#__banner_tracks')) + ->columns( + [ + $db->quoteName('count'), + $db->quoteName('track_type'), + $db->quoteName('banner_id'), + $db->quoteName('track_date'), + ] + ) + ->values('1, 2 , :id, :trackDate') + ->bind(':id', $id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } + } + } + + /** + * Get the data for a banner. + * + * @return object + * + * @since 1.6 + */ + public function &getItem() + { + if (!isset($this->_item)) { + /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ + $cache = Factory::getCache('com_banners', 'callback'); + + $id = (int) $this->getState('banner.id'); + + // For PHP 5.3 compat we can't use $this in the lambda function below, so grab the database driver now to use it + $db = $this->getDatabase(); + + $loader = function ($id) use ($db) { + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('a.clickurl'), + $db->quoteName('a.cid'), + $db->quoteName('a.track_clicks'), + $db->quoteName('cl.track_clicks', 'client_track_clicks'), + ] + ) + ->from($db->quoteName('#__banners', 'a')) + ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) + ->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + return $db->loadObject(); + }; + + try { + $this->_item = $cache->get($loader, array($id), md5(__METHOD__ . $id)); + } catch (CacheExceptionInterface $e) { + $this->_item = $loader($id); + } + } + + return $this->_item; + } + + /** + * Get the URL for a banner + * + * @return string + * + * @since 1.5 + */ + public function getUrl() + { + $item = $this->getItem(); + $url = $item->clickurl; + + // Check for links + if (!preg_match('#http[s]?://|index[2]?\.php#', $url)) { + $url = "http://$url"; + } + + return $url; + } } diff --git a/code/components/com_banners/src/Model/BannersModel.php b/code/components/com_banners/src/Model/BannersModel.php index a16c7dc6..09b57a62 100644 --- a/code/components/com_banners/src/Model/BannersModel.php +++ b/code/components/com_banners/src/Model/BannersModel.php @@ -1,4 +1,5 @@ getState('filter.search'); - $id .= ':' . $this->getState('filter.tag_search'); - $id .= ':' . $this->getState('filter.client_id'); - $id .= ':' . serialize($this->getState('filter.category_id')); - $id .= ':' . serialize($this->getState('filter.keywords')); - - return parent::getStoreId($id); - } - - /** - * Method to get a DatabaseQuery object for retrieving the data set from a database. - * - * @return DatabaseQuery A DatabaseQuery object to retrieve the data set. - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - $ordering = $this->getState('filter.ordering'); - $tagSearch = $this->getState('filter.tag_search'); - $cid = (int) $this->getState('filter.client_id'); - $categoryId = $this->getState('filter.category_id'); - $keywords = $this->getState('filter.keywords'); - $randomise = ($ordering === 'random'); - $nowDate = Factory::getDate()->toSql(); - - $query->select( - [ - $db->quoteName('a.id'), - $db->quoteName('a.type'), - $db->quoteName('a.name'), - $db->quoteName('a.clickurl'), - $db->quoteName('a.sticky'), - $db->quoteName('a.cid'), - $db->quoteName('a.description'), - $db->quoteName('a.params'), - $db->quoteName('a.custombannercode'), - $db->quoteName('a.track_impressions'), - $db->quoteName('cl.track_impressions', 'client_track_impressions'), - ] - ) - ->from($db->quoteName('#__banners', 'a')) - ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) - ->where($db->quoteName('a.state') . ' = 1') - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_up') . ' IS NULL', - $db->quoteName('a.publish_up') . ' <= :nowDate1', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_down') . ' IS NULL', - $db->quoteName('a.publish_down') . ' >= :nowDate2', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.imptotal') . ' = 0', - $db->quoteName('a.impmade') . ' < ' . $db->quoteName('a.imptotal'), - ], - 'OR' - ) - ->bind([':nowDate1', ':nowDate2'], $nowDate); - - if ($cid) - { - $query->where( - [ - $db->quoteName('a.cid') . ' = :clientId', - $db->quoteName('cl.state') . ' = 1', - ] - ) - ->bind(':clientId', $cid, ParameterType::INTEGER); - } - - // Filter by a single or group of categories - if (is_numeric($categoryId)) - { - $categoryId = (int) $categoryId; - $type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> '; - - // Add subcategory check - if ($this->getState('filter.subcategories', false)) - { - $levels = (int) $this->getState('filter.max_category_levels', '1'); - - // Create a subquery for the subcategory list - $subQuery = $db->getQuery(true); - $subQuery->select($db->quoteName('sub.id')) - ->from($db->quoteName('#__categories', 'sub')) - ->join( - 'INNER', - $db->quoteName('#__categories', 'this'), - $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') - . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') - ) - ->where( - [ - $db->quoteName('this.id') . ' = :categoryId1', - $db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels', - ] - ); - - // Add the subquery to the main query - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.catid') . $type . ':categoryId2', - $db->quoteName('a.catid') . ' IN (' . $subQuery . ')', - ], - 'OR' - ) - ->bind([':categoryId1', ':categoryId2'], $categoryId, ParameterType::INTEGER) - ->bind(':levels', $levels, ParameterType::INTEGER); - } - else - { - $query->where($db->quoteName('a.catid') . $type . ':categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - } - elseif (is_array($categoryId) && (count($categoryId) > 0)) - { - $categoryId = ArrayHelper::toInteger($categoryId); - - if ($this->getState('filter.category_id.include', true)) - { - $query->whereIn($db->quoteName('a.catid'), $categoryId); - } - else - { - $query->whereNotIn($db->quoteName('a.catid'), $categoryId); - } - } - - if ($tagSearch) - { - if (!$keywords) - { - // No keywords, select nothing. - $query->where('0 != 0'); - } - else - { - $temp = array(); - $config = ComponentHelper::getParams('com_banners'); - $prefix = $config->get('metakey_prefix'); - - if ($categoryId) - { - $query->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('a.catid') . ' = ' . $db->quoteName('cat.id')); - } - - foreach ($keywords as $key => $keyword) - { - $regexp = '[[:<:]]' . $keyword . '[[:>:]]'; - $valuesToBind = [$keyword, $keyword, $regexp]; - - if ($cid) - { - $valuesToBind[] = $regexp; - } - - if ($categoryId) - { - $valuesToBind[] = $regexp; - } - - // Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting. - $bounded = $query->bindArray($valuesToBind, ParameterType::STRING); - - $condition1 = $db->quoteName('a.own_prefix') . ' = 1' - . ' AND ' . $db->quoteName('a.metakey_prefix') - . ' = SUBSTRING(' . $bounded[0] . ',1,LENGTH(' . $db->quoteName('a.metakey_prefix') . '))' - . ' OR ' . $db->quoteName('a.own_prefix') . ' = 0' - . ' AND ' . $db->quoteName('cl.own_prefix') . ' = 1' - . ' AND ' . $db->quoteName('cl.metakey_prefix') - . ' = SUBSTRING(' . $bounded[1] . ',1,LENGTH(' . $db->quoteName('cl.metakey_prefix') . '))' - . ' OR ' . $db->quoteName('a.own_prefix') . ' = 0' - . ' AND ' . $db->quoteName('cl.own_prefix') . ' = 0' - . ' AND ' . ($prefix == substr($keyword, 0, strlen($prefix)) ? '0 = 0' : '0 != 0'); - - $condition2 = $db->quoteName('a.metakey') . ' ' . $query->regexp($bounded[2]); - - if ($cid) - { - $condition2 .= ' OR ' . $db->quoteName('cl.metakey') . ' ' . $query->regexp($bounded[3]) . ' '; - } - - if ($categoryId) - { - $condition2 .= ' OR ' . $db->quoteName('cat.metakey') . ' ' . $query->regexp($bounded[4]) . ' '; - } - - $temp[] = "($condition1) AND ($condition2)"; - } - - $query->where('(' . implode(' OR ', $temp) . ')'); - } - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - $query->order($db->quoteName('a.sticky') . ' DESC, ' . ($randomise ? $query->rand() : $db->quoteName('a.ordering'))); - - return $query; - } - - /** - * Get a list of banners. - * - * @return array - * - * @since 1.6 - */ - public function getItems() - { - if ($this->getState('filter.tag_search')) - { - // Filter out empty keywords. - $keywords = array_values(array_filter(array_map('trim', $this->getState('filter.keywords')), 'strlen')); - - // Re-set state before running the query. - $this->setState('filter.keywords', $keywords); - - // If no keywords are provided, avoid running the query. - if (!$keywords) - { - $this->cache['items'] = array(); - - return $this->cache['items']; - } - } - - if (!isset($this->cache['items'])) - { - $this->cache['items'] = parent::getItems(); - - foreach ($this->cache['items'] as &$item) - { - $item->params = new Registry($item->params); - } - } - - return $this->cache['items']; - } - - /** - * Makes impressions on a list of banners - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - public function impress() - { - $trackDate = Factory::getDate()->format('Y-m-d H:00:00'); - $trackDate = Factory::getDate($trackDate)->toSql(); - $items = $this->getItems(); - $db = $this->getDbo(); - $bid = []; - - if (!count($items)) - { - return; - } - - foreach ($items as $item) - { - $bid[] = (int) $item->id; - } - - // Increment impression made - $query = $db->getQuery(true); - $query->update($db->quoteName('#__banners')) - ->set($db->quoteName('impmade') . ' = ' . $db->quoteName('impmade') . ' + 1') - ->whereIn($db->quoteName('id'), $bid); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - foreach ($items as $item) - { - // Track impressions - $trackImpressions = $item->track_impressions; - - if ($trackImpressions < 0 && $item->cid) - { - $trackImpressions = $item->client_track_impressions; - } - - if ($trackImpressions < 0) - { - $config = ComponentHelper::getParams('com_banners'); - $trackImpressions = $config->get('track_impressions'); - } - - if ($trackImpressions > 0) - { - // Is track already created? - // Update count - $query = $db->getQuery(true); - $query->update($db->quoteName('#__banner_tracks')) - ->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1') - ->where( - [ - $db->quoteName('track_type') . ' = 1', - $db->quoteName('banner_id') . ' = :id', - $db->quoteName('track_date') . ' = :trackDate', - ] - ) - ->bind(':id', $item->id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - if ($db->getAffectedRows() === 0) - { - // Insert new count - $query = $db->getQuery(true); - $query->insert($db->quoteName('#__banner_tracks')) - ->columns( - [ - $db->quoteName('count'), - $db->quoteName('track_type'), - $db->quoteName('banner_id'), - $db->quoteName('track_date'), - ] - ) - ->values('1, 1, :id, :trackDate') - ->bind(':id', $item->id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - throw new \Exception($e->getMessage(), 500); - } - } - } - } - } + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.tag_search'); + $id .= ':' . $this->getState('filter.client_id'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . serialize($this->getState('filter.keywords')); + + return parent::getStoreId($id); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return DatabaseQuery A DatabaseQuery object to retrieve the data set. + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $ordering = $this->getState('filter.ordering'); + $tagSearch = $this->getState('filter.tag_search'); + $cid = (int) $this->getState('filter.client_id'); + $categoryId = $this->getState('filter.category_id'); + $keywords = $this->getState('filter.keywords'); + $randomise = ($ordering === 'random'); + $nowDate = Factory::getDate()->toSql(); + + $query->select( + [ + $db->quoteName('a.id'), + $db->quoteName('a.type'), + $db->quoteName('a.name'), + $db->quoteName('a.clickurl'), + $db->quoteName('a.sticky'), + $db->quoteName('a.cid'), + $db->quoteName('a.description'), + $db->quoteName('a.params'), + $db->quoteName('a.custombannercode'), + $db->quoteName('a.track_impressions'), + $db->quoteName('cl.track_impressions', 'client_track_impressions'), + ] + ) + ->from($db->quoteName('#__banners', 'a')) + ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) + ->where($db->quoteName('a.state') . ' = 1') + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_up') . ' IS NULL', + $db->quoteName('a.publish_up') . ' <= :nowDate1', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_down') . ' IS NULL', + $db->quoteName('a.publish_down') . ' >= :nowDate2', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.imptotal') . ' = 0', + $db->quoteName('a.impmade') . ' < ' . $db->quoteName('a.imptotal'), + ], + 'OR' + ) + ->bind([':nowDate1', ':nowDate2'], $nowDate); + + if ($cid) { + $query->where( + [ + $db->quoteName('a.cid') . ' = :clientId', + $db->quoteName('cl.state') . ' = 1', + ] + ) + ->bind(':clientId', $cid, ParameterType::INTEGER); + } + + // Filter by a single or group of categories + if (is_numeric($categoryId)) { + $categoryId = (int) $categoryId; + $type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> '; + + // Add subcategory check + if ($this->getState('filter.subcategories', false)) { + $levels = (int) $this->getState('filter.max_category_levels', '1'); + + // Create a subquery for the subcategory list + $subQuery = $db->getQuery(true); + $subQuery->select($db->quoteName('sub.id')) + ->from($db->quoteName('#__categories', 'sub')) + ->join( + 'INNER', + $db->quoteName('#__categories', 'this'), + $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') + . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') + ) + ->where( + [ + $db->quoteName('this.id') . ' = :categoryId1', + $db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels', + ] + ); + + // Add the subquery to the main query + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.catid') . $type . ':categoryId2', + $db->quoteName('a.catid') . ' IN (' . $subQuery . ')', + ], + 'OR' + ) + ->bind([':categoryId1', ':categoryId2'], $categoryId, ParameterType::INTEGER) + ->bind(':levels', $levels, ParameterType::INTEGER); + } else { + $query->where($db->quoteName('a.catid') . $type . ':categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + } elseif (is_array($categoryId) && (count($categoryId) > 0)) { + $categoryId = ArrayHelper::toInteger($categoryId); + + if ($this->getState('filter.category_id.include', true)) { + $query->whereIn($db->quoteName('a.catid'), $categoryId); + } else { + $query->whereNotIn($db->quoteName('a.catid'), $categoryId); + } + } + + if ($tagSearch) { + if (!$keywords) { + // No keywords, select nothing. + $query->where('0 != 0'); + } else { + $temp = array(); + $config = ComponentHelper::getParams('com_banners'); + $prefix = $config->get('metakey_prefix'); + + if ($categoryId) { + $query->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('a.catid') . ' = ' . $db->quoteName('cat.id')); + } + + foreach ($keywords as $key => $keyword) { + $regexp = '[[:<:]]' . $keyword . '[[:>:]]'; + $valuesToBind = [$keyword, $keyword, $regexp]; + + if ($cid) { + $valuesToBind[] = $regexp; + } + + if ($categoryId) { + $valuesToBind[] = $regexp; + } + + // Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting. + $bounded = $query->bindArray($valuesToBind, ParameterType::STRING); + + $condition1 = $db->quoteName('a.own_prefix') . ' = 1' + . ' AND ' . $db->quoteName('a.metakey_prefix') + . ' = SUBSTRING(' . $bounded[0] . ',1,LENGTH(' . $db->quoteName('a.metakey_prefix') . '))' + . ' OR ' . $db->quoteName('a.own_prefix') . ' = 0' + . ' AND ' . $db->quoteName('cl.own_prefix') . ' = 1' + . ' AND ' . $db->quoteName('cl.metakey_prefix') + . ' = SUBSTRING(' . $bounded[1] . ',1,LENGTH(' . $db->quoteName('cl.metakey_prefix') . '))' + . ' OR ' . $db->quoteName('a.own_prefix') . ' = 0' + . ' AND ' . $db->quoteName('cl.own_prefix') . ' = 0' + . ' AND ' . ($prefix == substr($keyword, 0, strlen($prefix)) ? '0 = 0' : '0 != 0'); + + $condition2 = $db->quoteName('a.metakey') . ' ' . $query->regexp($bounded[2]); + + if ($cid) { + $condition2 .= ' OR ' . $db->quoteName('cl.metakey') . ' ' . $query->regexp($bounded[3]) . ' '; + } + + if ($categoryId) { + $condition2 .= ' OR ' . $db->quoteName('cat.metakey') . ' ' . $query->regexp($bounded[4]) . ' '; + } + + $temp[] = "($condition1) AND ($condition2)"; + } + + $query->where('(' . implode(' OR ', $temp) . ')'); + } + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + $query->order($db->quoteName('a.sticky') . ' DESC, ' . ($randomise ? $query->rand() : $db->quoteName('a.ordering'))); + + return $query; + } + + /** + * Get a list of banners. + * + * @return array + * + * @since 1.6 + */ + public function getItems() + { + if ($this->getState('filter.tag_search')) { + // Filter out empty keywords. + $keywords = array_values(array_filter(array_map('trim', $this->getState('filter.keywords')), 'strlen')); + + // Re-set state before running the query. + $this->setState('filter.keywords', $keywords); + + // If no keywords are provided, avoid running the query. + if (!$keywords) { + $this->cache['items'] = array(); + + return $this->cache['items']; + } + } + + if (!isset($this->cache['items'])) { + $this->cache['items'] = parent::getItems(); + + foreach ($this->cache['items'] as &$item) { + $item->params = new Registry($item->params); + } + } + + return $this->cache['items']; + } + + /** + * Makes impressions on a list of banners + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + public function impress() + { + $trackDate = Factory::getDate()->format('Y-m-d H:00:00'); + $trackDate = Factory::getDate($trackDate)->toSql(); + $items = $this->getItems(); + $db = $this->getDatabase(); + $bid = []; + + if (!count($items)) { + return; + } + + foreach ($items as $item) { + $bid[] = (int) $item->id; + } + + // Increment impression made + $query = $db->getQuery(true); + $query->update($db->quoteName('#__banners')) + ->set($db->quoteName('impmade') . ' = ' . $db->quoteName('impmade') . ' + 1') + ->whereIn($db->quoteName('id'), $bid); + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + throw new \Exception($e->getMessage(), 500); + } + + foreach ($items as $item) { + // Track impressions + $trackImpressions = $item->track_impressions; + + if ($trackImpressions < 0 && $item->cid) { + $trackImpressions = $item->client_track_impressions; + } + + if ($trackImpressions < 0) { + $config = ComponentHelper::getParams('com_banners'); + $trackImpressions = $config->get('track_impressions'); + } + + if ($trackImpressions > 0) { + // Is track already created? + // Update count + $query = $db->getQuery(true); + $query->update($db->quoteName('#__banner_tracks')) + ->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1') + ->where( + [ + $db->quoteName('track_type') . ' = 1', + $db->quoteName('banner_id') . ' = :id', + $db->quoteName('track_date') . ' = :trackDate', + ] + ) + ->bind(':id', $item->id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + throw new \Exception($e->getMessage(), 500); + } + + if ($db->getAffectedRows() === 0) { + // Insert new count + $query = $db->getQuery(true); + $query->insert($db->quoteName('#__banner_tracks')) + ->columns( + [ + $db->quoteName('count'), + $db->quoteName('track_type'), + $db->quoteName('banner_id'), + $db->quoteName('track_date'), + ] + ) + ->values('1, 1, :id, :trackDate') + ->bind(':id', $item->id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + throw new \Exception($e->getMessage(), 500); + } + } + } + } + } } diff --git a/code/components/com_banners/src/Service/Category.php b/code/components/com_banners/src/Service/Category.php index 7c020830..8934c098 100644 --- a/code/components/com_banners/src/Service/Category.php +++ b/code/components/com_banners/src/Service/Category.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Method to handle cancel - * - * @return void - * - * @since 3.2 - */ - public function cancel() - { - // Redirect back to home(base) page - $this->setRedirect(Uri::base()); - } - - /** - * Method to save global configuration. - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR')); - $this->app->redirect('index.php'); - } - - // Set FTP credentials, if given. - ClientHelper::setCredentialsFromRequest('ftp'); - - $model = $this->getModel(); - - $form = $model->getForm(); - $data = $this->app->input->post->get('jform', array(), 'array'); - - // Validate the posted data. - $return = $model->validate($form, $data); - - // Check for validation errors. - if ($return === false) - { - /* - * The validate method enqueued all messages for us, so we just need to redirect back. - */ - - // Save the data in the session. - $this->app->setUserState('com_config.config.global.data', $data); - - // Redirect back to the edit screen. - $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); - } - - // Attempt to save the configuration. - $data = $return; - - // Access backend com_config - $saveClass = $this->factory->createController('Application', 'Administrator', [], $this->app, $this->input); - - // Get a document object - $document = $this->app->getDocument(); - - // Set backend required params - $document->setType('json'); - - // Execute backend controller - $return = $saveClass->save(); - - // Reset params back after requesting from service - $document->setType('html'); - - // Check the return value. - if ($return === false) - { - /* - * The save method enqueued all messages for us, so we just need to redirect back. - */ - - // Save the data in the session. - $this->app->setUserState('com_config.config.global.data', $data); - - // Save failed, go back to the screen and display a notice. - $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); - } - - // Redirect back to com_config display - $this->app->enqueueMessage(Text::_('COM_CONFIG_SAVE_SUCCESS')); - $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); - - return true; - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The JApplication for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('apply', 'save'); + } + + /** + * Method to handle cancel + * + * @return void + * + * @since 3.2 + */ + public function cancel() + { + // Redirect back to home(base) page + $this->setRedirect(Uri::base()); + } + + /** + * Method to save global configuration. + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR')); + $this->app->redirect('index.php'); + } + + // Set FTP credentials, if given. + ClientHelper::setCredentialsFromRequest('ftp'); + + $model = $this->getModel(); + + $form = $model->getForm(); + $data = $this->app->input->post->get('jform', array(), 'array'); + + // Validate the posted data. + $return = $model->validate($form, $data); + + // Check for validation errors. + if ($return === false) { + /* + * The validate method enqueued all messages for us, so we just need to redirect back. + */ + + // Save the data in the session. + $this->app->setUserState('com_config.config.global.data', $data); + + // Redirect back to the edit screen. + $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); + } + + // Attempt to save the configuration. + $data = $return; + + // Access backend com_config + $saveClass = $this->factory->createController('Application', 'Administrator', [], $this->app, $this->input); + + // Get a document object + $document = $this->app->getDocument(); + + // Set backend required params + $document->setType('json'); + + // Execute backend controller + $return = $saveClass->save(); + + // Reset params back after requesting from service + $document->setType('html'); + + // Check the return value. + if ($return === false) { + /* + * The save method enqueued all messages for us, so we just need to redirect back. + */ + + // Save the data in the session. + $this->app->setUserState('com_config.config.global.data', $data); + + // Save failed, go back to the screen and display a notice. + $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); + } + + // Redirect back to com_config display + $this->app->enqueueMessage(Text::_('COM_CONFIG_SAVE_SUCCESS')); + $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); + + return true; + } } diff --git a/code/components/com_config/src/Controller/DisplayController.php b/code/components/com_config/src/Controller/DisplayController.php index daadff24..a6458391 100644 --- a/code/components/com_config/src/Controller/DisplayController.php +++ b/code/components/com_config/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ setRedirect(Uri::base()); - } + /** + * Method to handle cancel + * + * @return void + * + * @since 3.2 + */ + public function cancel() + { + // Redirect back to home(base) page + $this->setRedirect(Uri::base()); + } } diff --git a/code/components/com_config/src/Controller/ModulesController.php b/code/components/com_config/src/Controller/ModulesController.php index 5000e4cc..aad1303d 100644 --- a/code/components/com_config/src/Controller/ModulesController.php +++ b/code/components/com_config/src/Controller/ModulesController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Method to handle cancel - * - * @return void - * - * @since 3.2 - */ - public function cancel() - { - // Redirect back to home(base) page - $this->setRedirect(Uri::base()); - } - - /** - * Method to save module editing. - * - * @return void - * - * @since 3.2 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - // Check if the user is authorized to do this. - $user = $this->app->getIdentity(); - - if (!$user->authorise('module.edit.frontend', 'com_modules.module.' . $this->input->get('id'))) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $this->app->redirect('index.php'); - } - - // Set FTP credentials, if given. - ClientHelper::setCredentialsFromRequest('ftp'); - - // Get submitted module id - $moduleId = '&id=' . $this->input->getInt('id'); - - // Get returnUri - $returnUri = $this->input->post->get('return', null, 'base64'); - $redirect = ''; - - if (!empty($returnUri)) - { - $redirect = '&return=' . $returnUri; - } - - /** @var AdministratorApplication $app */ - $app = Factory::getContainer()->get(AdministratorApplication::class); - - // Reset Uri cache. - Uri::reset(); - - // Get a document object - $document = $this->app->getDocument(); - - // Load application dependencies. - $app->loadLanguage($this->app->getLanguage()); - $app->loadDocument($document); - $app->loadIdentity($user); - - /** @var \Joomla\CMS\Dispatcher\ComponentDispatcher $dispatcher */ - $dispatcher = $app->bootComponent('com_modules')->getDispatcher($app); - - /** @var ModuleController $controllerClass */ - $controllerClass = $dispatcher->getController('Module'); - - // Set backend required params - $document->setType('json'); - - // Execute backend controller - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/forms'); - $return = $controllerClass->save(); - - // Reset params back after requesting from service - $document->setType('html'); - - // Check the return value. - if ($return === false) - { - // Save the data in the session. - $data = $this->input->post->get('jform', array(), 'array'); - - $this->app->setUserState('com_config.modules.global.data', $data); - - // Save failed, go back to the screen and display a notice. - $this->app->enqueueMessage(Text::_('JERROR_SAVE_FAILED')); - $this->app->redirect(Route::_('index.php?option=com_config&view=modules' . $moduleId . $redirect, false)); - } - - // Redirect back to com_config display - $this->app->enqueueMessage(Text::_('COM_CONFIG_MODULES_SAVE_SUCCESS')); - - // Set the redirect based on the task. - switch ($this->input->getCmd('task')) - { - case 'apply': - $this->app->redirect(Route::_('index.php?option=com_config&view=modules' . $moduleId . $redirect, false)); - break; - - case 'save': - default: - - if (!empty($returnUri)) - { - $redirect = base64_decode(urldecode($returnUri)); - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = Uri::base(); - } - } - else - { - $redirect = Uri::base(); - } - - $this->setRedirect($redirect); - break; - } - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('apply', 'save'); + } + + /** + * Method to handle cancel + * + * @return void + * + * @since 3.2 + */ + public function cancel() + { + // Redirect back to home(base) page + $this->setRedirect(Uri::base()); + } + + /** + * Method to save module editing. + * + * @return void + * + * @since 3.2 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + // Check if the user is authorized to do this. + $user = $this->app->getIdentity(); + + if (!$user->authorise('module.edit.frontend', 'com_modules.module.' . $this->input->get('id'))) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $this->app->redirect('index.php'); + } + + // Set FTP credentials, if given. + ClientHelper::setCredentialsFromRequest('ftp'); + + // Get submitted module id + $moduleId = '&id=' . $this->input->getInt('id'); + + // Get returnUri + $returnUri = $this->input->post->get('return', null, 'base64'); + $redirect = ''; + + if (!empty($returnUri)) { + $redirect = '&return=' . $returnUri; + } + + /** @var AdministratorApplication $app */ + $app = Factory::getContainer()->get(AdministratorApplication::class); + + // Reset Uri cache. + Uri::reset(); + + // Get a document object + $document = $this->app->getDocument(); + + // Load application dependencies. + $app->loadLanguage($this->app->getLanguage()); + $app->loadDocument($document); + $app->loadIdentity($user); + + /** @var \Joomla\CMS\Dispatcher\ComponentDispatcher $dispatcher */ + $dispatcher = $app->bootComponent('com_modules')->getDispatcher($app); + + /** @var ModuleController $controllerClass */ + $controllerClass = $dispatcher->getController('Module'); + + // Set backend required params + $document->setType('json'); + + // Execute backend controller + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/forms'); + $return = $controllerClass->save(); + + // Reset params back after requesting from service + $document->setType('html'); + + // Check the return value. + if ($return === false) { + // Save the data in the session. + $data = $this->input->post->get('jform', array(), 'array'); + + $this->app->setUserState('com_config.modules.global.data', $data); + + // Save failed, go back to the screen and display a notice. + $this->app->enqueueMessage(Text::_('JERROR_SAVE_FAILED')); + $this->app->redirect(Route::_('index.php?option=com_config&view=modules' . $moduleId . $redirect, false)); + } + + // Redirect back to com_config display + $this->app->enqueueMessage(Text::_('COM_CONFIG_MODULES_SAVE_SUCCESS')); + + // Set the redirect based on the task. + switch ($this->input->getCmd('task')) { + case 'apply': + $this->app->redirect(Route::_('index.php?option=com_config&view=modules' . $moduleId . $redirect, false)); + break; + + case 'save': + default: + if (!empty($returnUri)) { + $redirect = base64_decode(urldecode($returnUri)); + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = Uri::base(); + } + } else { + $redirect = Uri::base(); + } + + $this->setRedirect($redirect); + break; + } + } } diff --git a/code/components/com_config/src/Controller/TemplatesController.php b/code/components/com_config/src/Controller/TemplatesController.php index 6cc36fca..87d19e8c 100644 --- a/code/components/com_config/src/Controller/TemplatesController.php +++ b/code/components/com_config/src/Controller/TemplatesController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Method to handle cancel - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function cancel() - { - // Redirect back to home(base) page - $this->setRedirect(Uri::base()); - } - - /** - * Method to save global configuration. - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - - // Set FTP credentials, if given. - ClientHelper::setCredentialsFromRequest('ftp'); - - $app = $this->app; - - // Access backend com_templates - $controllerClass = $app->bootComponent('com_templates') - ->getMVCFactory()->createController('Style', 'Administrator', [], $app, $app->input); - - // Get a document object - $document = $app->getDocument(); - - // Set backend required params - $document->setType('json'); - $this->input->set('id', $app->getTemplate(true)->id); - - // Execute backend controller - $return = $controllerClass->save(); - - // Reset params back after requesting from service - $document->setType('html'); - - // Check the return value. - if ($return === false) - { - // Save failed, go back to the screen and display a notice. - $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED'), 'error'); - $this->setRedirect(Route::_('index.php?option=com_config&view=templates', false)); - - return false; - } - - // Set the success message. - $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS')); - - // Redirect back to com_config display - $this->setRedirect(Route::_('index.php?option=com_config&view=templates', false)); - - return true; - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Apply, Save & New, and Save As copy should be standard on forms. + $this->registerTask('apply', 'save'); + } + + /** + * Method to handle cancel + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function cancel() + { + // Redirect back to home(base) page + $this->setRedirect(Uri::base()); + } + + /** + * Method to save global configuration. + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } + + // Set FTP credentials, if given. + ClientHelper::setCredentialsFromRequest('ftp'); + + $app = $this->app; + + // Access backend com_templates + $controllerClass = $app->bootComponent('com_templates') + ->getMVCFactory()->createController('Style', 'Administrator', [], $app, $app->input); + + // Get a document object + $document = $app->getDocument(); + + // Set backend required params + $document->setType('json'); + $this->input->set('id', $app->getTemplate(true)->id); + + // Execute backend controller + $return = $controllerClass->save(); + + // Reset params back after requesting from service + $document->setType('html'); + + // Check the return value. + if ($return === false) { + // Save failed, go back to the screen and display a notice. + $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_config&view=templates', false)); + + return false; + } + + // Set the success message. + $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS')); + + // Redirect back to com_config display + $this->setRedirect(Route::_('index.php?option=com_config&view=templates', false)); + + return true; + } } diff --git a/code/components/com_config/src/Dispatcher/Dispatcher.php b/code/components/com_config/src/Dispatcher/Dispatcher.php index 336f520b..105a0905 100644 --- a/code/components/com_config/src/Dispatcher/Dispatcher.php +++ b/code/components/com_config/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->getCmd('task', 'display'); - $view = $this->input->get('view'); - $user = $this->app->getIdentity(); + $task = $this->input->getCmd('task', 'display'); + $view = $this->input->get('view'); + $user = $this->app->getIdentity(); - if (substr($task, 0, 8) === 'modules.' || $view === 'modules') - { - if (!$user->authorise('module.edit.frontend', 'com_modules.module.' . $this->input->get('id'))) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } - elseif (!$user->authorise('core.admin')) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + if (substr($task, 0, 8) === 'modules.' || $view === 'modules') { + if (!$user->authorise('module.edit.frontend', 'com_modules.module.' . $this->input->get('id'))) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } elseif (!$user->authorise('core.admin')) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/code/components/com_config/src/Model/ConfigModel.php b/code/components/com_config/src/Model/ConfigModel.php index 381ff67a..fbc83728 100644 --- a/code/components/com_config/src/Model/ConfigModel.php +++ b/code/components/com_config/src/Model/ConfigModel.php @@ -1,4 +1,5 @@ loadForm('com_config.config', 'config', array('control' => 'jform', 'load_data' => $loadData)); + /** + * Method to get a form object. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return mixed A JForm object on success, false on failure + * + * @since 3.2 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_config.config', 'config', array('control' => 'jform', 'load_data' => $loadData)); - if (empty($form)) - { - return false; - } + if (empty($form)) { + return false; + } - return $form; - } + return $form; + } } diff --git a/code/components/com_config/src/Model/FormModel.php b/code/components/com_config/src/Model/FormModel.php index ccdbf711..34dade96 100644 --- a/code/components/com_config/src/Model/FormModel.php +++ b/code/components/com_config/src/Model/FormModel.php @@ -1,4 +1,5 @@ getTable(); - - if (!$table->load($pk)) - { - throw new \RuntimeException($table->getError()); - } - - // Check if this is the user has previously checked out the row. - if (!is_null($table->checked_out) && $table->checked_out != $user->get('id') && !$user->authorise('core.admin', 'com_checkin')) - { - throw new \RuntimeException($table->getError()); - } - - // Attempt to check the row in. - if (!$table->checkIn($pk)) - { - throw new \RuntimeException($table->getError()); - } - } - - return true; - } - - /** - * Method to check-out a row for editing. - * - * @param integer $pk The numeric id of the primary key. - * - * @return boolean False on failure or error, true otherwise. - * - * @since 3.2 - */ - public function checkout($pk = null) - { - // Only attempt to check the row in if it exists. - if ($pk) - { - $user = Factory::getUser(); - - // Get an instance of the row to checkout. - $table = $this->getTable(); - - if (!$table->load($pk)) - { - throw new \RuntimeException($table->getError()); - } - - // Check if this is the user having previously checked out the row. - if (!is_null($table->checked_out) && $table->checked_out != $user->get('id')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CHECKOUT_USER_MISMATCH')); - } - - // Attempt to check the row out. - if (!$table->checkOut($user->get('id'), $pk)) - { - throw new \RuntimeException($table->getError()); - } - } - - return true; - } - - /** - * Method to get a form object. - * - * @param string $name The name of the form. - * @param string $source The form source. Can be XML string if file flag is set to false. - * @param array $options Optional array of options for the form creation. - * @param boolean $clear Optional argument to force load a new form. - * @param string $xpath An optional xpath to search for the fields. - * - * @return mixed JForm object on success, False on error. - * - * @see JForm - * @since 3.2 - */ - protected function loadForm($name, $source = null, $options = array(), $clear = false, $xpath = false) - { - // Handle the optional arguments. - $options['control'] = ArrayHelper::getValue($options, 'control', false); - - // Create a signature hash. - $hash = sha1($source . serialize($options)); - - // Check if we can use a previously loaded form. - if (isset($this->_forms[$hash]) && !$clear) - { - return $this->_forms[$hash]; - } - - // Register the paths for the form. - Form::addFormPath(JPATH_SITE . '/components/com_config/forms'); - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_config/forms'); - - try - { - // Get the form. - $form = Form::getInstance($name, $source, $options, false, $xpath); - - if (isset($options['load_data']) && $options['load_data']) - { - // Get the data for the form. - $data = $this->loadFormData(); - } - else - { - $data = array(); - } - - // Allow for additional modification of the form, and events to be triggered. - // We pass the data because plugins may require it. - $this->preprocessForm($form, $data); - - // Load the data into the form after the plugins have operated. - $form->bind($data); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage()); - - return false; - } - - // Store the form for later. - $this->_forms[$hash] = $form; - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 3.2 - */ - protected function loadFormData() - { - return array(); - } - - /** - * Method to allow derived classes to preprocess the data. - * - * @param string $context The context identifier. - * @param mixed &$data The data to be processed. It gets altered directly. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 3.2 - */ - protected function preprocessData($context, &$data, $group = 'content') - { - // Get the dispatcher and load the users plugins. - PluginHelper::importPlugin('content'); - - // Trigger the data preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareData', array($context, $data)); - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @see \Joomla\CMS\Form\FormField - * @since 3.2 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - // Import the appropriate plugin group. - PluginHelper::importPlugin($group); - - // Trigger the form preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareForm', array($form, $data)); - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return mixed Array of filtered data if valid, false otherwise. - * - * @see \Joomla\CMS\Form\FormRule - * @see JFilterInput - * @since 3.2 - */ - public function validate($form, $data, $group = null) - { - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data, $group); - - // Check for an error. - if ($return instanceof \Exception) - { - Factory::getApplication()->enqueueMessage($return->getMessage(), 'error'); - - return false; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $message) - { - if ($message instanceof \Exception) - { - $message = $message->getMessage(); - } - - Factory::getApplication()->enqueueMessage($message, 'error'); - } - - return false; - } - - return $data; - } + /** + * Array of form objects. + * + * @var array + * @since 3.2 + */ + protected $forms = array(); + + /** + * Method to checkin a row. + * + * @param integer $pk The numeric id of the primary key. + * + * @return boolean False on failure or error, true otherwise. + * + * @since 3.2 + * @throws \RuntimeException + */ + public function checkin($pk = null) + { + // Only attempt to check the row in if it exists. + if ($pk) { + $user = Factory::getUser(); + + // Get an instance of the row to checkin. + $table = $this->getTable(); + + if (!$table->load($pk)) { + throw new \RuntimeException($table->getError()); + } + + // Check if this is the user has previously checked out the row. + if (!is_null($table->checked_out) && $table->checked_out != $user->get('id') && !$user->authorise('core.admin', 'com_checkin')) { + throw new \RuntimeException($table->getError()); + } + + // Attempt to check the row in. + if (!$table->checkIn($pk)) { + throw new \RuntimeException($table->getError()); + } + } + + return true; + } + + /** + * Method to check-out a row for editing. + * + * @param integer $pk The numeric id of the primary key. + * + * @return boolean False on failure or error, true otherwise. + * + * @since 3.2 + */ + public function checkout($pk = null) + { + // Only attempt to check the row in if it exists. + if ($pk) { + $user = Factory::getUser(); + + // Get an instance of the row to checkout. + $table = $this->getTable(); + + if (!$table->load($pk)) { + throw new \RuntimeException($table->getError()); + } + + // Check if this is the user having previously checked out the row. + if (!is_null($table->checked_out) && $table->checked_out != $user->get('id')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CHECKOUT_USER_MISMATCH')); + } + + // Attempt to check the row out. + if (!$table->checkOut($user->get('id'), $pk)) { + throw new \RuntimeException($table->getError()); + } + } + + return true; + } + + /** + * Method to get a form object. + * + * @param string $name The name of the form. + * @param string $source The form source. Can be XML string if file flag is set to false. + * @param array $options Optional array of options for the form creation. + * @param boolean $clear Optional argument to force load a new form. + * @param string $xpath An optional xpath to search for the fields. + * + * @return mixed JForm object on success, False on error. + * + * @see JForm + * @since 3.2 + */ + protected function loadForm($name, $source = null, $options = array(), $clear = false, $xpath = false) + { + // Handle the optional arguments. + $options['control'] = ArrayHelper::getValue($options, 'control', false); + + // Create a signature hash. + $hash = sha1($source . serialize($options)); + + // Check if we can use a previously loaded form. + if (isset($this->_forms[$hash]) && !$clear) { + return $this->_forms[$hash]; + } + + // Register the paths for the form. + Form::addFormPath(JPATH_SITE . '/components/com_config/forms'); + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_config/forms'); + + try { + // Get the form. + $form = Form::getInstance($name, $source, $options, false, $xpath); + + if (isset($options['load_data']) && $options['load_data']) { + // Get the data for the form. + $data = $this->loadFormData(); + } else { + $data = array(); + } + + // Allow for additional modification of the form, and events to be triggered. + // We pass the data because plugins may require it. + $this->preprocessForm($form, $data); + + // Load the data into the form after the plugins have operated. + $form->bind($data); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage()); + + return false; + } + + // Store the form for later. + $this->_forms[$hash] = $form; + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 3.2 + */ + protected function loadFormData() + { + return array(); + } + + /** + * Method to allow derived classes to preprocess the data. + * + * @param string $context The context identifier. + * @param mixed &$data The data to be processed. It gets altered directly. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 3.2 + */ + protected function preprocessData($context, &$data, $group = 'content') + { + // Get the dispatcher and load the users plugins. + PluginHelper::importPlugin('content'); + + // Trigger the data preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareData', array($context, $data)); + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @see \Joomla\CMS\Form\FormField + * @since 3.2 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + // Import the appropriate plugin group. + PluginHelper::importPlugin($group); + + // Trigger the form preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareForm', array($form, $data)); + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return mixed Array of filtered data if valid, false otherwise. + * + * @see \Joomla\CMS\Form\FormRule + * @see JFilterInput + * @since 3.2 + */ + public function validate($form, $data, $group = null) + { + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data, $group); + + // Check for an error. + if ($return instanceof \Exception) { + Factory::getApplication()->enqueueMessage($return->getMessage(), 'error'); + + return false; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $message) { + if ($message instanceof \Exception) { + $message = $message->getMessage(); + } + + Factory::getApplication()->enqueueMessage($message, 'error'); + } + + return false; + } + + return $data; + } } diff --git a/code/components/com_config/src/Model/ModulesModel.php b/code/components/com_config/src/Model/ModulesModel.php index 9c887118..8146ea43 100644 --- a/code/components/com_config/src/Model/ModulesModel.php +++ b/code/components/com_config/src/Model/ModulesModel.php @@ -1,4 +1,5 @@ input->getInt('id'); - - $this->setState('module.id', $pk); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 3.2 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_config.modules', 'modules', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 3.2 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $lang = Factory::getLanguage(); - $module = $this->getState()->get('module.name'); - $basePath = JPATH_BASE; - - $formFile = Path::clean($basePath . '/modules/' . $module . '/' . $module . '.xml'); - - // Load the core and/or local language file(s). - $lang->load($module, $basePath) - || $lang->load($module, $basePath . '/modules/' . $module); - - if (file_exists($formFile)) - { - // Get the module form. - if (!$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - // Load the default advanced params - Form::addFormPath(JPATH_BASE . '/components/com_config/model/form'); - $form->loadFile('modules_advanced', false); - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to get list of module positions in current template - * - * @return array - * - * @since 3.2 - */ - public function getPositions() - { - $lang = Factory::getLanguage(); - $templateName = Factory::getApplication()->getTemplate(); - - // Load templateDetails.xml file - $path = Path::clean(JPATH_BASE . '/templates/' . $templateName . '/templateDetails.xml'); - $currentTemplatePositions = array(); - - if (file_exists($path)) - { - $xml = simplexml_load_file($path); - - if (isset($xml->positions[0])) - { - // Load language files - $lang->load('tpl_' . $templateName . '.sys', JPATH_BASE) - || $lang->load('tpl_' . $templateName . '.sys', JPATH_BASE . '/templates/' . $templateName); - - foreach ($xml->positions[0] as $position) - { - $value = (string) $position; - $text = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'TPL_' . strtoupper($templateName) . '_POSITION_' . strtoupper($value)); - - // Construct list of positions - $currentTemplatePositions[] = self::createOption($value, Text::_($text) . ' [' . $value . ']'); - } - } - } - - $templateGroups = array(); - - // Add an empty value to be able to deselect a module position - $option = self::createOption(); - $templateGroups[''] = self::createOptionGroup('', array($option)); - - $templateGroups[$templateName] = self::createOptionGroup($templateName, $currentTemplatePositions); - - // Add custom position to options - $customGroupText = Text::_('COM_MODULES_CUSTOM_POSITION'); - - $editPositions = true; - $customPositions = self::getActivePositions(0, $editPositions); - $templateGroups[$customGroupText] = self::createOptionGroup($customGroupText, $customPositions); - - return $templateGroups; - } - - /** - * Get a list of modules positions - * - * @param integer $clientId Client ID - * @param boolean $editPositions Allow to edit the positions - * - * @return array A list of positions - * - * @since 3.6.3 - */ - public static function getActivePositions($clientId, $editPositions = false) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT position') - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = ' . (int) $clientId) - ->order($db->quoteName('position')); - - $db->setQuery($query); - - try - { - $positions = $db->loadColumn(); - $positions = is_array($positions) ? $positions : array(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return; - } - - // Build the list - $options = array(); - - foreach ($positions as $position) - { - if (!$position && !$editPositions) - { - $options[] = HTMLHelper::_('select.option', 'none', ':: ' . Text::_('JNONE') . ' ::'); - } - else - { - $options[] = HTMLHelper::_('select.option', $position, $position); - } - } - - return $options; - } - - /** - * Create and return a new Option - * - * @param string $value The option value [optional] - * @param string $text The option text [optional] - * - * @return object The option as an object (stdClass instance) - * - * @since 3.6.3 - */ - private static function createOption($value = '', $text = '') - { - if (empty($text)) - { - $text = $value; - } - - $option = new \stdClass; - $option->value = $value; - $option->text = $text; - - return $option; - } - - /** - * Create and return a new Option Group - * - * @param string $label Value and label for group [optional] - * @param array $options Array of options to insert into group [optional] - * - * @return array Return the new group as an array - * - * @since 3.6.3 - */ - private static function createOptionGroup($label = '', $options = array()) - { - $group = array(); - $group['value'] = $label; - $group['text'] = $label; - $group['items'] = $options; - - return $group; - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.2 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + + $this->setState('module.id', $pk); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 3.2 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_config.modules', 'modules', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 3.2 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $lang = Factory::getLanguage(); + $module = $this->getState()->get('module.name'); + $basePath = JPATH_BASE; + + $formFile = Path::clean($basePath . '/modules/' . $module . '/' . $module . '.xml'); + + // Load the core and/or local language file(s). + $lang->load($module, $basePath) + || $lang->load($module, $basePath . '/modules/' . $module); + + if (file_exists($formFile)) { + // Get the module form. + if (!$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + // Load the default advanced params + Form::addFormPath(JPATH_BASE . '/components/com_config/model/form'); + $form->loadFile('modules_advanced', false); + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to get list of module positions in current template + * + * @return array + * + * @since 3.2 + */ + public function getPositions() + { + $lang = Factory::getLanguage(); + $templateName = Factory::getApplication()->getTemplate(); + + // Load templateDetails.xml file + $path = Path::clean(JPATH_BASE . '/templates/' . $templateName . '/templateDetails.xml'); + $currentTemplatePositions = array(); + + if (file_exists($path)) { + $xml = simplexml_load_file($path); + + if (isset($xml->positions[0])) { + // Load language files + $lang->load('tpl_' . $templateName . '.sys', JPATH_BASE) + || $lang->load('tpl_' . $templateName . '.sys', JPATH_BASE . '/templates/' . $templateName); + + foreach ($xml->positions[0] as $position) { + $value = (string) $position; + $text = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'TPL_' . strtoupper($templateName) . '_POSITION_' . strtoupper($value)); + + // Construct list of positions + $currentTemplatePositions[] = self::createOption($value, Text::_($text) . ' [' . $value . ']'); + } + } + } + + $templateGroups = array(); + + // Add an empty value to be able to deselect a module position + $option = self::createOption(); + $templateGroups[''] = self::createOptionGroup('', array($option)); + + $templateGroups[$templateName] = self::createOptionGroup($templateName, $currentTemplatePositions); + + // Add custom position to options + $customGroupText = Text::_('COM_MODULES_CUSTOM_POSITION'); + + $editPositions = true; + $customPositions = self::getActivePositions(0, $editPositions); + $templateGroups[$customGroupText] = self::createOptionGroup($customGroupText, $customPositions); + + return $templateGroups; + } + + /** + * Get a list of modules positions + * + * @param integer $clientId Client ID + * @param boolean $editPositions Allow to edit the positions + * + * @return array A list of positions + * + * @since 3.6.3 + */ + public static function getActivePositions($clientId, $editPositions = false) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT position') + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = ' . (int) $clientId) + ->order($db->quoteName('position')); + + $db->setQuery($query); + + try { + $positions = $db->loadColumn(); + $positions = is_array($positions) ? $positions : array(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return; + } + + // Build the list + $options = array(); + + foreach ($positions as $position) { + if (!$position && !$editPositions) { + $options[] = HTMLHelper::_('select.option', 'none', ':: ' . Text::_('JNONE') . ' ::'); + } else { + $options[] = HTMLHelper::_('select.option', $position, $position); + } + } + + return $options; + } + + /** + * Create and return a new Option + * + * @param string $value The option value [optional] + * @param string $text The option text [optional] + * + * @return object The option as an object (stdClass instance) + * + * @since 3.6.3 + */ + private static function createOption($value = '', $text = '') + { + if (empty($text)) { + $text = $value; + } + + $option = new \stdClass(); + $option->value = $value; + $option->text = $text; + + return $option; + } + + /** + * Create and return a new Option Group + * + * @param string $label Value and label for group [optional] + * @param array $options Array of options to insert into group [optional] + * + * @return array Return the new group as an array + * + * @since 3.6.3 + */ + private static function createOptionGroup($label = '', $options = array()) + { + $group = array(); + $group['value'] = $label; + $group['text'] = $label; + $group['items'] = $options; + + return $group; + } } diff --git a/code/components/com_config/src/Model/TemplatesModel.php b/code/components/com_config/src/Model/TemplatesModel.php index f9f90167..b4616101 100644 --- a/code/components/com_config/src/Model/TemplatesModel.php +++ b/code/components/com_config/src/Model/TemplatesModel.php @@ -1,4 +1,5 @@ setState('params', ComponentHelper::getParams('com_templates')); - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A JForm object on success, false on failure - * - * @since 3.2 - */ - public function getForm($data = array(), $loadData = true) - { - try - { - // Get the form. - $form = $this->loadForm('com_config.templates', 'templates', array('load_data' => $loadData)); - - $data = array(); - $this->preprocessForm($form, $data); - - // Load the data into the form - $form->bind($data); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage()); - - return false; - } - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group Plugin group to load - * - * @return void - * - * @since 3.2 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $lang = Factory::getLanguage(); - - $template = Factory::getApplication()->getTemplate(); - - // Load the core and/or local language file(s). - $lang->load('tpl_' . $template, JPATH_BASE) - || $lang->load('tpl_' . $template, JPATH_BASE . '/templates/' . $template); - - // Look for com_config.xml, which contains fields to display - $formFile = Path::clean(JPATH_BASE . '/templates/' . $template . '/com_config.xml'); - - if (!file_exists($formFile)) - { - // If com_config.xml not found, fall back to templateDetails.xml - $formFile = Path::clean(JPATH_BASE . '/templates/' . $template . '/templateDetails.xml'); - } - - // Get the template form. - if (file_exists($formFile) && !$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return null + * + * @since 3.2 + */ + protected function populateState() + { + parent::populateState(); + + $this->setState('params', ComponentHelper::getParams('com_templates')); + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A JForm object on success, false on failure + * + * @since 3.2 + */ + public function getForm($data = array(), $loadData = true) + { + try { + // Get the form. + $form = $this->loadForm('com_config.templates', 'templates', array('load_data' => $loadData)); + + $data = array(); + $this->preprocessForm($form, $data); + + // Load the data into the form + $form->bind($data); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage()); + + return false; + } + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group Plugin group to load + * + * @return void + * + * @since 3.2 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $lang = Factory::getLanguage(); + + $template = Factory::getApplication()->getTemplate(); + + // Load the core and/or local language file(s). + $lang->load('tpl_' . $template, JPATH_BASE) + || $lang->load('tpl_' . $template, JPATH_BASE . '/templates/' . $template); + + // Look for com_config.xml, which contains fields to display + $formFile = Path::clean(JPATH_BASE . '/templates/' . $template . '/com_config.xml'); + + if (!file_exists($formFile)) { + // If com_config.xml not found, fall back to templateDetails.xml + $formFile = Path::clean(JPATH_BASE . '/templates/' . $template . '/templateDetails.xml'); + } + + // Get the template form. + if (file_exists($formFile) && !$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } } diff --git a/code/components/com_config/src/Service/Router.php b/code/components/com_config/src/Service/Router.php index a2935a94..30cfd33b 100644 --- a/code/components/com_config/src/Service/Router.php +++ b/code/components/com_config/src/Service/Router.php @@ -1,4 +1,5 @@ registerView(new RouterViewConfiguration('config')); - $this->registerView(new RouterViewConfiguration('templates')); + /** + * Config Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + */ + public function __construct(SiteApplication $app, AbstractMenu $menu) + { + $this->registerView(new RouterViewConfiguration('config')); + $this->registerView(new RouterViewConfiguration('templates')); - parent::__construct($app, $menu); + parent::__construct($app, $menu); - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } } diff --git a/code/components/com_config/src/View/Config/HtmlView.php b/code/components/com_config/src/View/Config/HtmlView.php index 437ee88f..92e0d7ce 100644 --- a/code/components/com_config/src/View/Config/HtmlView.php +++ b/code/components/com_config/src/View/Config/HtmlView.php @@ -1,4 +1,5 @@ userIsSuperAdmin = $user->authorise('core.admin'); - - // Access backend com_config - $requestController = new RequestController; - - // Execute backend controller - $serviceData = json_decode($requestController->getJson(), true); - - $form = $this->getForm(); - - if ($form) - { - $form->bind($serviceData); - } - - $this->form = $form; - $this->data = $serviceData; - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 4.0.0 - */ - protected function _prepareDocument() - { - $params = Factory::getApplication()->getParams(); - - // Because the application sets a default page title, we need to get it - // right from the menu item itself - - $this->setDocumentTitle($params->get('page_title', '')); - - if ($params->get('menu-meta_description')) - { - $this->document->setDescription($params->get('menu-meta_description')); - } - - if ($params->get('robots')) - { - $this->document->setMetaData('robots', $params->get('robots')); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - $this->params = &$params; - } + /** + * The form object + * + * @var \Joomla\CMS\Form\Form + * + * @since 3.2 + */ + public $form; + + /** + * The data to be displayed in the form + * + * @var array + * + * @since 3.2 + */ + public $data; + + /** + * Is the current user a super administrator? + * + * @var boolean + * + * @since 3.2 + */ + protected $userIsSuperAdmin; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + $this->userIsSuperAdmin = $user->authorise('core.admin'); + + // Access backend com_config + $requestController = new RequestController(); + + // Execute backend controller + $serviceData = json_decode($requestController->getJson(), true); + + $form = $this->getForm(); + + if ($form) { + $form->bind($serviceData); + } + + $this->form = $form; + $this->data = $serviceData; + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 4.0.0 + */ + protected function _prepareDocument() + { + $params = Factory::getApplication()->getParams(); + + // Because the application sets a default page title, we need to get it + // right from the menu item itself + + $this->setDocumentTitle($params->get('page_title', '')); + + if ($params->get('menu-meta_description')) { + $this->document->setDescription($params->get('menu-meta_description')); + } + + if ($params->get('robots')) { + $this->document->setMetaData('robots', $params->get('robots')); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + $this->params = &$params; + } } diff --git a/code/components/com_config/src/View/Modules/HtmlView.php b/code/components/com_config/src/View/Modules/HtmlView.php index e5e3f5be..615e5d73 100644 --- a/code/components/com_config/src/View/Modules/HtmlView.php +++ b/code/components/com_config/src/View/Modules/HtmlView.php @@ -1,4 +1,5 @@ getLanguage(); - $lang->load('', JPATH_ADMINISTRATOR, $lang->getTag()); - $lang->load('com_modules', JPATH_ADMINISTRATOR, $lang->getTag()); - - // @todo Move and clean up - $module = (new \Joomla\Component\Modules\Administrator\Model\ModuleModel)->getItem(Factory::getApplication()->input->getInt('id')); - - $moduleData = $module->getProperties(); - unset($moduleData['xml']); - - /** @var \Joomla\Component\Config\Site\Model\ModulesModel $model */ - $model = $this->getModel(); - - // Need to add module name to the state of model - $model->getState()->set('module.name', $moduleData['module']); - - /** @var Form form */ - $this->form = $this->get('form'); - $this->positions = $this->get('positions'); - $this->item = $moduleData; - - if ($this->form) - { - $this->form->bind($moduleData); - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 4.0.0 - */ - protected function _prepareDocument() - { - // There is no menu item for this so we have to use the title from the component - $this->setDocumentTitle(Text::_('COM_CONFIG_MODULES_SETTINGS_TITLE')); - } + /** + * The module to be rendered + * + * @var array + * + * @since 3.2 + */ + public $item; + + /** + * The form object + * + * @var Form + * + * @since 3.2 + */ + public $form; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $lang = Factory::getApplication()->getLanguage(); + $lang->load('', JPATH_ADMINISTRATOR, $lang->getTag()); + $lang->load('com_modules', JPATH_ADMINISTRATOR, $lang->getTag()); + + // @todo Move and clean up + $module = (new \Joomla\Component\Modules\Administrator\Model\ModuleModel())->getItem(Factory::getApplication()->input->getInt('id')); + + $moduleData = $module->getProperties(); + unset($moduleData['xml']); + + /** @var \Joomla\Component\Config\Site\Model\ModulesModel $model */ + $model = $this->getModel(); + + // Need to add module name to the state of model + $model->getState()->set('module.name', $moduleData['module']); + + /** @var Form form */ + $this->form = $this->get('form'); + $this->positions = $this->get('positions'); + $this->item = $moduleData; + + if ($this->form) { + $this->form->bind($moduleData); + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 4.0.0 + */ + protected function _prepareDocument() + { + // There is no menu item for this so we have to use the title from the component + $this->setDocumentTitle(Text::_('COM_CONFIG_MODULES_SETTINGS_TITLE')); + } } diff --git a/code/components/com_config/src/View/Templates/HtmlView.php b/code/components/com_config/src/View/Templates/HtmlView.php index f8e6783b..7d222560 100644 --- a/code/components/com_config/src/View/Templates/HtmlView.php +++ b/code/components/com_config/src/View/Templates/HtmlView.php @@ -1,4 +1,5 @@ userIsSuperAdmin = $user->authorise('core.admin'); - - $app = Factory::getApplication(); - - $app->input->set('id', $app->getTemplate(true)->id); - - /** @var MVCFactory $factory */ - $factory = $app->bootComponent('com_templates')->getMVCFactory(); - - $view = $factory->createView('Style', 'Administrator', 'Json'); - $view->setModel($factory->createModel('Style', 'Administrator'), true); - - $view->document = $this->document; - - $json = $view->display(); - - // Execute backend controller - $serviceData = json_decode($json, true); - - // Access backend com_config - $requestController = new RequestController; - - // Execute backend controller - $configData = json_decode($requestController->getJson(), true); - - $data = array_merge($configData, $serviceData); - - /** @var Form $form */ - $form = $this->getForm(); - - if ($form) - { - $form->bind($data); - } - - $this->form = $form; - - $this->data = $serviceData; - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 4.0.0 - */ - protected function _prepareDocument() - { - $params = Factory::getApplication()->getParams(); - - // Because the application sets a default page title, we need to get it - // right from the menu item itself - $this->setDocumentTitle($params->get('page_title', '')); - - if ($params->get('menu-meta_description')) - { - $this->document->setDescription($params->get('menu-meta_description')); - } - - if ($params->get('robots')) - { - $this->document->setMetaData('robots', $params->get('robots')); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - $this->params = &$params; - } + /** + * The data to be displayed in the form + * + * @var array + * + * @since 3.2 + */ + public $item; + + /** + * The form object + * + * @var Form + * + * @since 3.2 + */ + public $form; + + /** + * Is the current user a super administrator? + * + * @var boolean + * + * @since 3.2 + */ + protected $userIsSuperAdmin; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Method to render the view. + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + $this->userIsSuperAdmin = $user->authorise('core.admin'); + + $app = Factory::getApplication(); + + $app->input->set('id', $app->getTemplate(true)->id); + + /** @var MVCFactory $factory */ + $factory = $app->bootComponent('com_templates')->getMVCFactory(); + + $view = $factory->createView('Style', 'Administrator', 'Json'); + $view->setModel($factory->createModel('Style', 'Administrator'), true); + + $view->document = $this->document; + + $json = $view->display(); + + // Execute backend controller + $serviceData = json_decode($json, true); + + // Access backend com_config + $requestController = new RequestController(); + + // Execute backend controller + $configData = json_decode($requestController->getJson(), true); + + $data = array_merge($configData, $serviceData); + + /** @var Form $form */ + $form = $this->getForm(); + + if ($form) { + $form->bind($data); + } + + $this->form = $form; + + $this->data = $serviceData; + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 4.0.0 + */ + protected function _prepareDocument() + { + $params = Factory::getApplication()->getParams(); + + // Because the application sets a default page title, we need to get it + // right from the menu item itself + $this->setDocumentTitle($params->get('page_title', '')); + + if ($params->get('menu-meta_description')) { + $this->document->setDescription($params->get('menu-meta_description')); + } + + if ($params->get('robots')) { + $this->document->setMetaData('robots', $params->get('robots')); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + $this->params = &$params; + } } diff --git a/code/components/com_config/tmpl/config/default.php b/code/components/com_config/tmpl/config/default.php index 95398edb..f7f88c3e 100644 --- a/code/components/com_config/tmpl/config/default.php +++ b/code/components/com_config/tmpl/config/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_config.config') - ->useScript('inlinehelp'); + ->useScript('form.validate') + ->useScript('com_config.config') + ->useScript('inlinehelp'); ?> params->get('show_page_heading')) : ?> - +
    -
    - -
    - - loadTemplate('site'); ?> - loadTemplate('seo'); ?> - loadTemplate('metadata'); ?> - - - - -
    - - -
    +
    + +
    + + loadTemplate('site'); ?> + loadTemplate('seo'); ?> + loadTemplate('metadata'); ?> + + + + +
    + + +
    diff --git a/code/components/com_config/tmpl/config/default_metadata.php b/code/components/com_config/tmpl/config/default_metadata.php index 4daf35c5..f361d592 100644 --- a/code/components/com_config/tmpl/config/default_metadata.php +++ b/code/components/com_config/tmpl/config/default_metadata.php @@ -1,4 +1,5 @@
    - + - form->getFieldset('metadata') as $field) : ?> -
    - label; ?> - input; ?> - description): ?> -
    - description) ?> -
    - -
    - + form->getFieldset('metadata') as $field) : ?> +
    + label; ?> + input; ?> + description) : ?> +
    + description) ?> +
    + +
    +
    diff --git a/code/components/com_config/tmpl/config/default_seo.php b/code/components/com_config/tmpl/config/default_seo.php index 0c26e03d..91139098 100644 --- a/code/components/com_config/tmpl/config/default_seo.php +++ b/code/components/com_config/tmpl/config/default_seo.php @@ -1,4 +1,5 @@
    - + - form->getFieldset('seo') as $field) : ?> -
    - label; ?> - input; ?> - description): ?> -
    - description) ?> -
    - -
    - + form->getFieldset('seo') as $field) : ?> +
    + label; ?> + input; ?> + description) : ?> +
    + description) ?> +
    + +
    +
    diff --git a/code/components/com_config/tmpl/config/default_site.php b/code/components/com_config/tmpl/config/default_site.php index 1e13994e..571b7de7 100644 --- a/code/components/com_config/tmpl/config/default_site.php +++ b/code/components/com_config/tmpl/config/default_site.php @@ -1,4 +1,5 @@
    - + - form->getFieldset('site') as $field) : ?> -
    - label; ?> - input; ?> - description): ?> -
    - description) ?> -
    - -
    - + form->getFieldset('site') as $field) : ?> +
    + label; ?> + input; ?> + description) : ?> +
    + description) ?> +
    + +
    +
    diff --git a/code/components/com_config/tmpl/modules/default.php b/code/components/com_config/tmpl/modules/default.php index fe76134c..6fc48f51 100644 --- a/code/components/com_config/tmpl/modules/default.php +++ b/code/components/com_config/tmpl/modules/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_config.modules'); + ->useScript('form.validate') + ->useScript('com_config.modules'); $editorText = false; $moduleXml = JPATH_SITE . '/modules/' . $this->item['module'] . '/' . $this->item['module'] . '.xml'; -if (File::exists($moduleXml)) -{ - $xml = simplexml_load_file($moduleXml); +if (File::exists($moduleXml)) { + $xml = simplexml_load_file($moduleXml); - if (isset($xml->customContent)) - { - $editorText = true; - } + if (isset($xml->customContent)) { + $editorText = true; + } } // If multi-language site, make language read-only -if (Multilanguage::isEnabled()) -{ - $this->form->setFieldAttribute('language', 'readonly', 'true'); +if (Multilanguage::isEnabled()) { + $this->form->setFieldAttribute('language', 'readonly', 'true'); } ?>
    -
    -
    - - -
    - - item['title']; ?> -    - - item['module']; ?> -
    -
    - -
    -
    - -
    -
    - form->getLabel('title'); ?> -
    -
    - form->getInput('title'); ?> -
    -
    -
    -
    - form->getLabel('showtitle'); ?> -
    -
    - form->getInput('showtitle'); ?> -
    -
    -
    -
    - form->getLabel('position'); ?> -
    -
    - form->getInput('position'); ?> -
    -
    - -
    - - authorise('core.edit.state', 'com_modules.module.' . $this->item['id'])) : ?> -
    -
    - form->getLabel('published'); ?> -
    -
    - form->getInput('published'); ?> -
    -
    - - -
    -
    - form->getLabel('publish_up'); ?> -
    -
    - form->getInput('publish_up'); ?> -
    -
    -
    -
    - form->getLabel('publish_down'); ?> -
    -
    - form->getInput('publish_down'); ?> -
    -
    - -
    -
    - form->getLabel('access'); ?> -
    -
    - form->getInput('access'); ?> -
    -
    -
    -
    - form->getLabel('ordering'); ?> -
    -
    - form->getInput('ordering'); ?> -
    -
    - - -
    -
    - form->getLabel('language'); ?> -
    -
    - form->getInput('language'); ?> -
    -
    - - -
    -
    - form->getLabel('note'); ?> -
    -
    - form->getInput('note'); ?> -
    -
    - -
    - -
    - loadTemplate('options'); ?> -
    - - -
    - form->getInput('content'); ?> -
    - -
    - - - - - -
    -
    - - - -
    -
    -
    +
    +
    + + +
    + + item['title']; ?> +    + + item['module']; ?> +
    +
    + +
    +
    + +
    +
    + form->getLabel('title'); ?> +
    +
    + form->getInput('title'); ?> +
    +
    +
    +
    + form->getLabel('showtitle'); ?> +
    +
    + form->getInput('showtitle'); ?> +
    +
    +
    +
    + form->getLabel('position'); ?> +
    +
    + form->getInput('position'); ?> +
    +
    + +
    + + authorise('core.edit.state', 'com_modules.module.' . $this->item['id'])) : ?> +
    +
    + form->getLabel('published'); ?> +
    +
    + form->getInput('published'); ?> +
    +
    + + +
    +
    + form->getLabel('publish_up'); ?> +
    +
    + form->getInput('publish_up'); ?> +
    +
    +
    +
    + form->getLabel('publish_down'); ?> +
    +
    + form->getInput('publish_down'); ?> +
    +
    + +
    +
    + form->getLabel('access'); ?> +
    +
    + form->getInput('access'); ?> +
    +
    +
    +
    + form->getLabel('ordering'); ?> +
    +
    + form->getInput('ordering'); ?> +
    +
    + + +
    +
    + form->getLabel('language'); ?> +
    +
    + form->getInput('language'); ?> +
    +
    + + +
    +
    + form->getLabel('note'); ?> +
    +
    + form->getInput('note'); ?> +
    +
    + +
    + +
    + loadTemplate('options'); ?> +
    + + +
    + form->getInput('content'); ?> +
    + +
    + + + + + +
    +
    + + + +
    +
    +
    diff --git a/code/components/com_config/tmpl/modules/default_options.php b/code/components/com_config/tmpl/modules/default_options.php index 28c4c730..e2232515 100644 --- a/code/components/com_config/tmpl/modules/default_options.php +++ b/code/components/com_config/tmpl/modules/default_options.php @@ -1,4 +1,5 @@ $fieldSet) : - -$label = !empty($fieldSet->label) ? $fieldSet->label : 'COM_MODULES_' . strtoupper($name) . '_FIELDSET_LABEL'; -$class = isset($fieldSet->class) && !empty($fieldSet->class) ? $fieldSet->class : ''; + $label = !empty($fieldSet->label) ? $fieldSet->label : 'COM_MODULES_' . strtoupper($name) . '_FIELDSET_LABEL'; + $class = isset($fieldSet->class) && !empty($fieldSet->class) ? $fieldSet->class : ''; -if (isset($fieldSet->description) && trim($fieldSet->description)) : -echo '

    ' . $this->escape(Text::_($fieldSet->description)) . '

    '; -endif; -?> - + if (isset($fieldSet->description) && trim($fieldSet->description)) : + echo '

    ' . $this->escape(Text::_($fieldSet->description)) . '

    '; + endif; + ?> + - + diff --git a/code/components/com_config/tmpl/templates/default.php b/code/components/com_config/tmpl/templates/default.php index 2c0a8613..c3531496 100644 --- a/code/components/com_config/tmpl/templates/default.php +++ b/code/components/com_config/tmpl/templates/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_config.templates'); + ->useScript('form.validate') + ->useScript('com_config.templates'); ?> params->get('show_page_heading')) : ?> - +
    -
    -
    -
    - loadTemplate('options'); ?> -
    -
    -
    - - - - -
    - - +
    +
    +
    + loadTemplate('options'); ?> +
    +
    +
    + + + + +
    + +
    diff --git a/code/components/com_config/tmpl/templates/default_options.php b/code/components/com_config/tmpl/templates/default_options.php index ca9f1225..a8c8be62 100644 --- a/code/components/com_config/tmpl/templates/default_options.php +++ b/code/components/com_config/tmpl/templates/default_options.php @@ -1,4 +1,5 @@ form->renderFieldset('com_config'); -} -else -{ - // Fall-back to display all in params - foreach ($fieldSets as $name => $fieldSet) - { - $label = !empty($fieldSet->label) ? $fieldSet->label : 'COM_CONFIG_' . $name . '_FIELDSET_LABEL'; - - if (isset($fieldSet->description) && trim($fieldSet->description)) - { - echo '

    ' . $this->escape(Text::_($fieldSet->description)) . '

    '; - } - - echo $this->form->renderFieldset($name); - - } +if (!empty($fieldSets['com_config'])) { + echo $this->form->renderFieldset('com_config'); +} else { + // Fall-back to display all in params + foreach ($fieldSets as $name => $fieldSet) { + $label = !empty($fieldSet->label) ? $fieldSet->label : 'COM_CONFIG_' . $name . '_FIELDSET_LABEL'; + + if (isset($fieldSet->description) && trim($fieldSet->description)) { + echo '

    ' . $this->escape(Text::_($fieldSet->description)) . '

    '; + } + + echo $this->form->renderFieldset($name); + } } diff --git a/code/components/com_contact/helpers/route.php b/code/components/com_contact/helpers/route.php index 797298b1..cf6af8f7 100644 --- a/code/components/com_contact/helpers/route.php +++ b/code/components/com_contact/helpers/route.php @@ -1,16 +1,21 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\Component\Contact\Site\Helper\RouteHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Contact Component Route Helper * diff --git a/code/components/com_contact/layouts/field/render.php b/code/components/com_contact/layouts/field/render.php index 79d3be46..3583365b 100644 --- a/code/components/com_contact/layouts/field/render.php +++ b/code/components/com_contact/layouts/field/render.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; -if (!array_key_exists('field', $displayData)) -{ - return; +if (!array_key_exists('field', $displayData)) { + return; } $field = $displayData['field']; @@ -22,26 +23,24 @@ $showLabel = $field->params->get('showlabel'); $labelClass = $field->params->get('label_render_class'); -if ($field->context == 'com_contact.mail') -{ - // Prepare the value for the contact form mail - $value = html_entity_decode($value); +if ($field->context == 'com_contact.mail') { + // Prepare the value for the contact form mail + $value = html_entity_decode($value); - echo ($showLabel ? $label . ': ' : '') . $value . "\r\n"; - return; + echo ($showLabel ? $label . ': ' : '') . $value . "\r\n"; + return; } -if (!strlen($value)) -{ - return; +if (!strlen($value)) { + return; } ?>
    - - : - + + : +
    - +
    diff --git a/code/components/com_contact/layouts/fields/render.php b/code/components/com_contact/layouts/fields/render.php index eb0ab9ec..1f4821b5 100644 --- a/code/components/com_contact/layouts/fields/render.php +++ b/code/components/com_contact/layouts/fields/render.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\Component\Fields\Administrator\Helper\FieldsHelper; // Check if we have all the data -if (!array_key_exists('item', $displayData) || !array_key_exists('context', $displayData)) -{ - return; +if (!array_key_exists('item', $displayData) || !array_key_exists('context', $displayData)) { + return; } // Setting up for display $item = $displayData['item']; -if (!$item) -{ - return; +if (!$item) { + return; } $context = $displayData['context']; -if (!$context) -{ - return; +if (!$context) { + return; } $parts = explode('.', $context); $component = $parts[0]; $fields = null; -if (array_key_exists('fields', $displayData)) -{ - $fields = $displayData['fields']; -} -else -{ - $fields = $item->jcfields ?: FieldsHelper::getFields($context, $item, true); +if (array_key_exists('fields', $displayData)) { + $fields = $displayData['fields']; +} else { + $fields = $item->jcfields ?: FieldsHelper::getFields($context, $item, true); } -if (!$fields) -{ - return; +if (!$fields) { + return; } // Check if we have mail context in first element $isMail = (reset($fields)->context == 'com_contact.mail'); -if (!$isMail) -{ - // Print the container tag - echo '
    '; +if (!$isMail) { + // Print the container tag + echo '
    '; } // Loop through the fields and print them -foreach ($fields as $field) -{ - // If the value is empty do nothing - if (!strlen($field->value) && !$isMail) - { - continue; - } - - $layout = $field->params->get('layout', 'render'); - echo FieldsHelper::render($context, 'field.' . $layout, array('field' => $field)); -} +foreach ($fields as $field) { + // If the value is empty do nothing + if (!strlen($field->value) && !$isMail) { + continue; + } -if (!$isMail) -{ - // Close the container - echo '
    '; + $layout = $field->params->get('layout', 'render'); + echo FieldsHelper::render($context, 'field.' . $layout, array('field' => $field)); } +if (!$isMail) { + // Close the container + echo '
    '; +} diff --git a/code/components/com_contact/src/Controller/ContactController.php b/code/components/com_contact/src/Controller/ContactController.php index 48b1130e..a64317be 100644 --- a/code/components/com_contact/src/Controller/ContactController.php +++ b/code/components/com_contact/src/Controller/ContactController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, array('ignore_request' => false)); - } - - /** - * Method to submit the contact form and send an email. - * - * @return boolean True on success sending the email. False on failure. - * - * @since 1.5.19 - */ - public function submit() - { - // Check for request forgeries. - $this->checkToken(); - - $app = Factory::getApplication(); - $model = $this->getModel('contact'); - $stub = $this->input->getString('id'); - $id = (int) $stub; - - // Get the data from POST - $data = $this->input->post->get('jform', array(), 'array'); - - // Get item - $model->setState('filter.published', 1); - $contact = $model->getItem($id); - - if ($contact === false) - { - $this->setMessage($model->getError(), 'error'); - - return false; - } - - // Get item params, take menu parameters into account if necessary - $active = $app->getMenu()->getActive(); - $stateParams = clone $model->getState()->get('params'); - - // If the current view is the active item and a contact view for this contact, then the menu item params take priority - if ($active && strpos($active->link, 'view=contact') && strpos($active->link, '&id=' . (int) $contact->id)) - { - // $item->params are the contact params, $temp are the menu item params - // Merge so that the menu item params take priority - $contact->params->merge($stateParams); - } - else - { - // Current view is not a single contact, so the contact params take priority here - $stateParams->merge($contact->params); - $contact->params = $stateParams; - } - - // Check if the contact form is enabled - if (!$contact->params->get('show_email_form')) - { - $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); - - return false; - } - - // Check for a valid session cookie - if ($contact->params->get('validate_session', 0)) - { - if (Factory::getSession()->getState() !== 'active') - { - $this->app->enqueueMessage(Text::_('JLIB_ENVIRONMENT_SESSION_INVALID'), 'warning'); - - // Save the data in the session. - $this->app->setUserState('com_contact.contact.data', $data); - - // Redirect back to the contact form. - $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); - - return false; - } - } - - // Contact plugins - PluginHelper::importPlugin('contact'); - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - if (!$model->validate($form, $data)) - { - $errors = $model->getErrors(); - - foreach ($errors as $error) - { - $errorMessage = $error; - - if ($error instanceof \Exception) - { - $errorMessage = $error->getMessage(); - } - - $app->enqueueMessage($errorMessage, 'error'); - } - - $app->setUserState('com_contact.contact.data', $data); - - $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); - - return false; - } - - // Validation succeeded, continue with custom handlers - $results = $this->app->triggerEvent('onValidateContact', array(&$contact, &$data)); - - foreach ($results as $result) - { - if ($result instanceof \Exception) - { - return false; - } - } - - // Passed Validation: Process the contact plugins to integrate with other applications - $this->app->triggerEvent('onSubmitContact', array(&$contact, &$data)); - - // Send the email - $sent = false; - - if (!$contact->params->get('custom_reply')) - { - $sent = $this->_sendEmail($data, $contact, $contact->params->get('show_email_copy', 0)); - } - - $msg = ''; - - // Set the success message if it was a success - if ($sent) - { - $msg = Text::_('COM_CONTACT_EMAIL_THANKS'); - } - - // Flush the data from the session - $this->app->setUserState('com_contact.contact.data', null); - - // Redirect if it is set in the parameters, otherwise redirect back to where we came from - if ($contact->params->get('redirect')) - { - $this->setRedirect($contact->params->get('redirect'), $msg); - } - else - { - $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false), $msg); - } - - return true; - } - - /** - * Method to get a model object, loading it if required. - * - * @param array $data The data to send in the email. - * @param \stdClass $contact The user information to send the email to - * @param boolean $emailCopyToSender True to send a copy of the email to the user. - * - * @return boolean True on success sending the email, false on failure. - * - * @since 1.6.4 - */ - private function _sendEmail($data, $contact, $emailCopyToSender) - { - $app = $this->app; - - if ($contact->email_to == '' && $contact->user_id != 0) - { - $contact_user = User::getInstance($contact->user_id); - $contact->email_to = $contact_user->get('email'); - } - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'name' => $data['contact_name'], - 'contactname' => $contact->name, - 'email' => PunycodeHelper::emailToPunycode($data['contact_email']), - 'subject' => $data['contact_subject'], - 'body' => stripslashes($data['contact_message']), - 'url' => Uri::base(), - 'customfields' => '' - ]; - - // Load the custom fields - if (!empty($data['com_fields']) && $fields = FieldsHelper::getFields('com_contact.mail', $contact, true, $data['com_fields'])) - { - $output = FieldsHelper::render( - 'com_contact.mail', - 'fields.render', - array( - 'context' => 'com_contact.mail', - 'item' => $contact, - 'fields' => $fields, - ) - ); - - if ($output) - { - $templateData['customfields'] = $output; - } - } - - try - { - $mailer = new MailTemplate('com_contact.mail', $app->getLanguage()->getTag()); - $mailer->addRecipient($contact->email_to); - $mailer->setReplyTo($templateData['email'], $templateData['name']); - $mailer->addTemplateData($templateData); - $sent = $mailer->send(); - - // If we are supposed to copy the sender, do so. - if ($emailCopyToSender == true && !empty($data['contact_email_copy'])) - { - $mailer = new MailTemplate('com_contact.mail.copy', $app->getLanguage()->getTag()); - $mailer->addRecipient($templateData['email']); - $mailer->setReplyTo($templateData['email'], $templateData['name']); - $mailer->addTemplateData($templateData); - $sent = $mailer->send(); - } - } - catch (MailDisabledException | phpMailerException $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $sent = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $sent = false; - } - } - - return $sent; - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = array()) - { - if ($categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('catid'), 'int')) - { - $user = $this->app->getIdentity(); - - // If the category has been passed in the data or URL check it. - return $user->authorise('core.create', 'com_contact.category.' . $categoryId); - } - - // In the absence of better information, revert to the component permissions. - return parent::allowAdd(); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key; default is id. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - - if (!$recordId) - { - return false; - } - - // Need to do a lookup from the model. - $record = $this->getModel()->getItem($recordId); - $categoryId = (int) $record->catid; - - if ($categoryId) - { - $user = $this->app->getIdentity(); - - // The category has been set. Check the category permissions. - if ($user->authorise('core.edit', $this->option . '.category.' . $categoryId)) - { - return true; - } - - // Fallback on edit.own. - if ($user->authorise('core.edit.own', $this->option . '.category.' . $categoryId)) - { - return ($record->created_by === $user->id); - } - - return false; - } - - // Since there is no asset tracking, revert to the component permissions. - return parent::allowEdit($data, $key); - } - - /** - * Method to cancel an edit. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 4.0.0 - */ - public function cancel($key = null) - { - $result = parent::cancel($key); - - $this->setRedirect(Route::_($this->getReturnPage(), false)); - - return $result; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = 0, $urlVar = 'id') - { - // Need to override the parent method completely. - $tmpl = $this->input->get('tmpl'); - - $append = ''; - - // Setup redirect info. - if ($tmpl) - { - $append .= '&tmpl=' . $tmpl; - } - - $append .= '&layout=edit'; - - $append .= '&' . $urlVar . '=' . (int) $recordId; - - $itemId = $this->input->getInt('Itemid'); - $return = $this->getReturnPage(); - $catId = $this->input->getInt('catid'); - - if ($itemId) - { - $append .= '&Itemid=' . $itemId; - } - - if ($catId) - { - $append .= '&catid=' . $catId; - } - - if ($return) - { - $append .= '&return=' . base64_encode($return); - } - - return $append; - } - - /** - * Get the return URL. - * - * If a "return" variable has been passed in the request - * - * @return string The return URL. - * - * @since 4.0.0 - */ - protected function getReturnPage() - { - $return = $this->input->get('return', null, 'base64'); - - if (empty($return) || !Uri::isInternal(base64_decode($return))) - { - return Uri::base(); - } - - return base64_decode($return); - } + use VersionableControllerTrait; + + /** + * The URL view item variable. + * + * @var string + * @since 4.0.0 + */ + protected $view_item = 'form'; + + /** + * The URL view list variable. + * + * @var string + * @since 4.0.0 + */ + protected $view_list = 'categories'; + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6.4 + */ + public function getModel($name = 'form', $prefix = '', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, array('ignore_request' => false)); + } + + /** + * Method to submit the contact form and send an email. + * + * @return boolean True on success sending the email. False on failure. + * + * @since 1.5.19 + */ + public function submit() + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + $model = $this->getModel('contact'); + $stub = $this->input->getString('id'); + $id = (int) $stub; + + // Get the data from POST + $data = $this->input->post->get('jform', array(), 'array'); + + // Get item + $model->setState('filter.published', 1); + $contact = $model->getItem($id); + + if ($contact === false) { + $this->setMessage($model->getError(), 'error'); + + return false; + } + + // Get item params, take menu parameters into account if necessary + $active = $app->getMenu()->getActive(); + $stateParams = clone $model->getState()->get('params'); + + // If the current view is the active item and a contact view for this contact, then the menu item params take priority + if ($active && strpos($active->link, 'view=contact') && strpos($active->link, '&id=' . (int) $contact->id)) { + // $item->params are the contact params, $temp are the menu item params + // Merge so that the menu item params take priority + $contact->params->merge($stateParams); + } else { + // Current view is not a single contact, so the contact params take priority here + $stateParams->merge($contact->params); + $contact->params = $stateParams; + } + + // Check if the contact form is enabled + if (!$contact->params->get('show_email_form')) { + $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); + + return false; + } + + // Check for a valid session cookie + if ($contact->params->get('validate_session', 0)) { + if (Factory::getSession()->getState() !== 'active') { + $this->app->enqueueMessage(Text::_('JLIB_ENVIRONMENT_SESSION_INVALID'), 'warning'); + + // Save the data in the session. + $this->app->setUserState('com_contact.contact.data', $data); + + // Redirect back to the contact form. + $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); + + return false; + } + } + + // Contact plugins + PluginHelper::importPlugin('contact'); + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + if (!$model->validate($form, $data)) { + $errors = $model->getErrors(); + + foreach ($errors as $error) { + $errorMessage = $error; + + if ($error instanceof \Exception) { + $errorMessage = $error->getMessage(); + } + + $app->enqueueMessage($errorMessage, 'error'); + } + + $app->setUserState('com_contact.contact.data', $data); + + $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); + + return false; + } + + // Validation succeeded, continue with custom handlers + $results = $this->app->triggerEvent('onValidateContact', array(&$contact, &$data)); + + foreach ($results as $result) { + if ($result instanceof \Exception) { + return false; + } + } + + // Passed Validation: Process the contact plugins to integrate with other applications + $this->app->triggerEvent('onSubmitContact', array(&$contact, &$data)); + + // Send the email + $sent = false; + + if (!$contact->params->get('custom_reply')) { + $sent = $this->_sendEmail($data, $contact, $contact->params->get('show_email_copy', 0)); + } + + $msg = ''; + + // Set the success message if it was a success + if ($sent) { + $msg = Text::_('COM_CONTACT_EMAIL_THANKS'); + } + + // Flush the data from the session + $this->app->setUserState('com_contact.contact.data', null); + + // Redirect if it is set in the parameters, otherwise redirect back to where we came from + if ($contact->params->get('redirect')) { + $this->setRedirect($contact->params->get('redirect'), $msg); + } else { + $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false), $msg); + } + + return true; + } + + /** + * Method to get a model object, loading it if required. + * + * @param array $data The data to send in the email. + * @param \stdClass $contact The user information to send the email to + * @param boolean $emailCopyToSender True to send a copy of the email to the user. + * + * @return boolean True on success sending the email, false on failure. + * + * @since 1.6.4 + */ + private function _sendEmail($data, $contact, $emailCopyToSender) + { + $app = $this->app; + + if ($contact->email_to == '' && $contact->user_id != 0) { + $contact_user = User::getInstance($contact->user_id); + $contact->email_to = $contact_user->get('email'); + } + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'name' => $data['contact_name'], + 'contactname' => $contact->name, + 'email' => PunycodeHelper::emailToPunycode($data['contact_email']), + 'subject' => $data['contact_subject'], + 'body' => stripslashes($data['contact_message']), + 'url' => Uri::base(), + 'customfields' => '' + ]; + + // Load the custom fields + if (!empty($data['com_fields']) && $fields = FieldsHelper::getFields('com_contact.mail', $contact, true, $data['com_fields'])) { + $output = FieldsHelper::render( + 'com_contact.mail', + 'fields.render', + array( + 'context' => 'com_contact.mail', + 'item' => $contact, + 'fields' => $fields, + ) + ); + + if ($output) { + $templateData['customfields'] = $output; + } + } + + try { + $mailer = new MailTemplate('com_contact.mail', $app->getLanguage()->getTag()); + $mailer->addRecipient($contact->email_to); + $mailer->setReplyTo($templateData['email'], $templateData['name']); + $mailer->addTemplateData($templateData); + $sent = $mailer->send(); + + // If we are supposed to copy the sender, do so. + if ($emailCopyToSender == true && !empty($data['contact_email_copy'])) { + $mailer = new MailTemplate('com_contact.mail.copy', $app->getLanguage()->getTag()); + $mailer->addRecipient($templateData['email']); + $mailer->setReplyTo($templateData['email'], $templateData['name']); + $mailer->addTemplateData($templateData); + $sent = $mailer->send(); + } + } catch (MailDisabledException | phpMailerException $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $sent = false; + } catch (\RuntimeException $exception) { + $this->app->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $sent = false; + } + } + + return $sent; + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + if ($categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('catid'), 'int')) { + $user = $this->app->getIdentity(); + + // If the category has been passed in the data or URL check it. + return $user->authorise('core.create', 'com_contact.category.' . $categoryId); + } + + // In the absence of better information, revert to the component permissions. + return parent::allowAdd(); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key; default is id. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + + if (!$recordId) { + return false; + } + + // Need to do a lookup from the model. + $record = $this->getModel()->getItem($recordId); + $categoryId = (int) $record->catid; + + if ($categoryId) { + $user = $this->app->getIdentity(); + + // The category has been set. Check the category permissions. + if ($user->authorise('core.edit', $this->option . '.category.' . $categoryId)) { + return true; + } + + // Fallback on edit.own. + if ($user->authorise('core.edit.own', $this->option . '.category.' . $categoryId)) { + return ($record->created_by === $user->id); + } + + return false; + } + + // Since there is no asset tracking, revert to the component permissions. + return parent::allowEdit($data, $key); + } + + /** + * Method to cancel an edit. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 4.0.0 + */ + public function cancel($key = null) + { + $result = parent::cancel($key); + + $this->setRedirect(Route::_($this->getReturnPage(), false)); + + return $result; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = 0, $urlVar = 'id') + { + // Need to override the parent method completely. + $tmpl = $this->input->get('tmpl'); + + $append = ''; + + // Setup redirect info. + if ($tmpl) { + $append .= '&tmpl=' . $tmpl; + } + + $append .= '&layout=edit'; + + $append .= '&' . $urlVar . '=' . (int) $recordId; + + $itemId = $this->input->getInt('Itemid'); + $return = $this->getReturnPage(); + $catId = $this->input->getInt('catid'); + + if ($itemId) { + $append .= '&Itemid=' . $itemId; + } + + if ($catId) { + $append .= '&catid=' . $catId; + } + + if ($return) { + $append .= '&return=' . base64_encode($return); + } + + return $append; + } + + /** + * Get the return URL. + * + * If a "return" variable has been passed in the request + * + * @return string The return URL. + * + * @since 4.0.0 + */ + protected function getReturnPage() + { + $return = $this->input->get('return', null, 'base64'); + + if (empty($return) || !Uri::isInternal(base64_decode($return))) { + return Uri::base(); + } + + return base64_decode($return); + } } diff --git a/code/components/com_contact/src/Controller/DisplayController.php b/code/components/com_contact/src/Controller/DisplayController.php index 6c1788b5..171a4d8e 100644 --- a/code/components/com_contact/src/Controller/DisplayController.php +++ b/code/components/com_contact/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input; + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + // Contact frontpage Editor contacts proxying. + $input = Factory::getApplication()->input; - if ($input->get('view') === 'contacts' && $input->get('layout') === 'modal') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } + if ($input->get('view') === 'contacts' && $input->get('layout') === 'modal') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } - parent::__construct($config, $factory, $app, $input); - } + parent::__construct($config, $factory, $app, $input); + } - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached - * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. - * - * @return DisplayController This object to support chaining. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = array()) - { - if (Factory::getApplication()->getUserState('com_contact.contact.data') === null) - { - $cachable = true; - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return DisplayController This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + if ($this->app->getUserState('com_contact.contact.data') === null) { + $cachable = true; + } - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'categories'); - $this->input->set('view', $vName); + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'categories'); + $this->input->set('view', $vName); - if ($this->app->getIdentity()->get('id')) - { - $cachable = false; - } + if ($this->app->getIdentity()->get('id')) { + $cachable = false; + } - $safeurlparams = array('catid' => 'INT', 'id' => 'INT', 'cid' => 'ARRAY', 'year' => 'INT', 'month' => 'INT', - 'limit' => 'UINT', 'limitstart' => 'UINT', 'showall' => 'INT', 'return' => 'BASE64', 'filter' => 'STRING', - 'filter_order' => 'CMD', 'filter_order_Dir' => 'CMD', 'filter-search' => 'STRING', 'print' => 'BOOLEAN', - 'lang' => 'CMD'); + $safeurlparams = array('catid' => 'INT', 'id' => 'INT', 'cid' => 'ARRAY', 'year' => 'INT', 'month' => 'INT', + 'limit' => 'UINT', 'limitstart' => 'UINT', 'showall' => 'INT', 'return' => 'BASE64', 'filter' => 'STRING', + 'filter_order' => 'CMD', 'filter_order_Dir' => 'CMD', 'filter-search' => 'STRING', 'print' => 'BOOLEAN', + 'lang' => 'CMD'); - parent::display($cachable, $safeurlparams); + parent::display($cachable, $safeurlparams); - return $this; - } + return $this; + } } diff --git a/code/components/com_contact/src/Dispatcher/Dispatcher.php b/code/components/com_contact/src/Dispatcher/Dispatcher.php index 537798c9..68f01e24 100644 --- a/code/components/com_contact/src/Dispatcher/Dispatcher.php +++ b/code/components/com_contact/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->get('view') === 'contacts' && $this->input->get('layout') === 'modal') - { - if (!$this->app->getIdentity()->authorise('core.create', 'com_contact')) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'warning'); - - return; - } - - $this->app->getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); - } - - parent::dispatch(); - } + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since 4.0.0 + */ + public function dispatch() + { + if ($this->input->get('view') === 'contacts' && $this->input->get('layout') === 'modal') { + if (!$this->app->getIdentity()->authorise('core.create', 'com_contact')) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'warning'); + + return; + } + + $this->app->getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); + } + + parent::dispatch(); + } } diff --git a/code/components/com_contact/src/Helper/AssociationHelper.php b/code/components/com_contact/src/Helper/AssociationHelper.php index 14e014ee..3b7a8361 100644 --- a/code/components/com_contact/src/Helper/AssociationHelper.php +++ b/code/components/com_contact/src/Helper/AssociationHelper.php @@ -1,4 +1,5 @@ input; - $view = $view ?? $jinput->get('view'); - $id = empty($id) ? $jinput->getInt('id') : $id; - - if ($view === 'contact') - { - if ($id) - { - $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $id); + /** + * Method to get the associations for a given item + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 3.0 + */ + public static function getAssociations($id = 0, $view = null) + { + $jinput = Factory::getApplication()->input; + $view = $view ?? $jinput->get('view'); + $id = empty($id) ? $jinput->getInt('id') : $id; - $return = array(); + if ($view === 'contact') { + if ($id) { + $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $id); - foreach ($associations as $tag => $item) - { - $return[$tag] = RouteHelper::getContactRoute($item->id, (int) $item->catid, $item->language); - } + $return = array(); - return $return; - } - } + foreach ($associations as $tag => $item) { + $return[$tag] = RouteHelper::getContactRoute($item->id, (int) $item->catid, $item->language); + } - if ($view === 'category' || $view === 'categories') - { - return self::getCategoryAssociations($id, 'com_contact'); - } + return $return; + } + } - return array(); + if ($view === 'category' || $view === 'categories') { + return self::getCategoryAssociations($id, 'com_contact'); + } - } + return array(); + } } diff --git a/code/components/com_contact/src/Helper/RouteHelper.php b/code/components/com_contact/src/Helper/RouteHelper.php index 10937c0d..073dc4d4 100644 --- a/code/components/com_contact/src/Helper/RouteHelper.php +++ b/code/components/com_contact/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ 1) - { - $link .= '&catid=' . $catid; - } + if ($catid > 1) { + $link .= '&catid=' . $catid; + } - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } - return $link; - } + return $link; + } - /** - * Get the URL route for a contact category from a contact category ID and language - * - * @param mixed $catid The id of the contact's category either an integer id or an instance of CategoryNode - * @param mixed $language The id of the language being used. - * - * @return string The link to the contact - * - * @since 1.5 - */ - public static function getCategoryRoute($catid, $language = 0) - { - if ($catid instanceof CategoryNode) - { - $id = $catid->id; - } - else - { - $id = (int) $catid; - } + /** + * Get the URL route for a contact category from a contact category ID and language + * + * @param mixed $catid The id of the contact's category either an integer id or an instance of CategoryNode + * @param mixed $language The id of the language being used. + * + * @return string The link to the contact + * + * @since 1.5 + */ + public static function getCategoryRoute($catid, $language = 0) + { + if ($catid instanceof CategoryNode) { + $id = $catid->id; + } else { + $id = (int) $catid; + } - if ($id < 1) - { - $link = ''; - } - else - { - // Create the link - $link = 'index.php?option=com_contact&view=category&id=' . $id; + if ($id < 1) { + $link = ''; + } else { + // Create the link + $link = 'index.php?option=com_contact&view=category&id=' . $id; - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } + } - return $link; - } + return $link; + } } diff --git a/code/components/com_contact/src/Model/CategoriesModel.php b/code/components/com_contact/src/Model/CategoriesModel.php index d5ee42aa..c90405f1 100644 --- a/code/components/com_contact/src/Model/CategoriesModel.php +++ b/code/components/com_contact/src/Model/CategoriesModel.php @@ -1,4 +1,5 @@ setState('filter.extension', $this->_extension); - - // Get the parent id if defined. - $parentId = $app->input->getInt('id'); - $this->setState('filter.parentId', $parentId); - - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('filter.published', 1); - $this->setState('filter.access', true); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.parentId'); - - return parent::getStoreId($id); - } - - /** - * Redefine the function and add some properties to make the styling easier - * - * @return mixed An array of data items on success, false on failure. - */ - public function getItems() - { - if ($this->_items === null) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_items_cat', 1) || !$params->get('show_empty_categories_cat', 0); - $categories = Categories::getInstance('Contact', $options); - $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); - - if (is_object($this->_parent)) - { - $this->_items = $this->_parent->getChildren(); - } - else - { - $this->_items = false; - } - } - - return $this->_items; - } - - /** - * Gets the id of the parent category for the selected list of categories - * - * @return integer The id of the parent category - * - * @since 1.6.0 - */ - public function getParent() - { - if (!is_object($this->_parent)) - { - $this->getItems(); - } - - return $this->_parent; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_contact.categories'; + + /** + * The category context (allows other extensions to derived from this model). + * + * @var string + */ + protected $_extension = 'com_contact'; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + private $_parent = null; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + private $_items = null; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $this->setState('filter.extension', $this->_extension); + + // Get the parent id if defined. + $parentId = $app->input->getInt('id'); + $this->setState('filter.parentId', $parentId); + + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('filter.published', 1); + $this->setState('filter.access', true); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.parentId'); + + return parent::getStoreId($id); + } + + /** + * Redefine the function and add some properties to make the styling easier + * + * @return mixed An array of data items on success, false on failure. + */ + public function getItems() + { + if ($this->_items === null) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_items_cat', 1) || !$params->get('show_empty_categories_cat', 0); + $categories = Categories::getInstance('Contact', $options); + $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); + + if (is_object($this->_parent)) { + $this->_items = $this->_parent->getChildren(); + } else { + $this->_items = false; + } + } + + return $this->_items; + } + + /** + * Gets the id of the parent category for the selected list of categories + * + * @return integer The id of the parent category + * + * @since 1.6.0 + */ + public function getParent() + { + if (!is_object($this->_parent)) { + $this->getItems(); + } + + return $this->_parent; + } } diff --git a/code/components/com_contact/src/Model/CategoryModel.php b/code/components/com_contact/src/Model/CategoryModel.php index 830b165f..648d033c 100644 --- a/code/components/com_contact/src/Model/CategoryModel.php +++ b/code/components/com_contact/src/Model/CategoryModel.php @@ -1,4 +1,5 @@ _params)) - { - $item->params = new Registry($item->params); - } - - // Some contexts may not use tags data at all, so we allow callers to disable loading tag data - if ($this->getState('load_tags', true)) - { - $this->tags = new TagsHelper; - $this->tags->getItemTags('com_contact.contact', $item->id); - } - } - - return $items; - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query->select($this->getState('list.select', 'a.*')) - ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') - ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') - /** - * @todo: we actually should be doing it but it's wrong this way - * . ' CASE WHEN CHAR_LENGTH(a.alias) THEN CONCAT_WS(\':\', a.id, a.alias) ELSE a.id END as slug, ' - * . ' CASE WHEN CHAR_LENGTH(c.alias) THEN CONCAT_WS(\':\', c.id, c.alias) ELSE c.id END AS catslug '); - */ - ->from($db->quoteName('#__contact_details', 'a')) - ->leftJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') - ->whereIn($db->quoteName('a.access'), $groups); - - // Filter by category. - if ($categoryId = $this->getState('category.id')) - { - $query->where($db->quoteName('a.catid') . ' = :acatid') - ->whereIn($db->quoteName('c.access'), $groups); - $query->bind(':acatid', $categoryId, ParameterType::INTEGER); - } - - // Join over the users for the author and modified_by names. - $query->select("CASE WHEN a.created_by_alias > ' ' THEN a.created_by_alias ELSE ua.name END AS author") - ->select('ua.email AS author_email') - ->leftJoin($db->quoteName('#__users', 'ua') . ' ON ua.id = a.created_by') - ->leftJoin($db->quoteName('#__users', 'uam') . ' ON uam.id = a.modified_by'); - - // Filter by state - $state = $this->getState('filter.published'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('a.published') . ' = :published'); - $query->bind(':published', $state, ParameterType::INTEGER); - } - else - { - $query->whereIn($db->quoteName('c.published'), [0,1,2]); - } - - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - if ($this->getState('filter.publish_date')) - { - $query->where('(' . $db->quoteName('a.publish_up') - . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)' - ) - ->where('(' . $db->quoteName('a.publish_down') - . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)' - ) - ->bind(':publish_up', $nowDate) - ->bind(':publish_down', $nowDate); - } - - // Filter by search in title - $search = $this->getState('list.filter'); - - if (!empty($search)) - { - $search = '%' . trim($search) . '%'; - $query->where($db->quoteName('a.name') . ' LIKE :name '); - $query->bind(':name', $search); - } - - // Filter on the language. - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - // Set sortname ordering if selected - if ($this->getState('list.ordering') === 'sortname') - { - $query->order($db->escape('a.sortname1') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))) - ->order($db->escape('a.sortname2') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))) - ->order($db->escape('a.sortname3') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - } - elseif ($this->getState('list.ordering') === 'featuredordering') - { - $query->order($db->escape('a.featured') . ' DESC') - ->order($db->escape('a.ordering') . ' ASC'); - } - else - { - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - } - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = null, $direction = null) - { - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_contact'); - - // Get list ordering default from the parameters - if ($menu = $app->getMenu()->getActive()) - { - $menuParams = $menu->getParams(); - } - else - { - $menuParams = new Registry; - } - - $mergedParams = clone $params; - $mergedParams->merge($menuParams); - - // List state information - $format = $app->input->getWord('format'); - - if ($format === 'feed') - { - $limit = $app->get('feed_limit'); - } - else - { - $limit = $app->getUserStateFromRequest( - 'com_contact.category.list.limit', - 'limit', - $mergedParams->get('contacts_display_num', $app->get('list_limit')), - 'uint' - ); - } - - $this->setState('list.limit', $limit); - - $limitstart = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $limitstart); - - // Optional filter text - $itemid = $app->input->get('Itemid', 0, 'int'); - $search = $app->getUserStateFromRequest('com_contact.category.list.' . $itemid . '.filter-search', 'filter-search', '', 'string'); - $this->setState('list.filter', $search); - - $orderCol = $app->input->get('filter_order', $mergedParams->get('initial_sort', 'ordering')); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->input->get('filter_order_Dir', 'ASC'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $id = $app->input->get('id', 0, 'int'); - $this->setState('category.id', $id); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) - { - // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); - - // Filter by start and end dates. - $this->setState('filter.publish_date', true); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to get category data for the current category - * - * @return object The category object - * - * @since 1.5 - */ - public function getCategory() - { - if (!is_object($this->_item)) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); - $categories = Categories::getInstance('Contact', $options); - $this->_item = $categories->get($this->getState('category.id', 'root')); - - if (is_object($this->_item)) - { - $this->_children = $this->_item->getChildren(); - $this->_parent = false; - - if ($this->_item->getParent()) - { - $this->_parent = $this->_item->getParent(); - } - - $this->_rightsibling = $this->_item->getSibling(); - $this->_leftsibling = $this->_item->getSibling(false); - } - else - { - $this->_children = false; - $this->_parent = false; - } - } - - return $this->_item; - } - - /** - * Get the parent category. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function getParent() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_parent; - } - - /** - * Get the sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getLeftSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_leftsibling; - } - - /** - * Get the sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getRightSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_rightsibling; - } - - /** - * Get the child categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getChildren() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_children; - } - - /** - * Generate column expression for slug or catslug. - * - * @param \Joomla\Database\DatabaseQuery $query Current query instance. - * @param string $id Column id name. - * @param string $alias Column alias name. - * - * @return string - * - * @since 4.0.0 - */ - private function getSlugColumn($query, $id, $alias) - { - return 'CASE WHEN ' - . $query->charLength($alias, '!=', '0') - . ' THEN ' - . $query->concatenate(array($query->castAsChar($id), $alias), ':') - . ' ELSE ' - . $query->castAsChar($id) . ' END'; - } - - /** - * Increment the hit counter for the category. - * - * @param integer $pk Optional primary key of the category to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - * - * @since 3.2 - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); - - $table = Table::getInstance('Category'); - $table->hit($pk); - } - - return true; - } + /** + * Category item data + * + * @var CategoryNode + */ + protected $_item; + + /** + * Array of contacts in the category + * + * @var \stdClass[] + */ + protected $_articles; + + /** + * Category left and right of this one + * + * @var CategoryNode[]|null + */ + protected $_siblings; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + protected $_children; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + protected $_parent; + + /** + * The category that applies. + * + * @var object + */ + protected $_category; + + /** + * The list of other contact categories. + * + * @var array + */ + protected $_categories; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'con_position', 'a.con_position', + 'suburb', 'a.suburb', + 'state', 'a.state', + 'country', 'a.country', + 'ordering', 'a.ordering', + 'sortname', + 'sortname1', 'a.sortname1', + 'sortname2', 'a.sortname2', + 'sortname3', 'a.sortname3', + 'featuredordering', 'a.featured' + ); + } + + parent::__construct($config); + } + + /** + * Method to get a list of items. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + // Invoke the parent getItems method to get the main list + $items = parent::getItems(); + + if ($items === false) { + return false; + } + + $taggedItems = []; + + // Convert the params field into an object, saving original in _params + foreach ($items as $item) { + if (!isset($this->_params)) { + $item->params = new Registry($item->params); + } + + // Some contexts may not use tags data at all, so we allow callers to disable loading tag data + if ($this->getState('load_tags', true)) { + $item->tags = new TagsHelper(); + $taggedItems[$item->id] = $item; + } + } + + // Load tags of all items. + if ($taggedItems) { + $tagsHelper = new TagsHelper(); + $itemIds = \array_keys($taggedItems); + + foreach ($tagsHelper->getMultipleItemTags('com_contact.contact', $itemIds) as $id => $tags) { + $taggedItems[$id]->tags->itemTags = $tags; + } + } + + return $items; + } + + /** + * Method to build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + // Create a new query object. + $db = $this->getDatabase(); + + /** @var \Joomla\Database\DatabaseQuery $query */ + $query = $db->getQuery(true); + + $query->select($this->getState('list.select', 'a.*')) + ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') + ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') + /** + * @todo: we actually should be doing it but it's wrong this way + * . ' CASE WHEN CHAR_LENGTH(a.alias) THEN CONCAT_WS(\':\', a.id, a.alias) ELSE a.id END as slug, ' + * . ' CASE WHEN CHAR_LENGTH(c.alias) THEN CONCAT_WS(\':\', c.id, c.alias) ELSE c.id END AS catslug '); + */ + ->from($db->quoteName('#__contact_details', 'a')) + ->leftJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') + ->whereIn($db->quoteName('a.access'), $groups); + + // Filter by category. + if ($categoryId = $this->getState('category.id')) { + $query->where($db->quoteName('a.catid') . ' = :acatid') + ->whereIn($db->quoteName('c.access'), $groups); + $query->bind(':acatid', $categoryId, ParameterType::INTEGER); + } + + // Join over the users for the author and modified_by names. + $query->select("CASE WHEN a.created_by_alias > ' ' THEN a.created_by_alias ELSE ua.name END AS author") + ->select('ua.email AS author_email') + ->leftJoin($db->quoteName('#__users', 'ua') . ' ON ua.id = a.created_by') + ->leftJoin($db->quoteName('#__users', 'uam') . ' ON uam.id = a.modified_by'); + + // Filter by state + $state = $this->getState('filter.published'); + + if (is_numeric($state)) { + $query->where($db->quoteName('a.published') . ' = :published'); + $query->bind(':published', $state, ParameterType::INTEGER); + } else { + $query->whereIn($db->quoteName('c.published'), [0,1,2]); + } + + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + if ($this->getState('filter.publish_date')) { + $query->where('(' . $db->quoteName('a.publish_up') + . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)') + ->where('(' . $db->quoteName('a.publish_down') + . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)') + ->bind(':publish_up', $nowDate) + ->bind(':publish_down', $nowDate); + } + + // Filter by search in title + $search = $this->getState('list.filter'); + + if (!empty($search)) { + $search = '%' . trim($search) . '%'; + $query->where($db->quoteName('a.name') . ' LIKE :name '); + $query->bind(':name', $search); + } + + // Filter on the language. + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + // Set sortname ordering if selected + if ($this->getState('list.ordering') === 'sortname') { + $query->order($db->escape('a.sortname1') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))) + ->order($db->escape('a.sortname2') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))) + ->order($db->escape('a.sortname3') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + } elseif ($this->getState('list.ordering') === 'featuredordering') { + $query->order($db->escape('a.featured') . ' DESC') + ->order($db->escape('a.ordering') . ' ASC'); + } else { + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + } + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_contact'); + + // Get list ordering default from the parameters + if ($menu = $app->getMenu()->getActive()) { + $menuParams = $menu->getParams(); + } else { + $menuParams = new Registry(); + } + + $mergedParams = clone $params; + $mergedParams->merge($menuParams); + + // List state information + $format = $app->input->getWord('format'); + + if ($format === 'feed') { + $limit = $app->get('feed_limit'); + } else { + $limit = $app->getUserStateFromRequest( + 'com_contact.category.list.limit', + 'limit', + $mergedParams->get('contacts_display_num', $app->get('list_limit')), + 'uint' + ); + } + + $this->setState('list.limit', $limit); + + $limitstart = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $limitstart); + + // Optional filter text + $itemid = $app->input->get('Itemid', 0, 'int'); + $search = $app->getUserStateFromRequest('com_contact.category.list.' . $itemid . '.filter-search', 'filter-search', '', 'string'); + $this->setState('list.filter', $search); + + $orderCol = $app->input->get('filter_order', $mergedParams->get('initial_sort', 'ordering')); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->input->get('filter_order_Dir', 'ASC'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $id = $app->input->get('id', 0, 'int'); + $this->setState('category.id', $id); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) { + // Limit to published for people who can't edit or edit.state. + $this->setState('filter.published', 1); + + // Filter by start and end dates. + $this->setState('filter.publish_date', true); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to get category data for the current category + * + * @return object The category object + * + * @since 1.5 + */ + public function getCategory() + { + if (!is_object($this->_item)) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); + $categories = Categories::getInstance('Contact', $options); + $this->_item = $categories->get($this->getState('category.id', 'root')); + + if (is_object($this->_item)) { + $this->_children = $this->_item->getChildren(); + $this->_parent = false; + + if ($this->_item->getParent()) { + $this->_parent = $this->_item->getParent(); + } + + $this->_rightsibling = $this->_item->getSibling(); + $this->_leftsibling = $this->_item->getSibling(false); + } else { + $this->_children = false; + $this->_parent = false; + } + } + + return $this->_item; + } + + /** + * Get the parent category. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function getParent() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_parent; + } + + /** + * Get the sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getLeftSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_leftsibling; + } + + /** + * Get the sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getRightSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_rightsibling; + } + + /** + * Get the child categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getChildren() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_children; + } + + /** + * Generate column expression for slug or catslug. + * + * @param \Joomla\Database\DatabaseQuery $query Current query instance. + * @param string $id Column id name. + * @param string $alias Column alias name. + * + * @return string + * + * @since 4.0.0 + */ + private function getSlugColumn($query, $id, $alias) + { + return 'CASE WHEN ' + . $query->charLength($alias, '!=', '0') + . ' THEN ' + . $query->concatenate(array($query->castAsChar($id), $alias), ':') + . ' ELSE ' + . $query->castAsChar($id) . ' END'; + } + + /** + * Increment the hit counter for the category. + * + * @param integer $pk Optional primary key of the category to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + * + * @since 3.2 + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); + + $table = Table::getInstance('Category'); + $table->hit($pk); + } + + return true; + } } diff --git a/code/components/com_contact/src/Model/ContactModel.php b/code/components/com_contact/src/Model/ContactModel.php index 0a865d49..13790344 100644 --- a/code/components/com_contact/src/Model/ContactModel.php +++ b/code/components/com_contact/src/Model/ContactModel.php @@ -1,4 +1,5 @@ get(SiteApplication::class); - - if (Factory::getApplication()->isClient('api')) - { - // @todo: remove this - $app->loadLanguage(); - $this->setState('contact.id', Factory::getApplication()->input->post->getInt('id')); - } - else - { - $this->setState('contact.id', $app->input->getInt('id')); - } - - $this->setState('params', $app->getParams()); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) - { - $this->setState('filter.published', 1); - $this->setState('filter.archived', 2); - } - } - - /** - * Method to get the contact form. - * The base form is loaded from XML and then an event is fired - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - $form = $this->loadForm('com_contact.contact', 'contact', array('control' => 'jform', 'load_data' => true)); - - if (empty($form)) - { - return false; - } - - $temp = clone $this->getState('params'); - $contact = $this->_item[$this->getState('contact.id')]; - $active = Factory::getContainer()->get(SiteApplication::class)->getMenu()->getActive(); - - if ($active) - { - // If the current view is the active item and a contact view for this contact, then the menu item params take priority - if (strpos($active->link, 'view=contact') && strpos($active->link, '&id=' . (int) $contact->id)) - { - // $contact->params are the contact params, $temp are the menu item params - // Merge so that the menu item params take priority - $contact->params->merge($temp); - } - else - { - // Current view is not a single contact, so the contact params take priority here - // Merge the menu item params with the contact params so that the contact params take priority - $temp->merge($contact->params); - $contact->params = $temp; - } - } - else - { - // Merge so that contact params take priority - $temp->merge($contact->params); - $contact->params = $temp; - } - - if (!$contact->params->get('show_email_copy', 0)) - { - $form->removeField('contact_email_copy'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 1.6.2 - */ - protected function loadFormData() - { - $data = (array) Factory::getApplication()->getUserState('com_contact.contact.data', array()); - - if (empty($data['language']) && Multilanguage::isEnabled()) - { - $data['language'] = Factory::getLanguage()->getTag(); - } - - // Add contact catid to contact form data, so fields plugin can work properly - if (empty($data['catid'])) - { - $data['catid'] = $this->getItem()->catid; - } - - $this->preprocessData('com_contact.contact', $data); - - return $data; - } - - /** - * Gets a contact - * - * @param integer $pk Id for the contact - * - * @return mixed Object or null - * - * @since 1.6.0 - */ - public function getItem($pk = null) - { - $pk = $pk ?: (int) $this->getState('contact.id'); - - if ($this->_item === null) - { - $this->_item = array(); - } - - if (!isset($this->_item[$pk])) - { - try - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query->select($this->getState('item.select', 'a.*')) - ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') - ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') - ->from($db->quoteName('#__contact_details', 'a')) - - // Join on category table. - ->select('c.title AS category_title, c.alias AS category_alias, c.access AS category_access') - ->leftJoin($db->quoteName('#__categories', 'c'), 'c.id = a.catid') - - // Join over the categories to get parent category titles - ->select('parent.title AS parent_title, parent.id AS parent_id, parent.path AS parent_route, parent.alias AS parent_alias') - ->leftJoin($db->quoteName('#__categories', 'parent'), 'parent.id = c.parent_id') - ->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - // Filter by published state. - $published = $this->getState('filter.published'); - $archived = $this->getState('filter.archived'); - - if (is_numeric($published)) - { - $queryString = $db->quoteName('a.published') . ' = :published'; - - if ($archived !== null) - { - $queryString = '(' . $queryString . ' OR ' . $db->quoteName('a.published') . ' = :archived)'; - $query->bind(':archived', $archived, ParameterType::INTEGER); - } - - $query->where($queryString) - ->where('(' . $db->quoteName('a.publish_up') . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)') - ->where('(' . $db->quoteName('a.publish_down') . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)') - ->bind(':published', $published, ParameterType::INTEGER) - ->bind(':publish_up', $nowDate) - ->bind(':publish_down', $nowDate); - } - - $db->setQuery($query); - $data = $db->loadObject(); - - if (empty($data)) - { - throw new \Exception(Text::_('COM_CONTACT_ERROR_CONTACT_NOT_FOUND'), 404); - } - - // Check for published state if filter set. - if ((is_numeric($published) || is_numeric($archived)) && (($data->published != $published) && ($data->published != $archived))) - { - throw new \Exception(Text::_('COM_CONTACT_ERROR_CONTACT_NOT_FOUND'), 404); - } - - /** - * In case some entity params have been set to "use global", those are - * represented as an empty string and must be "overridden" by merging - * the component and / or menu params here. - */ - $registry = new Registry($data->params); - - $data->params = clone $this->getState('params'); - $data->params->merge($registry); - - $registry = new Registry($data->metadata); - $data->metadata = $registry; - - // Some contexts may not use tags data at all, so we allow callers to disable loading tag data - if ($this->getState('load_tags', true)) - { - $data->tags = new TagsHelper; - $data->tags->getItemTags('com_contact.contact', $data->id); - } - - // Compute access permissions. - if (($access = $this->getState('filter.access'))) - { - // If the access filter has been set, we already know this user can view. - $data->params->set('access-view', true); - } - else - { - // If no access filter is set, the layout takes some responsibility for display of limited information. - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - if ($data->catid == 0 || $data->category_access === null) - { - $data->params->set('access-view', in_array($data->access, $groups)); - } - else - { - $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); - } - } - - $this->_item[$pk] = $data; - } - catch (\Exception $e) - { - if ($e->getCode() == 404) - { - // Need to go through the error handler to allow Redirect to work. - throw $e; - } - else - { - $this->setError($e); - $this->_item[$pk] = false; - } - } - } - - if ($this->_item[$pk]) - { - $this->buildContactExtendedData($this->_item[$pk]); - } - - return $this->_item[$pk]; - } - - /** - * Load extended data (profile, articles) for a contact - * - * @param object $contact The contact object - * - * @return void - */ - protected function buildContactExtendedData($contact) - { - $db = $this->getDbo(); - $nowDate = Factory::getDate()->toSql(); - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - $published = $this->getState('filter.published'); - $query = $db->getQuery(true); - - // If we are showing a contact list, then the contact parameters take priority - // So merge the contact parameters with the merged parameters - if ($this->getState('params')->get('show_contact_list')) - { - $this->getState('params')->merge($contact->params); - } - - // Get the com_content articles by the linked user - if ((int) $contact->user_id && $this->getState('params')->get('show_articles')) - { - $query->select('a.id') - ->select('a.title') - ->select('a.state') - ->select('a.access') - ->select('a.catid') - ->select('a.created') - ->select('a.language') - ->select('a.publish_up') - ->select('a.introtext') - ->select('a.images') - ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') - ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') - ->from($db->quoteName('#__content', 'a')) - ->leftJoin($db->quoteName('#__categories', 'c') . ' ON a.catid = c.id') - ->where($db->quoteName('a.created_by') . ' = :created_by') - ->whereIn($db->quoteName('a.access'), $groups) - ->bind(':created_by', $contact->user_id, ParameterType::INTEGER) - ->order('a.publish_up DESC'); - - // Filter per language if plugin published - if (Multilanguage::isEnabled()) - { - $language = [Factory::getLanguage()->getTag(), $db->quote('*')]; - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - - if (is_numeric($published)) - { - $query->where('a.state IN (1,2)') - ->where('(' . $db->quoteName('a.publish_up') . ' IS NULL' . - ' OR ' . $db->quoteName('a.publish_up') . ' <= :now1)' - ) - ->where('(' . $db->quoteName('a.publish_down') . ' IS NULL' . - ' OR ' . $db->quoteName('a.publish_down') . ' >= :now2)' - ) - ->bind([':now1', ':now2'], $nowDate); - } - - // Number of articles to display from config/menu params - $articles_display_num = $this->getState('params')->get('articles_display_num', 10); - - // Use contact setting? - if ($articles_display_num === 'use_contact') - { - $articles_display_num = $contact->params->get('articles_display_num', 10); - - // Use global? - if ((string) $articles_display_num === '') - { - $articles_display_num = ComponentHelper::getParams('com_contact')->get('articles_display_num', 10); - } - } - - $query->setLimit((int) $articles_display_num); - $db->setQuery($query); - $contact->articles = $db->loadObjectList(); - } - else - { - $contact->articles = null; - } - - // Get the profile information for the linked user - $userModel = $this->bootComponent('com_users')->getMVCFactory() - ->createModel('User', 'Administrator', ['ignore_request' => true]); - $data = $userModel->getItem((int) $contact->user_id); - - PluginHelper::importPlugin('user'); - - // Get the form. - Form::addFormPath(JPATH_SITE . '/components/com_users/forms'); - - $form = Form::getInstance('com_users.profile', 'profile'); - - // Trigger the form preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareForm', array($form, $data)); - - // Trigger the data preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.profile', $data)); - - // Load the data into the form after the plugins have operated. - $form->bind($data); - $contact->profile = $form; - } - - /** - * Generate column expression for slug or catslug. - * - * @param QueryInterface $query Current query instance. - * @param string $id Column id name. - * @param string $alias Column alias name. - * - * @return string - * - * @since 4.0.0 - */ - private function getSlugColumn($query, $id, $alias) - { - return 'CASE WHEN ' - . $query->charLength($alias, '!=', '0') - . ' THEN ' - . $query->concatenate(array($query->castAsChar($id), $alias), ':') - . ' ELSE ' - . $query->castAsChar($id) . ' END'; - } - - /** - * Increment the hit counter for the contact. - * - * @param integer $pk Optional primary key of the contact to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - * - * @since 3.0 - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = $pk ?: (int) $this->getState('contact.id'); - - $table = $this->getTable('Contact'); - $table->hit($pk); - } - - return true; - } + /** + * The name of the view for a single item + * + * @var string + * @since 1.6 + */ + protected $view_item = 'contact'; + + /** + * A loaded item + * + * @var \stdClass + * @since 1.6 + */ + protected $_item = null; + + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_contact.contact'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + /** @var SiteApplication $app */ + $app = Factory::getContainer()->get(SiteApplication::class); + + if (Factory::getApplication()->isClient('api')) { + // @todo: remove this + $app->loadLanguage(); + $this->setState('contact.id', Factory::getApplication()->input->post->getInt('id')); + } else { + $this->setState('contact.id', $app->input->getInt('id')); + } + + $this->setState('params', $app->getParams()); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) { + $this->setState('filter.published', 1); + $this->setState('filter.archived', 2); + } + } + + /** + * Method to get the contact form. + * The base form is loaded from XML and then an event is fired + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $form = $this->loadForm('com_contact.contact', 'contact', array('control' => 'jform', 'load_data' => true)); + + if (empty($form)) { + return false; + } + + $temp = clone $this->getState('params'); + $contact = $this->_item[$this->getState('contact.id')]; + $active = Factory::getContainer()->get(SiteApplication::class)->getMenu()->getActive(); + + if ($active) { + // If the current view is the active item and a contact view for this contact, then the menu item params take priority + if (strpos($active->link, 'view=contact') && strpos($active->link, '&id=' . (int) $contact->id)) { + // $contact->params are the contact params, $temp are the menu item params + // Merge so that the menu item params take priority + $contact->params->merge($temp); + } else { + // Current view is not a single contact, so the contact params take priority here + // Merge the menu item params with the contact params so that the contact params take priority + $temp->merge($contact->params); + $contact->params = $temp; + } + } else { + // Merge so that contact params take priority + $temp->merge($contact->params); + $contact->params = $temp; + } + + if (!$contact->params->get('show_email_copy', 0)) { + $form->removeField('contact_email_copy'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 1.6.2 + */ + protected function loadFormData() + { + $data = (array) Factory::getApplication()->getUserState('com_contact.contact.data', array()); + + if (empty($data['language']) && Multilanguage::isEnabled()) { + $data['language'] = Factory::getLanguage()->getTag(); + } + + // Add contact catid to contact form data, so fields plugin can work properly + if (empty($data['catid'])) { + $data['catid'] = $this->getItem()->catid; + } + + $this->preprocessData('com_contact.contact', $data); + + return $data; + } + + /** + * Gets a contact + * + * @param integer $pk Id for the contact + * + * @return mixed Object or null + * + * @since 1.6.0 + */ + public function getItem($pk = null) + { + $pk = $pk ?: (int) $this->getState('contact.id'); + + if ($this->_item === null) { + $this->_item = array(); + } + + if (!isset($this->_item[$pk])) { + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($this->getState('item.select', 'a.*')) + ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') + ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') + ->from($db->quoteName('#__contact_details', 'a')) + + // Join on category table. + ->select('c.title AS category_title, c.alias AS category_alias, c.access AS category_access') + ->leftJoin($db->quoteName('#__categories', 'c'), 'c.id = a.catid') + + // Join over the categories to get parent category titles + ->select('parent.title AS parent_title, parent.id AS parent_id, parent.path AS parent_route, parent.alias AS parent_alias') + ->leftJoin($db->quoteName('#__categories', 'parent'), 'parent.id = c.parent_id') + ->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + // Filter by published state. + $published = $this->getState('filter.published'); + $archived = $this->getState('filter.archived'); + + if (is_numeric($published)) { + $queryString = $db->quoteName('a.published') . ' = :published'; + + if ($archived !== null) { + $queryString = '(' . $queryString . ' OR ' . $db->quoteName('a.published') . ' = :archived)'; + $query->bind(':archived', $archived, ParameterType::INTEGER); + } + + $query->where($queryString) + ->where('(' . $db->quoteName('a.publish_up') . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)') + ->where('(' . $db->quoteName('a.publish_down') . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)') + ->bind(':published', $published, ParameterType::INTEGER) + ->bind(':publish_up', $nowDate) + ->bind(':publish_down', $nowDate); + } + + $db->setQuery($query); + $data = $db->loadObject(); + + if (empty($data)) { + throw new \Exception(Text::_('COM_CONTACT_ERROR_CONTACT_NOT_FOUND'), 404); + } + + // Check for published state if filter set. + if ((is_numeric($published) || is_numeric($archived)) && (($data->published != $published) && ($data->published != $archived))) { + throw new \Exception(Text::_('COM_CONTACT_ERROR_CONTACT_NOT_FOUND'), 404); + } + + /** + * In case some entity params have been set to "use global", those are + * represented as an empty string and must be "overridden" by merging + * the component and / or menu params here. + */ + $registry = new Registry($data->params); + + $data->params = clone $this->getState('params'); + $data->params->merge($registry); + + $registry = new Registry($data->metadata); + $data->metadata = $registry; + + // Some contexts may not use tags data at all, so we allow callers to disable loading tag data + if ($this->getState('load_tags', true)) { + $data->tags = new TagsHelper(); + $data->tags->getItemTags('com_contact.contact', $data->id); + } + + // Compute access permissions. + if (($access = $this->getState('filter.access'))) { + // If the access filter has been set, we already know this user can view. + $data->params->set('access-view', true); + } else { + // If no access filter is set, the layout takes some responsibility for display of limited information. + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + if ($data->catid == 0 || $data->category_access === null) { + $data->params->set('access-view', in_array($data->access, $groups)); + } else { + $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); + } + } + + $this->_item[$pk] = $data; + } catch (\Exception $e) { + if ($e->getCode() == 404) { + // Need to go through the error handler to allow Redirect to work. + throw $e; + } else { + $this->setError($e); + $this->_item[$pk] = false; + } + } + } + + if ($this->_item[$pk]) { + $this->buildContactExtendedData($this->_item[$pk]); + } + + return $this->_item[$pk]; + } + + /** + * Load extended data (profile, articles) for a contact + * + * @param object $contact The contact object + * + * @return void + */ + protected function buildContactExtendedData($contact) + { + $db = $this->getDatabase(); + $nowDate = Factory::getDate()->toSql(); + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + $published = $this->getState('filter.published'); + $query = $db->getQuery(true); + + // If we are showing a contact list, then the contact parameters take priority + // So merge the contact parameters with the merged parameters + if ($this->getState('params')->get('show_contact_list')) { + $this->getState('params')->merge($contact->params); + } + + // Get the com_content articles by the linked user + if ((int) $contact->user_id && $this->getState('params')->get('show_articles')) { + $query->select('a.id') + ->select('a.title') + ->select('a.state') + ->select('a.access') + ->select('a.catid') + ->select('a.created') + ->select('a.language') + ->select('a.publish_up') + ->select('a.introtext') + ->select('a.images') + ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') + ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') + ->from($db->quoteName('#__content', 'a')) + ->leftJoin($db->quoteName('#__categories', 'c') . ' ON a.catid = c.id') + ->where($db->quoteName('a.created_by') . ' = :created_by') + ->whereIn($db->quoteName('a.access'), $groups) + ->bind(':created_by', $contact->user_id, ParameterType::INTEGER) + ->order('a.publish_up DESC'); + + // Filter per language if plugin published + if (Multilanguage::isEnabled()) { + $language = [Factory::getLanguage()->getTag(), $db->quote('*')]; + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } + + if (is_numeric($published)) { + $query->where('a.state IN (1,2)') + ->where('(' . $db->quoteName('a.publish_up') . ' IS NULL' . + ' OR ' . $db->quoteName('a.publish_up') . ' <= :now1)') + ->where('(' . $db->quoteName('a.publish_down') . ' IS NULL' . + ' OR ' . $db->quoteName('a.publish_down') . ' >= :now2)') + ->bind([':now1', ':now2'], $nowDate); + } + + // Number of articles to display from config/menu params + $articles_display_num = $this->getState('params')->get('articles_display_num', 10); + + // Use contact setting? + if ($articles_display_num === 'use_contact') { + $articles_display_num = $contact->params->get('articles_display_num', 10); + + // Use global? + if ((string) $articles_display_num === '') { + $articles_display_num = ComponentHelper::getParams('com_contact')->get('articles_display_num', 10); + } + } + + $query->setLimit((int) $articles_display_num); + $db->setQuery($query); + $contact->articles = $db->loadObjectList(); + } else { + $contact->articles = null; + } + + // Get the profile information for the linked user + $userModel = $this->bootComponent('com_users')->getMVCFactory() + ->createModel('User', 'Administrator', ['ignore_request' => true]); + $data = $userModel->getItem((int) $contact->user_id); + + PluginHelper::importPlugin('user'); + + // Get the form. + Form::addFormPath(JPATH_SITE . '/components/com_users/forms'); + + $form = Form::getInstance('com_users.profile', 'profile'); + + // Trigger the form preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareForm', array($form, $data)); + + // Trigger the data preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.profile', $data)); + + // Load the data into the form after the plugins have operated. + $form->bind($data); + $contact->profile = $form; + } + + /** + * Generate column expression for slug or catslug. + * + * @param QueryInterface $query Current query instance. + * @param string $id Column id name. + * @param string $alias Column alias name. + * + * @return string + * + * @since 4.0.0 + */ + private function getSlugColumn($query, $id, $alias) + { + return 'CASE WHEN ' + . $query->charLength($alias, '!=', '0') + . ' THEN ' + . $query->concatenate(array($query->castAsChar($id), $alias), ':') + . ' ELSE ' + . $query->castAsChar($id) . ' END'; + } + + /** + * Increment the hit counter for the contact. + * + * @param integer $pk Optional primary key of the contact to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + * + * @since 3.0 + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = $pk ?: (int) $this->getState('contact.id'); + + $table = $this->getTable('Contact'); + $table->hit($pk); + } + + return true; + } } diff --git a/code/components/com_contact/src/Model/FeaturedModel.php b/code/components/com_contact/src/Model/FeaturedModel.php index bde2e530..1918ca92 100644 --- a/code/components/com_contact/src/Model/FeaturedModel.php +++ b/code/components/com_contact/src/Model/FeaturedModel.php @@ -1,4 +1,5 @@ _params)) - { - $item->params = new Registry($item->params); - } - } - - return $items; - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select required fields from the categories. - $query->select($this->getState('list.select', 'a.*')) - ->from($db->quoteName('#__contact_details', 'a')) - ->where($db->quoteName('a.featured') . ' = 1') - ->whereIn($db->quoteName('a.access'), $groups) - ->innerJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') - ->whereIn($db->quoteName('c.access'), $groups); - - // Filter by category. - if ($categoryId = $this->getState('category.id')) - { - $query->where($db->quoteName('a.catid') . ' = :catid'); - $query->bind(':catid', $categoryId, ParameterType::INTEGER); - } - - $query->select('c.published as cat_published, c.published AS parents_published') - ->where('c.published = 1'); - - // Filter by state - $state = $this->getState('filter.published'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('a.published') . ' = :published'); - $query->bind(':published', $state, ParameterType::INTEGER); - - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - $query->where('(' . $db->quoteName('a.publish_up') . - ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)' - ) - ->where('(' . $db->quoteName('a.publish_down') . - ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)' - ) - ->bind(':publish_up', $nowDate) - ->bind(':publish_down', $nowDate); - } - - // Filter by search in title - $search = $this->getState('list.filter'); - - // Filter by search in title - if (!empty($search)) - { - $search = '%' . trim($search) . '%'; - $query->where($db->quoteName('a.name') . ' LIKE :name '); - $query->bind(':name', $search); - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = null, $direction = null) - { - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_contact'); - - // List state information - $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); - $this->setState('list.limit', $limit); - - $limitstart = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $limitstart); - - // Optional filter text - $this->setState('list.filter', $app->input->getString('filter-search')); - - $orderCol = $app->input->get('filter_order', 'ordering'); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->input->get('filter_order_Dir', 'ASC'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) - { - // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); - - // Filter by start and end dates. - $this->setState('filter.publish_date', true); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - // Load the parameters. - $this->setState('params', $params); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'con_position', 'a.con_position', + 'suburb', 'a.suburb', + 'state', 'a.state', + 'country', 'a.country', + 'ordering', 'a.ordering', + ); + } + + parent::__construct($config); + } + + /** + * Method to get a list of items. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + // Invoke the parent getItems method to get the main list + $items = parent::getItems(); + + // Convert the params field into an object, saving original in _params + for ($i = 0, $n = count($items); $i < $n; $i++) { + $item = &$items[$i]; + + if (!isset($this->_params)) { + $item->params = new Registry($item->params); + } + } + + return $items; + } + + /** + * Method to build an SQL query to load the list data. + * + * @return string An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select required fields from the categories. + $query->select($this->getState('list.select', 'a.*')) + ->from($db->quoteName('#__contact_details', 'a')) + ->where($db->quoteName('a.featured') . ' = 1') + ->whereIn($db->quoteName('a.access'), $groups) + ->innerJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') + ->whereIn($db->quoteName('c.access'), $groups); + + // Filter by category. + if ($categoryId = $this->getState('category.id')) { + $query->where($db->quoteName('a.catid') . ' = :catid'); + $query->bind(':catid', $categoryId, ParameterType::INTEGER); + } + + $query->select('c.published as cat_published, c.published AS parents_published') + ->where('c.published = 1'); + + // Filter by state + $state = $this->getState('filter.published'); + + if (is_numeric($state)) { + $query->where($db->quoteName('a.published') . ' = :published'); + $query->bind(':published', $state, ParameterType::INTEGER); + + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + $query->where('(' . $db->quoteName('a.publish_up') . + ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)') + ->where('(' . $db->quoteName('a.publish_down') . + ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)') + ->bind(':publish_up', $nowDate) + ->bind(':publish_down', $nowDate); + } + + // Filter by search in title + $search = $this->getState('list.filter'); + + // Filter by search in title + if (!empty($search)) { + $search = '%' . trim($search) . '%'; + $query->where($db->quoteName('a.name') . ' LIKE :name '); + $query->bind(':name', $search); + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_contact'); + + // List state information + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); + $this->setState('list.limit', $limit); + + $limitstart = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $limitstart); + + // Optional filter text + $this->setState('list.filter', $app->input->getString('filter-search')); + + $orderCol = $app->input->get('filter_order', 'ordering'); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->input->get('filter_order_Dir', 'ASC'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) { + // Limit to published for people who can't edit or edit.state. + $this->setState('filter.published', 1); + + // Filter by start and end dates. + $this->setState('filter.publish_date', true); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + // Load the parameters. + $this->setState('params', $params); + } } diff --git a/code/components/com_contact/src/Model/FormModel.php b/code/components/com_contact/src/Model/FormModel.php index 8afefd64..6dd0319a 100644 --- a/code/components/com_contact/src/Model/FormModel.php +++ b/code/components/com_contact/src/Model/FormModel.php @@ -1,4 +1,5 @@ getState('contact.id') && Associations::isEnabled()) - { - $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $id); - - // Make fields read only - if (!empty($associations)) - { - $form->setFieldAttribute('language', 'readonly', 'true'); - $form->setFieldAttribute('language', 'filter', 'unset'); - } - } - - return $form; - } - - /** - * Method to get contact data. - * - * @param integer $itemId The id of the contact. - * - * @return mixed Contact item data object on success, false on failure. - * - * @throws Exception - * - * @since 4.0.0 - */ - public function getItem($itemId = null) - { - $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('contact.id'); - - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - try - { - if (!$table->load($itemId)) - { - return false; - } - } - catch (Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage()); - - return false; - } - - $properties = $table->getProperties(); - $value = ArrayHelper::toObject($properties, \Joomla\CMS\Object\CMSObject::class); - - // Convert field to Registry. - $value->params = new Registry($value->params); - - // Convert the metadata field to an array. - $registry = new Registry($value->metadata); - $value->metadata = $registry->toArray(); - - if ($itemId) - { - $value->tags = new TagsHelper; - $value->tags->getTagIds($value->id, 'com_contact.contact'); - $value->metadata['tags'] = $value->tags; - } - - return $value; - } - - /** - * Get the return URL. - * - * @return string The return URL. - * - * @since 4.0.0 - */ - public function getReturnPage() - { - return base64_encode($this->getState('return_page', '')); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 4.0.0 - * - * @throws Exception - */ - public function save($data) - { - // Associations are not edited in frontend ATM so we have to inherit them - if (Associations::isEnabled() && !empty($data['id']) - && $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $data['id'])) - { - foreach ($associations as $tag => $associated) - { - $associations[$tag] = (int) $associated->id; - } - - $data['associations'] = $associations; - } - - return parent::save($data); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 4.0.0 - * - * @throws Exception - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load state from the request. - $pk = $app->input->getInt('id'); - $this->setState('contact.id', $pk); - - $this->setState('contact.catid', $app->input->getInt('catid')); - - $return = $app->input->get('return', '', 'base64'); - $this->setState('return_page', base64_decode($return)); - - // Load the parameters. - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('layout', $app->input->getString('layout')); - } - - /** - * Allows preprocessing of the JForm object. - * - * @param Form $form The form object - * @param array $data The data to be merged into the form object - * @param string $group The plugin group to be executed - * - * @return void - * - * @since 4.0.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'contact') - { - if (!Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'hidden'); - $form->setFieldAttribute('language', 'default', '*'); - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return bool|Table A Table object - * - * @since 4.0.0 - - * @throws Exception - */ - public function getTable($name = 'Contact', $prefix = 'Administrator', $options = array()) - { - return parent::getTable($name, $prefix, $options); - } + /** + * Model typeAlias string. Used for version history. + * + * @var string + * + * @since 4.0.0 + */ + public $typeAlias = 'com_contact.contact'; + + /** + * Name of the form + * + * @var string + * + * @since 4.0.0 + */ + protected $formName = 'form'; + + /** + * Method to get the row form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + $form = parent::getForm($data, $loadData); + + // Prevent messing with article language and category when editing existing contact with associations + if ($id = $this->getState('contact.id') && Associations::isEnabled()) { + $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $id); + + // Make fields read only + if (!empty($associations)) { + $form->setFieldAttribute('language', 'readonly', 'true'); + $form->setFieldAttribute('language', 'filter', 'unset'); + } + } + + return $form; + } + + /** + * Method to get contact data. + * + * @param integer $itemId The id of the contact. + * + * @return mixed Contact item data object on success, false on failure. + * + * @throws Exception + * + * @since 4.0.0 + */ + public function getItem($itemId = null) + { + $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('contact.id'); + + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + try { + if (!$table->load($itemId)) { + return false; + } + } catch (Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage()); + + return false; + } + + $properties = $table->getProperties(); + $value = ArrayHelper::toObject($properties, \Joomla\CMS\Object\CMSObject::class); + + // Convert field to Registry. + $value->params = new Registry($value->params); + + // Convert the metadata field to an array. + $registry = new Registry($value->metadata); + $value->metadata = $registry->toArray(); + + if ($itemId) { + $value->tags = new TagsHelper(); + $value->tags->getTagIds($value->id, 'com_contact.contact'); + $value->metadata['tags'] = $value->tags; + } + + return $value; + } + + /** + * Get the return URL. + * + * @return string The return URL. + * + * @since 4.0.0 + */ + public function getReturnPage() + { + return base64_encode($this->getState('return_page', '')); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 4.0.0 + * + * @throws Exception + */ + public function save($data) + { + // Associations are not edited in frontend ATM so we have to inherit them + if ( + Associations::isEnabled() && !empty($data['id']) + && $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $data['id']) + ) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } + + $data['associations'] = $associations; + } + + return parent::save($data); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 4.0.0 + * + * @throws Exception + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load state from the request. + $pk = $app->input->getInt('id'); + $this->setState('contact.id', $pk); + + $this->setState('contact.catid', $app->input->getInt('catid')); + + $return = $app->input->get('return', '', 'base64'); + $this->setState('return_page', base64_decode($return)); + + // Load the parameters. + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('layout', $app->input->getString('layout')); + } + + /** + * Allows preprocessing of the JForm object. + * + * @param Form $form The form object + * @param array $data The data to be merged into the form object + * @param string $group The plugin group to be executed + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'contact') + { + if (!Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'hidden'); + $form->setFieldAttribute('language', 'default', '*'); + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return bool|Table A Table object + * + * @since 4.0.0 + + * @throws Exception + */ + public function getTable($name = 'Contact', $prefix = 'Administrator', $options = array()) + { + return parent::getTable($name, $prefix, $options); + } } diff --git a/code/components/com_contact/src/Rule/ContactEmailMessageRule.php b/code/components/com_contact/src/Rule/ContactEmailMessageRule.php index 3bbfb5ea..f935ead4 100644 --- a/code/components/com_contact/src/Rule/ContactEmailMessageRule.php +++ b/code/components/com_contact/src/Rule/ContactEmailMessageRule.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $params = ComponentHelper::getParams('com_contact'); - $banned = $params->get('banned_text'); + /** + * Method to test a message for banned words + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $params = ComponentHelper::getParams('com_contact'); + $banned = $params->get('banned_text'); - if ($banned) - { - foreach (explode(';', $banned) as $item) - { - if ($item != '' && StringHelper::stristr($value, $item) !== false) - { - return false; - } - } - } + if ($banned) { + foreach (explode(';', $banned) as $item) { + if ($item != '' && StringHelper::stristr($value, $item) !== false) { + return false; + } + } + } - return true; - } + return true; + } } diff --git a/code/components/com_contact/src/Rule/ContactEmailRule.php b/code/components/com_contact/src/Rule/ContactEmailRule.php index f9bdf9d9..2ed1e808 100644 --- a/code/components/com_contact/src/Rule/ContactEmailRule.php +++ b/code/components/com_contact/src/Rule/ContactEmailRule.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - if (!parent::test($element, $value, $group, $input, $form)) - { - return false; - } - - $params = ComponentHelper::getParams('com_contact'); - $banned = $params->get('banned_email'); - - if ($banned) - { - foreach (explode(';', $banned) as $item) - { - if ($item != '' && StringHelper::stristr($value, $item) !== false) - { - return false; - } - } - } - - return true; - } + /** + * Method to test for banned email addresses + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + if (!parent::test($element, $value, $group, $input, $form)) { + return false; + } + + $params = ComponentHelper::getParams('com_contact'); + $banned = $params->get('banned_email'); + + if ($banned) { + foreach (explode(';', $banned) as $item) { + if ($item != '' && StringHelper::stristr($value, $item) !== false) { + return false; + } + } + } + + return true; + } } diff --git a/code/components/com_contact/src/Rule/ContactEmailSubjectRule.php b/code/components/com_contact/src/Rule/ContactEmailSubjectRule.php index cc3ea224..ff6dad3e 100644 --- a/code/components/com_contact/src/Rule/ContactEmailSubjectRule.php +++ b/code/components/com_contact/src/Rule/ContactEmailSubjectRule.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $params = ComponentHelper::getParams('com_contact'); - $banned = $params->get('banned_subject'); + /** + * Method to test for a banned subject + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $params = ComponentHelper::getParams('com_contact'); + $banned = $params->get('banned_subject'); - if ($banned) - { - foreach (explode(';', $banned) as $item) - { - if ($item != '' && StringHelper::stristr($value, $item) !== false) - { - return false; - } - } - } + if ($banned) { + foreach (explode(';', $banned) as $item) { + if ($item != '' && StringHelper::stristr($value, $item) !== false) { + return false; + } + } + } - return true; - } + return true; + } } diff --git a/code/components/com_contact/src/Service/Category.php b/code/components/com_contact/src/Service/Category.php index 0b45769e..16581c92 100644 --- a/code/components/com_contact/src/Service/Category.php +++ b/code/components/com_contact/src/Service/Category.php @@ -1,4 +1,5 @@ categoryFactory = $categoryFactory; - $this->db = $db; - - $params = ComponentHelper::getParams('com_contact'); - $this->noIDs = (bool) $params->get('sef_ids'); - $categories = new RouterViewConfiguration('categories'); - $categories->setKey('id'); - $this->registerView($categories); - $category = new RouterViewConfiguration('category'); - $category->setKey('id')->setParent($categories, 'catid')->setNestable(); - $this->registerView($category); - $contact = new RouterViewConfiguration('contact'); - $contact->setKey('id')->setParent($category, 'catid'); - $this->registerView($contact); - $this->registerView(new RouterViewConfiguration('featured')); - $form = new RouterViewConfiguration('form'); - $form->setKey('id'); - $this->registerView($form); - - parent::__construct($app, $menu); - - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategorySegment($id, $query) - { - $category = $this->getCategories()->get($id); - - if ($category) - { - $path = array_reverse($category->getPath(), true); - $path[0] = '1:root'; - - if ($this->noIDs) - { - foreach ($path as &$segment) - { - list($id, $segment) = explode(':', $segment, 2); - } - } - - return $path; - } - - return array(); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategoriesSegment($id, $query) - { - return $this->getCategorySegment($id, $query); - } - - /** - * Method to get the segment(s) for a contact - * - * @param string $id ID of the contact to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getContactSegment($id, $query) - { - if (!strpos($id, ':')) - { - $id = (int) $id; - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('alias')) - ->from($this->db->quoteName('#__contact_details')) - ->where($this->db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - $id .= ':' . $this->db->loadResult(); - } - - if ($this->noIDs) - { - list($void, $segment) = explode(':', $id, 2); - - return array($void => $segment); - } - - return array((int) $id => $id); - } - - /** - * Method to get the segment(s) for a form - * - * @param string $id ID of the contact form to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - * - * @since 4.0.0 - */ - public function getFormSegment($id, $query) - { - return $this->getContactSegment($id, $query); - } - - /** - * Method to get the id for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoryId($segment, $query) - { - if (isset($query['id'])) - { - $category = $this->getCategories(['access' => false])->get($query['id']); - - if ($category) - { - foreach ($category->getChildren() as $child) - { - if ($this->noIDs) - { - if ($child->alias == $segment) - { - return $child->id; - } - } - else - { - if ($child->id == (int) $segment) - { - return $child->id; - } - } - } - } - } - - return false; - } - - /** - * Method to get the segment(s) for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoriesId($segment, $query) - { - return $this->getCategoryId($segment, $query); - } - - /** - * Method to get the segment(s) for a contact - * - * @param string $segment Segment of the contact to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getContactId($segment, $query) - { - if ($this->noIDs) - { - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__contact_details')) - ->where( - [ - $this->db->quoteName('alias') . ' = :alias', - $this->db->quoteName('catid') . ' = :catid', - ] - ) - ->bind(':alias', $segment) - ->bind(':catid', $query['id'], ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - return (int) $this->db->loadResult(); - } - - return (int) $segment; - } - - /** - * Method to get categories from cache - * - * @param array $options The options for retrieving categories - * - * @return CategoryInterface The object containing categories - * - * @since 4.0.0 - */ - private function getCategories(array $options = []): CategoryInterface - { - $key = serialize($options); - - if (!isset($this->categoryCache[$key])) - { - $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); - } - - return $this->categoryCache[$key]; - } + /** + * Flag to remove IDs + * + * @var boolean + */ + protected $noIDs = false; + + /** + * The category factory + * + * @var CategoryFactoryInterface + * + * @since 4.0.0 + */ + private $categoryFactory; + + /** + * The category cache + * + * @var array + * + * @since 4.0.0 + */ + private $categoryCache = []; + + /** + * The db + * + * @var DatabaseInterface + * + * @since 4.0.0 + */ + private $db; + + /** + * Content Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + */ + public function __construct(SiteApplication $app, AbstractMenu $menu, CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) + { + $this->categoryFactory = $categoryFactory; + $this->db = $db; + + $params = ComponentHelper::getParams('com_contact'); + $this->noIDs = (bool) $params->get('sef_ids'); + $categories = new RouterViewConfiguration('categories'); + $categories->setKey('id'); + $this->registerView($categories); + $category = new RouterViewConfiguration('category'); + $category->setKey('id')->setParent($categories, 'catid')->setNestable(); + $this->registerView($category); + $contact = new RouterViewConfiguration('contact'); + $contact->setKey('id')->setParent($category, 'catid'); + $this->registerView($contact); + $this->registerView(new RouterViewConfiguration('featured')); + $form = new RouterViewConfiguration('form'); + $form->setKey('id'); + $this->registerView($form); + + parent::__construct($app, $menu); + + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategorySegment($id, $query) + { + $category = $this->getCategories()->get($id); + + if ($category) { + $path = array_reverse($category->getPath(), true); + $path[0] = '1:root'; + + if ($this->noIDs) { + foreach ($path as &$segment) { + list($id, $segment) = explode(':', $segment, 2); + } + } + + return $path; + } + + return array(); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategoriesSegment($id, $query) + { + return $this->getCategorySegment($id, $query); + } + + /** + * Method to get the segment(s) for a contact + * + * @param string $id ID of the contact to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getContactSegment($id, $query) + { + if (!strpos($id, ':')) { + $id = (int) $id; + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('alias')) + ->from($this->db->quoteName('#__contact_details')) + ->where($this->db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + $id .= ':' . $this->db->loadResult(); + } + + if ($this->noIDs) { + list($void, $segment) = explode(':', $id, 2); + + return array($void => $segment); + } + + return array((int) $id => $id); + } + + /** + * Method to get the segment(s) for a form + * + * @param string $id ID of the contact form to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + * + * @since 4.0.0 + */ + public function getFormSegment($id, $query) + { + return $this->getContactSegment($id, $query); + } + + /** + * Method to get the id for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoryId($segment, $query) + { + if (isset($query['id'])) { + $category = $this->getCategories(['access' => false])->get($query['id']); + + if ($category) { + foreach ($category->getChildren() as $child) { + if ($this->noIDs) { + if ($child->alias == $segment) { + return $child->id; + } + } else { + if ($child->id == (int) $segment) { + return $child->id; + } + } + } + } + } + + return false; + } + + /** + * Method to get the segment(s) for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoriesId($segment, $query) + { + return $this->getCategoryId($segment, $query); + } + + /** + * Method to get the segment(s) for a contact + * + * @param string $segment Segment of the contact to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getContactId($segment, $query) + { + if ($this->noIDs) { + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__contact_details')) + ->where( + [ + $this->db->quoteName('alias') . ' = :alias', + $this->db->quoteName('catid') . ' = :catid', + ] + ) + ->bind(':alias', $segment) + ->bind(':catid', $query['id'], ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + return (int) $this->db->loadResult(); + } + + return (int) $segment; + } + + /** + * Method to get categories from cache + * + * @param array $options The options for retrieving categories + * + * @return CategoryInterface The object containing categories + * + * @since 4.0.0 + */ + private function getCategories(array $options = []): CategoryInterface + { + $key = serialize($options); + + if (!isset($this->categoryCache[$key])) { + $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); + } + + return $this->categoryCache[$key]; + } } diff --git a/code/components/com_contact/src/View/Categories/HtmlView.php b/code/components/com_contact/src/View/Categories/HtmlView.php index 9f447cbe..5d9fa185 100644 --- a/code/components/com_contact/src/View/Categories/HtmlView.php +++ b/code/components/com_contact/src/View/Categories/HtmlView.php @@ -1,4 +1,5 @@ description = $item->address; - } + $item->description = $item->address; + } } diff --git a/code/components/com_contact/src/View/Category/HtmlView.php b/code/components/com_contact/src/View/Category/HtmlView.php index 74bf75ca..17c5fe70 100644 --- a/code/components/com_contact/src/View/Category/HtmlView.php +++ b/code/components/com_contact/src/View/Category/HtmlView.php @@ -1,4 +1,5 @@ pagination->hideEmptyLimitstart = true; - - // Prepare the data. - // Compute the contact slug. - foreach ($this->items as $item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - $temp = $item->params; - $item->params = clone $this->params; - $item->params->merge($temp); - - if ($item->params->get('show_email_headings', 0) == 1) - { - $item->email_to = trim($item->email_to); - - if (!empty($item->email_to) && MailHelper::isEmailAddress($item->email_to)) - { - $item->email_to = HTMLHelper::_('email.cloak', $item->email_to); - } - else - { - $item->email_to = ''; - } - } - } - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function prepareDocument() - { - parent::prepareDocument(); - - parent::addFeed(); - - if ($this->menuItemMatchCategory) - { - // If the active menu item is linked directly to the category being displayed, no further process is needed - return; - } - - // Get ID of the category from active menu item - $menu = $this->menu; - - if ($menu && $menu->component == 'com_contact' && isset($menu->query['view']) - && in_array($menu->query['view'], ['categories', 'category'])) - { - $id = $menu->query['id']; - } - else - { - $id = 0; - } - - $path = [['title' => $this->category->title, 'link' => '']]; - $category = $this->category->getParent(); - - while ($category !== null && $category->id != $id && $category->id !== 'root') - { - $path[] = ['title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)]; - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $this->pathway->addItem($item['title'], $item['link']); - } - } + /** + * @var string The name of the extension for the category + * @since 3.2 + */ + protected $extension = 'com_contact'; + + /** + * @var string Default title to use for page title + * @since 3.2 + */ + protected $defaultPageTitle = 'COM_CONTACT_DEFAULT_PAGE_TITLE'; + + /** + * @var string The name of the view to link individual items to + * @since 3.2 + */ + protected $viewName = 'contact'; + + /** + * Run the standard Joomla plugins + * + * @var boolean + * @since 3.5 + */ + protected $runPlugins = true; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + parent::commonCategoryDisplay(); + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + // Prepare the data. + // Compute the contact slug. + foreach ($this->items as $item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + $temp = $item->params; + $item->params = clone $this->params; + $item->params->merge($temp); + + if ($item->params->get('show_email_headings', 0) == 1) { + $item->email_to = trim($item->email_to); + + if (!empty($item->email_to) && MailHelper::isEmailAddress($item->email_to)) { + $item->email_to = HTMLHelper::_('email.cloak', $item->email_to); + } else { + $item->email_to = ''; + } + } + } + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function prepareDocument() + { + parent::prepareDocument(); + + parent::addFeed(); + + if ($this->menuItemMatchCategory) { + // If the active menu item is linked directly to the category being displayed, no further process is needed + return; + } + + // Get ID of the category from active menu item + $menu = $this->menu; + + if ( + $menu && $menu->component == 'com_contact' && isset($menu->query['view']) + && in_array($menu->query['view'], ['categories', 'category']) + ) { + $id = $menu->query['id']; + } else { + $id = 0; + } + + $path = [['title' => $this->category->title, 'link' => '']]; + $category = $this->category->getParent(); + + while ($category !== null && $category->id != $id && $category->id !== 'root') { + $path[] = ['title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)]; + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $this->pathway->addItem($item['title'], $item['link']); + } + } } diff --git a/code/components/com_contact/src/View/Contact/HtmlView.php b/code/components/com_contact/src/View/Contact/HtmlView.php index af207dfb..ca05bbb1 100644 --- a/code/components/com_contact/src/View/Contact/HtmlView.php +++ b/code/components/com_contact/src/View/Contact/HtmlView.php @@ -1,4 +1,5 @@ get('State'); - $item = $this->get('Item'); - $this->form = $this->get('Form'); - $params = $state->get('params'); - $contacts = []; - - $temp = clone $params; - - $active = $app->getMenu()->getActive(); - - // If the current view is the active item and a contact view for this contact, then the menu item params take priority - if ($active - && $active->component == 'com_contact' - && isset($active->query['view'], $active->query['id']) - && $active->query['view'] == 'contact' - && $active->query['id'] == $item->id) - { - $this->menuItemMatchContact = true; - - // Load layout from active query (in case it is an alternative menu item) - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - // Check for alternative layout of contact - elseif ($layout = $item->params->get('contact_layout')) - { - $this->setLayout($layout); - } - - $item->params->merge($temp); - } - else - { - // Merge so that contact params take priority - $temp->merge($item->params); - $item->params = $temp; - - if ($layout = $item->params->get('contact_layout')) - { - $this->setLayout($layout); - } - } - - // Collect extra contact information when this information is required - if ($item && $item->params->get('show_contact_list')) - { - // Get Category Model data - /** @var \Joomla\Component\Contact\Site\Model\CategoryModel $categoryModel */ - $categoryModel = $app->bootComponent('com_contact')->getMVCFactory() - ->createModel('Category', 'Site', ['ignore_request' => true]); - - $categoryModel->setState('category.id', $item->catid); - $categoryModel->setState('list.ordering', 'a.name'); - $categoryModel->setState('list.direction', 'asc'); - $categoryModel->setState('filter.published', 1); - - $contacts = $categoryModel->getItems(); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check if access is not public - $groups = $user->getAuthorisedViewLevels(); - - if (!in_array($item->access, $groups) || !in_array($item->category_access, $groups)) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return false; - } - - $options['category_id'] = $item->catid; - $options['order by'] = 'a.default_con DESC, a.ordering ASC'; - - /** - * Handle email cloaking - * - * Keep a copy of the raw email address so it can - * still be accessed in the layout if needed. - */ - $item->email_raw = $item->email_to; - - if ($item->email_to && $item->params->get('show_email')) - { - $item->email_to = HTMLHelper::_('email.cloak', $item->email_to, (bool) $item->params->get('add_mailto_link', true)); - } - - if ($item->params->get('show_street_address') || $item->params->get('show_suburb') || $item->params->get('show_state') - || $item->params->get('show_postcode') || $item->params->get('show_country')) - { - if (!empty($item->address) || !empty($item->suburb) || !empty($item->state) || !empty($item->country) || !empty($item->postcode)) - { - $item->params->set('address_check', 1); - } - } - else - { - $item->params->set('address_check', 0); - } - - // Manage the display mode for contact detail groups - switch ($item->params->get('contact_icons')) - { - case 1: - // Text - $item->params->set('marker_address', Text::_('COM_CONTACT_ADDRESS') . ': '); - $item->params->set('marker_email', Text::_('JGLOBAL_EMAIL') . ': '); - $item->params->set('marker_telephone', Text::_('COM_CONTACT_TELEPHONE') . ': '); - $item->params->set('marker_fax', Text::_('COM_CONTACT_FAX') . ': '); - $item->params->set('marker_mobile', Text::_('COM_CONTACT_MOBILE') . ': '); - $item->params->set('marker_webpage', Text::_('COM_CONTACT_WEBPAGE') . ': '); - $item->params->set('marker_misc', Text::_('COM_CONTACT_OTHER_INFORMATION') . ': '); - $item->params->set('marker_class', 'jicons-text'); - break; - - case 2: - // None - $item->params->set('marker_address', ''); - $item->params->set('marker_email', ''); - $item->params->set('marker_telephone', ''); - $item->params->set('marker_mobile', ''); - $item->params->set('marker_fax', ''); - $item->params->set('marker_misc', ''); - $item->params->set('marker_webpage', ''); - $item->params->set('marker_class', 'jicons-none'); - break; - - default: - if ($item->params->get('icon_address')) - { - $item->params->set( - 'marker_address', - HTMLHelper::_('image', $item->params->get('icon_address', ''), Text::_('COM_CONTACT_ADDRESS'), false) - ); - } - - if ($item->params->get('icon_email')) - { - $item->params->set( - 'marker_email', - HTMLHelper::_('image', $item->params->get('icon_email', ''), Text::_('COM_CONTACT_EMAIL'), false) - ); - } - - if ($item->params->get('icon_telephone')) - { - $item->params->set( - 'marker_telephone', - HTMLHelper::_('image', $item->params->get('icon_telephone', ''), Text::_('COM_CONTACT_TELEPHONE'), false) - ); - } - - if ($item->params->get('icon_fax', '')) - { - $item->params->set( - 'marker_fax', - HTMLHelper::_('image', $item->params->get('icon_fax', ''), Text::_('COM_CONTACT_FAX'), false) - ); - } - - if ($item->params->get('icon_misc')) - { - $item->params->set( - 'marker_misc', - HTMLHelper::_('image', $item->params->get('icon_misc', ''), Text::_('COM_CONTACT_OTHER_INFORMATION'), false) - ); - } - - if ($item->params->get('icon_mobile')) - { - $item->params->set( - 'marker_mobile', - HTMLHelper::_('image', $item->params->get('icon_mobile', ''), Text::_('COM_CONTACT_MOBILE'), false) - ); - } - - if ($item->params->get('icon_webpage')) - { - $item->params->set( - 'marker_webpage', - HTMLHelper::_('image', $item->params->get('icon_webpage', ''), Text::_('COM_CONTACT_WEBPAGE'), false) - ); - } - - $item->params->set('marker_class', 'jicons-icons'); - break; - } - - // Add links to contacts - if ($item->params->get('show_contact_list') && count($contacts) > 1) - { - foreach ($contacts as &$contact) - { - $contact->link = Route::_(RouteHelper::getContactRoute($contact->slug, $contact->catid, $contact->language)); - } - - $item->link = Route::_(RouteHelper::getContactRoute($item->slug, $item->catid, $item->language), false); - } - - // Process the content plugins. - PluginHelper::importPlugin('content'); - $offset = $state->get('list.offset'); - - // Fix for where some plugins require a text attribute - $item->text = ''; - - if (!empty($item->misc)) - { - $item->text = $item->misc; - } - - $app->triggerEvent('onContentPrepare', array ('com_contact.contact', &$item, &$item->params, $offset)); - - // Store the events for later - $item->event = new \stdClass; - $results = $app->triggerEvent('onContentAfterTitle', array('com_contact.contact', &$item, &$item->params, $offset)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = $app->triggerEvent('onContentBeforeDisplay', array('com_contact.contact', &$item, &$item->params, $offset)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = $app->triggerEvent('onContentAfterDisplay', array('com_contact.contact', &$item, &$item->params, $offset)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - - if (!empty($item->text)) - { - $item->misc = $item->text; - } - - $contactUser = null; - - if ($item->params->get('show_user_custom_fields') && $item->user_id && $contactUser = Factory::getUser($item->user_id)) - { - $contactUser->text = ''; - $app->triggerEvent('onContentPrepare', array ('com_users.user', &$contactUser, &$item->params, 0)); - - if (!isset($contactUser->jcfields)) - { - $contactUser->jcfields = array(); - } - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($item->params->get('pageclass_sfx', '')); - - $this->params = &$item->params; - $this->state = &$state; - $this->item = &$item; - $this->user = &$user; - $this->contacts = &$contacts; - $this->contactUser = $contactUser; - - $model = $this->getModel(); - $model->hit(); - - $captchaSet = $item->params->get('captcha', $app->get('captcha', '0')); - - foreach (PluginHelper::getPlugin('captcha') as $plugin) - { - if ($captchaSet === $plugin->name) - { - $this->captchaEnabled = true; - break; - } - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - $pathway = $app->getPathway(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_CONTACT_DEFAULT_PAGE_TITLE')); - } - - $title = $this->params->get('page_title', ''); - - // If the menu item does not concern this contact - if (!$this->menuItemMatchContact) - { - // If this is not a single contact menu item, set the page title to the contact title - if ($this->item->name) - { - $title = $this->item->name; - } - - // Get ID of the category from active menu item - if ($menu && $menu->component == 'com_contact' && isset($menu->query['view']) - && in_array($menu->query['view'], ['categories', 'category'])) - { - $id = $menu->query['id']; - } - else - { - $id = 0; - } - - $path = array(array('title' => $this->item->name, 'link' => '')); - $category = Categories::getInstance('Contact')->get($this->item->catid); - - while ($category !== null && $category->id != $id && $category->id !== 'root') - { - $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $pathway->addItem($item['title'], $item['link']); - } - } - - if (empty($title)) - { - $title = $this->item->name; - } - - $this->setDocumentTitle($title); - - if ($this->item->metadesc) - { - $this->document->setDescription($this->item->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - $mdata = $this->item->metadata->toArray(); - - foreach ($mdata as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - } + /** + * The item model state + * + * @var \Joomla\Registry\Registry + * + * @since 1.6 + */ + protected $state; + + /** + * The form object for the contact item + * + * @var \Joomla\CMS\Form\Form + * + * @since 1.6 + */ + protected $form; + + /** + * The item object details + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 1.6 + */ + protected $item; + + /** + * The page to return to on submission + * + * @var string + * + * @since 1.6 + * + * @todo Implement this functionality + */ + protected $return_page = ''; + + /** + * Should we show a captcha form for the submission of the contact request? + * + * @var boolean + * + * @since 3.6.3 + */ + protected $captchaEnabled = false; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * The user object + * + * @var \Joomla\CMS\User\User + * + * @since 4.0.0 + */ + protected $user; + + /** + * Other contacts in this contacts category + * + * @var array + * + * @since 4.0.0 + */ + protected $contacts; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The flag to mark if the active menu item is linked to the contact being displayed + * + * @var boolean + * + * @since 4.0.0 + */ + protected $menuItemMatchContact = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $user = $this->getCurrentUser(); + $state = $this->get('State'); + $item = $this->get('Item'); + $this->form = $this->get('Form'); + $params = $state->get('params'); + $contacts = []; + + $temp = clone $params; + + $active = $app->getMenu()->getActive(); + + // If the current view is the active item and a contact view for this contact, then the menu item params take priority + if ( + $active + && $active->component == 'com_contact' + && isset($active->query['view'], $active->query['id']) + && $active->query['view'] == 'contact' + && $active->query['id'] == $item->id + ) { + $this->menuItemMatchContact = true; + + // Load layout from active query (in case it is an alternative menu item) + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } elseif ($layout = $item->params->get('contact_layout')) { + // Check for alternative layout of contact + $this->setLayout($layout); + } + + $item->params->merge($temp); + } else { + // Merge so that contact params take priority + $temp->merge($item->params); + $item->params = $temp; + + if ($layout = $item->params->get('contact_layout')) { + $this->setLayout($layout); + } + } + + // Collect extra contact information when this information is required + if ($item && $item->params->get('show_contact_list')) { + // Get Category Model data + /** @var \Joomla\Component\Contact\Site\Model\CategoryModel $categoryModel */ + $categoryModel = $app->bootComponent('com_contact')->getMVCFactory() + ->createModel('Category', 'Site', ['ignore_request' => true]); + + $categoryModel->setState('category.id', $item->catid); + $categoryModel->setState('list.ordering', 'a.name'); + $categoryModel->setState('list.direction', 'asc'); + $categoryModel->setState('filter.published', 1); + + $contacts = $categoryModel->getItems(); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check if access is not public + $groups = $user->getAuthorisedViewLevels(); + + if (!in_array($item->access, $groups) || !in_array($item->category_access, $groups)) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return false; + } + + $options['category_id'] = $item->catid; + $options['order by'] = 'a.default_con DESC, a.ordering ASC'; + + /** + * Handle email cloaking + * + * Keep a copy of the raw email address so it can + * still be accessed in the layout if needed. + */ + $item->email_raw = $item->email_to; + + if ($item->email_to && $item->params->get('show_email')) { + $item->email_to = HTMLHelper::_('email.cloak', $item->email_to, (bool) $item->params->get('add_mailto_link', true)); + } + + if ( + $item->params->get('show_street_address') || $item->params->get('show_suburb') || $item->params->get('show_state') + || $item->params->get('show_postcode') || $item->params->get('show_country') + ) { + if (!empty($item->address) || !empty($item->suburb) || !empty($item->state) || !empty($item->country) || !empty($item->postcode)) { + $item->params->set('address_check', 1); + } + } else { + $item->params->set('address_check', 0); + } + + // Manage the display mode for contact detail groups + switch ($item->params->get('contact_icons')) { + case 1: + // Text + $item->params->set('marker_address', Text::_('COM_CONTACT_ADDRESS') . ': '); + $item->params->set('marker_email', Text::_('JGLOBAL_EMAIL') . ': '); + $item->params->set('marker_telephone', Text::_('COM_CONTACT_TELEPHONE') . ': '); + $item->params->set('marker_fax', Text::_('COM_CONTACT_FAX') . ': '); + $item->params->set('marker_mobile', Text::_('COM_CONTACT_MOBILE') . ': '); + $item->params->set('marker_webpage', Text::_('COM_CONTACT_WEBPAGE') . ': '); + $item->params->set('marker_misc', Text::_('COM_CONTACT_OTHER_INFORMATION') . ': '); + $item->params->set('marker_class', 'jicons-text'); + break; + + case 2: + // None + $item->params->set('marker_address', ''); + $item->params->set('marker_email', ''); + $item->params->set('marker_telephone', ''); + $item->params->set('marker_mobile', ''); + $item->params->set('marker_fax', ''); + $item->params->set('marker_misc', ''); + $item->params->set('marker_webpage', ''); + $item->params->set('marker_class', 'jicons-none'); + break; + + default: + if ($item->params->get('icon_address')) { + $item->params->set( + 'marker_address', + HTMLHelper::_('image', $item->params->get('icon_address', ''), Text::_('COM_CONTACT_ADDRESS'), false) + ); + } + + if ($item->params->get('icon_email')) { + $item->params->set( + 'marker_email', + HTMLHelper::_('image', $item->params->get('icon_email', ''), Text::_('COM_CONTACT_EMAIL'), false) + ); + } + + if ($item->params->get('icon_telephone')) { + $item->params->set( + 'marker_telephone', + HTMLHelper::_('image', $item->params->get('icon_telephone', ''), Text::_('COM_CONTACT_TELEPHONE'), false) + ); + } + + if ($item->params->get('icon_fax', '')) { + $item->params->set( + 'marker_fax', + HTMLHelper::_('image', $item->params->get('icon_fax', ''), Text::_('COM_CONTACT_FAX'), false) + ); + } + + if ($item->params->get('icon_misc')) { + $item->params->set( + 'marker_misc', + HTMLHelper::_('image', $item->params->get('icon_misc', ''), Text::_('COM_CONTACT_OTHER_INFORMATION'), false) + ); + } + + if ($item->params->get('icon_mobile')) { + $item->params->set( + 'marker_mobile', + HTMLHelper::_('image', $item->params->get('icon_mobile', ''), Text::_('COM_CONTACT_MOBILE'), false) + ); + } + + if ($item->params->get('icon_webpage')) { + $item->params->set( + 'marker_webpage', + HTMLHelper::_('image', $item->params->get('icon_webpage', ''), Text::_('COM_CONTACT_WEBPAGE'), false) + ); + } + + $item->params->set('marker_class', 'jicons-icons'); + break; + } + + // Add links to contacts + if ($item->params->get('show_contact_list') && count($contacts) > 1) { + foreach ($contacts as &$contact) { + $contact->link = Route::_(RouteHelper::getContactRoute($contact->slug, $contact->catid, $contact->language)); + } + + $item->link = Route::_(RouteHelper::getContactRoute($item->slug, $item->catid, $item->language), false); + } + + // Process the content plugins. + PluginHelper::importPlugin('content'); + $offset = $state->get('list.offset'); + + // Fix for where some plugins require a text attribute + $item->text = ''; + + if (!empty($item->misc)) { + $item->text = $item->misc; + } + + $app->triggerEvent('onContentPrepare', array ('com_contact.contact', &$item, &$item->params, $offset)); + + // Store the events for later + $item->event = new \stdClass(); + $results = $app->triggerEvent('onContentAfterTitle', array('com_contact.contact', &$item, &$item->params, $offset)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = $app->triggerEvent('onContentBeforeDisplay', array('com_contact.contact', &$item, &$item->params, $offset)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = $app->triggerEvent('onContentAfterDisplay', array('com_contact.contact', &$item, &$item->params, $offset)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + + if (!empty($item->text)) { + $item->misc = $item->text; + } + + $contactUser = null; + + if ($item->params->get('show_user_custom_fields') && $item->user_id && $contactUser = Factory::getUser($item->user_id)) { + $contactUser->text = ''; + $app->triggerEvent('onContentPrepare', array ('com_users.user', &$contactUser, &$item->params, 0)); + + if (!isset($contactUser->jcfields)) { + $contactUser->jcfields = array(); + } + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($item->params->get('pageclass_sfx', '')); + + $this->params = &$item->params; + $this->state = &$state; + $this->item = &$item; + $this->user = &$user; + $this->contacts = &$contacts; + $this->contactUser = $contactUser; + + $model = $this->getModel(); + $model->hit(); + + $captchaSet = $item->params->get('captcha', $app->get('captcha', '0')); + + foreach (PluginHelper::getPlugin('captcha') as $plugin) { + if ($captchaSet === $plugin->name) { + $this->captchaEnabled = true; + break; + } + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + $pathway = $app->getPathway(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_CONTACT_DEFAULT_PAGE_TITLE')); + } + + $title = $this->params->get('page_title', ''); + + // If the menu item does not concern this contact + if (!$this->menuItemMatchContact) { + // If this is not a single contact menu item, set the page title to the contact title + if ($this->item->name) { + $title = $this->item->name; + } + + // Get ID of the category from active menu item + if ( + $menu && $menu->component == 'com_contact' && isset($menu->query['view']) + && in_array($menu->query['view'], ['categories', 'category']) + ) { + $id = $menu->query['id']; + } else { + $id = 0; + } + + $path = array(array('title' => $this->item->name, 'link' => '')); + $category = Categories::getInstance('Contact')->get($this->item->catid); + + while ($category !== null && $category->id != $id && $category->id !== 'root') { + $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $pathway->addItem($item['title'], $item['link']); + } + } + + if (empty($title)) { + $title = $this->item->name; + } + + $this->setDocumentTitle($title); + + if ($this->item->metadesc) { + $this->document->setDescription($this->item->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + $mdata = $this->item->metadata->toArray(); + + foreach ($mdata as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + } } diff --git a/code/components/com_contact/src/View/Contact/VcfView.php b/code/components/com_contact/src/View/Contact/VcfView.php index 14d05d97..0897b980 100644 --- a/code/components/com_contact/src/View/Contact/VcfView.php +++ b/code/components/com_contact/src/View/Contact/VcfView.php @@ -1,4 +1,5 @@ get('Item'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->document->setMimeEncoding('text/directory', true); - - // Compute lastname, firstname and middlename - $item->name = trim($item->name); - - // "Lastname, Firstname Middlename" format support - // e.g. "de Gaulle, Charles" - $namearray = explode(',', $item->name); - - if (count($namearray) > 1) - { - $lastname = $namearray[0]; - $card_name = $lastname; - $name_and_midname = trim($namearray[1]); - - $firstname = ''; - - if (!empty($name_and_midname)) - { - $namearray = explode(' ', $name_and_midname); - - $firstname = $namearray[0]; - $middlename = (count($namearray) > 1) ? $namearray[1] : ''; - $card_name = $firstname . ' ' . ($middlename ? $middlename . ' ' : '') . $card_name; - } - } - // "Firstname Middlename Lastname" format support - else - { - $namearray = explode(' ', $item->name); - - $middlename = (count($namearray) > 2) ? $namearray[1] : ''; - $firstname = array_shift($namearray); - $lastname = count($namearray) ? end($namearray) : ''; - $card_name = $firstname . ($middlename ? ' ' . $middlename : '') . ($lastname ? ' ' . $lastname : ''); - } - - $rev = date('c', strtotime($item->modified)); - - Factory::getApplication()->setHeader('Content-disposition', 'attachment; filename="' . $card_name . '.vcf"', true); - - $vcard = []; - $vcard[] .= 'BEGIN:VCARD'; - $vcard[] .= 'VERSION:3.0'; - $vcard[] = 'N:' . $lastname . ';' . $firstname . ';' . $middlename; - $vcard[] = 'FN:' . $item->name; - $vcard[] = 'TITLE:' . $item->con_position; - $vcard[] = 'TEL;TYPE=WORK,VOICE:' . $item->telephone; - $vcard[] = 'TEL;TYPE=WORK,FAX:' . $item->fax; - $vcard[] = 'TEL;TYPE=WORK,MOBILE:' . $item->mobile; - $vcard[] = 'ADR;TYPE=WORK:;;' . $item->address . ';' . $item->suburb . ';' . $item->state . ';' . $item->postcode . ';' . $item->country; - $vcard[] = 'LABEL;TYPE=WORK:' . $item->address . "\n" . $item->suburb . "\n" . $item->state . "\n" . $item->postcode . "\n" . $item->country; - $vcard[] = 'EMAIL;TYPE=PREF,INTERNET:' . $item->email_to; - $vcard[] = 'URL:' . $item->webpage; - $vcard[] = 'REV:' . $rev . 'Z'; - $vcard[] = 'END:VCARD'; - - echo implode("\n", $vcard); - } + /** + * The contact item + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $item; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return string A string if successful + * + * @throws GenericDataException + */ + public function display($tpl = null) + { + // Get model data. + $item = $this->get('Item'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->document->setMimeEncoding('text/directory', true); + + // Compute lastname, firstname and middlename + $item->name = trim($item->name); + + // "Lastname, Firstname Middlename" format support + // e.g. "de Gaulle, Charles" + $namearray = explode(',', $item->name); + + if (count($namearray) > 1) { + $lastname = $namearray[0]; + $card_name = $lastname; + $name_and_midname = trim($namearray[1]); + + $firstname = ''; + + if (!empty($name_and_midname)) { + $namearray = explode(' ', $name_and_midname); + + $firstname = $namearray[0]; + $middlename = (count($namearray) > 1) ? $namearray[1] : ''; + $card_name = $firstname . ' ' . ($middlename ? $middlename . ' ' : '') . $card_name; + } + } else { + // "Firstname Middlename Lastname" format support + $namearray = explode(' ', $item->name); + + $middlename = (count($namearray) > 2) ? $namearray[1] : ''; + $firstname = array_shift($namearray); + $lastname = count($namearray) ? end($namearray) : ''; + $card_name = $firstname . ($middlename ? ' ' . $middlename : '') . ($lastname ? ' ' . $lastname : ''); + } + + $rev = date('c', strtotime($item->modified)); + + Factory::getApplication()->setHeader('Content-disposition', 'attachment; filename="' . $card_name . '.vcf"', true); + + $vcard = []; + $vcard[] .= 'BEGIN:VCARD'; + $vcard[] .= 'VERSION:3.0'; + $vcard[] = 'N:' . $lastname . ';' . $firstname . ';' . $middlename; + $vcard[] = 'FN:' . $item->name; + $vcard[] = 'TITLE:' . $item->con_position; + $vcard[] = 'TEL;TYPE=WORK,VOICE:' . $item->telephone; + $vcard[] = 'TEL;TYPE=WORK,FAX:' . $item->fax; + $vcard[] = 'TEL;TYPE=WORK,MOBILE:' . $item->mobile; + $vcard[] = 'ADR;TYPE=WORK:;;' . $item->address . ';' . $item->suburb . ';' . $item->state . ';' . $item->postcode . ';' . $item->country; + $vcard[] = 'LABEL;TYPE=WORK:' . $item->address . "\n" . $item->suburb . "\n" . $item->state . "\n" . $item->postcode . "\n" . $item->country; + $vcard[] = 'EMAIL;TYPE=PREF,INTERNET:' . $item->email_to; + $vcard[] = 'URL:' . $item->webpage; + $vcard[] = 'REV:' . $rev . 'Z'; + $vcard[] = 'END:VCARD'; + + echo implode("\n", $vcard); + } } diff --git a/code/components/com_contact/src/View/Featured/HtmlView.php b/code/components/com_contact/src/View/Featured/HtmlView.php index 61e37084..38acf428 100644 --- a/code/components/com_contact/src/View/Featured/HtmlView.php +++ b/code/components/com_contact/src/View/Featured/HtmlView.php @@ -1,4 +1,5 @@ getParams(); - - // Get some data from the models - $state = $this->get('State'); - $items = $this->get('Items'); - $category = $this->get('Category'); - $children = $this->get('Children'); - $parent = $this->get('Parent'); - $pagination = $this->get('Pagination'); - - // Flag indicates to not add limitstart=0 to URL - $pagination->hideEmptyLimitstart = true; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Prepare the data. - // Compute the contact slug. - for ($i = 0, $n = count($items); $i < $n; $i++) - { - $item = &$items[$i]; - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - $temp = $item->params; - $item->params = clone $params; - $item->params->merge($temp); - - if ($item->params->get('show_email', 0) == 1) - { - $item->email_to = trim($item->email_to); - - if (!empty($item->email_to) && MailHelper::isEmailAddress($item->email_to)) - { - $item->email_to = HTMLHelper::_('email.cloak', $item->email_to); - } - else - { - $item->email_to = ''; - } - } - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $maxLevel = $params->get('maxLevel', -1); - $this->maxLevel = &$maxLevel; - $this->state = &$state; - $this->items = &$items; - $this->category = &$category; - $this->children = &$children; - $this->params = &$params; - $this->parent = &$parent; - $this->pagination = &$pagination; - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - */ - protected function _prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_CONTACT_DEFAULT_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The item model state + * + * @var \Joomla\Registry\Registry + * + * @since 1.6.0 + */ + protected $state; + + /** + * The item details + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 1.6.0 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 1.6.0 + */ + protected $pagination; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Method to display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $params = $app->getParams(); + + // Get some data from the models + $state = $this->get('State'); + $items = $this->get('Items'); + $category = $this->get('Category'); + $children = $this->get('Children'); + $parent = $this->get('Parent'); + $pagination = $this->get('Pagination'); + + // Flag indicates to not add limitstart=0 to URL + $pagination->hideEmptyLimitstart = true; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Prepare the data. + // Compute the contact slug. + for ($i = 0, $n = count($items); $i < $n; $i++) { + $item = &$items[$i]; + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + $temp = $item->params; + $item->params = clone $params; + $item->params->merge($temp); + + if ($item->params->get('show_email', 0) == 1) { + $item->email_to = trim($item->email_to); + + if (!empty($item->email_to) && MailHelper::isEmailAddress($item->email_to)) { + $item->email_to = HTMLHelper::_('email.cloak', $item->email_to); + } else { + $item->email_to = ''; + } + } + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $maxLevel = $params->get('maxLevel', -1); + $this->maxLevel = &$maxLevel; + $this->state = &$state; + $this->items = &$items; + $this->category = &$category; + $this->children = &$children; + $this->params = &$params; + $this->parent = &$parent; + $this->pagination = &$pagination; + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + */ + protected function _prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_CONTACT_DEFAULT_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/code/components/com_contact/src/View/Form/HtmlView.php b/code/components/com_contact/src/View/Form/HtmlView.php index d72d14df..89b86683 100644 --- a/code/components/com_contact/src/View/Form/HtmlView.php +++ b/code/components/com_contact/src/View/Form/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - $this->return_page = $this->get('ReturnPage'); - - if (empty($this->item->id)) - { - $authorised = $user->authorise('core.create', 'com_contact') || count($user->getAuthorisedCategories('com_contact', 'core.create')); - } - else - { - // Since we don't track these assets at the item level, use the category id. - $canDo = ContactHelper::getActions('com_contact', 'category', $this->item->catid); - $authorised = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by === $user->id); - } - - if ($authorised !== true) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return false; - } - - $this->item->tags = new TagsHelper; - - if (!empty($this->item->id)) - { - $this->item->tags->getItemTags('com_contact.contact', $this->item->id); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - $app->enqueueMessage(implode("\n", $errors), 'error'); - - return false; - } - - // Create a shortcut to the parameters. - $this->params = $this->state->params; - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); - - // Override global params with contact specific params - $this->params->merge($this->item->params); - - // Propose current language as default when creating new contact - if (empty($this->item->id) && Multilanguage::isEnabled()) - { - $lang = Factory::getLanguage()->getTag(); - $this->form->setFieldAttribute('language', 'default', $lang); - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @throws \Exception - * - * @since 4.0.0 - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_CONTACT_FORM_EDIT_CONTACT')); - } - - $title = $this->params->def('page_title', Text::_('COM_CONTACT_FORM_EDIT_CONTACT')); - - $this->setDocumentTitle($title); - - $pathway = $app->getPathWay(); - $pathway->addItem($title, ''); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('menu-meta_keywords')) - { - $this->document->setMetaData('keywords', $this->params->get('menu-meta_keywords')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * @var \Joomla\CMS\Form\Form + * @since 4.0.0 + */ + protected $form; + + /** + * @var object + * @since 4.0.0 + */ + protected $item; + + /** + * @var string + * @since 4.0.0 + */ + protected $return_page; + + /** + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx; + + /** + * @var \Joomla\Registry\Registry + * @since 4.0.0 + */ + protected $state; + + /** + * @var \Joomla\Registry\Registry + * @since 4.0.0 + */ + protected $params; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + * + * @throws \Exception + * @since 4.0.0 + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + $app = Factory::getApplication(); + + // Get model data. + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + $this->return_page = $this->get('ReturnPage'); + + if (empty($this->item->id)) { + $authorised = $user->authorise('core.create', 'com_contact') || count($user->getAuthorisedCategories('com_contact', 'core.create')); + } else { + // Since we don't track these assets at the item level, use the category id. + $canDo = ContactHelper::getActions('com_contact', 'category', $this->item->catid); + $authorised = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by === $user->id); + } + + if ($authorised !== true) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return false; + } + + $this->item->tags = new TagsHelper(); + + if (!empty($this->item->id)) { + $this->item->tags->getItemTags('com_contact.contact', $this->item->id); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + $app->enqueueMessage(implode("\n", $errors), 'error'); + + return false; + } + + // Create a shortcut to the parameters. + $this->params = $this->state->params; + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); + + // Override global params with contact specific params + $this->params->merge($this->item->params); + + // Propose current language as default when creating new contact + if (empty($this->item->id) && Multilanguage::isEnabled()) { + $lang = Factory::getLanguage()->getTag(); + $this->form->setFieldAttribute('language', 'default', $lang); + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @throws \Exception + * + * @since 4.0.0 + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_CONTACT_FORM_EDIT_CONTACT')); + } + + $title = $this->params->def('page_title', Text::_('COM_CONTACT_FORM_EDIT_CONTACT')); + + $this->setDocumentTitle($title); + + $pathway = $app->getPathWay(); + $pathway->addItem($title, ''); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('menu-meta_keywords')) { + $this->document->setMetaData('keywords', $this->params->get('menu-meta_keywords')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/code/components/com_contact/tmpl/categories/default.php b/code/components/com_contact/tmpl/categories/default.php index a8c558da..2e23579b 100644 --- a/code/components/com_contact/tmpl/categories/default.php +++ b/code/components/com_contact/tmpl/categories/default.php @@ -1,4 +1,5 @@
    - loadTemplate('items'); - ?> + loadTemplate('items'); + ?>
    diff --git a/code/components/com_contact/tmpl/categories/default_items.php b/code/components/com_contact/tmpl/categories/default_items.php index d18c34fc..5f8a812d 100644 --- a/code/components/com_contact/tmpl/categories/default_items.php +++ b/code/components/com_contact/tmpl/categories/default_items.php @@ -1,4 +1,5 @@ maxLevelcat != 0 && count($this->items[$this->parent->id]) > 0) : -?> - items[$this->parent->id] as $id => $item) : ?> - params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> -
    - - params->get('show_subcat_desc_cat') == 1) : ?> - description) : ?> -
    - description, '', 'com_contact.categories'); ?> -
    - - + ?> + items[$this->parent->id] as $id => $item) : ?> + params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> +
    + + params->get('show_subcat_desc_cat') == 1) : ?> + description) : ?> +
    + description, '', 'com_contact.categories'); ?> +
    + + - maxLevelcat > 1 && count($item->getChildren()) > 0) : ?> -
    - items[$item->id] = $item->getChildren(); - $this->parent = $item; - $this->maxLevelcat--; - echo $this->loadTemplate('items'); - $this->parent = $item->getParent(); - $this->maxLevelcat++; - ?> -
    - -
    - - + maxLevelcat > 1 && count($item->getChildren()) > 0) : ?> +
    + items[$item->id] = $item->getChildren(); + $this->parent = $item; + $this->maxLevelcat--; + echo $this->loadTemplate('items'); + $this->parent = $item->getParent(); + $this->maxLevelcat++; + ?> +
    + +
    + + diff --git a/code/components/com_contact/tmpl/category/default.php b/code/components/com_contact/tmpl/category/default.php index 84581b57..b25d7830 100644 --- a/code/components/com_contact/tmpl/category/default.php +++ b/code/components/com_contact/tmpl/category/default.php @@ -1,4 +1,5 @@
    - subtemplatename = 'items'; - echo LayoutHelper::render('joomla.content.category_default', $this); - ?> + subtemplatename = 'items'; + echo LayoutHelper::render('joomla.content.category_default', $this); + ?>
    diff --git a/code/components/com_contact/tmpl/category/default_children.php b/code/components/com_contact/tmpl/category/default_children.php index ed2ed640..0febb97d 100644 --- a/code/components/com_contact/tmpl/category/default_children.php +++ b/code/components/com_contact/tmpl/category/default_children.php @@ -1,4 +1,5 @@ maxLevel != 0 && count($this->children[$this->category->id]) > 0) : -?> + ?>
      -children[$this->category->id] as $id => $child) : ?> - params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> -
    • -

      - - escape($child->title); ?> - + children[$this->category->id] as $id => $child) : ?> + params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> +
    • +

      + + escape($child->title); ?> + - params->get('show_cat_items') == 1) : ?> - numitems; ?> - -

      + params->get('show_cat_items') == 1) : ?> + numitems; ?> + +
    • - params->get('show_subcat_desc') == 1) : ?> - description) : ?> -
      - description, '', 'com_contact.category'); ?> -
      - - + params->get('show_subcat_desc') == 1) : ?> + description) : ?> +
      + description, '', 'com_contact.category'); ?> +
      + + - getChildren()) > 0 ) : - $this->children[$child->id] = $child->getChildren(); - $this->category = $child; - $this->maxLevel--; - echo $this->loadTemplate('children'); - $this->category = $child->getParent(); - $this->maxLevel++; - endif; ?> -
    • - - + getChildren()) > 0) : + $this->children[$child->id] = $child->getChildren(); + $this->category = $child; + $this->maxLevel--; + echo $this->loadTemplate('children'); + $this->category = $child->getParent(); + $this->maxLevel++; + endif; ?> + + +
    diff --git a/code/components/com_contact/tmpl/category/default_items.php b/code/components/com_contact/tmpl/category/default_items.php index 05229d77..e073ca25 100644 --- a/code/components/com_contact/tmpl/category/default_items.php +++ b/code/components/com_contact/tmpl/category/default_items.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_contact.contacts-list') - ->useScript('core'); + ->useScript('core'); $canDo = ContactHelper::getActions('com_contact', 'category', $this->category->id); $canEdit = $canDo->get('core.edit'); @@ -31,185 +32,185 @@ $listDirn = $this->escape($this->state->get('list.direction')); ?>
    -
    - params->get('filter_field')) : ?> -
    - - - - -
    - - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - - - items)) : ?> - params->get('show_no_contacts', 1)) : ?> -
    - - -
    - - - - - - params->get('show_headings')) : ?> - - - - - get('core.edit.own') && $item->created_by === $userId)) : ?> - - - - - - - items as $i => $item) : ?> - items[$i]->published == 0) : ?> - - - - - - - get('core.edit.own') && $item->created_by === $userId)) : ?> - - - - - - - - get('core.create')) : ?> - category, $this->category->params); ?> - - - params->get('show_pagination', 2)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - - pagination->getPagesLinks(); ?> -
    - -
    - - -
    -
    +
    + params->get('filter_field')) : ?> +
    + + + + +
    + + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + + + items)) : ?> + params->get('show_no_contacts', 1)) : ?> +
    + + +
    + + + + + + params->get('show_headings')) : ?> + + + + + get('core.edit.own') && $item->created_by === $userId)) : ?> + + + + + + + items as $i => $item) : ?> + items[$i]->published == 0) : ?> + + + + + + + get('core.edit.own') && $item->created_by === $userId)) : ?> + + + + + + + + get('core.create')) : ?> + category, $this->category->params); ?> + + + params->get('show_pagination', 2)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + + pagination->getPagesLinks(); ?> +
    + +
    + + +
    +
    diff --git a/code/components/com_contact/tmpl/contact/default.php b/code/components/com_contact/tmpl/contact/default.php index 24d32658..2d216b1e 100644 --- a/code/components/com_contact/tmpl/contact/default.php +++ b/code/components/com_contact/tmpl/contact/default.php @@ -1,4 +1,5 @@
    - get('show_page_heading')) : ?> -

    - escape($tparams->get('page_heading')); ?> -

    - - - item->name && $tparams->get('show_name')) : ?> - - - - -
    -
    -
    - item, $tparams); ?> -
    -
    -
    - - - get('show_contact_category'); ?> - - -

    - item->category_title; ?> -

    - - item->catid, $this->item->language); ?> -

    - - escape($this->item->category_title); ?> - -

    - - - item->event->afterDisplayTitle; ?> - - get('show_contact_list') && count($this->contacts) > 1) : ?> -
    - - contacts, - 'select_contact', - 'class="form-select" onchange="document.location.href = this.value"', 'link', 'name', $this->item->link); - ?> -
    - - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> -
    - item->tagLayout = new FileLayout('joomla.content.tags'); ?> - item->tagLayout->render($this->item->tags->itemTags); ?> -
    - - - item->event->beforeDisplayContent; ?> - - params->get('show_info', 1)) : ?> - -
    - ' . Text::_('COM_CONTACT_DETAILS') . '

    '; ?> - - item->image && $tparams->get('show_image')) : ?> -
    - $this->item->image, - 'alt' => $this->item->name, - 'itemprop' => 'image', - ] - ); ?> -
    - - - item->con_position && $tparams->get('show_position')) : ?> -
    -
    :
    -
    - item->con_position; ?> -
    -
    - - -
    - loadTemplate('address'); ?> - - get('allow_vcard')) : ?> - - - - -
    - - - - - get('show_email_form') && ($this->item->email_to || $this->item->user_id)) : ?> - ' . Text::_('COM_CONTACT_EMAIL_FORM') . '

    '; ?> - - loadTemplate('form'); ?> - - - get('show_links')) : ?> - loadTemplate('links'); ?> - - - get('show_articles') && $this->item->user_id && $this->item->articles) : ?> - ' . Text::_('JGLOBAL_ARTICLES') . ''; ?> - - loadTemplate('articles'); ?> - - - get('show_profile') && $this->item->user_id && PluginHelper::isEnabled('user', 'profile')) : ?> - ' . Text::_('COM_CONTACT_PROFILE') . ''; ?> - - loadTemplate('profile'); ?> - - - get('show_user_custom_fields') && $this->contactUser) : ?> - loadTemplate('user_custom_fields'); ?> - - - item->misc && $tparams->get('show_misc')) : ?> - ' . Text::_('COM_CONTACT_OTHER_INFORMATION') . ''; ?> - -
    -
    -
    - params->get('marker_misc')) : ?> - - - - - params->get('marker_misc'); ?> - - -
    -
    - - item->misc; ?> - -
    -
    -
    - - item->event->afterDisplayContent; ?> + get('show_page_heading')) : ?> +

    + escape($tparams->get('page_heading')); ?> +

    + + + item->name && $tparams->get('show_name')) : ?> + + + + +
    +
    +
    + item, $tparams); ?> +
    +
    +
    + + + get('show_contact_category'); ?> + + +

    + item->category_title; ?> +

    + + item->catid, $this->item->language); ?> +

    + + escape($this->item->category_title); ?> + +

    + + + item->event->afterDisplayTitle; ?> + + get('show_contact_list') && count($this->contacts) > 1) : ?> +
    + + contacts, + 'select_contact', + 'class="form-select" onchange="document.location.href = this.value"', + 'link', + 'name', + $this->item->link + ); + ?> +
    + + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> +
    + item->tagLayout = new FileLayout('joomla.content.tags'); ?> + item->tagLayout->render($this->item->tags->itemTags); ?> +
    + + + item->event->beforeDisplayContent; ?> + + params->get('show_info', 1)) : ?> +
    + ' . Text::_('COM_CONTACT_DETAILS') . ''; ?> + + item->image && $tparams->get('show_image')) : ?> +
    + $this->item->image, + 'alt' => $this->item->name, + 'itemprop' => 'image', + ] + ); ?> +
    + + + item->con_position && $tparams->get('show_position')) : ?> +
    +
    :
    +
    + item->con_position; ?> +
    +
    + + +
    + loadTemplate('address'); ?> + + get('allow_vcard')) : ?> + + + + +
    +
    + + + + get('show_email_form') && ($this->item->email_to || $this->item->user_id)) : ?> + ' . Text::_('COM_CONTACT_EMAIL_FORM') . ''; ?> + + loadTemplate('form'); ?> + + + get('show_links')) : ?> + loadTemplate('links'); ?> + + + get('show_articles') && $this->item->user_id && $this->item->articles) : ?> + ' . Text::_('JGLOBAL_ARTICLES') . ''; ?> + + loadTemplate('articles'); ?> + + + get('show_profile') && $this->item->user_id && PluginHelper::isEnabled('user', 'profile')) : ?> + ' . Text::_('COM_CONTACT_PROFILE') . ''; ?> + + loadTemplate('profile'); ?> + + + get('show_user_custom_fields') && $this->contactUser) : ?> + loadTemplate('user_custom_fields'); ?> + + + item->misc && $tparams->get('show_misc')) : ?> + ' . Text::_('COM_CONTACT_OTHER_INFORMATION') . ''; ?> + +
    +
    +
    + params->get('marker_misc')) : ?> + + + + + params->get('marker_misc'); ?> + + +
    +
    + + item->misc; ?> + +
    +
    +
    + + item->event->afterDisplayContent; ?> diff --git a/code/components/com_contact/tmpl/contact/default_address.php b/code/components/com_contact/tmpl/contact/default_address.php index fbec2f6e..8133fb9b 100644 --- a/code/components/com_contact/tmpl/contact/default_address.php +++ b/code/components/com_contact/tmpl/contact/default_address.php @@ -1,4 +1,5 @@
    - params->get('address_check') > 0) && - ($this->item->address || $this->item->suburb || $this->item->state || $this->item->country || $this->item->postcode)) : ?> -
    - params->get('marker_address')) : ?> - - - - params->get('marker_address'); ?> - - -
    + params->get('address_check') > 0) && + ($this->item->address || $this->item->suburb || $this->item->state || $this->item->country || $this->item->postcode) + ) : ?> +
    + params->get('marker_address')) : ?> + + + + params->get('marker_address'); ?> + + +
    - item->address && $this->params->get('show_street_address')) : ?> -
    - - item->address, false); ?> - -
    - + item->address && $this->params->get('show_street_address')) : ?> +
    + + item->address, false); ?> + +
    + - item->suburb && $this->params->get('show_suburb')) : ?> -
    - - item->suburb; ?> - -
    - - item->state && $this->params->get('show_state')) : ?> -
    - - item->state; ?> - -
    - - item->postcode && $this->params->get('show_postcode')) : ?> -
    - - item->postcode; ?> - -
    - - item->country && $this->params->get('show_country')) : ?> -
    - - item->country; ?> - -
    - - + item->suburb && $this->params->get('show_suburb')) : ?> +
    + + item->suburb; ?> + +
    + + item->state && $this->params->get('show_state')) : ?> +
    + + item->state; ?> + +
    + + item->postcode && $this->params->get('show_postcode')) : ?> +
    + + item->postcode; ?> + +
    + + item->country && $this->params->get('show_country')) : ?> +
    + + item->country; ?> + +
    + + item->email_to && $this->params->get('show_email')) : ?> -
    - params->get('marker_email')) : ?> - - - - params->get('marker_email'); ?> - - -
    -
    - - item->email_to; ?> - -
    +
    + params->get('marker_email')) : ?> + + + + params->get('marker_email'); ?> + + +
    +
    + + item->email_to; ?> + +
    item->telephone && $this->params->get('show_telephone')) : ?> -
    - params->get('marker_telephone')) : ?> - - - - params->get('marker_telephone'); ?> - - -
    -
    - - item->telephone; ?> - -
    +
    + params->get('marker_telephone')) : ?> + + + + params->get('marker_telephone'); ?> + + +
    +
    + + item->telephone; ?> + +
    item->fax && $this->params->get('show_fax')) : ?> -
    - params->get('marker_fax')) : ?> - - - - params->get('marker_fax'); ?> - - -
    -
    - - item->fax; ?> - -
    +
    + params->get('marker_fax')) : ?> + + + + params->get('marker_fax'); ?> + + +
    +
    + + item->fax; ?> + +
    item->mobile && $this->params->get('show_mobile')) : ?> -
    - params->get('marker_mobile')) : ?> - - - - params->get('marker_mobile'); ?> - - -
    -
    - - item->mobile; ?> - -
    +
    + params->get('marker_mobile')) : ?> + + + + params->get('marker_mobile'); ?> + + +
    +
    + + item->mobile; ?> + +
    item->webpage && $this->params->get('show_webpage')) : ?> -
    - params->get('marker_webpage')) : ?> - - - - params->get('marker_webpage'); ?> - - -
    -
    - - - -
    +
    + params->get('marker_webpage')) : ?> + + + + params->get('marker_webpage'); ?> + + +
    +
    + + + +
    diff --git a/code/components/com_contact/tmpl/contact/default_articles.php b/code/components/com_contact/tmpl/contact/default_articles.php index c16a95e5..c17d00c5 100644 --- a/code/components/com_contact/tmpl/contact/default_articles.php +++ b/code/components/com_contact/tmpl/contact/default_articles.php @@ -1,4 +1,5 @@ params->get('show_articles')) : ?>
    -
      - item->articles as $article) : ?> -
    • - slug, $article->catid, $article->language)), htmlspecialchars($article->title, ENT_COMPAT, 'UTF-8')); ?> -
    • - -
    +
      + item->articles as $article) : ?> +
    • + slug, $article->catid, $article->language)), htmlspecialchars($article->title, ENT_COMPAT, 'UTF-8')); ?> +
    • + +
    diff --git a/code/components/com_contact/tmpl/contact/default_form.php b/code/components/com_contact/tmpl/contact/default_form.php index 8bfe273b..41221c99 100644 --- a/code/components/com_contact/tmpl/contact/default_form.php +++ b/code/components/com_contact/tmpl/contact/default_form.php @@ -1,4 +1,5 @@
    -
    - form->getFieldsets() as $fieldset) : ?> - name === 'captcha' && !$this->captchaEnabled) : ?> - - - form->getFieldset($fieldset->name); ?> - -
    - label) && ($legend = trim(Text::_($fieldset->label))) !== '') : ?> - - - - renderField(); ?> - -
    - - -
    -
    - - - - - - -
    -
    -
    +
    + form->getFieldsets() as $fieldset) : ?> + name === 'captcha' && $this->captchaEnabled) : ?> + + + form->getFieldset($fieldset->name); ?> + +
    + label) && ($legend = trim(Text::_($fieldset->label))) !== '') : ?> + + + + renderField(); ?> + +
    + + + captchaEnabled) : ?> + form->renderFieldset('captcha'); ?> + +
    +
    + + + + + + +
    +
    +
    diff --git a/code/components/com_contact/tmpl/contact/default_links.php b/code/components/com_contact/tmpl/contact/default_links.php index 64b95ad8..0b95aae7 100644 --- a/code/components/com_contact/tmpl/contact/default_links.php +++ b/code/components/com_contact/tmpl/contact/default_links.php @@ -1,4 +1,5 @@ ' . Text::_('COM_CONTACT_LINKS') . ''; ?> diff --git a/code/components/com_contact/tmpl/contact/default_profile.php b/code/components/com_contact/tmpl/contact/default_profile.php index f64d411a..b4e0134a 100644 --- a/code/components/com_contact/tmpl/contact/default_profile.php +++ b/code/components/com_contact/tmpl/contact/default_profile.php @@ -1,4 +1,5 @@ item->profile->getFieldset('profile'); ?> -
    -
    - value) : - echo '
    ' . $profile->label . '
    '; - $profile->text = htmlspecialchars($profile->value, ENT_COMPAT, 'UTF-8'); - - switch ($profile->id) : - case 'profile_website': - $v_http = substr($profile->value, 0, 4); - - if ($v_http === 'http') : - echo '
    ' . PunycodeHelper::urlToUTF8($profile->text) . '
    '; - else : - echo '
    ' . PunycodeHelper::urlToUTF8($profile->text) . '
    '; - endif; - break; - - case 'profile_dob': - echo '
    ' . HTMLHelper::_('date', $profile->text, Text::_('DATE_FORMAT_LC4'), false) . '
    '; - break; - - default: - echo '
    ' . $profile->text . '
    '; - break; - endswitch; - endif; - endforeach; ?> -
    -
    + $fields = $this->item->profile->getFieldset('profile'); ?> +
    +
    + value) : + echo '
    ' . $profile->label . '
    '; + $profile->text = htmlspecialchars($profile->value, ENT_COMPAT, 'UTF-8'); + + switch ($profile->id) : + case 'profile_website': + $v_http = substr($profile->value, 0, 4); + + if ($v_http === 'http') : + echo '
    ' . PunycodeHelper::urlToUTF8($profile->text) . '
    '; + else : + echo '
    ' . PunycodeHelper::urlToUTF8($profile->text) . '
    '; + endif; + break; + + case 'profile_dob': + echo '
    ' . HTMLHelper::_('date', $profile->text, Text::_('DATE_FORMAT_LC4'), false) . '
    '; + break; + + default: + echo '
    ' . $profile->text . '
    '; + break; + endswitch; + endif; + endforeach; ?> +
    +
    diff --git a/code/components/com_contact/tmpl/contact/default_user_custom_fields.php b/code/components/com_contact/tmpl/contact/default_user_custom_fields.php index 839a70c7..a00b9412 100644 --- a/code/components/com_contact/tmpl/contact/default_user_custom_fields.php +++ b/code/components/com_contact/tmpl/contact/default_user_custom_fields.php @@ -1,4 +1,5 @@ contactUser) : ?> - + contactUser->jcfields as $field) : ?> - value && (in_array('-1', $displayGroups) || in_array($field->group_id, $displayGroups))) : ?> - group_title][] = $field; ?> - + value && (in_array('-1', $displayGroups) || in_array($field->group_id, $displayGroups))) : ?> + group_title][] = $field; ?> + $fields) : ?> - - ' . ($groupTitle ?: Text::_('COM_CONTACT_USER_FIELDS')) . ''; ?> - -
    -
    - - value) : ?> - - - - params->get('showlabel')) : ?> - ' . Text::_($field->label) . ''; ?> - - - ' . $field->value . ''; ?> - -
    -
    + + ' . ($groupTitle ?: Text::_('COM_CONTACT_USER_FIELDS')) . ''; ?> + +
    +
    + + value) : ?> + + + + params->get('showlabel')) : ?> + ' . Text::_($field->label) . ''; ?> + + + ' . $field->value . ''; ?> + +
    +
    diff --git a/code/components/com_contact/tmpl/featured/default.php b/code/components/com_contact/tmpl/featured/default.php index f0093ac6..d87e8d67 100644 --- a/code/components/com_contact/tmpl/featured/default.php +++ b/code/components/com_contact/tmpl/featured/default.php @@ -1,4 +1,5 @@ diff --git a/code/components/com_contact/tmpl/featured/default_items.php b/code/components/com_contact/tmpl/featured/default_items.php index 75b31fe8..1fd0130a 100644 --- a/code/components/com_contact/tmpl/featured/default_items.php +++ b/code/components/com_contact/tmpl/featured/default_items.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_contact.contacts-list') - ->useScript('core'); + ->useScript('core'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?> diff --git a/code/components/com_contact/tmpl/form/edit.php b/code/components/com_contact/tmpl/form/edit.php index cdb20b89..d5f33d6f 100644 --- a/code/components/com_contact/tmpl/form/edit.php +++ b/code/components/com_contact/tmpl/form/edit.php @@ -1,4 +1,5 @@ useCoreUI = true; ?>
    - params->get('show_page_heading')) : ?> - - + params->get('show_page_heading')) : ?> + + -
    -
    - tab_name, ['active' => 'details', 'recall' => true, 'breakpoint' => 768]); ?> - tab_name, 'details', empty($this->item->id) ? Text::_('COM_CONTACT_NEW_CONTACT') : Text::_('COM_CONTACT_EDIT_CONTACT')); ?> - form->renderField('name'); ?> + +
    + tab_name, ['active' => 'details', 'recall' => true, 'breakpoint' => 768]); ?> + tab_name, 'details', empty($this->item->id) ? Text::_('COM_CONTACT_NEW_CONTACT') : Text::_('COM_CONTACT_EDIT_CONTACT')); ?> + form->renderField('name'); ?> - item->id)) : ?> - form->renderField('alias'); ?> - + item->id)) : ?> + form->renderField('alias'); ?> + - form->renderFieldset('details'); ?> - + form->renderFieldset('details'); ?> + - tab_name, 'misc', Text::_('COM_CONTACT_FIELDSET_MISCELLANEOUS')); ?> - form->getInput('misc'); ?> - + tab_name, 'misc', Text::_('COM_CONTACT_FIELDSET_MISCELLANEOUS')); ?> + form->getInput('misc'); ?> + - - tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL')); ?> - form->renderField('language'); ?> - - - form->renderField('language'); ?> - + + tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL')); ?> + form->renderField('language'); ?> + + + form->renderField('language'); ?> + - - + + - - - -
    -
    - - - params->get('save_history', 0) && $this->item->id) : ?> - form->getInput('contenthistory'); ?> - -
    - + + + +
    +
    + + + params->get('save_history', 0) && $this->item->id) : ?> + form->getInput('contenthistory'); ?> + +
    +
    diff --git a/code/components/com_content/helpers/icon.php b/code/components/com_content/helpers/icon.php index 3aa3126a..9ba5d03d 100644 --- a/code/components/com_content/helpers/icon.php +++ b/code/components/com_content/helpers/icon.php @@ -1,17 +1,22 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\CMS\Language\Text; use Joomla\Registry\Registry; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Content Component HTML Helper * @@ -20,86 +25,86 @@ */ abstract class JHtmlIcon { - /** - * Method to generate a link to the create item page for the given category - * - * @param object $category The category information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML markup for the create item link - * - * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead - */ - public static function create($category, $params, $attribs = array(), $legacy = false) - { - return self::getIcon()->create($category, $params, $attribs, $legacy); - } + /** + * Method to generate a link to the create item page for the given category + * + * @param object $category The category information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the create item link + * + * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead + */ + public static function create($category, $params, $attribs = array(), $legacy = false) + { + return self::getIcon()->create($category, $params, $attribs, $legacy); + } - /** - * Display an edit icon for the article. - * - * This icon will not display in a popup window, nor if the article is trashed. - * Edit access checks must be performed in the calling code. - * - * @param object $article The article information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML for the article edit icon. - * - * @since 1.6 - * - * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead - */ - public static function edit($article, $params, $attribs = array(), $legacy = false) - { - return self::getIcon()->edit($article, $params, $attribs, $legacy); - } + /** + * Display an edit icon for the article. + * + * This icon will not display in a popup window, nor if the article is trashed. + * Edit access checks must be performed in the calling code. + * + * @param object $article The article information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML for the article edit icon. + * + * @since 1.6 + * + * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead + */ + public static function edit($article, $params, $attribs = array(), $legacy = false) + { + return self::getIcon()->edit($article, $params, $attribs, $legacy); + } - /** - * Method to generate a popup link to print an article - * - * @param object $article The article information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML markup for the popup link - * - * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead - */ - public static function print_popup($article, $params, $attribs = array(), $legacy = false) - { - throw new \Exception(Text::_('COM_CONTENT_ERROR_PRINT_POPUP')); - } + /** + * Method to generate a popup link to print an article + * + * @param object $article The article information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the popup link + * + * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead + */ + public static function print_popup($article, $params, $attribs = array(), $legacy = false) + { + throw new \Exception(Text::_('COM_CONTENT_ERROR_PRINT_POPUP')); + } - /** - * Method to generate a link to print an article - * - * @param object $article Not used, @deprecated for 4.0 - * @param Registry $params The item parameters - * @param array $attribs Not used, @deprecated for 4.0 - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML markup for the popup link - * - * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead - */ - public static function print_screen($article, $params, $attribs = array(), $legacy = false) - { - return self::getIcon()->print_screen($params, $legacy); - } + /** + * Method to generate a link to print an article + * + * @param object $article Not used, @deprecated for 4.0 + * @param Registry $params The item parameters + * @param array $attribs Not used, @deprecated for 4.0 + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the popup link + * + * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead + */ + public static function print_screen($article, $params, $attribs = array(), $legacy = false) + { + return self::getIcon()->print_screen($params, $legacy); + } - /** - * Creates an icon instance. - * - * @return \Joomla\Component\Content\Administrator\Service\HTML\Icon - */ - private static function getIcon() - { - return (new \Joomla\Component\Content\Administrator\Service\HTML\Icon(Joomla\CMS\Factory::getApplication())); - } + /** + * Creates an icon instance. + * + * @return \Joomla\Component\Content\Administrator\Service\HTML\Icon + */ + private static function getIcon() + { + return (new \Joomla\Component\Content\Administrator\Service\HTML\Icon(Joomla\CMS\Factory::getApplication())); + } } diff --git a/code/components/com_content/src/Controller/ArticleController.php b/code/components/com_content/src/Controller/ArticleController.php index 2c440673..019dec7c 100644 --- a/code/components/com_content/src/Controller/ArticleController.php +++ b/code/components/com_content/src/Controller/ArticleController.php @@ -1,4 +1,5 @@ setRedirect($this->getReturnPage()); - - return; - } - - // Redirect to the edit screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item . '&a_id=0' - . $this->getRedirectToItemAppend(), false - ) - ); - - return true; - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowAdd($data = array()) - { - $user = $this->app->getIdentity(); - $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('catid'), 'int'); - $allow = null; - - if ($categoryId) - { - // If the category has been passed in the data or URL check it. - $allow = $user->authorise('core.create', 'com_content.category.' . $categoryId); - } - - if ($allow === null) - { - // In the absence of better information, revert to the component permissions. - return parent::allowAdd(); - } - else - { - return $allow; - } - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key; default is id. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Zero record (id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($user->authorise('core.edit', 'com_content.article.' . $recordId)) - { - return true; - } - - // Check edit own on the record asset (explicit or inherited) - if ($user->authorise('core.edit.own', 'com_content.article.' . $recordId)) - { - // Existing record already has an owner, get it - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - // Grant if current user is owner of the record - return $user->get('id') == $record->created_by; - } - - return false; - } - - /** - * Method to cancel an edit. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 1.6 - */ - public function cancel($key = 'a_id') - { - $result = parent::cancel($key); - - /** @var SiteApplication $app */ - $app = Factory::getApplication(); - - // Load the parameters. - $params = $app->getParams(); - - $customCancelRedir = (bool) $params->get('custom_cancel_redirect'); - - if ($customCancelRedir) - { - $cancelMenuitemId = (int) $params->get('cancel_redirect_menuitem'); - - if ($cancelMenuitemId > 0) - { - $item = $app->getMenu()->getItem($cancelMenuitemId); - $lang = ''; - - if (Multilanguage::isEnabled()) - { - $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; - } - - // Redirect to the user specified return page. - $redirlink = $item->link . $lang . '&Itemid=' . $cancelMenuitemId; - } - else - { - // Redirect to the same article submission form (clean form). - $redirlink = $app->getMenu()->getActive()->link . '&Itemid=' . $app->getMenu()->getActive()->id; - } - } - else - { - $menuitemId = (int) $params->get('redirect_menuitem'); - - if ($menuitemId > 0) - { - $lang = ''; - $item = $app->getMenu()->getItem($menuitemId); - - if (Multilanguage::isEnabled()) - { - $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; - } - - // Redirect to the general (redirect_menuitem) user specified return page. - $redirlink = $item->link . $lang . '&Itemid=' . $menuitemId; - } - else - { - // Redirect to the return page. - $redirlink = $this->getReturnPage(); - } - } - - $this->setRedirect(Route::_($redirlink, false)); - - return $result; - } - - /** - * Method to edit an existing record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key - * (sometimes required to avoid router collisions). - * - * @return boolean True if access level check and checkout passes, false otherwise. - * - * @since 1.6 - */ - public function edit($key = null, $urlVar = 'a_id') - { - $result = parent::edit($key, $urlVar); - - if (!$result) - { - $this->setRedirect(Route::_($this->getReturnPage(), false)); - } - - return $result; - } - - /** - * Method to get a model object, loading it if required. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.5 - */ - public function getModel($name = 'Form', $prefix = 'Site', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 1.6 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'a_id') - { - // Need to override the parent method completely. - $tmpl = $this->input->get('tmpl'); - - $append = ''; - - // Setup redirect info. - if ($tmpl) - { - $append .= '&tmpl=' . $tmpl; - } - - // @todo This is a bandaid, not a long term solution. - /** - * if ($layout) - * { - * $append .= '&layout=' . $layout; - * } - */ - - $append .= '&layout=edit'; - - if ($recordId) - { - $append .= '&' . $urlVar . '=' . $recordId; - } - - $itemId = $this->input->getInt('Itemid'); - $return = $this->getReturnPage(); - $catId = $this->input->getInt('catid'); - - if ($itemId) - { - $append .= '&Itemid=' . $itemId; - } - - if ($catId) - { - $append .= '&catid=' . $catId; - } - - if ($return) - { - $append .= '&return=' . base64_encode($return); - } - - return $append; - } - - /** - * Get the return URL. - * - * If a "return" variable has been passed in the request - * - * @return string The return URL. - * - * @since 1.6 - */ - protected function getReturnPage() - { - $return = $this->input->get('return', null, 'base64'); - - if (empty($return) || !Uri::isInternal(base64_decode($return))) - { - return Uri::base(); - } - else - { - return base64_decode($return); - } - } - - /** - * Method to save a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 1.6 - */ - public function save($key = null, $urlVar = 'a_id') - { - $result = parent::save($key, $urlVar); - $app = Factory::getApplication(); - $articleId = $app->input->getInt('a_id'); - - // Load the parameters. - $params = $app->getParams(); - $menuitem = (int) $params->get('redirect_menuitem'); - - // Check for redirection after submission when creating a new article only - if ($menuitem > 0 && $articleId == 0) - { - $lang = ''; - - if (Multilanguage::isEnabled()) - { - $item = $app->getMenu()->getItem($menuitem); - $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; - } - - // If ok, redirect to the return page. - if ($result) - { - $this->setRedirect(Route::_('index.php?Itemid=' . $menuitem . $lang, false)); - } - } - elseif ($this->getTask() === 'save2copy') - { - // Redirect to the article page, use the redirect url set from parent controller - } - else - { - // If ok, redirect to the return page. - if ($result) - { - $this->setRedirect(Route::_($this->getReturnPage(), false)); - } - } - - return $result; - } - - /** - * Method to reload a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return void - * - * @since 3.8.0 - */ - public function reload($key = null, $urlVar = 'a_id') - { - parent::reload($key, $urlVar); - } - - /** - * Method to save a vote. - * - * @return void - * - * @since 1.6 - */ - public function vote() - { - // Check for request forgeries. - $this->checkToken(); - - $user_rating = $this->input->getInt('user_rating', -1); - - if ($user_rating > -1) - { - $url = $this->input->getString('url', ''); - $id = $this->input->getInt('id', 0); - $viewName = $this->input->getString('view', $this->default_view); - $model = $this->getModel($viewName); - - // Don't redirect to an external URL. - if (!Uri::isInternal($url)) - { - $url = Route::_('index.php'); - } - - if ($model->storeVote($id, $user_rating)) - { - $this->setRedirect($url, Text::_('COM_CONTENT_ARTICLE_VOTE_SUCCESS')); - } - else - { - $this->setRedirect($url, Text::_('COM_CONTENT_ARTICLE_VOTE_FAILURE')); - } - } - } + use VersionableControllerTrait; + + /** + * The URL view item variable. + * + * @var string + * @since 1.6 + */ + protected $view_item = 'form'; + + /** + * The URL view list variable. + * + * @var string + * @since 1.6 + */ + protected $view_list = 'categories'; + + /** + * The URL edit variable. + * + * @var string + * @since 3.2 + */ + protected $urlVar = 'a.id'; + + /** + * Method to add a new record. + * + * @return mixed True if the record can be added, an error object if not. + * + * @since 1.6 + */ + public function add() + { + if (!parent::add()) { + // Redirect to the return page. + $this->setRedirect($this->getReturnPage()); + + return; + } + + // Redirect to the edit screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item . '&a_id=0' + . $this->getRedirectToItemAppend(), + false + ) + ); + + return true; + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $user = $this->app->getIdentity(); + $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('catid'), 'int'); + $allow = null; + + if ($categoryId) { + // If the category has been passed in the data or URL check it. + $allow = $user->authorise('core.create', 'com_content.category.' . $categoryId); + } + + if ($allow === null) { + // In the absence of better information, revert to the component permissions. + return parent::allowAdd(); + } else { + return $allow; + } + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key; default is id. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Zero record (id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($user->authorise('core.edit', 'com_content.article.' . $recordId)) { + return true; + } + + // Check edit own on the record asset (explicit or inherited) + if ($user->authorise('core.edit.own', 'com_content.article.' . $recordId)) { + // Existing record already has an owner, get it + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + // Grant if current user is owner of the record + return $user->get('id') == $record->created_by; + } + + return false; + } + + /** + * Method to cancel an edit. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 1.6 + */ + public function cancel($key = 'a_id') + { + $result = parent::cancel($key); + + /** @var SiteApplication $app */ + $app = $this->app; + + // Load the parameters. + $params = $app->getParams(); + + $customCancelRedir = (bool) $params->get('custom_cancel_redirect'); + + if ($customCancelRedir) { + $cancelMenuitemId = (int) $params->get('cancel_redirect_menuitem'); + + if ($cancelMenuitemId > 0) { + $item = $app->getMenu()->getItem($cancelMenuitemId); + $lang = ''; + + if (Multilanguage::isEnabled()) { + $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; + } + + // Redirect to the user specified return page. + $redirlink = $item->link . $lang . '&Itemid=' . $cancelMenuitemId; + } else { + // Redirect to the same article submission form (clean form). + $redirlink = $app->getMenu()->getActive()->link . '&Itemid=' . $app->getMenu()->getActive()->id; + } + } else { + $menuitemId = (int) $params->get('redirect_menuitem'); + + if ($menuitemId > 0) { + $lang = ''; + $item = $app->getMenu()->getItem($menuitemId); + + if (Multilanguage::isEnabled()) { + $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; + } + + // Redirect to the general (redirect_menuitem) user specified return page. + $redirlink = $item->link . $lang . '&Itemid=' . $menuitemId; + } else { + // Redirect to the return page. + $redirlink = $this->getReturnPage(); + } + } + + $this->setRedirect(Route::_($redirlink, false)); + + return $result; + } + + /** + * Method to edit an existing record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * (sometimes required to avoid router collisions). + * + * @return boolean True if access level check and checkout passes, false otherwise. + * + * @since 1.6 + */ + public function edit($key = null, $urlVar = 'a_id') + { + $result = parent::edit($key, $urlVar); + + if (!$result) { + $this->setRedirect(Route::_($this->getReturnPage(), false)); + } + + return $result; + } + + /** + * Method to get a model object, loading it if required. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.5 + */ + public function getModel($name = 'Form', $prefix = 'Site', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 1.6 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'a_id') + { + // Need to override the parent method completely. + $tmpl = $this->input->get('tmpl'); + + $append = ''; + + // Setup redirect info. + if ($tmpl) { + $append .= '&tmpl=' . $tmpl; + } + + // @todo This is a bandaid, not a long term solution. + /** + * if ($layout) + * { + * $append .= '&layout=' . $layout; + * } + */ + + $append .= '&layout=edit'; + + if ($recordId) { + $append .= '&' . $urlVar . '=' . $recordId; + } + + $itemId = $this->input->getInt('Itemid'); + $return = $this->getReturnPage(); + $catId = $this->input->getInt('catid'); + + if ($itemId) { + $append .= '&Itemid=' . $itemId; + } + + if ($catId) { + $append .= '&catid=' . $catId; + } + + if ($return) { + $append .= '&return=' . base64_encode($return); + } + + return $append; + } + + /** + * Get the return URL. + * + * If a "return" variable has been passed in the request + * + * @return string The return URL. + * + * @since 1.6 + */ + protected function getReturnPage() + { + $return = $this->input->get('return', null, 'base64'); + + if (empty($return) || !Uri::isInternal(base64_decode($return))) { + return Uri::base(); + } else { + return base64_decode($return); + } + } + + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 1.6 + */ + public function save($key = null, $urlVar = 'a_id') + { + $result = parent::save($key, $urlVar); + + if (\in_array($this->getTask(), ['save2copy', 'apply'], true)) { + return $result; + } + + $app = $this->app; + $articleId = $app->input->getInt('a_id'); + + // Load the parameters. + $params = $app->getParams(); + $menuitem = (int) $params->get('redirect_menuitem'); + + // Check for redirection after submission when creating a new article only + if ($menuitem > 0 && $articleId == 0) { + $lang = ''; + + if (Multilanguage::isEnabled()) { + $item = $app->getMenu()->getItem($menuitem); + $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; + } + + // If ok, redirect to the return page. + if ($result) { + $this->setRedirect(Route::_('index.php?Itemid=' . $menuitem . $lang, false)); + } + } elseif ($this->getTask() === 'save2copy') { + // Redirect to the article page, use the redirect url set from parent controller + } else { + // If ok, redirect to the return page. + if ($result) { + $this->setRedirect(Route::_($this->getReturnPage(), false)); + } + } + + return $result; + } + + /** + * Method to reload a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return void + * + * @since 3.8.0 + */ + public function reload($key = null, $urlVar = 'a_id') + { + parent::reload($key, $urlVar); + } + + /** + * Method to save a vote. + * + * @return void + * + * @since 1.6 + */ + public function vote() + { + // Check for request forgeries. + $this->checkToken(); + + $user_rating = $this->input->getInt('user_rating', -1); + + if ($user_rating > -1) { + $url = $this->input->getString('url', ''); + $id = $this->input->getInt('id', 0); + $viewName = $this->input->getString('view', $this->default_view); + $model = $this->getModel($viewName); + + // Don't redirect to an external URL. + if (!Uri::isInternal($url)) { + $url = Route::_('index.php'); + } + + if ($model->storeVote($id, $user_rating)) { + $this->setRedirect($url, Text::_('COM_CONTENT_ARTICLE_VOTE_SUCCESS')); + } else { + $this->setRedirect($url, Text::_('COM_CONTENT_ARTICLE_VOTE_FAILURE')); + } + } + } } diff --git a/code/components/com_content/src/Controller/DisplayController.php b/code/components/com_content/src/Controller/DisplayController.php index 498e5760..03f5ca3a 100644 --- a/code/components/com_content/src/Controller/DisplayController.php +++ b/code/components/com_content/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input = Factory::getApplication()->input; - - // Article frontpage Editor pagebreak proxying: - if ($this->input->get('view') === 'article' && $this->input->get('layout') === 'pagebreak') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } - // Article frontpage Editor article proxying: - elseif ($this->input->get('view') === 'articles' && $this->input->get('layout') === 'modal') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } - - parent::__construct($config, $factory, $app, $input); - } - - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached. - * @param boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. - * - * @return DisplayController This object to support chaining. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = false) - { - $cachable = true; - - /** - * Set the default view name and format from the Request. - * Note we are using a_id to avoid collisions with the router and the return page. - * Frontend is a bit messier than the backend. - */ - $id = $this->input->getInt('a_id'); - $vName = $this->input->getCmd('view', 'categories'); - $this->input->set('view', $vName); - - $user = $this->app->getIdentity(); - - if ($user->get('id') - || ($this->input->getMethod() === 'POST' - && (($vName === 'category' && $this->input->get('layout') !== 'blog') || $vName === 'archive' ))) - { - $cachable = false; - } - - $safeurlparams = array( - 'catid' => 'INT', - 'id' => 'INT', - 'cid' => 'ARRAY', - 'year' => 'INT', - 'month' => 'INT', - 'limit' => 'UINT', - 'limitstart' => 'UINT', - 'showall' => 'INT', - 'return' => 'BASE64', - 'filter' => 'STRING', - 'filter_order' => 'CMD', - 'filter_order_Dir' => 'CMD', - 'filter-search' => 'STRING', - 'print' => 'BOOLEAN', - 'lang' => 'CMD', - 'Itemid' => 'INT'); - - // Check for edit form. - if ($vName === 'form' && !$this->checkEditId('com_content.edit.article', $id)) - { - // Somehow the person just went to the form - we don't allow that. - throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 403); - } - - if ($vName === 'article') - { - // Get/Create the model - if ($model = $this->getModel($vName)) - { - if (ComponentHelper::getParams('com_content')->get('record_hits', 1) == 1) - { - $model->hit(); - } - } - } - - parent::display($cachable, $safeurlparams); - - return $this; - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 3.0.1 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + $this->input = Factory::getApplication()->input; + + // Article frontpage Editor pagebreak proxying: + if ($this->input->get('view') === 'article' && $this->input->get('layout') === 'pagebreak') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } elseif ($this->input->get('view') === 'articles' && $this->input->get('layout') === 'modal') { + // Article frontpage Editor article proxying: + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } + + parent::__construct($config, $factory, $app, $input); + } + + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return DisplayController This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $cachable = true; + + /** + * Set the default view name and format from the Request. + * Note we are using a_id to avoid collisions with the router and the return page. + * Frontend is a bit messier than the backend. + */ + $id = $this->input->getInt('a_id'); + $vName = $this->input->getCmd('view', 'categories'); + $this->input->set('view', $vName); + + $user = $this->app->getIdentity(); + + if ( + $user->get('id') + || ($this->input->getMethod() === 'POST' + && (($vName === 'category' && $this->input->get('layout') !== 'blog') || $vName === 'archive' )) + ) { + $cachable = false; + } + + $safeurlparams = array( + 'catid' => 'INT', + 'id' => 'INT', + 'cid' => 'ARRAY', + 'year' => 'INT', + 'month' => 'INT', + 'limit' => 'UINT', + 'limitstart' => 'UINT', + 'showall' => 'INT', + 'return' => 'BASE64', + 'filter' => 'STRING', + 'filter_order' => 'CMD', + 'filter_order_Dir' => 'CMD', + 'filter-search' => 'STRING', + 'print' => 'BOOLEAN', + 'lang' => 'CMD', + 'Itemid' => 'INT'); + + // Check for edit form. + if ($vName === 'form' && !$this->checkEditId('com_content.edit.article', $id)) { + // Somehow the person just went to the form - we don't allow that. + throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 403); + } + + if ($vName === 'article') { + // Get/Create the model + if ($model = $this->getModel($vName)) { + if (ComponentHelper::getParams('com_content')->get('record_hits', 1) == 1) { + $model->hit(); + } + } + } + + parent::display($cachable, $safeurlparams); + + return $this; + } } diff --git a/code/components/com_content/src/Dispatcher/Dispatcher.php b/code/components/com_content/src/Dispatcher/Dispatcher.php index 7efdf0c3..16e3237a 100644 --- a/code/components/com_content/src/Dispatcher/Dispatcher.php +++ b/code/components/com_content/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->get('view') === 'articles' && $this->input->get('layout') === 'modal') - || ($this->input->get('view') === 'article' && $this->input->get('layout') === 'pagebreak'); - - if ($checkCreateEdit) - { - // Can create in any category (component permission) or at least in one category - $canCreateRecords = $this->app->getIdentity()->authorise('core.create', 'com_content') - || count($this->app->getIdentity()->getAuthorisedCategories('com_content', 'core.create')) > 0; - - // Instead of checking edit on all records, we can use **same** check as the form editing view - $values = (array) $this->app->getUserState('com_content.edit.article.id'); - $isEditingRecords = count($values); - $hasAccess = $canCreateRecords || $isEditingRecords; - - if (!$hasAccess) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'warning'); - - return; - } - } - - parent::dispatch(); - } + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since 4.0.0 + */ + public function dispatch() + { + $checkCreateEdit = ($this->input->get('view') === 'articles' && $this->input->get('layout') === 'modal') + || ($this->input->get('view') === 'article' && $this->input->get('layout') === 'pagebreak'); + + if ($checkCreateEdit) { + // Can create in any category (component permission) or at least in one category + $canCreateRecords = $this->app->getIdentity()->authorise('core.create', 'com_content') + || count($this->app->getIdentity()->getAuthorisedCategories('com_content', 'core.create')) > 0; + + // Instead of checking edit on all records, we can use **same** check as the form editing view + $values = (array) $this->app->getUserState('com_content.edit.article.id'); + $isEditingRecords = count($values); + $hasAccess = $canCreateRecords || $isEditingRecords; + + if (!$hasAccess) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'warning'); + + return; + } + } + + parent::dispatch(); + } } diff --git a/code/components/com_content/src/Helper/AssociationHelper.php b/code/components/com_content/src/Helper/AssociationHelper.php index 0699a3c1..c81050e3 100644 --- a/code/components/com_content/src/Helper/AssociationHelper.php +++ b/code/components/com_content/src/Helper/AssociationHelper.php @@ -1,4 +1,5 @@ input; - $view = $view ?? $jinput->get('view'); - $component = $jinput->getCmd('option'); - $id = empty($id) ? $jinput->getInt('id') : $id; - - if ($layout === null && $jinput->get('view') == $view && $component == 'com_content') - { - $layout = $jinput->get('layout', '', 'string'); - } - - if ($view === 'article') - { - if ($id) - { - $user = Factory::getUser(); - $groups = implode(',', $user->getAuthorisedViewLevels()); - $db = Factory::getDbo(); - $advClause = array(); - - // Filter by user groups - $advClause[] = 'c2.access IN (' . $groups . ')'; - - // Filter by current language - $advClause[] = 'c2.language != ' . $db->quote(Factory::getLanguage()->getTag()); - - if (!$user->authorise('core.edit.state', 'com_content') && !$user->authorise('core.edit', 'com_content')) - { - // Filter by start and end dates. - $date = Factory::getDate(); - - $nowDate = $db->quote($date->toSql()); - - $advClause[] = '(c2.publish_up IS NULL OR c2.publish_up <= ' . $nowDate . ')'; - $advClause[] = '(c2.publish_down IS NULL OR c2.publish_down >= ' . $nowDate . ')'; - - // Filter by published - $advClause[] = 'c2.state = 1'; - } - - $associations = Associations::getAssociations( - 'com_content', - '#__content', - 'com_content.item', - $id, - 'id', - 'alias', - 'catid', - $advClause - ); - - $return = array(); - - foreach ($associations as $tag => $item) - { - $return[$tag] = RouteHelper::getArticleRoute($item->id, (int) $item->catid, $item->language, $layout); - } - - return $return; - } - } - - if ($view === 'category' || $view === 'categories') - { - return self::getCategoryAssociations($id, 'com_content', $layout); - } - - return array(); - } - - /** - * Method to display in frontend the associations for a given article - * - * @param integer $id Id of the article - * - * @return array An array containing the association URL and the related language object - * - * @since 3.7.0 - */ - public static function displayAssociations($id) - { - $return = array(); - - if ($associations = self::getAssociations($id, 'article')) - { - $levels = Factory::getUser()->getAuthorisedViewLevels(); - $languages = LanguageHelper::getLanguages(); - - foreach ($languages as $language) - { - // Do not display language when no association - if (empty($associations[$language->lang_code])) - { - continue; - } - - // Do not display language without frontend UI - if (!array_key_exists($language->lang_code, LanguageHelper::getInstalledLanguages(0))) - { - continue; - } - - // Do not display language without specific home menu - if (!array_key_exists($language->lang_code, Multilanguage::getSiteHomePages())) - { - continue; - } - - // Do not display language without authorized access level - if (isset($language->access) && $language->access && !in_array($language->access, $levels)) - { - continue; - } - - $return[$language->lang_code] = array('item' => $associations[$language->lang_code], 'language' => $language); - } - } - - return $return; - } + /** + * Method to get the associations for a given item + * + * @param integer $id Id of the item + * @param string $view Name of the view + * @param string $layout View layout + * + * @return array Array of associations for the item + * + * @since 3.0 + */ + public static function getAssociations($id = 0, $view = null, $layout = null) + { + $jinput = Factory::getApplication()->input; + $view = $view ?? $jinput->get('view'); + $component = $jinput->getCmd('option'); + $id = empty($id) ? $jinput->getInt('id') : $id; + + if ($layout === null && $jinput->get('view') == $view && $component == 'com_content') { + $layout = $jinput->get('layout', '', 'string'); + } + + if ($view === 'article') { + if ($id) { + $user = Factory::getUser(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + $db = Factory::getDbo(); + $advClause = array(); + + // Filter by user groups + $advClause[] = 'c2.access IN (' . $groups . ')'; + + // Filter by current language + $advClause[] = 'c2.language != ' . $db->quote(Factory::getLanguage()->getTag()); + + if (!$user->authorise('core.edit.state', 'com_content') && !$user->authorise('core.edit', 'com_content')) { + // Filter by start and end dates. + $date = Factory::getDate(); + + $nowDate = $db->quote($date->toSql()); + + $advClause[] = '(c2.publish_up IS NULL OR c2.publish_up <= ' . $nowDate . ')'; + $advClause[] = '(c2.publish_down IS NULL OR c2.publish_down >= ' . $nowDate . ')'; + + // Filter by published + $advClause[] = 'c2.state = 1'; + } + + $associations = Associations::getAssociations( + 'com_content', + '#__content', + 'com_content.item', + $id, + 'id', + 'alias', + 'catid', + $advClause + ); + + $return = array(); + + foreach ($associations as $tag => $item) { + $return[$tag] = RouteHelper::getArticleRoute($item->id, (int) $item->catid, $item->language, $layout); + } + + return $return; + } + } + + if ($view === 'category' || $view === 'categories') { + return self::getCategoryAssociations($id, 'com_content', $layout); + } + + return array(); + } + + /** + * Method to display in frontend the associations for a given article + * + * @param integer $id Id of the article + * + * @return array An array containing the association URL and the related language object + * + * @since 3.7.0 + */ + public static function displayAssociations($id) + { + $return = array(); + + if ($associations = self::getAssociations($id, 'article')) { + $levels = Factory::getUser()->getAuthorisedViewLevels(); + $languages = LanguageHelper::getLanguages(); + + foreach ($languages as $language) { + // Do not display language when no association + if (empty($associations[$language->lang_code])) { + continue; + } + + // Do not display language without frontend UI + if (!array_key_exists($language->lang_code, LanguageHelper::getInstalledLanguages(0))) { + continue; + } + + // Do not display language without specific home menu + if (!array_key_exists($language->lang_code, Multilanguage::getSiteHomePages())) { + continue; + } + + // Do not display language without authorized access level + if (isset($language->access) && $language->access && !in_array($language->access, $levels)) { + continue; + } + + $return[$language->lang_code] = array('item' => $associations[$language->lang_code], 'language' => $language); + } + } + + return $return; + } } diff --git a/code/components/com_content/src/Helper/QueryHelper.php b/code/components/com_content/src/Helper/QueryHelper.php index 3de4456b..5a59a636 100644 --- a/code/components/com_content/src/Helper/QueryHelper.php +++ b/code/components/com_content/src/Helper/QueryHelper.php @@ -1,4 +1,5 @@ getQuery(true)->rand(); - break; - - case 'vote': - $orderby = 'a.id DESC '; - - if (PluginHelper::isEnabled('content', 'vote')) - { - $orderby = 'rating_count DESC '; - } - break; - - case 'rvote': - $orderby = 'a.id ASC '; - - if (PluginHelper::isEnabled('content', 'vote')) - { - $orderby = 'rating_count ASC '; - } - break; - - case 'rank': - $orderby = 'a.id DESC '; - - if (PluginHelper::isEnabled('content', 'vote')) - { - $orderby = 'rating DESC '; - } - break; - - case 'rrank': - $orderby = 'a.id ASC '; - - if (PluginHelper::isEnabled('content', 'vote')) - { - $orderby = 'rating ASC '; - } - break; - - default: - $orderby = 'a.ordering'; - break; - } - - return $orderby; - } - - /** - * Translate an order code to a field for primary category ordering. - * - * @param string $orderDate The ordering code. - * @param DatabaseInterface $db The database - * - * @return string The SQL field(s) to order by. - * - * @since 1.6 - */ - public static function getQueryDate($orderDate, DatabaseInterface $db = null) - { - $db = $db ?: Factory::getDbo(); - - switch ($orderDate) - { - case 'modified' : - $queryDate = ' CASE WHEN a.modified IS NULL THEN a.created ELSE a.modified END'; - break; - - // Use created if publish_up is not set - case 'published' : - $queryDate = ' CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END '; - break; - - case 'unpublished' : - $queryDate = ' CASE WHEN a.publish_down IS NULL THEN a.created ELSE a.publish_down END '; - break; - case 'created' : - default : - $queryDate = ' a.created '; - break; - } - - return $queryDate; - } - - /** - * Get join information for the voting query. - * - * @param \Joomla\Registry\Registry $params An options object for the article. - * - * @return array A named array with "select" and "join" keys. - * - * @since 1.5 - * - * @deprecated 5.0 Deprecated without replacement, not used in core - */ - public static function buildVotingQuery($params = null) - { - if (!$params) - { - $params = ComponentHelper::getParams('com_content'); - } - - $voting = $params->get('show_vote'); - - if ($voting) - { - // Calculate voting count - $select = ' , ROUND(v.rating_sum / v.rating_count) AS rating, v.rating_count'; - $join = ' LEFT JOIN #__content_rating AS v ON a.id = v.content_id'; - } - else - { - $select = ''; - $join = ''; - } - - return array('select' => $select, 'join' => $join); - } + /** + * Translate an order code to a field for category ordering. + * + * @param string $orderby The ordering code. + * + * @return string The SQL field(s) to order by. + * + * @since 1.5 + */ + public static function orderbyPrimary($orderby) + { + switch ($orderby) { + case 'alpha': + $orderby = 'c.path, '; + break; + + case 'ralpha': + $orderby = 'c.path DESC, '; + break; + + case 'order': + $orderby = 'c.lft, '; + break; + + default: + $orderby = ''; + break; + } + + return $orderby; + } + + /** + * Translate an order code to a field for article ordering. + * + * @param string $orderby The ordering code. + * @param string $orderDate The ordering code for the date. + * @param DatabaseInterface $db The database + * + * @return string The SQL field(s) to order by. + * + * @since 1.5 + */ + public static function orderbySecondary($orderby, $orderDate = 'created', DatabaseInterface $db = null) + { + $db = $db ?: Factory::getDbo(); + + $queryDate = self::getQueryDate($orderDate, $db); + + switch ($orderby) { + case 'date': + $orderby = $queryDate; + break; + + case 'rdate': + $orderby = $queryDate . ' DESC '; + break; + + case 'alpha': + $orderby = 'a.title'; + break; + + case 'ralpha': + $orderby = 'a.title DESC'; + break; + + case 'hits': + $orderby = 'a.hits DESC'; + break; + + case 'rhits': + $orderby = 'a.hits'; + break; + + case 'rorder': + $orderby = 'a.ordering DESC'; + break; + + case 'author': + $orderby = 'author'; + break; + + case 'rauthor': + $orderby = 'author DESC'; + break; + + case 'front': + $orderby = 'a.featured DESC, fp.ordering, ' . $queryDate . ' DESC '; + break; + + case 'random': + $orderby = $db->getQuery(true)->rand(); + break; + + case 'vote': + $orderby = 'a.id DESC '; + + if (PluginHelper::isEnabled('content', 'vote')) { + $orderby = 'rating_count DESC '; + } + break; + + case 'rvote': + $orderby = 'a.id ASC '; + + if (PluginHelper::isEnabled('content', 'vote')) { + $orderby = 'rating_count ASC '; + } + break; + + case 'rank': + $orderby = 'a.id DESC '; + + if (PluginHelper::isEnabled('content', 'vote')) { + $orderby = 'rating DESC '; + } + break; + + case 'rrank': + $orderby = 'a.id ASC '; + + if (PluginHelper::isEnabled('content', 'vote')) { + $orderby = 'rating ASC '; + } + break; + + default: + $orderby = 'a.ordering'; + break; + } + + return $orderby; + } + + /** + * Translate an order code to a field for date ordering. + * + * @param string $orderDate The ordering code. + * @param DatabaseInterface $db The database + * + * @return string The SQL field(s) to order by. + * + * @since 1.6 + */ + public static function getQueryDate($orderDate, DatabaseInterface $db = null) + { + $db = $db ?: Factory::getDbo(); + + switch ($orderDate) { + case 'modified': + $queryDate = ' CASE WHEN a.modified IS NULL THEN a.created ELSE a.modified END'; + break; + + // Use created if publish_up is not set + case 'published': + $queryDate = ' CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END '; + break; + + case 'unpublished': + $queryDate = ' CASE WHEN a.publish_down IS NULL THEN a.created ELSE a.publish_down END '; + break; + case 'created': + default: + $queryDate = ' a.created '; + break; + } + + return $queryDate; + } + + /** + * Get join information for the voting query. + * + * @param \Joomla\Registry\Registry $params An options object for the article. + * + * @return array A named array with "select" and "join" keys. + * + * @since 1.5 + * + * @deprecated 5.0 Deprecated without replacement, not used in core + */ + public static function buildVotingQuery($params = null) + { + if (!$params) { + $params = ComponentHelper::getParams('com_content'); + } + + $voting = $params->get('show_vote'); + + if ($voting) { + // Calculate voting count + $select = ' , ROUND(v.rating_sum / v.rating_count) AS rating, v.rating_count'; + $join = ' LEFT JOIN #__content_rating AS v ON a.id = v.content_id'; + } else { + $select = ''; + $join = ''; + } + + return array('select' => $select, 'join' => $join); + } } diff --git a/code/components/com_content/src/Helper/RouteHelper.php b/code/components/com_content/src/Helper/RouteHelper.php index 91b3441f..36e0622c 100644 --- a/code/components/com_content/src/Helper/RouteHelper.php +++ b/code/components/com_content/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ 1) - { - $link .= '&catid=' . $catid; - } - - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } - - if ($layout) - { - $link .= '&layout=' . $layout; - } - - return $link; - } - - /** - * Get the category route. - * - * @param integer $catid The category ID. - * @param integer $language The language code. - * @param string $layout The layout value. - * - * @return string The article route. - * - * @since 1.5 - */ - public static function getCategoryRoute($catid, $language = 0, $layout = null) - { - if ($catid instanceof CategoryNode) - { - $id = $catid->id; - } - else - { - $id = (int) $catid; - } - - if ($id < 1) - { - return ''; - } - - $link = 'index.php?option=com_content&view=category&id=' . $id; - - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } - - if ($layout) - { - $link .= '&layout=' . $layout; - } - - return $link; - } - - /** - * Get the form route. - * - * @param integer $id The form ID. - * - * @return string The article route. - * - * @since 1.5 - */ - public static function getFormRoute($id) - { - return 'index.php?option=com_content&task=article.edit&a_id=' . (int) $id; - } + /** + * Get the article route. + * + * @param integer $id The route of the content item. + * @param integer $catid The category ID. + * @param integer $language The language code. + * @param string $layout The layout value. + * + * @return string The article route. + * + * @since 1.5 + */ + public static function getArticleRoute($id, $catid = 0, $language = 0, $layout = null) + { + // Create the link + $link = 'index.php?option=com_content&view=article&id=' . $id; + + if ((int) $catid > 1) { + $link .= '&catid=' . $catid; + } + + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } + + if ($layout) { + $link .= '&layout=' . $layout; + } + + return $link; + } + + /** + * Get the category route. + * + * @param integer $catid The category ID. + * @param integer $language The language code. + * @param string $layout The layout value. + * + * @return string The article route. + * + * @since 1.5 + */ + public static function getCategoryRoute($catid, $language = 0, $layout = null) + { + if ($catid instanceof CategoryNode) { + $id = $catid->id; + } else { + $id = (int) $catid; + } + + if ($id < 1) { + return ''; + } + + $link = 'index.php?option=com_content&view=category&id=' . $id; + + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } + + if ($layout) { + $link .= '&layout=' . $layout; + } + + return $link; + } + + /** + * Get the form route. + * + * @param integer $id The form ID. + * + * @return string The article route. + * + * @since 1.5 + */ + public static function getFormRoute($id) + { + return 'index.php?option=com_content&task=article.edit&a_id=' . (int) $id; + } } diff --git a/code/components/com_content/src/Model/ArchiveModel.php b/code/components/com_content/src/Model/ArchiveModel.php index 48ebd8ba..c6935eb8 100644 --- a/code/components/com_content/src/Model/ArchiveModel.php +++ b/code/components/com_content/src/Model/ArchiveModel.php @@ -1,4 +1,5 @@ state->get('params'); - - // Filter on archived articles - $this->setState('filter.published', ContentComponent::CONDITION_ARCHIVED); - - // Filter on month, year - $this->setState('filter.month', $app->input->getInt('month')); - $this->setState('filter.year', $app->input->getInt('year')); - - // Optional filter text - $this->setState('list.filter', $app->input->getString('filter-search')); - - // Get list limit - $itemid = $app->input->get('Itemid', 0, 'int'); - $limit = $app->getUserStateFromRequest('com_content.archive.list' . $itemid . '.limit', 'limit', $params->get('display_num', 20), 'uint'); - $this->setState('list.limit', $limit); - - // Set the archive ordering - $articleOrderby = $params->get('orderby_sec', 'rdate'); - $articleOrderDate = $params->get('order_date'); - - // No category ordering - $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDbo()); - - $this->setState('list.ordering', $secondary . ', a.created DESC'); - $this->setState('list.direction', ''); - } - - /** - * Get the master query for retrieving a list of articles subject to the model state. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - $params = $this->state->params; - $app = Factory::getApplication(); - $catids = $app->input->get('catid', array(), 'array'); - $catids = array_values(array_diff($catids, array(''))); - - $articleOrderDate = $params->get('order_date'); - - // Create a new query object. - $db = $this->getDbo(); - $query = parent::getListQuery(); - - // Add routing for archive - $query->select( - [ - $this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS ' . $db->quoteName('slug'), - $this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS ' . $db->quoteName('catslug'), - ] - ); - - // Filter on month, year - // First, get the date field - $queryDate = QueryHelper::getQueryDate($articleOrderDate, $this->getDbo()); - - if ($month = (int) $this->getState('filter.month')) - { - $query->where($query->month($queryDate) . ' = :month') - ->bind(':month', $month, ParameterType::INTEGER); - } - - if ($year = (int) $this->getState('filter.year')) - { - $query->where($query->year($queryDate) . ' = :year') - ->bind(':year', $year, ParameterType::INTEGER); - } - - if (count($catids) > 0) - { - $query->whereIn($db->quoteName('c.id'), $catids); - } - - return $query; - } - - /** - * Method to get the archived article list - * - * @access public - * @return array - */ - public function getData() - { - $app = Factory::getApplication(); - - // Lets load the content if it doesn't already exist - if (empty($this->_data)) - { - // Get the page/component configuration - $params = $app->getParams(); - - // Get the pagination request variables - $limit = $app->input->get('limit', $params->get('display_num', 20), 'uint'); - $limitstart = $app->input->get('limitstart', 0, 'uint'); - - $query = $this->_buildQuery(); - - $this->_data = $this->_getList($query, $limitstart, $limit); - } - - return $this->_data; - } - - /** - * Gets the archived articles years - * - * @return array - * - * @since 3.6.0 - */ - public function getYears() - { - $db = $this->getDbo(); - $nowDate = Factory::getDate()->toSql(); - $query = $db->getQuery(true); - $years = $query->year($db->quoteName('c.created')); - - $query->select('DISTINCT ' . $years) - ->from($db->quoteName('#__content', 'c')) - ->where($db->quoteName('c.state') . ' = ' . ContentComponent::CONDITION_ARCHIVED) - ->extendWhere( - 'AND', - [ - $db->quoteName('c.publish_up') . ' IS NULL', - $db->quoteName('c.publish_up') . ' <= :publishUp', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('c.publish_down') . ' IS NULL', - $db->quoteName('c.publish_down') . ' >= :publishDown', - ], - 'OR' - ) - ->bind(':publishUp', $nowDate) - ->bind(':publishDown', $nowDate) - ->order('1 ASC'); - - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Generate column expression for slug or catslug. - * - * @param \Joomla\Database\DatabaseQuery $query Current query instance. - * @param string $id Column id name. - * @param string $alias Column alias name. - * - * @return string - * - * @since 4.0.0 - */ - private function getSlugColumn($query, $id, $alias) - { - $db = $this->getDbo(); - - return 'CASE WHEN ' - . $query->charLength($db->quoteName($alias), '!=', '0') - . ' THEN ' - . $query->concatenate([$query->castAsChar($db->quoteName($id)), $db->quoteName($alias)], ':') - . ' ELSE ' - . $query->castAsChar($id) . ' END'; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_content.archive'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering The field to order on. + * @param string $direction The direction to order on. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + parent::populateState(); + + $app = Factory::getApplication(); + + // Add archive properties + $params = $this->state->get('params'); + + // Filter on archived articles + $this->setState('filter.published', ContentComponent::CONDITION_ARCHIVED); + + // Filter on month, year + $this->setState('filter.month', $app->input->getInt('month')); + $this->setState('filter.year', $app->input->getInt('year')); + + // Optional filter text + $this->setState('list.filter', $app->input->getString('filter-search')); + + // Get list limit + $itemid = $app->input->get('Itemid', 0, 'int'); + $limit = $app->getUserStateFromRequest('com_content.archive.list' . $itemid . '.limit', 'limit', $params->get('display_num', 20), 'uint'); + $this->setState('list.limit', $limit); + + // Set the archive ordering + $articleOrderby = $params->get('orderby_sec', 'rdate'); + $articleOrderDate = $params->get('order_date'); + + // No category ordering + $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()); + + $this->setState('list.ordering', $secondary . ', a.created DESC'); + $this->setState('list.direction', ''); + } + + /** + * Get the master query for retrieving a list of articles subject to the model state. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + $params = $this->state->params; + $app = Factory::getApplication(); + $catids = $app->input->get('catid', array(), 'array'); + $catids = array_values(array_diff($catids, array(''))); + + $articleOrderDate = $params->get('order_date'); + + // Create a new query object. + $db = $this->getDatabase(); + $query = parent::getListQuery(); + + // Add routing for archive + $query->select( + [ + $this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS ' . $db->quoteName('slug'), + $this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS ' . $db->quoteName('catslug'), + ] + ); + + // Filter on month, year + // First, get the date field + $queryDate = QueryHelper::getQueryDate($articleOrderDate, $this->getDatabase()); + + if ($month = (int) $this->getState('filter.month')) { + $query->where($query->month($queryDate) . ' = :month') + ->bind(':month', $month, ParameterType::INTEGER); + } + + if ($year = (int) $this->getState('filter.year')) { + $query->where($query->year($queryDate) . ' = :year') + ->bind(':year', $year, ParameterType::INTEGER); + } + + if (count($catids) > 0) { + $query->whereIn($db->quoteName('c.id'), $catids); + } + + return $query; + } + + /** + * Method to get the archived article list + * + * @access public + * @return array + */ + public function getData() + { + $app = Factory::getApplication(); + + // Lets load the content if it doesn't already exist + if (empty($this->_data)) { + // Get the page/component configuration + $params = $app->getParams(); + + // Get the pagination request variables + $limit = $app->input->get('limit', $params->get('display_num', 20), 'uint'); + $limitstart = $app->input->get('limitstart', 0, 'uint'); + + $query = $this->_buildQuery(); + + $this->_data = $this->_getList($query, $limitstart, $limit); + } + + return $this->_data; + } + + /** + * Gets the archived articles years + * + * @return array + * + * @since 3.6.0 + */ + public function getYears() + { + $db = $this->getDatabase(); + $nowDate = Factory::getDate()->toSql(); + $query = $db->getQuery(true); + $years = $query->year($db->quoteName('c.created')); + + $query->select('DISTINCT ' . $years) + ->from($db->quoteName('#__content', 'c')) + ->where($db->quoteName('c.state') . ' = ' . ContentComponent::CONDITION_ARCHIVED) + ->extendWhere( + 'AND', + [ + $db->quoteName('c.publish_up') . ' IS NULL', + $db->quoteName('c.publish_up') . ' <= :publishUp', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('c.publish_down') . ' IS NULL', + $db->quoteName('c.publish_down') . ' >= :publishDown', + ], + 'OR' + ) + ->bind(':publishUp', $nowDate) + ->bind(':publishDown', $nowDate) + ->order('1 ASC'); + + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Generate column expression for slug or catslug. + * + * @param \Joomla\Database\DatabaseQuery $query Current query instance. + * @param string $id Column id name. + * @param string $alias Column alias name. + * + * @return string + * + * @since 4.0.0 + */ + private function getSlugColumn($query, $id, $alias) + { + $db = $this->getDatabase(); + + return 'CASE WHEN ' + . $query->charLength($db->quoteName($alias), '!=', '0') + . ' THEN ' + . $query->concatenate([$query->castAsChar($db->quoteName($id)), $db->quoteName($alias)], ':') + . ' ELSE ' + . $query->castAsChar($id) . ' END'; + } } diff --git a/code/components/com_content/src/Model/ArticleModel.php b/code/components/com_content/src/Model/ArticleModel.php index 31274c5a..6d7764f7 100644 --- a/code/components/com_content/src/Model/ArticleModel.php +++ b/code/components/com_content/src/Model/ArticleModel.php @@ -1,4 +1,5 @@ input->getInt('id'); - $this->setState('article.id', $pk); - - $offset = $app->input->getUint('limitstart'); - $this->setState('list.offset', $offset); - - // Load the parameters. - $params = $app->getParams(); - $this->setState('params', $params); - - $user = Factory::getUser(); - - // If $pk is set then authorise on complete asset, else on component only - $asset = empty($pk) ? 'com_content' : 'com_content.article.' . $pk; - - if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) - { - $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); - $this->setState('filter.archived', ContentComponent::CONDITION_ARCHIVED); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - } - - /** - * Method to get article data. - * - * @param integer $pk The id of the article. - * - * @return object|boolean Menu item data object on success, boolean false - */ - public function getItem($pk = null) - { - $user = Factory::getUser(); - - $pk = (int) ($pk ?: $this->getState('article.id')); - - if ($this->_item === null) - { - $this->_item = array(); - } - - if (!isset($this->_item[$pk])) - { - try - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query->select( - $this->getState( - 'item.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.asset_id'), - $db->quoteName('a.title'), - $db->quoteName('a.alias'), - $db->quoteName('a.introtext'), - $db->quoteName('a.fulltext'), - $db->quoteName('a.state'), - $db->quoteName('a.catid'), - $db->quoteName('a.created'), - $db->quoteName('a.created_by'), - $db->quoteName('a.created_by_alias'), - $db->quoteName('a.modified'), - $db->quoteName('a.modified_by'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - $db->quoteName('a.images'), - $db->quoteName('a.urls'), - $db->quoteName('a.attribs'), - $db->quoteName('a.version'), - $db->quoteName('a.ordering'), - $db->quoteName('a.metakey'), - $db->quoteName('a.metadesc'), - $db->quoteName('a.access'), - $db->quoteName('a.hits'), - $db->quoteName('a.metadata'), - $db->quoteName('a.featured'), - $db->quoteName('a.language'), - ] - ) - ) - ->select( - [ - $db->quoteName('fp.featured_up'), - $db->quoteName('fp.featured_down'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('c.alias', 'category_alias'), - $db->quoteName('c.access', 'category_access'), - $db->quoteName('c.language', 'category_language'), - $db->quoteName('fp.ordering'), - $db->quoteName('u.name', 'author'), - $db->quoteName('parent.title', 'parent_title'), - $db->quoteName('parent.id', 'parent_id'), - $db->quoteName('parent.path', 'parent_route'), - $db->quoteName('parent.alias', 'parent_alias'), - $db->quoteName('parent.language', 'parent_language'), - 'ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1) AS ' - . $db->quoteName('rating'), - $db->quoteName('v.rating_count', 'rating_count'), - ] - ) - ->from($db->quoteName('#__content', 'a')) - ->join( - 'INNER', - $db->quoteName('#__categories', 'c'), - $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid') - ) - ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by')) - ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) - ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')) - ->where( - [ - $db->quoteName('a.id') . ' = :pk', - $db->quoteName('c.published') . ' > 0', - ] - ) - ->bind(':pk', $pk, ParameterType::INTEGER); - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - if (!$user->authorise('core.edit.state', 'com_content.article.' . $pk) - && !$user->authorise('core.edit', 'com_content.article.' . $pk) - ) - { - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_up') . ' IS NULL', - $db->quoteName('a.publish_up') . ' <= :publishUp', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_down') . ' IS NULL', - $db->quoteName('a.publish_down') . ' >= :publishDown', - ], - 'OR' - ) - ->bind([':publishUp', ':publishDown'], $nowDate); - } - - // Filter by published state. - $published = $this->getState('filter.published'); - $archived = $this->getState('filter.archived'); - - if (is_numeric($published)) - { - $query->whereIn($db->quoteName('a.state'), [(int) $published, (int) $archived]); - } - - $db->setQuery($query); - - $data = $db->loadObject(); - - if (empty($data)) - { - throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); - } - - // Check for published state if filter set. - if ((is_numeric($published) || is_numeric($archived)) && ($data->state != $published && $data->state != $archived)) - { - throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); - } - - // Convert parameter fields to objects. - $registry = new Registry($data->attribs); - - $data->params = clone $this->getState('params'); - $data->params->merge($registry); - - $data->metadata = new Registry($data->metadata); - - // Technically guest could edit an article, but lets not check that to improve performance a little. - if (!$user->get('guest')) - { - $userId = $user->get('id'); - $asset = 'com_content.article.' . $data->id; - - // Check general edit permission first. - if ($user->authorise('core.edit', $asset)) - { - $data->params->set('access-edit', true); - } - - // Now check if edit.own is available. - elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) - { - // Check for a valid user and that they are the owner. - if ($userId == $data->created_by) - { - $data->params->set('access-edit', true); - } - } - } - - // Compute view access permissions. - if ($access = $this->getState('filter.access')) - { - // If the access filter has been set, we already know this user can view. - $data->params->set('access-view', true); - } - else - { - // If no access filter is set, the layout takes some responsibility for display of limited information. - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - if ($data->catid == 0 || $data->category_access === null) - { - $data->params->set('access-view', in_array($data->access, $groups)); - } - else - { - $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); - } - } - - $this->_item[$pk] = $data; - } - catch (\Exception $e) - { - if ($e->getCode() == 404) - { - // Need to go through the error handler to allow Redirect to work. - throw $e; - } - else - { - $this->setError($e); - $this->_item[$pk] = false; - } - } - } - - return $this->_item[$pk]; - } - - /** - * Increment the hit counter for the article. - * - * @param integer $pk Optional primary key of the article to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('article.id'); - - $table = Table::getInstance('Content', 'JTable'); - $table->hit($pk); - } - - return true; - } - - /** - * Save user vote on article - * - * @param integer $pk Joomla Article Id - * @param integer $rate Voting rate - * - * @return boolean Return true on success - */ - public function storeVote($pk = 0, $rate = 0) - { - $pk = (int) $pk; - $rate = (int) $rate; - - if ($rate >= 1 && $rate <= 5 && $pk > 0) - { - $userIP = IpHelper::getIp(); - - // Initialize variables. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Create the base select statement. - $query->select('*') - ->from($db->quoteName('#__content_rating')) - ->where($db->quoteName('content_id') . ' = :pk') - ->bind(':pk', $pk, ParameterType::INTEGER); - - // Set the query and load the result. - $db->setQuery($query); - - // Check for a database error. - try - { - $rating = $db->loadObject(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // There are no ratings yet, so lets insert our rating - if (!$rating) - { - $query = $db->getQuery(true); - - // Create the base insert statement. - $query->insert($db->quoteName('#__content_rating')) - ->columns( - [ - $db->quoteName('content_id'), - $db->quoteName('lastip'), - $db->quoteName('rating_sum'), - $db->quoteName('rating_count'), - ] - ) - ->values(':pk, :ip, :rate, 1') - ->bind(':pk', $pk, ParameterType::INTEGER) - ->bind(':ip', $userIP) - ->bind(':rate', $rate, ParameterType::INTEGER); - - // Set the query and execute the insert. - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - else - { - if ($userIP != $rating->lastip) - { - $query = $db->getQuery(true); - - // Create the base update statement. - $query->update($db->quoteName('#__content_rating')) - ->set( - [ - $db->quoteName('rating_count') . ' = ' . $db->quoteName('rating_count') . ' + 1', - $db->quoteName('rating_sum') . ' = ' . $db->quoteName('rating_sum') . ' + :rate', - $db->quoteName('lastip') . ' = :ip', - ] - ) - ->where($db->quoteName('content_id') . ' = :pk') - ->bind(':rate', $rate, ParameterType::INTEGER) - ->bind(':ip', $userIP) - ->bind(':pk', $pk, ParameterType::INTEGER); - - // Set the query and execute the update. - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - else - { - return false; - } - } - - $this->cleanCache(); - - return true; - } - - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_CONTENT_INVALID_RATING', $rate), 'error'); - - return false; - } - - /** - * Cleans the cache of com_content and content modules - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 3.9.9 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_content'); - parent::cleanCache('mod_articles_archive'); - parent::cleanCache('mod_articles_categories'); - parent::cleanCache('mod_articles_category'); - parent::cleanCache('mod_articles_latest'); - parent::cleanCache('mod_articles_news'); - parent::cleanCache('mod_articles_popular'); - } + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_content.article'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @since 1.6 + * + * @return void + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load state from the request. + $pk = $app->input->getInt('id'); + $this->setState('article.id', $pk); + + $offset = $app->input->getUint('limitstart'); + $this->setState('list.offset', $offset); + + // Load the parameters. + $params = $app->getParams(); + $this->setState('params', $params); + + $user = Factory::getUser(); + + // If $pk is set then authorise on complete asset, else on component only + $asset = empty($pk) ? 'com_content' : 'com_content.article.' . $pk; + + if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) { + $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); + $this->setState('filter.archived', ContentComponent::CONDITION_ARCHIVED); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + } + + /** + * Method to get article data. + * + * @param integer $pk The id of the article. + * + * @return object|boolean Menu item data object on success, boolean false + */ + public function getItem($pk = null) + { + $user = Factory::getUser(); + + $pk = (int) ($pk ?: $this->getState('article.id')); + + if ($this->_item === null) { + $this->_item = array(); + } + + if (!isset($this->_item[$pk])) { + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + $this->getState( + 'item.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.asset_id'), + $db->quoteName('a.title'), + $db->quoteName('a.alias'), + $db->quoteName('a.introtext'), + $db->quoteName('a.fulltext'), + $db->quoteName('a.state'), + $db->quoteName('a.catid'), + $db->quoteName('a.created'), + $db->quoteName('a.created_by'), + $db->quoteName('a.created_by_alias'), + $db->quoteName('a.modified'), + $db->quoteName('a.modified_by'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + $db->quoteName('a.images'), + $db->quoteName('a.urls'), + $db->quoteName('a.attribs'), + $db->quoteName('a.version'), + $db->quoteName('a.ordering'), + $db->quoteName('a.metakey'), + $db->quoteName('a.metadesc'), + $db->quoteName('a.access'), + $db->quoteName('a.hits'), + $db->quoteName('a.metadata'), + $db->quoteName('a.featured'), + $db->quoteName('a.language'), + ] + ) + ) + ->select( + [ + $db->quoteName('fp.featured_up'), + $db->quoteName('fp.featured_down'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('c.alias', 'category_alias'), + $db->quoteName('c.access', 'category_access'), + $db->quoteName('c.language', 'category_language'), + $db->quoteName('fp.ordering'), + $db->quoteName('u.name', 'author'), + $db->quoteName('parent.title', 'parent_title'), + $db->quoteName('parent.id', 'parent_id'), + $db->quoteName('parent.path', 'parent_route'), + $db->quoteName('parent.alias', 'parent_alias'), + $db->quoteName('parent.language', 'parent_language'), + 'ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1) AS ' + . $db->quoteName('rating'), + $db->quoteName('v.rating_count', 'rating_count'), + ] + ) + ->from($db->quoteName('#__content', 'a')) + ->join( + 'INNER', + $db->quoteName('#__categories', 'c'), + $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid') + ) + ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by')) + ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) + ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')) + ->where( + [ + $db->quoteName('a.id') . ' = :pk', + $db->quoteName('c.published') . ' > 0', + ] + ) + ->bind(':pk', $pk, ParameterType::INTEGER); + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + if ( + !$user->authorise('core.edit.state', 'com_content.article.' . $pk) + && !$user->authorise('core.edit', 'com_content.article.' . $pk) + ) { + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_up') . ' IS NULL', + $db->quoteName('a.publish_up') . ' <= :publishUp', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_down') . ' IS NULL', + $db->quoteName('a.publish_down') . ' >= :publishDown', + ], + 'OR' + ) + ->bind([':publishUp', ':publishDown'], $nowDate); + } + + // Filter by published state. + $published = $this->getState('filter.published'); + $archived = $this->getState('filter.archived'); + + if (is_numeric($published)) { + $query->whereIn($db->quoteName('a.state'), [(int) $published, (int) $archived]); + } + + $db->setQuery($query); + + $data = $db->loadObject(); + + if (empty($data)) { + throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); + } + + // Check for published state if filter set. + if ((is_numeric($published) || is_numeric($archived)) && ($data->state != $published && $data->state != $archived)) { + throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); + } + + // Convert parameter fields to objects. + $registry = new Registry($data->attribs); + + $data->params = clone $this->getState('params'); + $data->params->merge($registry); + + $data->metadata = new Registry($data->metadata); + + // Technically guest could edit an article, but lets not check that to improve performance a little. + if (!$user->get('guest')) { + $userId = $user->get('id'); + $asset = 'com_content.article.' . $data->id; + + // Check general edit permission first. + if ($user->authorise('core.edit', $asset)) { + $data->params->set('access-edit', true); + } elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) { + // Now check if edit.own is available. + // Check for a valid user and that they are the owner. + if ($userId == $data->created_by) { + $data->params->set('access-edit', true); + } + } + } + + // Compute view access permissions. + if ($access = $this->getState('filter.access')) { + // If the access filter has been set, we already know this user can view. + $data->params->set('access-view', true); + } else { + // If no access filter is set, the layout takes some responsibility for display of limited information. + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + if ($data->catid == 0 || $data->category_access === null) { + $data->params->set('access-view', in_array($data->access, $groups)); + } else { + $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); + } + } + + $this->_item[$pk] = $data; + } catch (\Exception $e) { + if ($e->getCode() == 404) { + // Need to go through the error handler to allow Redirect to work. + throw $e; + } else { + $this->setError($e); + $this->_item[$pk] = false; + } + } + } + + return $this->_item[$pk]; + } + + /** + * Increment the hit counter for the article. + * + * @param integer $pk Optional primary key of the article to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('article.id'); + + $table = Table::getInstance('Content', 'JTable'); + $table->hit($pk); + } + + return true; + } + + /** + * Save user vote on article + * + * @param integer $pk Joomla Article Id + * @param integer $rate Voting rate + * + * @return boolean Return true on success + */ + public function storeVote($pk = 0, $rate = 0) + { + $pk = (int) $pk; + $rate = (int) $rate; + + if ($rate >= 1 && $rate <= 5 && $pk > 0) { + $userIP = IpHelper::getIp(); + + // Initialize variables. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Create the base select statement. + $query->select('*') + ->from($db->quoteName('#__content_rating')) + ->where($db->quoteName('content_id') . ' = :pk') + ->bind(':pk', $pk, ParameterType::INTEGER); + + // Set the query and load the result. + $db->setQuery($query); + + // Check for a database error. + try { + $rating = $db->loadObject(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // There are no ratings yet, so lets insert our rating + if (!$rating) { + $query = $db->getQuery(true); + + // Create the base insert statement. + $query->insert($db->quoteName('#__content_rating')) + ->columns( + [ + $db->quoteName('content_id'), + $db->quoteName('lastip'), + $db->quoteName('rating_sum'), + $db->quoteName('rating_count'), + ] + ) + ->values(':pk, :ip, :rate, 1') + ->bind(':pk', $pk, ParameterType::INTEGER) + ->bind(':ip', $userIP) + ->bind(':rate', $rate, ParameterType::INTEGER); + + // Set the query and execute the insert. + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } else { + if ($userIP != $rating->lastip) { + $query = $db->getQuery(true); + + // Create the base update statement. + $query->update($db->quoteName('#__content_rating')) + ->set( + [ + $db->quoteName('rating_count') . ' = ' . $db->quoteName('rating_count') . ' + 1', + $db->quoteName('rating_sum') . ' = ' . $db->quoteName('rating_sum') . ' + :rate', + $db->quoteName('lastip') . ' = :ip', + ] + ) + ->where($db->quoteName('content_id') . ' = :pk') + ->bind(':rate', $rate, ParameterType::INTEGER) + ->bind(':ip', $userIP) + ->bind(':pk', $pk, ParameterType::INTEGER); + + // Set the query and execute the update. + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } else { + return false; + } + } + + $this->cleanCache(); + + return true; + } + + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_CONTENT_INVALID_RATING', $rate), 'error'); + + return false; + } + + /** + * Cleans the cache of com_content and content modules + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 3.9.9 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_content'); + parent::cleanCache('mod_articles_archive'); + parent::cleanCache('mod_articles_categories'); + parent::cleanCache('mod_articles_category'); + parent::cleanCache('mod_articles_latest'); + parent::cleanCache('mod_articles_news'); + parent::cleanCache('mod_articles_popular'); + } } diff --git a/code/components/com_content/src/Model/ArticlesModel.php b/code/components/com_content/src/Model/ArticlesModel.php index a567cb14..7846f631 100644 --- a/code/components/com_content/src/Model/ArticlesModel.php +++ b/code/components/com_content/src/Model/ArticlesModel.php @@ -1,4 +1,5 @@ input->get('limit', $app->get('list_limit', 0), 'uint'); - $this->setState('list.limit', $value); - - $value = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $value); - - $value = $app->input->get('filter_tag', 0, 'uint'); - $this->setState('filter.tag', $value); - - $orderCol = $app->input->get('filter_order', 'a.ordering'); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'a.ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->input->get('filter_order_Dir', 'ASC'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $params = $app->getParams(); - $this->setState('params', $params); - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) - { - // Filter on published for those who do not have edit or edit.state rights. - $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - // Process show_noauth parameter - if ((!$params->get('show_noauth')) || (!ComponentHelper::getParams('com_content')->get('show_noauth'))) - { - $this->setState('filter.access', true); - } - else - { - $this->setState('filter.access', false); - } - - $this->setState('layout', $app->input->getString('layout')); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . serialize($this->getState('filter.published')); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.featured'); - $id .= ':' . serialize($this->getState('filter.article_id')); - $id .= ':' . $this->getState('filter.article_id.include'); - $id .= ':' . serialize($this->getState('filter.category_id')); - $id .= ':' . $this->getState('filter.category_id.include'); - $id .= ':' . serialize($this->getState('filter.author_id')); - $id .= ':' . $this->getState('filter.author_id.include'); - $id .= ':' . serialize($this->getState('filter.author_alias')); - $id .= ':' . $this->getState('filter.author_alias.include'); - $id .= ':' . $this->getState('filter.date_filtering'); - $id .= ':' . $this->getState('filter.date_field'); - $id .= ':' . $this->getState('filter.start_date_range'); - $id .= ':' . $this->getState('filter.end_date_range'); - $id .= ':' . $this->getState('filter.relative_date'); - $id .= ':' . serialize($this->getState('filter.tag')); - - return parent::getStoreId($id); - } - - /** - * Get the master query for retrieving a list of articles subject to the model state. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Get the current user for authorisation checks - $user = Factory::getUser(); - - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $nowDate = Factory::getDate()->toSql(); - - $conditionArchived = ContentComponent::CONDITION_ARCHIVED; - $conditionUnpublished = ContentComponent::CONDITION_UNPUBLISHED; - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.title'), - $db->quoteName('a.alias'), - $db->quoteName('a.introtext'), - $db->quoteName('a.fulltext'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.catid'), - $db->quoteName('a.created'), - $db->quoteName('a.created_by'), - $db->quoteName('a.created_by_alias'), - $db->quoteName('a.modified'), - $db->quoteName('a.modified_by'), - // Use created if publish_up is null - 'CASE WHEN ' . $db->quoteName('a.publish_up') . ' IS NULL THEN ' . $db->quoteName('a.created') - . ' ELSE ' . $db->quoteName('a.publish_up') . ' END AS ' . $db->quoteName('publish_up'), - $db->quoteName('a.publish_down'), - $db->quoteName('a.images'), - $db->quoteName('a.urls'), - $db->quoteName('a.attribs'), - $db->quoteName('a.metadata'), - $db->quoteName('a.metakey'), - $db->quoteName('a.metadesc'), - $db->quoteName('a.access'), - $db->quoteName('a.hits'), - $db->quoteName('a.featured'), - $db->quoteName('a.language'), - $query->length($db->quoteName('a.fulltext')) . ' AS ' . $db->quoteName('readmore'), - $db->quoteName('a.ordering'), - ] - ) - ) - ->select( - [ - $db->quoteName('fp.featured_up'), - $db->quoteName('fp.featured_down'), - // Published/archived article in archived category is treated as archived article. If category is not published then force 0. - 'CASE WHEN ' . $db->quoteName('c.published') . ' = 2 AND ' . $db->quoteName('a.state') . ' > 0 THEN ' . $conditionArchived - . ' WHEN ' . $db->quoteName('c.published') . ' != 1 THEN ' . $conditionUnpublished - . ' ELSE ' . $db->quoteName('a.state') . ' END AS ' . $db->quoteName('state'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('c.path', 'category_route'), - $db->quoteName('c.access', 'category_access'), - $db->quoteName('c.alias', 'category_alias'), - $db->quoteName('c.language', 'category_language'), - $db->quoteName('c.published'), - $db->quoteName('c.published', 'parents_published'), - $db->quoteName('c.lft'), - 'CASE WHEN ' . $db->quoteName('a.created_by_alias') . ' > ' . $db->quote(' ') . ' THEN ' . $db->quoteName('a.created_by_alias') - . ' ELSE ' . $db->quoteName('ua.name') . ' END AS ' . $db->quoteName('author'), - $db->quoteName('ua.email', 'author_email'), - $db->quoteName('uam.name', 'modified_by_name'), - $db->quoteName('parent.title', 'parent_title'), - $db->quoteName('parent.id', 'parent_id'), - $db->quoteName('parent.path', 'parent_route'), - $db->quoteName('parent.alias', 'parent_alias'), - $db->quoteName('parent.language', 'parent_language'), - ] - ) - ->from($db->quoteName('#__content', 'a')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')) - ->join('LEFT', $db->quoteName('#__users', 'uam'), $db->quoteName('uam.id') . ' = ' . $db->quoteName('a.modified_by')) - ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')); - - $params = $this->getState('params'); - $orderby_sec = $params->get('orderby_sec'); - - // Join over the frontpage articles if required. - $frontpageJoin = 'LEFT'; - - if ($this->getState('filter.frontpage')) - { - if ($orderby_sec === 'front') - { - $query->select($db->quoteName('fp.ordering')); - $frontpageJoin = 'INNER'; - } - else - { - $query->where($db->quoteName('a.featured') . ' = 1'); - } - - $query->where( - [ - '(' . $db->quoteName('fp.featured_up') . ' IS NULL OR ' . $db->quoteName('fp.featured_up') . ' <= :frontpageUp)', - '(' . $db->quoteName('fp.featured_down') . ' IS NULL OR ' . $db->quoteName('fp.featured_down') . ' >= :frontpageDown)', - ] - ) - ->bind(':frontpageUp', $nowDate) - ->bind(':frontpageDown', $nowDate); - } - elseif ($orderby_sec === 'front' || $this->getState('list.ordering') === 'fp.ordering') - { - $query->select($db->quoteName('fp.ordering')); - } - - $query->join($frontpageJoin, $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')); - - if (PluginHelper::isEnabled('content', 'vote')) - { - // Join on voting table - $query->select( - [ - 'COALESCE(NULLIF(ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1), 0), 0)' - . ' AS ' . $db->quoteName('rating'), - 'COALESCE(NULLIF(' . $db->quoteName('v.rating_count') . ', 0), 0) AS ' . $db->quoteName('rating_count'), - ] - ) - ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')); - } - - // Filter by access level. - if ($this->getState('filter.access', true)) - { - $groups = $this->getState('filter.viewlevels', $user->getAuthorisedViewLevels()); - $query->whereIn($db->quoteName('a.access'), $groups) - ->whereIn($db->quoteName('c.access'), $groups); - } - - // Filter by published state - $condition = $this->getState('filter.published'); - - if (is_numeric($condition) && $condition == 2) - { - /** - * If category is archived then article has to be published or archived. - * Or category is published then article has to be archived. - */ - $query->where('((' . $db->quoteName('c.published') . ' = 2 AND ' . $db->quoteName('a.state') . ' > :conditionUnpublished)' - . ' OR (' . $db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') . ' = :conditionArchived))' - ) - ->bind(':conditionUnpublished', $conditionUnpublished, ParameterType::INTEGER) - ->bind(':conditionArchived', $conditionArchived, ParameterType::INTEGER); - } - elseif (is_numeric($condition)) - { - $condition = (int) $condition; - - // Category has to be published - $query->where($db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') . ' = :condition') - ->bind(':condition', $condition, ParameterType::INTEGER); - } - elseif (is_array($condition)) - { - // Category has to be published - $query->where( - $db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') - . ' IN (' . implode(',', $query->bindArray($condition)) . ')' - ); - } - - // Filter by featured state - $featured = $this->getState('filter.featured'); - - switch ($featured) - { - case 'hide': - $query->where($db->quoteName('a.featured') . ' = 0'); - break; - - case 'only': - $query->where( - [ - $db->quoteName('a.featured') . ' = 1', - '(' . $db->quoteName('fp.featured_up') . ' IS NULL OR ' . $db->quoteName('fp.featured_up') . ' <= :featuredUp)', - '(' . $db->quoteName('fp.featured_down') . ' IS NULL OR ' . $db->quoteName('fp.featured_down') . ' >= :featuredDown)', - ] - ) - ->bind(':featuredUp', $nowDate) - ->bind(':featuredDown', $nowDate); - break; - - case 'show': - default: - // Normally we do not discriminate between featured/unfeatured items. - break; - } - - // Filter by a single or group of articles. - $articleId = $this->getState('filter.article_id'); - - if (is_numeric($articleId)) - { - $articleId = (int) $articleId; - $type = $this->getState('filter.article_id.include', true) ? ' = ' : ' <> '; - $query->where($db->quoteName('a.id') . $type . ':articleId') - ->bind(':articleId', $articleId, ParameterType::INTEGER); - } - elseif (is_array($articleId)) - { - $articleId = ArrayHelper::toInteger($articleId); - - if ($this->getState('filter.article_id.include', true)) - { - $query->whereIn($db->quoteName('a.id'), $articleId); - } - else - { - $query->whereNotIn($db->quoteName('a.id'), $articleId); - } - } - - // Filter by a single or group of categories - $categoryId = $this->getState('filter.category_id'); - - if (is_numeric($categoryId)) - { - $type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> '; - - // Add subcategory check - $includeSubcategories = $this->getState('filter.subcategories', false); - - if ($includeSubcategories) - { - $categoryId = (int) $categoryId; - $levels = (int) $this->getState('filter.max_category_levels', 1); - - // Create a subquery for the subcategory list - $subQuery = $db->getQuery(true) - ->select($db->quoteName('sub.id')) - ->from($db->quoteName('#__categories', 'sub')) - ->join( - 'INNER', - $db->quoteName('#__categories', 'this'), - $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') - . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') - ) - ->where($db->quoteName('this.id') . ' = :subCategoryId'); - - $query->bind(':subCategoryId', $categoryId, ParameterType::INTEGER); - - if ($levels >= 0) - { - $subQuery->where($db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels'); - $query->bind(':levels', $levels, ParameterType::INTEGER); - } - - // Add the subquery to the main query - $query->where( - '(' . $db->quoteName('a.catid') . $type . ':categoryId OR ' . $db->quoteName('a.catid') . ' IN (' . (string) $subQuery . '))' - ); - $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - else - { - $query->where($db->quoteName('a.catid') . $type . ':categoryId'); - $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - } - elseif (is_array($categoryId) && (count($categoryId) > 0)) - { - $categoryId = ArrayHelper::toInteger($categoryId); - - if (!empty($categoryId)) - { - if ($this->getState('filter.category_id.include', true)) - { - $query->whereIn($db->quoteName('a.catid'), $categoryId); - } - else - { - $query->whereNotIn($db->quoteName('a.catid'), $categoryId); - } - } - } - - // Filter by author - $authorId = $this->getState('filter.author_id'); - $authorWhere = ''; - - if (is_numeric($authorId)) - { - $authorId = (int) $authorId; - $type = $this->getState('filter.author_id.include', true) ? ' = ' : ' <> '; - $authorWhere = $db->quoteName('a.created_by') . $type . ':authorId'; - $query->bind(':authorId', $authorId, ParameterType::INTEGER); - } - elseif (is_array($authorId)) - { - $authorId = array_values(array_filter($authorId, 'is_numeric')); - - if ($authorId) - { - $type = $this->getState('filter.author_id.include', true) ? ' IN' : ' NOT IN'; - $authorWhere = $db->quoteName('a.created_by') . $type . ' (' . implode(',', $query->bindArray($authorId)) . ')'; - } - } - - // Filter by author alias - $authorAlias = $this->getState('filter.author_alias'); - $authorAliasWhere = ''; - - if (is_string($authorAlias)) - { - $type = $this->getState('filter.author_alias.include', true) ? ' = ' : ' <> '; - $authorAliasWhere = $db->quoteName('a.created_by_alias') . $type . ':authorAlias'; - $query->bind(':authorAlias', $authorAlias); - } - elseif (\is_array($authorAlias) && !empty($authorAlias)) - { - $type = $this->getState('filter.author_alias.include', true) ? ' IN' : ' NOT IN'; - $authorAliasWhere = $db->quoteName('a.created_by_alias') . $type - . ' (' . implode(',', $query->bindArray($authorAlias, ParameterType::STRING)) . ')'; - } - - if (!empty($authorWhere) && !empty($authorAliasWhere)) - { - $query->where('(' . $authorWhere . ' OR ' . $authorAliasWhere . ')'); - } - elseif (empty($authorWhere) && empty($authorAliasWhere)) - { - // If both are empty we don't want to add to the query - } - else - { - // One of these is empty, the other is not so we just add both - $query->where($authorWhere . $authorAliasWhere); - } - - // Filter by start and end dates. - if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) - { - $query->where( - [ - '(' . $db->quoteName('a.publish_up') . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publishUp)', - '(' . $db->quoteName('a.publish_down') . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publishDown)', - ] - ) - ->bind(':publishUp', $nowDate) - ->bind(':publishDown', $nowDate); - } - - // Filter by Date Range or Relative Date - $dateFiltering = $this->getState('filter.date_filtering', 'off'); - $dateField = $db->escape($this->getState('filter.date_field', 'a.created')); - - switch ($dateFiltering) - { - case 'range': - $startDateRange = $this->getState('filter.start_date_range', ''); - $endDateRange = $this->getState('filter.end_date_range', ''); - - if ($startDateRange || $endDateRange) - { - $query->where($db->quoteName($dateField) . ' IS NOT NULL'); - - if ($startDateRange) - { - $query->where($db->quoteName($dateField) . ' >= :startDateRange') - ->bind(':startDateRange', $startDateRange); - } - - if ($endDateRange) - { - $query->where($db->quoteName($dateField) . ' <= :endDateRange') - ->bind(':endDateRange', $endDateRange); - } - } - - break; - - case 'relative': - $relativeDate = (int) $this->getState('filter.relative_date', 0); - $query->where( - $db->quoteName($dateField) . ' IS NOT NULL AND ' - . $db->quoteName($dateField) . ' >= ' . $query->dateAdd($db->quote($nowDate), -1 * $relativeDate, 'DAY') - ); - break; - - case 'off': - default: - break; - } - - // Process the filter for list views with user-entered filters - if (is_object($params) && ($params->get('filter_field') !== 'hide') && ($filter = $this->getState('list.filter'))) - { - // Clean filter variable - $filter = StringHelper::strtolower($filter); - $monthFilter = $filter; - $hitsFilter = (int) $filter; - $textFilter = '%' . $filter . '%'; - - switch ($params->get('filter_field')) - { - case 'author': - $query->where( - 'LOWER(CASE WHEN ' . $db->quoteName('a.created_by_alias') . ' > ' . $db->quote(' ') - . ' THEN ' . $db->quoteName('a.created_by_alias') . ' ELSE ' . $db->quoteName('ua.name') . ' END) LIKE :search' - ) - ->bind(':search', $textFilter); - break; - - case 'hits': - $query->where($db->quoteName('a.hits') . ' >= :hits') - ->bind(':hits', $hitsFilter, ParameterType::INTEGER); - break; - - case 'month': - if ($monthFilter != '') - { - $monthStart = date("Y-m-d", strtotime($monthFilter)) . ' 00:00:00'; - $monthEnd = date("Y-m-t", strtotime($monthFilter)) . ' 23:59:59'; - - $query->where( - [ - ':monthStart <= CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END', - ':monthEnd >= CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END', - ] - ) - ->bind(':monthStart', $monthStart) - ->bind(':monthEnd', $monthEnd); - } - break; - - case 'title': - default: - // Default to 'title' if parameter is not valid - $query->where('LOWER(' . $db->quoteName('a.title') . ') LIKE :search') - ->bind(':search', $textFilter); - break; - } - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - // Filter by a single or group of tags. - $tagId = $this->getState('filter.tag'); - - if (is_array($tagId) && count($tagId) === 1) - { - $tagId = current($tagId); - } - - if (is_array($tagId)) - { - $tagId = ArrayHelper::toInteger($tagId); - - if ($tagId) - { - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tagId)) . ')', - $db->quoteName('type_alias') . ' = ' . $db->quote('com_content.article'), - ] - ); - - $query->join( - 'INNER', - '(' . (string) $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ); - } - } - elseif ($tagId = (int) $tagId) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - . ' AND ' . $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_content.article') - ) - ->where($db->quoteName('tagmap.tag_id') . ' = :tagId') - ->bind(':tagId', $tagId, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $query->order( - $db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Method to get a list of articles. - * - * Overridden to inject convert the attribs field into a Registry object. - * - * @return mixed An array of objects on success, false on failure. - * - * @since 1.6 - */ - public function getItems() - { - $items = parent::getItems(); - $user = Factory::getUser(); - $userId = $user->get('id'); - $guest = $user->get('guest'); - $groups = $user->getAuthorisedViewLevels(); - $input = Factory::getApplication()->input; - - // Get the global params - $globalParams = ComponentHelper::getParams('com_content', true); - - // Convert the parameter fields into objects. - foreach ($items as &$item) - { - $articleParams = new Registry($item->attribs); - - // Unpack readmore and layout params - $item->alternative_readmore = $articleParams->get('alternative_readmore'); - $item->layout = $articleParams->get('layout'); - - $item->params = clone $this->getState('params'); - - /** - * For blogs, article params override menu item params only if menu param = 'use_article' - * Otherwise, menu item params control the layout - * If menu item is 'use_article' and there is no article param, use global - */ - if (($input->getString('layout') === 'blog') || ($input->getString('view') === 'featured') - || ($this->getState('params')->get('layout_type') === 'blog')) - { - // Create an array of just the params set to 'use_article' - $menuParamsArray = $this->getState('params')->toArray(); - $articleArray = array(); - - foreach ($menuParamsArray as $key => $value) - { - if ($value === 'use_article') - { - // If the article has a value, use it - if ($articleParams->get($key) != '') - { - // Get the value from the article - $articleArray[$key] = $articleParams->get($key); - } - else - { - // Otherwise, use the global value - $articleArray[$key] = $globalParams->get($key); - } - } - } - - // Merge the selected article params - if (count($articleArray) > 0) - { - $articleParams = new Registry($articleArray); - $item->params->merge($articleParams); - } - } - else - { - // For non-blog layouts, merge all of the article params - $item->params->merge($articleParams); - } - - // Get display date - switch ($item->params->get('list_show_date')) - { - case 'modified': - $item->displayDate = $item->modified; - break; - - case 'published': - $item->displayDate = ($item->publish_up == 0) ? $item->created : $item->publish_up; - break; - - default: - case 'created': - $item->displayDate = $item->created; - break; - } - - /** - * Compute the asset access permissions. - * Technically guest could edit an article, but lets not check that to improve performance a little. - */ - if (!$guest) - { - $asset = 'com_content.article.' . $item->id; - - // Check general edit permission first. - if ($user->authorise('core.edit', $asset)) - { - $item->params->set('access-edit', true); - } - - // Now check if edit.own is available. - elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) - { - // Check for a valid user and that they are the owner. - if ($userId == $item->created_by) - { - $item->params->set('access-edit', true); - } - } - } - - $access = $this->getState('filter.access'); - - if ($access) - { - // If the access filter has been set, we already have only the articles this user can view. - $item->params->set('access-view', true); - } - else - { - // If no access filter is set, the layout takes some responsibility for display of limited information. - if ($item->catid == 0 || $item->category_access === null) - { - $item->params->set('access-view', in_array($item->access, $groups)); - } - else - { - $item->params->set('access-view', in_array($item->access, $groups) && in_array($item->category_access, $groups)); - } - } - - // Some contexts may not use tags data at all, so we allow callers to disable loading tag data - if ($this->getState('load_tags', $item->params->get('show_tags', '1'))) - { - $item->tags = new TagsHelper; - $item->tags->getItemTags('com_content.article', $item->id); - } - - if (Associations::isEnabled() && $item->params->get('show_associations')) - { - $item->associations = AssociationHelper::displayAssociations($item->id); - } - } - - return $items; - } - - /** - * Method to get the starting number of items for the data set. - * - * @return integer The starting number of items available in the data set. - * - * @since 3.0.1 - */ - public function getStart() - { - return $this->getState('list.start'); - } - - /** - * Count Items by Month - * - * @return mixed An array of objects on success, false on failure. - * - * @since 3.9.0 - */ - public function countItemsByMonth() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Get the list query. - $listQuery = $this->getListQuery(); - $bounded = $listQuery->getBounded(); - - // Bind list query variables to our new query. - $keys = array_keys($bounded); - $values = array_column($bounded, 'value'); - $dataTypes = array_column($bounded, 'dataType'); - - $query->bind($keys, $values, $dataTypes); - - $query - ->select( - 'DATE(' . - $query->concatenate( - array( - $query->year($db->quoteName('publish_up')), - $db->quote('-'), - $query->month($db->quoteName('publish_up')), - $db->quote('-01') - ) - ) . ') AS ' . $db->quoteName('d') - ) - ->select('COUNT(*) AS ' . $db->quoteName('c')) - ->from('(' . $this->getListQuery() . ') AS ' . $db->quoteName('b')) - ->group($db->quoteName('d')) - ->order($db->quoteName('d') . ' DESC'); - - return $db->setQuery($query)->loadObjectList(); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see \JController + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_title', + 'state', 'a.state', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'created_by', 'a.created_by', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'language', 'a.language', + 'hits', 'a.hits', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'images', 'a.images', + 'urls', 'a.urls', + 'filter_tag', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.0.1 + */ + protected function populateState($ordering = 'ordering', $direction = 'ASC') + { + $app = Factory::getApplication(); + + // List state information + $value = $app->input->get('limit', $app->get('list_limit', 0), 'uint'); + $this->setState('list.limit', $value); + + $value = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $value); + + $value = $app->input->get('filter_tag', 0, 'uint'); + $this->setState('filter.tag', $value); + + $orderCol = $app->input->get('filter_order', 'a.ordering'); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'a.ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->input->get('filter_order_Dir', 'ASC'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $params = $app->getParams(); + $this->setState('params', $params); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) { + // Filter on published for those who do not have edit or edit.state rights. + $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + // Process show_noauth parameter + if ((!$params->get('show_noauth')) || (!ComponentHelper::getParams('com_content')->get('show_noauth'))) { + $this->setState('filter.access', true); + } else { + $this->setState('filter.access', false); + } + + $this->setState('layout', $app->input->getString('layout')); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . serialize($this->getState('filter.published')); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.featured'); + $id .= ':' . serialize($this->getState('filter.article_id')); + $id .= ':' . $this->getState('filter.article_id.include'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . $this->getState('filter.category_id.include'); + $id .= ':' . serialize($this->getState('filter.author_id')); + $id .= ':' . $this->getState('filter.author_id.include'); + $id .= ':' . serialize($this->getState('filter.author_alias')); + $id .= ':' . $this->getState('filter.author_alias.include'); + $id .= ':' . $this->getState('filter.date_filtering'); + $id .= ':' . $this->getState('filter.date_field'); + $id .= ':' . $this->getState('filter.start_date_range'); + $id .= ':' . $this->getState('filter.end_date_range'); + $id .= ':' . $this->getState('filter.relative_date'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Get the master query for retrieving a list of articles subject to the model state. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + + // Create a new query object. + $db = $this->getDatabase(); + + /** @var \Joomla\Database\DatabaseQuery $query */ + $query = $db->getQuery(true); + + $nowDate = Factory::getDate()->toSql(); + + $conditionArchived = ContentComponent::CONDITION_ARCHIVED; + $conditionUnpublished = ContentComponent::CONDITION_UNPUBLISHED; + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.title'), + $db->quoteName('a.alias'), + $db->quoteName('a.introtext'), + $db->quoteName('a.fulltext'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.catid'), + $db->quoteName('a.created'), + $db->quoteName('a.created_by'), + $db->quoteName('a.created_by_alias'), + $db->quoteName('a.modified'), + $db->quoteName('a.modified_by'), + // Use created if publish_up is null + 'CASE WHEN ' . $db->quoteName('a.publish_up') . ' IS NULL THEN ' . $db->quoteName('a.created') + . ' ELSE ' . $db->quoteName('a.publish_up') . ' END AS ' . $db->quoteName('publish_up'), + $db->quoteName('a.publish_down'), + $db->quoteName('a.images'), + $db->quoteName('a.urls'), + $db->quoteName('a.attribs'), + $db->quoteName('a.metadata'), + $db->quoteName('a.metakey'), + $db->quoteName('a.metadesc'), + $db->quoteName('a.access'), + $db->quoteName('a.hits'), + $db->quoteName('a.featured'), + $db->quoteName('a.language'), + $query->length($db->quoteName('a.fulltext')) . ' AS ' . $db->quoteName('readmore'), + $db->quoteName('a.ordering'), + ] + ) + ) + ->select( + [ + $db->quoteName('fp.featured_up'), + $db->quoteName('fp.featured_down'), + // Published/archived article in archived category is treated as archived article. If category is not published then force 0. + 'CASE WHEN ' . $db->quoteName('c.published') . ' = 2 AND ' . $db->quoteName('a.state') . ' > 0 THEN ' . $conditionArchived + . ' WHEN ' . $db->quoteName('c.published') . ' != 1 THEN ' . $conditionUnpublished + . ' ELSE ' . $db->quoteName('a.state') . ' END AS ' . $db->quoteName('state'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('c.path', 'category_route'), + $db->quoteName('c.access', 'category_access'), + $db->quoteName('c.alias', 'category_alias'), + $db->quoteName('c.language', 'category_language'), + $db->quoteName('c.published'), + $db->quoteName('c.published', 'parents_published'), + $db->quoteName('c.lft'), + 'CASE WHEN ' . $db->quoteName('a.created_by_alias') . ' > ' . $db->quote(' ') . ' THEN ' . $db->quoteName('a.created_by_alias') + . ' ELSE ' . $db->quoteName('ua.name') . ' END AS ' . $db->quoteName('author'), + $db->quoteName('ua.email', 'author_email'), + $db->quoteName('uam.name', 'modified_by_name'), + $db->quoteName('parent.title', 'parent_title'), + $db->quoteName('parent.id', 'parent_id'), + $db->quoteName('parent.path', 'parent_route'), + $db->quoteName('parent.alias', 'parent_alias'), + $db->quoteName('parent.language', 'parent_language'), + ] + ) + ->from($db->quoteName('#__content', 'a')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')) + ->join('LEFT', $db->quoteName('#__users', 'uam'), $db->quoteName('uam.id') . ' = ' . $db->quoteName('a.modified_by')) + ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')); + + $params = $this->getState('params'); + $orderby_sec = $params->get('orderby_sec'); + + // Join over the frontpage articles if required. + $frontpageJoin = 'LEFT'; + + if ($this->getState('filter.frontpage')) { + if ($orderby_sec === 'front') { + $query->select($db->quoteName('fp.ordering')); + $frontpageJoin = 'INNER'; + } else { + $query->where($db->quoteName('a.featured') . ' = 1'); + } + + $query->where( + [ + '(' . $db->quoteName('fp.featured_up') . ' IS NULL OR ' . $db->quoteName('fp.featured_up') . ' <= :frontpageUp)', + '(' . $db->quoteName('fp.featured_down') . ' IS NULL OR ' . $db->quoteName('fp.featured_down') . ' >= :frontpageDown)', + ] + ) + ->bind(':frontpageUp', $nowDate) + ->bind(':frontpageDown', $nowDate); + } elseif ($orderby_sec === 'front' || $this->getState('list.ordering') === 'fp.ordering') { + $query->select($db->quoteName('fp.ordering')); + } + + $query->join($frontpageJoin, $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')); + + if (PluginHelper::isEnabled('content', 'vote')) { + // Join on voting table + $query->select( + [ + 'COALESCE(NULLIF(ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1), 0), 0)' + . ' AS ' . $db->quoteName('rating'), + 'COALESCE(NULLIF(' . $db->quoteName('v.rating_count') . ', 0), 0) AS ' . $db->quoteName('rating_count'), + ] + ) + ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')); + } + + // Filter by access level. + if ($this->getState('filter.access', true)) { + $groups = $this->getState('filter.viewlevels', $user->getAuthorisedViewLevels()); + $query->whereIn($db->quoteName('a.access'), $groups) + ->whereIn($db->quoteName('c.access'), $groups); + } + + // Filter by published state + $condition = $this->getState('filter.published'); + + if (is_numeric($condition) && $condition == 2) { + /** + * If category is archived then article has to be published or archived. + * Or category is published then article has to be archived. + */ + $query->where('((' . $db->quoteName('c.published') . ' = 2 AND ' . $db->quoteName('a.state') . ' > :conditionUnpublished)' + . ' OR (' . $db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') . ' = :conditionArchived))') + ->bind(':conditionUnpublished', $conditionUnpublished, ParameterType::INTEGER) + ->bind(':conditionArchived', $conditionArchived, ParameterType::INTEGER); + } elseif (is_numeric($condition)) { + $condition = (int) $condition; + + // Category has to be published + $query->where($db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') . ' = :condition') + ->bind(':condition', $condition, ParameterType::INTEGER); + } elseif (is_array($condition)) { + // Category has to be published + $query->where( + $db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') + . ' IN (' . implode(',', $query->bindArray($condition)) . ')' + ); + } + + // Filter by featured state + $featured = $this->getState('filter.featured'); + + switch ($featured) { + case 'hide': + $query->where($db->quoteName('a.featured') . ' = 0'); + break; + + case 'only': + $query->where( + [ + $db->quoteName('a.featured') . ' = 1', + '(' . $db->quoteName('fp.featured_up') . ' IS NULL OR ' . $db->quoteName('fp.featured_up') . ' <= :featuredUp)', + '(' . $db->quoteName('fp.featured_down') . ' IS NULL OR ' . $db->quoteName('fp.featured_down') . ' >= :featuredDown)', + ] + ) + ->bind(':featuredUp', $nowDate) + ->bind(':featuredDown', $nowDate); + break; + + case 'show': + default: + // Normally we do not discriminate between featured/unfeatured items. + break; + } + + // Filter by a single or group of articles. + $articleId = $this->getState('filter.article_id'); + + if (is_numeric($articleId)) { + $articleId = (int) $articleId; + $type = $this->getState('filter.article_id.include', true) ? ' = ' : ' <> '; + $query->where($db->quoteName('a.id') . $type . ':articleId') + ->bind(':articleId', $articleId, ParameterType::INTEGER); + } elseif (is_array($articleId)) { + $articleId = ArrayHelper::toInteger($articleId); + + if ($this->getState('filter.article_id.include', true)) { + $query->whereIn($db->quoteName('a.id'), $articleId); + } else { + $query->whereNotIn($db->quoteName('a.id'), $articleId); + } + } + + // Filter by a single or group of categories + $categoryId = $this->getState('filter.category_id'); + + if (is_numeric($categoryId)) { + $type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> '; + + // Add subcategory check + $includeSubcategories = $this->getState('filter.subcategories', false); + + if ($includeSubcategories) { + $categoryId = (int) $categoryId; + $levels = (int) $this->getState('filter.max_category_levels', 1); + + // Create a subquery for the subcategory list + $subQuery = $db->getQuery(true) + ->select($db->quoteName('sub.id')) + ->from($db->quoteName('#__categories', 'sub')) + ->join( + 'INNER', + $db->quoteName('#__categories', 'this'), + $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') + . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') + ) + ->where($db->quoteName('this.id') . ' = :subCategoryId'); + + $query->bind(':subCategoryId', $categoryId, ParameterType::INTEGER); + + if ($levels >= 0) { + $subQuery->where($db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels'); + $query->bind(':levels', $levels, ParameterType::INTEGER); + } + + // Add the subquery to the main query + $query->where( + '(' . $db->quoteName('a.catid') . $type . ':categoryId OR ' . $db->quoteName('a.catid') . ' IN (' . $subQuery . '))' + ); + $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } else { + $query->where($db->quoteName('a.catid') . $type . ':categoryId'); + $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + } elseif (is_array($categoryId) && (count($categoryId) > 0)) { + $categoryId = ArrayHelper::toInteger($categoryId); + + if (!empty($categoryId)) { + if ($this->getState('filter.category_id.include', true)) { + $query->whereIn($db->quoteName('a.catid'), $categoryId); + } else { + $query->whereNotIn($db->quoteName('a.catid'), $categoryId); + } + } + } + + // Filter by author + $authorId = $this->getState('filter.author_id'); + $authorWhere = ''; + + if (is_numeric($authorId)) { + $authorId = (int) $authorId; + $type = $this->getState('filter.author_id.include', true) ? ' = ' : ' <> '; + $authorWhere = $db->quoteName('a.created_by') . $type . ':authorId'; + $query->bind(':authorId', $authorId, ParameterType::INTEGER); + } elseif (is_array($authorId)) { + $authorId = array_values(array_filter($authorId, 'is_numeric')); + + if ($authorId) { + $type = $this->getState('filter.author_id.include', true) ? ' IN' : ' NOT IN'; + $authorWhere = $db->quoteName('a.created_by') . $type . ' (' . implode(',', $query->bindArray($authorId)) . ')'; + } + } + + // Filter by author alias + $authorAlias = $this->getState('filter.author_alias'); + $authorAliasWhere = ''; + + if (is_string($authorAlias)) { + $type = $this->getState('filter.author_alias.include', true) ? ' = ' : ' <> '; + $authorAliasWhere = $db->quoteName('a.created_by_alias') . $type . ':authorAlias'; + $query->bind(':authorAlias', $authorAlias); + } elseif (\is_array($authorAlias) && !empty($authorAlias)) { + $type = $this->getState('filter.author_alias.include', true) ? ' IN' : ' NOT IN'; + $authorAliasWhere = $db->quoteName('a.created_by_alias') . $type + . ' (' . implode(',', $query->bindArray($authorAlias, ParameterType::STRING)) . ')'; + } + + if (!empty($authorWhere) && !empty($authorAliasWhere)) { + $query->where('(' . $authorWhere . ' OR ' . $authorAliasWhere . ')'); + } elseif (!empty($authorWhere) || !empty($authorAliasWhere)) { + // One of these is empty, the other is not so we just add both + $query->where($authorWhere . $authorAliasWhere); + } + + // Filter by start and end dates. + if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) { + $query->where( + [ + '(' . $db->quoteName('a.publish_up') . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publishUp)', + '(' . $db->quoteName('a.publish_down') . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publishDown)', + ] + ) + ->bind(':publishUp', $nowDate) + ->bind(':publishDown', $nowDate); + } + + // Filter by Date Range or Relative Date + $dateFiltering = $this->getState('filter.date_filtering', 'off'); + $dateField = $db->escape($this->getState('filter.date_field', 'a.created')); + + switch ($dateFiltering) { + case 'range': + $startDateRange = $this->getState('filter.start_date_range', ''); + $endDateRange = $this->getState('filter.end_date_range', ''); + + if ($startDateRange || $endDateRange) { + $query->where($db->quoteName($dateField) . ' IS NOT NULL'); + + if ($startDateRange) { + $query->where($db->quoteName($dateField) . ' >= :startDateRange') + ->bind(':startDateRange', $startDateRange); + } + + if ($endDateRange) { + $query->where($db->quoteName($dateField) . ' <= :endDateRange') + ->bind(':endDateRange', $endDateRange); + } + } + + break; + + case 'relative': + $relativeDate = (int) $this->getState('filter.relative_date', 0); + $query->where( + $db->quoteName($dateField) . ' IS NOT NULL AND ' + . $db->quoteName($dateField) . ' >= ' . $query->dateAdd($db->quote($nowDate), -1 * $relativeDate, 'DAY') + ); + break; + + case 'off': + default: + break; + } + + // Process the filter for list views with user-entered filters + if (is_object($params) && ($params->get('filter_field') !== 'hide') && ($filter = $this->getState('list.filter'))) { + // Clean filter variable + $filter = StringHelper::strtolower($filter); + $monthFilter = $filter; + $hitsFilter = (int) $filter; + $textFilter = '%' . $filter . '%'; + + switch ($params->get('filter_field')) { + case 'author': + $query->where( + 'LOWER(CASE WHEN ' . $db->quoteName('a.created_by_alias') . ' > ' . $db->quote(' ') + . ' THEN ' . $db->quoteName('a.created_by_alias') . ' ELSE ' . $db->quoteName('ua.name') . ' END) LIKE :search' + ) + ->bind(':search', $textFilter); + break; + + case 'hits': + $query->where($db->quoteName('a.hits') . ' >= :hits') + ->bind(':hits', $hitsFilter, ParameterType::INTEGER); + break; + + case 'month': + if ($monthFilter != '') { + $monthStart = date("Y-m-d", strtotime($monthFilter)) . ' 00:00:00'; + $monthEnd = date("Y-m-t", strtotime($monthFilter)) . ' 23:59:59'; + + $query->where( + [ + ':monthStart <= CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END', + ':monthEnd >= CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END', + ] + ) + ->bind(':monthStart', $monthStart) + ->bind(':monthEnd', $monthEnd); + } + break; + + case 'title': + default: + // Default to 'title' if parameter is not valid + $query->where('LOWER(' . $db->quoteName('a.title') . ') LIKE :search') + ->bind(':search', $textFilter); + break; + } + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + // Filter by a single or group of tags. + $tagId = $this->getState('filter.tag'); + + if (is_array($tagId) && count($tagId) === 1) { + $tagId = current($tagId); + } + + if (is_array($tagId)) { + $tagId = ArrayHelper::toInteger($tagId); + + if ($tagId) { + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tagId)) . ')', + $db->quoteName('type_alias') . ' = ' . $db->quote('com_content.article'), + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ); + } + } elseif ($tagId = (int) $tagId) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + . ' AND ' . $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_content.article') + ) + ->where($db->quoteName('tagmap.tag_id') . ' = :tagId') + ->bind(':tagId', $tagId, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $query->order( + $db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) + ); + + return $query; + } + + /** + * Method to get a list of articles. + * + * Overridden to inject convert the attribs field into a Registry object. + * + * @return mixed An array of objects on success, false on failure. + * + * @since 1.6 + */ + public function getItems() + { + $items = parent::getItems(); + + $user = Factory::getUser(); + $userId = $user->get('id'); + $guest = $user->get('guest'); + $groups = $user->getAuthorisedViewLevels(); + $input = Factory::getApplication()->input; + + // Get the global params + $globalParams = ComponentHelper::getParams('com_content', true); + + $taggedItems = []; + + // Convert the parameter fields into objects. + foreach ($items as $item) { + $articleParams = new Registry($item->attribs); + + // Unpack readmore and layout params + $item->alternative_readmore = $articleParams->get('alternative_readmore'); + $item->layout = $articleParams->get('layout'); + + $item->params = clone $this->getState('params'); + + /** + * For blogs, article params override menu item params only if menu param = 'use_article' + * Otherwise, menu item params control the layout + * If menu item is 'use_article' and there is no article param, use global + */ + if ( + ($input->getString('layout') === 'blog') || ($input->getString('view') === 'featured') + || ($this->getState('params')->get('layout_type') === 'blog') + ) { + // Create an array of just the params set to 'use_article' + $menuParamsArray = $this->getState('params')->toArray(); + $articleArray = array(); + + foreach ($menuParamsArray as $key => $value) { + if ($value === 'use_article') { + // If the article has a value, use it + if ($articleParams->get($key) != '') { + // Get the value from the article + $articleArray[$key] = $articleParams->get($key); + } else { + // Otherwise, use the global value + $articleArray[$key] = $globalParams->get($key); + } + } + } + + // Merge the selected article params + if (count($articleArray) > 0) { + $articleParams = new Registry($articleArray); + $item->params->merge($articleParams); + } + } else { + // For non-blog layouts, merge all of the article params + $item->params->merge($articleParams); + } + + // Get display date + switch ($item->params->get('list_show_date')) { + case 'modified': + $item->displayDate = $item->modified; + break; + + case 'published': + $item->displayDate = ($item->publish_up == 0) ? $item->created : $item->publish_up; + break; + + default: + case 'created': + $item->displayDate = $item->created; + break; + } + + /** + * Compute the asset access permissions. + * Technically guest could edit an article, but lets not check that to improve performance a little. + */ + if (!$guest) { + $asset = 'com_content.article.' . $item->id; + + // Check general edit permission first. + if ($user->authorise('core.edit', $asset)) { + $item->params->set('access-edit', true); + } elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) { + // Now check if edit.own is available. + // Check for a valid user and that they are the owner. + if ($userId == $item->created_by) { + $item->params->set('access-edit', true); + } + } + } + + $access = $this->getState('filter.access'); + + if ($access) { + // If the access filter has been set, we already have only the articles this user can view. + $item->params->set('access-view', true); + } else { + // If no access filter is set, the layout takes some responsibility for display of limited information. + if ($item->catid == 0 || $item->category_access === null) { + $item->params->set('access-view', in_array($item->access, $groups)); + } else { + $item->params->set('access-view', in_array($item->access, $groups) && in_array($item->category_access, $groups)); + } + } + + // Some contexts may not use tags data at all, so we allow callers to disable loading tag data + if ($this->getState('load_tags', $item->params->get('show_tags', '1'))) { + $item->tags = new TagsHelper(); + $taggedItems[$item->id] = $item; + } + + if (Associations::isEnabled() && $item->params->get('show_associations')) { + $item->associations = AssociationHelper::displayAssociations($item->id); + } + } + + // Load tags of all items. + if ($taggedItems) { + $tagsHelper = new TagsHelper(); + $itemIds = \array_keys($taggedItems); + + foreach ($tagsHelper->getMultipleItemTags('com_content.article', $itemIds) as $id => $tags) { + $taggedItems[$id]->tags->itemTags = $tags; + } + } + + return $items; + } + + /** + * Method to get the starting number of items for the data set. + * + * @return integer The starting number of items available in the data set. + * + * @since 3.0.1 + */ + public function getStart() + { + return $this->getState('list.start'); + } + + /** + * Count Items by Month + * + * @return mixed An array of objects on success, false on failure. + * + * @since 3.9.0 + */ + public function countItemsByMonth() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Get the list query. + $listQuery = $this->getListQuery(); + $bounded = $listQuery->getBounded(); + + // Bind list query variables to our new query. + $keys = array_keys($bounded); + $values = array_column($bounded, 'value'); + $dataTypes = array_column($bounded, 'dataType'); + + $query->bind($keys, $values, $dataTypes); + + $query + ->select( + 'DATE(' . + $query->concatenate( + array( + $query->year($db->quoteName('publish_up')), + $db->quote('-'), + $query->month($db->quoteName('publish_up')), + $db->quote('-01') + ) + ) . ') AS ' . $db->quoteName('d') + ) + ->select('COUNT(*) AS ' . $db->quoteName('c')) + ->from('(' . $this->getListQuery() . ') AS ' . $db->quoteName('b')) + ->group($db->quoteName('d')) + ->order($db->quoteName('d') . ' DESC'); + + return $db->setQuery($query)->loadObjectList(); + } } diff --git a/code/components/com_content/src/Model/CategoriesModel.php b/code/components/com_content/src/Model/CategoriesModel.php index b3a3a4eb..52e9f0e3 100644 --- a/code/components/com_content/src/Model/CategoriesModel.php +++ b/code/components/com_content/src/Model/CategoriesModel.php @@ -1,4 +1,5 @@ setState('filter.extension', $this->_extension); - - // Get the parent id if defined. - $parentId = $app->input->getInt('id'); - $this->setState('filter.parentId', $parentId); - - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('filter.published', 1); - $this->setState('filter.access', true); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.parentId'); - - return parent::getStoreId($id); - } - - /** - * Redefine the function and add some properties to make the styling easier - * - * @param bool $recursive True if you want to return children recursively. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getItems($recursive = false) - { - $store = $this->getStoreId(); - - if (!isset($this->cache[$store])) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_num_articles_cat', 1) || !$params->get('show_empty_categories_cat', 0); - $categories = Categories::getInstance('Content', $options); - $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); - - if (is_object($this->_parent)) - { - $this->cache[$store] = $this->_parent->getChildren($recursive); - } - else - { - $this->cache[$store] = false; - } - } - - return $this->cache[$store]; - } - - /** - * Get the parent. - * - * @return object An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getParent() - { - if (!is_object($this->_parent)) - { - $this->getItems(); - } - - return $this->_parent; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_content.categories'; + + /** + * The category context (allows other extensions to derived from this model). + * + * @var string + */ + protected $_extension = 'com_content'; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + private $_parent = null; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering The field to order on. + * @param string $direction The direction to order on. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $this->setState('filter.extension', $this->_extension); + + // Get the parent id if defined. + $parentId = $app->input->getInt('id'); + $this->setState('filter.parentId', $parentId); + + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('filter.published', 1); + $this->setState('filter.access', true); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.parentId'); + + return parent::getStoreId($id); + } + + /** + * Redefine the function and add some properties to make the styling easier + * + * @param bool $recursive True if you want to return children recursively. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getItems($recursive = false) + { + $store = $this->getStoreId(); + + if (!isset($this->cache[$store])) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_num_articles_cat', 1) || !$params->get('show_empty_categories_cat', 0); + $categories = Categories::getInstance('Content', $options); + $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); + + if (is_object($this->_parent)) { + $this->cache[$store] = $this->_parent->getChildren($recursive); + } else { + $this->cache[$store] = false; + } + } + + return $this->cache[$store]; + } + + /** + * Get the parent. + * + * @return object An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getParent() + { + if (!is_object($this->_parent)) { + $this->getItems(); + } + + return $this->_parent; + } } diff --git a/code/components/com_content/src/Model/CategoryModel.php b/code/components/com_content/src/Model/CategoryModel.php index 10954b28..45b6ad20 100644 --- a/code/components/com_content/src/Model/CategoryModel.php +++ b/code/components/com_content/src/Model/CategoryModel.php @@ -1,4 +1,5 @@ input->getInt('id'); - - $this->setState('category.id', $pk); - - // Load the parameters. Merge Global and Menu Item params into new object - $params = $app->getParams(); - - if ($menu = $app->getMenu()->getActive()) - { - $menuParams = $menu->getParams(); - } - else - { - $menuParams = new Registry; - } - - $mergedParams = clone $menuParams; - $mergedParams->merge($params); - - $this->setState('params', $mergedParams); - $user = Factory::getUser(); - - $asset = 'com_content'; - - if ($pk) - { - $asset .= '.category.' . $pk; - } - - if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) - { - // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); - } - else - { - $this->setState('filter.published', [0, 1]); - } - - // Process show_noauth parameter - if (!$params->get('show_noauth')) - { - $this->setState('filter.access', true); - } - else - { - $this->setState('filter.access', false); - } - - $itemid = $app->input->get('id', 0, 'int') . ':' . $app->input->get('Itemid', 0, 'int'); - - $value = $this->getUserStateFromRequest('com_content.category.filter.' . $itemid . '.tag', 'filter_tag', 0, 'int', false); - $this->setState('filter.tag', $value); - - // Optional filter text - $search = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter-search', 'filter-search', '', 'string'); - $this->setState('list.filter', $search); - - // Filter.order - $orderCol = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'a.ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order_Dir', 'filter_order_Dir', '', 'cmd'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $this->setState('list.start', $app->input->get('limitstart', 0, 'uint')); - - // Set limit for query. If list, use parameter. If blog, add blog parameters for limit. - if (($app->input->get('layout') === 'blog') || $params->get('layout_type') === 'blog') - { - $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); - $this->setState('list.links', $params->get('num_links')); - } - else - { - $limit = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.limit', 'limit', $params->get('display_num'), 'uint'); - } - - $this->setState('list.limit', $limit); - - // Set the depth of the category query based on parameter - $showSubcategories = $params->get('show_subcategory_content', '0'); - - if ($showSubcategories) - { - $this->setState('filter.max_category_levels', $params->get('show_subcategory_content', '1')); - $this->setState('filter.subcategories', true); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - $this->setState('layout', $app->input->getString('layout')); - - // Set the featured articles state - $this->setState('filter.featured', $params->get('show_featured')); - } - - /** - * Get the articles in the category - * - * @return array|bool An array of articles or false if an error occurs. - * - * @since 1.5 - */ - public function getItems() - { - $limit = $this->getState('list.limit'); - - if ($this->_articles === null && $category = $this->getCategory()) - { - $model = $this->bootComponent('com_content')->getMVCFactory() - ->createModel('Articles', 'Site', ['ignore_request' => true]); - $model->setState('params', Factory::getApplication()->getParams()); - $model->setState('filter.category_id', $category->id); - $model->setState('filter.published', $this->getState('filter.published')); - $model->setState('filter.access', $this->getState('filter.access')); - $model->setState('filter.language', $this->getState('filter.language')); - $model->setState('filter.featured', $this->getState('filter.featured')); - $model->setState('list.ordering', $this->_buildContentOrderBy()); - $model->setState('list.start', $this->getState('list.start')); - $model->setState('list.limit', $limit); - $model->setState('list.direction', $this->getState('list.direction')); - $model->setState('list.filter', $this->getState('list.filter')); - $model->setState('filter.tag', $this->getState('filter.tag')); - - // Filter.subcategories indicates whether to include articles from subcategories in the list or blog - $model->setState('filter.subcategories', $this->getState('filter.subcategories')); - $model->setState('filter.max_category_levels', $this->getState('filter.max_category_levels')); - $model->setState('list.links', $this->getState('list.links')); - - if ($limit >= 0) - { - $this->_articles = $model->getItems(); - - if ($this->_articles === false) - { - $this->setError($model->getError()); - } - } - else - { - $this->_articles = array(); - } - - $this->_pagination = $model->getPagination(); - } - - return $this->_articles; - } - - /** - * Build the orderby for the query - * - * @return string $orderby portion of query - * - * @since 1.5 - */ - protected function _buildContentOrderBy() - { - $app = Factory::getApplication(); - $db = $this->getDbo(); - $params = $this->state->params; - $itemid = $app->input->get('id', 0, 'int') . ':' . $app->input->get('Itemid', 0, 'int'); - $orderCol = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); - $orderDirn = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order_Dir', 'filter_order_Dir', '', 'cmd'); - $orderby = ' '; - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = null; - } - - if (!in_array(strtoupper($orderDirn), array('ASC', 'DESC', ''))) - { - $orderDirn = 'ASC'; - } - - if ($orderCol && $orderDirn) - { - $orderby .= $db->escape($orderCol) . ' ' . $db->escape($orderDirn) . ', '; - } - - $articleOrderby = $params->get('orderby_sec', 'rdate'); - $articleOrderDate = $params->get('order_date'); - $categoryOrderby = $params->def('orderby_pri', ''); - $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDbo()) . ', '; - $primary = QueryHelper::orderbyPrimary($categoryOrderby); - - $orderby .= $primary . ' ' . $secondary . ' a.created '; - - return $orderby; - } - - /** - * Method to get a JPagination object for the data set. - * - * @return \Joomla\CMS\Pagination\Pagination A JPagination object for the data set. - * - * @since 3.0.1 - */ - public function getPagination() - { - if (empty($this->_pagination)) - { - return null; - } - - return $this->_pagination; - } - - /** - * Method to get category data for the current category - * - * @return object - * - * @since 1.5 - */ - public function getCategory() - { - if (!is_object($this->_item)) - { - if (isset($this->state->params)) - { - $params = $this->state->params; - $options = array(); - $options['countItems'] = $params->get('show_cat_num_articles', 1) || !$params->get('show_empty_categories_cat', 0); - $options['access'] = $params->get('check_access_rights', 1); - } - else - { - $options['countItems'] = 0; - } - - $categories = Categories::getInstance('Content', $options); - $this->_item = $categories->get($this->getState('category.id', 'root')); - - // Compute selected asset permissions. - if (is_object($this->_item)) - { - $user = Factory::getUser(); - $asset = 'com_content.category.' . $this->_item->id; - - // Check general create permission. - if ($user->authorise('core.create', $asset)) - { - $this->_item->getParams()->set('access-create', true); - } - - // @todo: Why aren't we lazy loading the children and siblings? - $this->_children = $this->_item->getChildren(); - $this->_parent = false; - - if ($this->_item->getParent()) - { - $this->_parent = $this->_item->getParent(); - } - - $this->_rightsibling = $this->_item->getSibling(); - $this->_leftsibling = $this->_item->getSibling(false); - } - else - { - $this->_children = false; - $this->_parent = false; - } - } - - return $this->_item; - } - - /** - * Get the parent category. - * - * @return mixed An array of categories or false if an error occurs. - * - * @since 1.6 - */ - public function getParent() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_parent; - } - - /** - * Get the left sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - * - * @since 1.6 - */ - public function &getLeftSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_leftsibling; - } - - /** - * Get the right sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - * - * @since 1.6 - */ - public function &getRightSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_rightsibling; - } - - /** - * Get the child categories. - * - * @return mixed An array of categories or false if an error occurs. - * - * @since 1.6 - */ - public function &getChildren() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - // Order subcategories - if ($this->_children) - { - $params = $this->getState()->get('params'); - - $orderByPri = $params->get('orderby_pri'); - - if ($orderByPri === 'alpha' || $orderByPri === 'ralpha') - { - $this->_children = ArrayHelper::sortObjects($this->_children, 'title', ($orderByPri === 'alpha') ? 1 : (-1)); - } - } - - return $this->_children; - } - - /** - * Increment the hit counter for the category. - * - * @param int $pk Optional primary key of the category to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); - - $table = Table::getInstance('Category', 'JTable'); - $table->hit($pk); - } - - return true; - } + /** + * Category items data + * + * @var array + */ + protected $_item = null; + + /** + * Array of articles in the category + * + * @var \stdClass[] + */ + protected $_articles = null; + + /** + * Category left and right of this one + * + * @var CategoryNode[]|null + */ + protected $_siblings = null; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + protected $_children = null; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + protected $_parent = null; + + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_content.category'; + + /** + * The category that applies. + * + * @var object + */ + protected $_category = null; + + /** + * The list of categories. + * + * @var array + */ + protected $_categories = null; + + /** + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_title', + 'state', 'a.state', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'created_by', 'a.created_by', + 'modified', 'a.modified', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'language', 'a.language', + 'hits', 'a.hits', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'author', 'a.author', + 'filter_tag' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering The field to order on. + * @param string $direction The direction to order on. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $pk = $app->input->getInt('id'); + + $this->setState('category.id', $pk); + + // Load the parameters. Merge Global and Menu Item params into new object + $params = $app->getParams(); + + if ($menu = $app->getMenu()->getActive()) { + $menuParams = $menu->getParams(); + } else { + $menuParams = new Registry(); + } + + $mergedParams = clone $menuParams; + $mergedParams->merge($params); + + $this->setState('params', $mergedParams); + $user = Factory::getUser(); + + $asset = 'com_content'; + + if ($pk) { + $asset .= '.category.' . $pk; + } + + if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) { + // Limit to published for people who can't edit or edit.state. + $this->setState('filter.published', 1); + } else { + $this->setState('filter.published', [0, 1]); + } + + // Process show_noauth parameter + if (!$params->get('show_noauth')) { + $this->setState('filter.access', true); + } else { + $this->setState('filter.access', false); + } + + $itemid = $app->input->get('id', 0, 'int') . ':' . $app->input->get('Itemid', 0, 'int'); + + $value = $this->getUserStateFromRequest('com_content.category.filter.' . $itemid . '.tag', 'filter_tag', 0, 'int', false); + $this->setState('filter.tag', $value); + + // Optional filter text + $search = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter-search', 'filter-search', '', 'string'); + $this->setState('list.filter', $search); + + // Filter.order + $orderCol = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'a.ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order_Dir', 'filter_order_Dir', '', 'cmd'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $this->setState('list.start', $app->input->get('limitstart', 0, 'uint')); + + // Set limit for query. If list, use parameter. If blog, add blog parameters for limit. + if (($app->input->get('layout') === 'blog') || $params->get('layout_type') === 'blog') { + $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); + $this->setState('list.links', $params->get('num_links')); + } else { + $limit = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.limit', 'limit', $params->get('display_num'), 'uint'); + } + + $this->setState('list.limit', $limit); + + // Set the depth of the category query based on parameter + $showSubcategories = $params->get('show_subcategory_content', '0'); + + if ($showSubcategories) { + $this->setState('filter.max_category_levels', $params->get('show_subcategory_content', '1')); + $this->setState('filter.subcategories', true); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + $this->setState('layout', $app->input->getString('layout')); + + // Set the featured articles state + $this->setState('filter.featured', $params->get('show_featured')); + } + + /** + * Get the articles in the category + * + * @return array|bool An array of articles or false if an error occurs. + * + * @since 1.5 + */ + public function getItems() + { + $limit = $this->getState('list.limit'); + + if ($this->_articles === null && $category = $this->getCategory()) { + $model = $this->bootComponent('com_content')->getMVCFactory() + ->createModel('Articles', 'Site', ['ignore_request' => true]); + $model->setState('params', Factory::getApplication()->getParams()); + $model->setState('filter.category_id', $category->id); + $model->setState('filter.published', $this->getState('filter.published')); + $model->setState('filter.access', $this->getState('filter.access')); + $model->setState('filter.language', $this->getState('filter.language')); + $model->setState('filter.featured', $this->getState('filter.featured')); + $model->setState('list.ordering', $this->_buildContentOrderBy()); + $model->setState('list.start', $this->getState('list.start')); + $model->setState('list.limit', $limit); + $model->setState('list.direction', $this->getState('list.direction')); + $model->setState('list.filter', $this->getState('list.filter')); + $model->setState('filter.tag', $this->getState('filter.tag')); + + // Filter.subcategories indicates whether to include articles from subcategories in the list or blog + $model->setState('filter.subcategories', $this->getState('filter.subcategories')); + $model->setState('filter.max_category_levels', $this->getState('filter.max_category_levels')); + $model->setState('list.links', $this->getState('list.links')); + + if ($limit >= 0) { + $this->_articles = $model->getItems(); + + if ($this->_articles === false) { + $this->setError($model->getError()); + } + } else { + $this->_articles = array(); + } + + $this->_pagination = $model->getPagination(); + } + + return $this->_articles; + } + + /** + * Build the orderby for the query + * + * @return string $orderby portion of query + * + * @since 1.5 + */ + protected function _buildContentOrderBy() + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $params = $this->state->params; + $itemid = $app->input->get('id', 0, 'int') . ':' . $app->input->get('Itemid', 0, 'int'); + $orderCol = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); + $orderDirn = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order_Dir', 'filter_order_Dir', '', 'cmd'); + $orderby = ' '; + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = null; + } + + if (!in_array(strtoupper($orderDirn), array('ASC', 'DESC', ''))) { + $orderDirn = 'ASC'; + } + + if ($orderCol && $orderDirn) { + $orderby .= $db->escape($orderCol) . ' ' . $db->escape($orderDirn) . ', '; + } + + $articleOrderby = $params->get('orderby_sec', 'rdate'); + $articleOrderDate = $params->get('order_date'); + $categoryOrderby = $params->def('orderby_pri', ''); + $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()) . ', '; + $primary = QueryHelper::orderbyPrimary($categoryOrderby); + + $orderby .= $primary . ' ' . $secondary . ' a.created '; + + return $orderby; + } + + /** + * Method to get a JPagination object for the data set. + * + * @return \Joomla\CMS\Pagination\Pagination A JPagination object for the data set. + * + * @since 3.0.1 + */ + public function getPagination() + { + if (empty($this->_pagination)) { + return null; + } + + return $this->_pagination; + } + + /** + * Method to get category data for the current category + * + * @return object + * + * @since 1.5 + */ + public function getCategory() + { + if (!is_object($this->_item)) { + if (isset($this->state->params)) { + $params = $this->state->params; + $options = array(); + $options['countItems'] = $params->get('show_cat_num_articles', 1) || !$params->get('show_empty_categories_cat', 0); + $options['access'] = $params->get('check_access_rights', 1); + } else { + $options['countItems'] = 0; + } + + $categories = Categories::getInstance('Content', $options); + $this->_item = $categories->get($this->getState('category.id', 'root')); + + // Compute selected asset permissions. + if (is_object($this->_item)) { + $user = Factory::getUser(); + $asset = 'com_content.category.' . $this->_item->id; + + // Check general create permission. + if ($user->authorise('core.create', $asset)) { + $this->_item->getParams()->set('access-create', true); + } + + // @todo: Why aren't we lazy loading the children and siblings? + $this->_children = $this->_item->getChildren(); + $this->_parent = false; + + if ($this->_item->getParent()) { + $this->_parent = $this->_item->getParent(); + } + + $this->_rightsibling = $this->_item->getSibling(); + $this->_leftsibling = $this->_item->getSibling(false); + } else { + $this->_children = false; + $this->_parent = false; + } + } + + return $this->_item; + } + + /** + * Get the parent category. + * + * @return mixed An array of categories or false if an error occurs. + * + * @since 1.6 + */ + public function getParent() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_parent; + } + + /** + * Get the left sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + * + * @since 1.6 + */ + public function &getLeftSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_leftsibling; + } + + /** + * Get the right sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + * + * @since 1.6 + */ + public function &getRightSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_rightsibling; + } + + /** + * Get the child categories. + * + * @return mixed An array of categories or false if an error occurs. + * + * @since 1.6 + */ + public function &getChildren() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + // Order subcategories + if ($this->_children) { + $params = $this->getState()->get('params'); + + $orderByPri = $params->get('orderby_pri'); + + if ($orderByPri === 'alpha' || $orderByPri === 'ralpha') { + $this->_children = ArrayHelper::sortObjects($this->_children, 'title', ($orderByPri === 'alpha') ? 1 : (-1)); + } + } + + return $this->_children; + } + + /** + * Increment the hit counter for the category. + * + * @param int $pk Optional primary key of the category to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); + + $table = Table::getInstance('Category', 'JTable'); + $table->hit($pk); + } + + return true; + } } diff --git a/code/components/com_content/src/Model/FeaturedModel.php b/code/components/com_content/src/Model/FeaturedModel.php index 251d670b..e187638e 100644 --- a/code/components/com_content/src/Model/FeaturedModel.php +++ b/code/components/com_content/src/Model/FeaturedModel.php @@ -1,4 +1,5 @@ input; - $user = $app->getIdentity(); - - // List state information - $limitstart = $input->getUint('limitstart', 0); - $this->setState('list.start', $limitstart); - - $params = $this->state->params; - - if ($menu = $app->getMenu()->getActive()) - { - $menuParams = $menu->getParams(); - } - else - { - $menuParams = new Registry; - } - - $mergedParams = clone $menuParams; - $mergedParams->merge($params); - - $this->setState('params', $mergedParams); - - $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); - $this->setState('list.limit', $limit); - $this->setState('list.links', $params->get('num_links')); - - $this->setState('filter.frontpage', true); - - if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) - { - // Filter on published for those who do not have edit or edit.state rights. - $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); - } - else - { - $this->setState('filter.published', [ContentComponent::CONDITION_UNPUBLISHED, ContentComponent::CONDITION_PUBLISHED]); - } - - // Process show_noauth parameter - if (!$params->get('show_noauth')) - { - $this->setState('filter.access', true); - } - else - { - $this->setState('filter.access', false); - } - - // Check for category selection - if ($params->get('featured_categories') && implode(',', $params->get('featured_categories')) == true) - { - $featuredCategories = $params->get('featured_categories'); - $this->setState('filter.frontpage.categories', $featuredCategories); - } - - $articleOrderby = $params->get('orderby_sec', 'rdate'); - $articleOrderDate = $params->get('order_date'); - $categoryOrderby = $params->def('orderby_pri', ''); - - $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDbo()); - $primary = QueryHelper::orderbyPrimary($categoryOrderby); - - $this->setState('list.ordering', $primary . $secondary . ', a.created DESC'); - $this->setState('list.direction', ''); - } - - /** - * Method to get a list of articles. - * - * @return mixed An array of objects on success, false on failure. - */ - public function getItems() - { - $params = clone $this->getState('params'); - $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); - - if ($limit > 0) - { - $this->setState('list.limit', $limit); - - return parent::getItems(); - } - - return array(); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= $this->getState('filter.frontpage'); - - return parent::getStoreId($id); - } - - /** - * Get the list of items. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $query = parent::getListQuery(); - - // Filter by categories - $featuredCategories = $this->getState('filter.frontpage.categories'); - - if (is_array($featuredCategories) && !in_array('', $featuredCategories)) - { - $query->where('a.catid IN (' . implode(',', ArrayHelper::toInteger($featuredCategories)) . ')'); - } - - return $query; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_content.frontpage'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering The field to order on. + * @param string $direction The direction to order on. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + parent::populateState($ordering, $direction); + + $app = Factory::getApplication(); + $input = $app->input; + $user = $app->getIdentity(); + + // List state information + $limitstart = $input->getUint('limitstart', 0); + $this->setState('list.start', $limitstart); + + $params = $this->state->params; + + if ($menu = $app->getMenu()->getActive()) { + $menuParams = $menu->getParams(); + } else { + $menuParams = new Registry(); + } + + $mergedParams = clone $menuParams; + $mergedParams->merge($params); + + $this->setState('params', $mergedParams); + + $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); + $this->setState('list.limit', $limit); + $this->setState('list.links', $params->get('num_links')); + + $this->setState('filter.frontpage', true); + + if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) { + // Filter on published for those who do not have edit or edit.state rights. + $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); + } else { + $this->setState('filter.published', [ContentComponent::CONDITION_UNPUBLISHED, ContentComponent::CONDITION_PUBLISHED]); + } + + // Process show_noauth parameter + if (!$params->get('show_noauth')) { + $this->setState('filter.access', true); + } else { + $this->setState('filter.access', false); + } + + // Check for category selection + if ($params->get('featured_categories') && implode(',', $params->get('featured_categories')) == true) { + $featuredCategories = $params->get('featured_categories'); + $this->setState('filter.frontpage.categories', $featuredCategories); + } + + $articleOrderby = $params->get('orderby_sec', 'rdate'); + $articleOrderDate = $params->get('order_date'); + $categoryOrderby = $params->def('orderby_pri', ''); + + $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()); + $primary = QueryHelper::orderbyPrimary($categoryOrderby); + + $this->setState('list.ordering', $primary . $secondary . ', a.created DESC'); + $this->setState('list.direction', ''); + } + + /** + * Method to get a list of articles. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + $params = clone $this->getState('params'); + $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); + + if ($limit > 0) { + $this->setState('list.limit', $limit); + + return parent::getItems(); + } + + return array(); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= $this->getState('filter.frontpage'); + + return parent::getStoreId($id); + } + + /** + * Get the list of items. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $query = parent::getListQuery(); + + // Filter by categories + $featuredCategories = $this->getState('filter.frontpage.categories'); + + if (is_array($featuredCategories) && !in_array('', $featuredCategories)) { + $query->where('a.catid IN (' . implode(',', ArrayHelper::toInteger($featuredCategories)) . ')'); + } + + return $query; + } } diff --git a/code/components/com_content/src/Model/FormModel.php b/code/components/com_content/src/Model/FormModel.php index 601ab1c5..25e95621 100644 --- a/code/components/com_content/src/Model/FormModel.php +++ b/code/components/com_content/src/Model/FormModel.php @@ -1,4 +1,5 @@ getParams(); - $this->setState('params', $params); - - if ($params && $params->get('enable_category') == 1 && $params->get('catid')) - { - $catId = $params->get('catid'); - } - else - { - $catId = 0; - } - - // Load state from the request. - $pk = $app->input->getInt('a_id'); - $this->setState('article.id', $pk); - - $this->setState('article.catid', $app->input->getInt('catid', $catId)); - - $return = $app->input->get('return', '', 'base64'); - $this->setState('return_page', base64_decode($return)); - - $this->setState('layout', $app->input->getString('layout')); - } - - /** - * Method to get article data. - * - * @param integer $itemId The id of the article. - * - * @return mixed Content item data object on success, false on failure. - */ - public function getItem($itemId = null) - { - $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('article.id'); - - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($itemId); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - - $properties = $table->getProperties(1); - $value = ArrayHelper::toObject($properties, CMSObject::class); - - // Convert attrib field to Registry. - $value->params = new Registry($value->attribs); - - // Compute selected asset permissions. - $user = Factory::getUser(); - $userId = $user->get('id'); - $asset = 'com_content.article.' . $value->id; - - // Check general edit permission first. - if ($user->authorise('core.edit', $asset)) - { - $value->params->set('access-edit', true); - } - - // Now check if edit.own is available. - elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) - { - // Check for a valid user and that they are the owner. - if ($userId == $value->created_by) - { - $value->params->set('access-edit', true); - } - } - - // Check edit state permission. - if ($itemId) - { - // Existing item - $value->params->set('access-change', $user->authorise('core.edit.state', $asset)); - } - else - { - // New item. - $catId = (int) $this->getState('article.catid'); - - if ($catId) - { - $value->params->set('access-change', $user->authorise('core.edit.state', 'com_content.category.' . $catId)); - $value->catid = $catId; - } - else - { - $value->params->set('access-change', $user->authorise('core.edit.state', 'com_content')); - } - } - - $value->articletext = $value->introtext; - - if (!empty($value->fulltext)) - { - $value->articletext .= '
    ' . $value->fulltext; - } - - // Convert the metadata field to an array. - $registry = new Registry($value->metadata); - $value->metadata = $registry->toArray(); - - if ($itemId) - { - $value->tags = new TagsHelper; - $value->tags->getTagIds($value->id, 'com_content.article'); - $value->metadata['tags'] = $value->tags; - } - - return $value; - } - - /** - * Get the return URL. - * - * @return string The return URL. - * - * @since 1.6 - */ - public function getReturnPage() - { - return base64_encode($this->getState('return_page', '')); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function save($data) - { - // Associations are not edited in frontend ATM so we have to inherit them - if (Associations::isEnabled() && !empty($data['id']) - && $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $data['id'])) - { - foreach ($associations as $tag => $associated) - { - $associations[$tag] = (int) $associated->id; - } - - $data['associations'] = $associations; - } - - if (!Multilanguage::isEnabled()) - { - $data['language'] = '*'; - } - - return parent::save($data); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = [], $loadData = true) - { - $form = parent::getForm($data, $loadData); - - if (empty($form)) - { - return false; - } - - $app = Factory::getApplication(); - $user = $app->getIdentity(); - - // On edit article, we get ID of article from article.id state, but on save, we use data from input - $id = (int) $this->getState('article.id', $app->input->getInt('a_id')); - - // Existing record. We can't edit the category in frontend if not edit.state. - if ($id > 0 && !$user->authorise('core.edit.state', 'com_content.article.' . $id)) - { - $form->setFieldAttribute('catid', 'readonly', 'true'); - $form->setFieldAttribute('catid', 'required', 'false'); - $form->setFieldAttribute('catid', 'filter', 'unset'); - } - - // Prevent messing with article language and category when editing existing article with associations - if ($this->getState('article.id') && Associations::isEnabled()) - { - $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $id); - - // Make fields read only - if (!empty($associations)) - { - $form->setFieldAttribute('language', 'readonly', 'true'); - $form->setFieldAttribute('catid', 'readonly', 'true'); - $form->setFieldAttribute('language', 'filter', 'unset'); - $form->setFieldAttribute('catid', 'filter', 'unset'); - } - } - - return $form; - } - - /** - * Allows preprocessing of the JForm object. - * - * @param Form $form The form object - * @param array $data The data to be merged into the form object - * @param string $group The plugin group to be executed - * - * @return void - * - * @since 3.7.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $params = $this->getState()->get('params'); - - if ($params && $params->get('enable_category') == 1 && $params->get('catid')) - { - $form->setFieldAttribute('catid', 'default', $params->get('catid')); - $form->setFieldAttribute('catid', 'readonly', 'true'); - - if (Multilanguage::isEnabled()) - { - $categoryId = (int) $params->get('catid'); - - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('language')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('id') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - $db->setQuery($query); - - $result = $db->loadResult(); - - if ($result != '*') - { - $form->setFieldAttribute('language', 'readonly', 'true'); - $form->setFieldAttribute('language', 'default', $result); - } - } - } - - if (!Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'hidden'); - $form->setFieldAttribute('language', 'default', '*'); - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 4.0.0 - * @throws \Exception - */ - public function getTable($name = 'Article', $prefix = 'Administrator', $options = array()) - { - return parent::getTable($name, $prefix, $options); - } + /** + * Model typeAlias string. Used for version history. + * + * @var string + */ + public $typeAlias = 'com_content.article'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the parameters. + $params = $app->getParams(); + $this->setState('params', $params); + + if ($params && $params->get('enable_category') == 1 && $params->get('catid')) { + $catId = $params->get('catid'); + } else { + $catId = 0; + } + + // Load state from the request. + $pk = $app->input->getInt('a_id'); + $this->setState('article.id', $pk); + + $this->setState('article.catid', $app->input->getInt('catid', $catId)); + + $return = $app->input->get('return', '', 'base64'); + $this->setState('return_page', base64_decode($return)); + + $this->setState('layout', $app->input->getString('layout')); + } + + /** + * Method to get article data. + * + * @param integer $itemId The id of the article. + * + * @return mixed Content item data object on success, false on failure. + */ + public function getItem($itemId = null) + { + $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('article.id'); + + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($itemId); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + + $properties = $table->getProperties(1); + $value = ArrayHelper::toObject($properties, CMSObject::class); + + // Convert attrib field to Registry. + $value->params = new Registry($value->attribs); + + // Compute selected asset permissions. + $user = Factory::getUser(); + $userId = $user->get('id'); + $asset = 'com_content.article.' . $value->id; + + // Check general edit permission first. + if ($user->authorise('core.edit', $asset)) { + $value->params->set('access-edit', true); + } elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) { + // Now check if edit.own is available. + // Check for a valid user and that they are the owner. + if ($userId == $value->created_by) { + $value->params->set('access-edit', true); + } + } + + // Check edit state permission. + if ($itemId) { + // Existing item + $value->params->set('access-change', $user->authorise('core.edit.state', $asset)); + } else { + // New item. + $catId = (int) $this->getState('article.catid'); + + if ($catId) { + $value->params->set('access-change', $user->authorise('core.edit.state', 'com_content.category.' . $catId)); + $value->catid = $catId; + } else { + $value->params->set('access-change', $user->authorise('core.edit.state', 'com_content')); + } + } + + $value->articletext = $value->introtext; + + if (!empty($value->fulltext)) { + $value->articletext .= '
    ' . $value->fulltext; + } + + // Convert the metadata field to an array. + $registry = new Registry($value->metadata); + $value->metadata = $registry->toArray(); + + if ($itemId) { + $value->tags = new TagsHelper(); + $value->tags->getTagIds($value->id, 'com_content.article'); + $value->metadata['tags'] = $value->tags; + } + + return $value; + } + + /** + * Get the return URL. + * + * @return string The return URL. + * + * @since 1.6 + */ + public function getReturnPage() + { + return base64_encode($this->getState('return_page', '')); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function save($data) + { + // Associations are not edited in frontend ATM so we have to inherit them + if ( + Associations::isEnabled() && !empty($data['id']) + && $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $data['id']) + ) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } + + $data['associations'] = $associations; + } + + if (!Multilanguage::isEnabled()) { + $data['language'] = '*'; + } + + return parent::save($data); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = [], $loadData = true) + { + $form = parent::getForm($data, $loadData); + + if (empty($form)) { + return false; + } + + $app = Factory::getApplication(); + $user = $app->getIdentity(); + + // On edit article, we get ID of article from article.id state, but on save, we use data from input + $id = (int) $this->getState('article.id', $app->input->getInt('a_id')); + + // Existing record. We can't edit the category in frontend if not edit.state. + if ($id > 0 && !$user->authorise('core.edit.state', 'com_content.article.' . $id)) { + $form->setFieldAttribute('catid', 'readonly', 'true'); + $form->setFieldAttribute('catid', 'required', 'false'); + $form->setFieldAttribute('catid', 'filter', 'unset'); + } + + // Prevent messing with article language and category when editing existing article with associations + if ($this->getState('article.id') && Associations::isEnabled()) { + $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $id); + + // Make fields read only + if (!empty($associations)) { + $form->setFieldAttribute('language', 'readonly', 'true'); + $form->setFieldAttribute('catid', 'readonly', 'true'); + $form->setFieldAttribute('language', 'filter', 'unset'); + $form->setFieldAttribute('catid', 'filter', 'unset'); + } + } + + return $form; + } + + /** + * Allows preprocessing of the JForm object. + * + * @param Form $form The form object + * @param array $data The data to be merged into the form object + * @param string $group The plugin group to be executed + * + * @return void + * + * @since 3.7.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $params = $this->getState()->get('params'); + + if ($params && $params->get('enable_category') == 1 && $params->get('catid')) { + $form->setFieldAttribute('catid', 'default', $params->get('catid')); + $form->setFieldAttribute('catid', 'readonly', 'true'); + + if (Multilanguage::isEnabled()) { + $categoryId = (int) $params->get('catid'); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('language')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + $db->setQuery($query); + + $result = $db->loadResult(); + + if ($result != '*') { + $form->setFieldAttribute('language', 'readonly', 'true'); + $form->setFieldAttribute('language', 'default', $result); + } + } + } + + if (!Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'hidden'); + $form->setFieldAttribute('language', 'default', '*'); + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 4.0.0 + * @throws \Exception + */ + public function getTable($name = 'Article', $prefix = 'Administrator', $options = array()) + { + return parent::getTable($name, $prefix, $options); + } } diff --git a/code/components/com_content/src/Service/Category.php b/code/components/com_content/src/Service/Category.php index 106f19b5..ccf004be 100644 --- a/code/components/com_content/src/Service/Category.php +++ b/code/components/com_content/src/Service/Category.php @@ -1,4 +1,5 @@ categoryFactory = $categoryFactory; - $this->db = $db; - - $params = ComponentHelper::getParams('com_content'); - $this->noIDs = (bool) $params->get('sef_ids'); - $categories = new RouterViewConfiguration('categories'); - $categories->setKey('id'); - $this->registerView($categories); - $category = new RouterViewConfiguration('category'); - $category->setKey('id')->setParent($categories, 'catid')->setNestable()->addLayout('blog'); - $this->registerView($category); - $article = new RouterViewConfiguration('article'); - $article->setKey('id')->setParent($category, 'catid'); - $this->registerView($article); - $this->registerView(new RouterViewConfiguration('archive')); - $this->registerView(new RouterViewConfiguration('featured')); - $form = new RouterViewConfiguration('form'); - $form->setKey('a_id'); - $this->registerView($form); - - parent::__construct($app, $menu); - - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategorySegment($id, $query) - { - $category = $this->getCategories(['access' => true])->get($id); - - if ($category) - { - $path = array_reverse($category->getPath(), true); - $path[0] = '1:root'; - - if ($this->noIDs) - { - foreach ($path as &$segment) - { - list($id, $segment) = explode(':', $segment, 2); - } - } - - return $path; - } - - return array(); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategoriesSegment($id, $query) - { - return $this->getCategorySegment($id, $query); - } - - /** - * Method to get the segment(s) for an article - * - * @param string $id ID of the article to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getArticleSegment($id, $query) - { - if (!strpos($id, ':')) - { - $id = (int) $id; - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('alias')) - ->from($this->db->quoteName('#__content')) - ->where($this->db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - $id .= ':' . $this->db->loadResult(); - } - - if ($this->noIDs) - { - list($void, $segment) = explode(':', $id, 2); - - return array($void => $segment); - } - - return array((int) $id => $id); - } - - /** - * Method to get the segment(s) for a form - * - * @param string $id ID of the article form to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - * - * @since 3.7.3 - */ - public function getFormSegment($id, $query) - { - return $this->getArticleSegment($id, $query); - } - - /** - * Method to get the id for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoryId($segment, $query) - { - if (isset($query['id'])) - { - $category = $this->getCategories(['access' => false])->get($query['id']); - - if ($category) - { - foreach ($category->getChildren() as $child) - { - if ($this->noIDs) - { - if ($child->alias == $segment) - { - return $child->id; - } - } - else - { - if ($child->id == (int) $segment) - { - return $child->id; - } - } - } - } - } - - return false; - } - - /** - * Method to get the segment(s) for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoriesId($segment, $query) - { - return $this->getCategoryId($segment, $query); - } - - /** - * Method to get the segment(s) for an article - * - * @param string $segment Segment of the article to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getArticleId($segment, $query) - { - if ($this->noIDs) - { - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__content')) - ->where( - [ - $this->db->quoteName('alias') . ' = :alias', - $this->db->quoteName('catid') . ' = :catid', - ] - ) - ->bind(':alias', $segment) - ->bind(':catid', $query['id'], ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - return (int) $this->db->loadResult(); - } - - return (int) $segment; - } - - /** - * Method to get categories from cache - * - * @param array $options The options for retrieving categories - * - * @return CategoryInterface The object containing categories - * - * @since 4.0.0 - */ - private function getCategories(array $options = []): CategoryInterface - { - $key = serialize($options); - - if (!isset($this->categoryCache[$key])) - { - $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); - } - - return $this->categoryCache[$key]; - } + /** + * Flag to remove IDs + * + * @var boolean + */ + protected $noIDs = false; + + /** + * The category factory + * + * @var CategoryFactoryInterface + * + * @since 4.0.0 + */ + private $categoryFactory; + + /** + * The category cache + * + * @var array + * + * @since 4.0.0 + */ + private $categoryCache = []; + + /** + * The db + * + * @var DatabaseInterface + * + * @since 4.0.0 + */ + private $db; + + /** + * Content Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + */ + public function __construct(SiteApplication $app, AbstractMenu $menu, CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) + { + $this->categoryFactory = $categoryFactory; + $this->db = $db; + + $params = ComponentHelper::getParams('com_content'); + $this->noIDs = (bool) $params->get('sef_ids'); + $categories = new RouterViewConfiguration('categories'); + $categories->setKey('id'); + $this->registerView($categories); + $category = new RouterViewConfiguration('category'); + $category->setKey('id')->setParent($categories, 'catid')->setNestable()->addLayout('blog'); + $this->registerView($category); + $article = new RouterViewConfiguration('article'); + $article->setKey('id')->setParent($category, 'catid'); + $this->registerView($article); + $this->registerView(new RouterViewConfiguration('archive')); + $this->registerView(new RouterViewConfiguration('featured')); + $form = new RouterViewConfiguration('form'); + $form->setKey('a_id'); + $this->registerView($form); + + parent::__construct($app, $menu); + + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategorySegment($id, $query) + { + $category = $this->getCategories(['access' => true])->get($id); + + if ($category) { + $path = array_reverse($category->getPath(), true); + $path[0] = '1:root'; + + if ($this->noIDs) { + foreach ($path as &$segment) { + list($id, $segment) = explode(':', $segment, 2); + } + } + + return $path; + } + + return array(); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategoriesSegment($id, $query) + { + return $this->getCategorySegment($id, $query); + } + + /** + * Method to get the segment(s) for an article + * + * @param string $id ID of the article to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getArticleSegment($id, $query) + { + if (!strpos($id, ':')) { + $id = (int) $id; + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('alias')) + ->from($this->db->quoteName('#__content')) + ->where($this->db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + $id .= ':' . $this->db->loadResult(); + } + + if ($this->noIDs) { + list($void, $segment) = explode(':', $id, 2); + + return array($void => $segment); + } + + return array((int) $id => $id); + } + + /** + * Method to get the segment(s) for a form + * + * @param string $id ID of the article form to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + * + * @since 3.7.3 + */ + public function getFormSegment($id, $query) + { + return $this->getArticleSegment($id, $query); + } + + /** + * Method to get the id for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoryId($segment, $query) + { + if (isset($query['id'])) { + $category = $this->getCategories(['access' => false])->get($query['id']); + + if ($category) { + foreach ($category->getChildren() as $child) { + if ($this->noIDs) { + if ($child->alias == $segment) { + return $child->id; + } + } else { + if ($child->id == (int) $segment) { + return $child->id; + } + } + } + } + } + + return false; + } + + /** + * Method to get the segment(s) for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoriesId($segment, $query) + { + return $this->getCategoryId($segment, $query); + } + + /** + * Method to get the segment(s) for an article + * + * @param string $segment Segment of the article to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getArticleId($segment, $query) + { + if ($this->noIDs) { + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__content')) + ->where( + [ + $this->db->quoteName('alias') . ' = :alias', + $this->db->quoteName('catid') . ' = :catid', + ] + ) + ->bind(':alias', $segment) + ->bind(':catid', $query['id'], ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + return (int) $this->db->loadResult(); + } + + return (int) $segment; + } + + /** + * Method to get categories from cache + * + * @param array $options The options for retrieving categories + * + * @return CategoryInterface The object containing categories + * + * @since 4.0.0 + */ + private function getCategories(array $options = []): CategoryInterface + { + $key = serialize($options); + + if (!isset($this->categoryCache[$key])) { + $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); + } + + return $this->categoryCache[$key]; + } } diff --git a/code/components/com_content/src/View/Archive/HtmlView.php b/code/components/com_content/src/View/Archive/HtmlView.php index a19c34cc..2ba020c2 100644 --- a/code/components/com_content/src/View/Archive/HtmlView.php +++ b/code/components/com_content/src/View/Archive/HtmlView.php @@ -1,4 +1,5 @@ get('State'); - $items = $this->get('Items'); - $pagination = $this->get('Pagination'); - - if ($errors = $this->getModel()->getErrors()) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Flag indicates to not add limitstart=0 to URL - $pagination->hideEmptyLimitstart = true; - - // Get the page/component configuration - $params = &$state->params; - - PluginHelper::importPlugin('content'); - - foreach ($items as $item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - - // No link for ROOT category - if ($item->parent_alias === 'root') - { - $item->parent_id = null; - } - - $item->event = new \stdClass; - - // Old plugins: Ensure that text property is available - if (!isset($item->text)) - { - $item->text = $item->introtext; - } - - Factory::getApplication()->triggerEvent('onContentPrepare', array('com_content.archive', &$item, &$item->params, 0)); - - // Old plugins: Use processed text as introtext - $item->introtext = $item->text; - - $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.archive', &$item, &$item->params, 0)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.archive', &$item, &$item->params, 0)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.archive', &$item, &$item->params, 0)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - } - - $form = new \stdClass; - - // Month Field - $months = array( - '' => Text::_('COM_CONTENT_MONTH'), - '1' => Text::_('JANUARY_SHORT'), - '2' => Text::_('FEBRUARY_SHORT'), - '3' => Text::_('MARCH_SHORT'), - '4' => Text::_('APRIL_SHORT'), - '5' => Text::_('MAY_SHORT'), - '6' => Text::_('JUNE_SHORT'), - '7' => Text::_('JULY_SHORT'), - '8' => Text::_('AUGUST_SHORT'), - '9' => Text::_('SEPTEMBER_SHORT'), - '10' => Text::_('OCTOBER_SHORT'), - '11' => Text::_('NOVEMBER_SHORT'), - '12' => Text::_('DECEMBER_SHORT') - ); - $form->monthField = HTMLHelper::_( - 'select.genericlist', - $months, - 'month', - array( - 'list.attr' => 'class="form-select"', - 'list.select' => $state->get('filter.month'), - 'option.key' => null - ) - ); - - // Year Field - $this->years = $this->getModel()->getYears(); - $years = array(); - $years[] = HTMLHelper::_('select.option', null, Text::_('JYEAR')); - - for ($i = 0, $iMax = count($this->years); $i < $iMax; $i++) - { - $years[] = HTMLHelper::_('select.option', $this->years[$i], $this->years[$i]); - } - - $form->yearField = HTMLHelper::_( - 'select.genericlist', - $years, - 'year', - array('list.attr' => 'class="form-select"', 'list.select' => $state->get('filter.year')) - ); - $form->limitField = $pagination->getLimitBox(); - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - $this->filter = $state->get('list.filter'); - $this->form = &$form; - $this->items = &$items; - $this->params = &$params; - $this->user = &$user; - $this->pagination = &$pagination; - $this->pagination->setAdditionalUrlParam('month', $state->get('filter.month')); - $this->pagination->setAdditionalUrlParam('year', $state->get('filter.year')); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function _prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state = null; + + /** + * An array containing archived articles + * + * @var \stdClass[] + */ + protected $items = array(); + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination|null + */ + protected $pagination = null; + + /** + * The years that are available to filter on. + * + * @var array + * + * @since 3.6.0 + */ + protected $years = array(); + + /** + * Object containing the year, month and limit field to be displayed + * + * @var \stdClass|null + * + * @since 4.0.0 + */ + protected $form = null; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * The search query used on any archived articles (note this may not be displayed depending on the value of the + * filter_field component parameter) + * + * @var string + * + * @since 4.0.0 + */ + protected $filter = ''; + + /** + * The user object + * + * @var \Joomla\CMS\User\User + * + * @since 4.0.0 + */ + protected $user = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws GenericDataException + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + $state = $this->get('State'); + $items = $this->get('Items'); + $pagination = $this->get('Pagination'); + + if ($errors = $this->getModel()->getErrors()) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Flag indicates to not add limitstart=0 to URL + $pagination->hideEmptyLimitstart = true; + + // Get the page/component configuration + $params = &$state->params; + + PluginHelper::importPlugin('content'); + + foreach ($items as $item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + + // No link for ROOT category + if ($item->parent_alias === 'root') { + $item->parent_id = null; + } + + $item->event = new \stdClass(); + + // Old plugins: Ensure that text property is available + if (!isset($item->text)) { + $item->text = $item->introtext; + } + + Factory::getApplication()->triggerEvent('onContentPrepare', array('com_content.archive', &$item, &$item->params, 0)); + + // Old plugins: Use processed text as introtext + $item->introtext = $item->text; + + $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.archive', &$item, &$item->params, 0)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.archive', &$item, &$item->params, 0)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.archive', &$item, &$item->params, 0)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + } + + $form = new \stdClass(); + + // Month Field + $months = array( + '' => Text::_('COM_CONTENT_MONTH'), + '1' => Text::_('JANUARY_SHORT'), + '2' => Text::_('FEBRUARY_SHORT'), + '3' => Text::_('MARCH_SHORT'), + '4' => Text::_('APRIL_SHORT'), + '5' => Text::_('MAY_SHORT'), + '6' => Text::_('JUNE_SHORT'), + '7' => Text::_('JULY_SHORT'), + '8' => Text::_('AUGUST_SHORT'), + '9' => Text::_('SEPTEMBER_SHORT'), + '10' => Text::_('OCTOBER_SHORT'), + '11' => Text::_('NOVEMBER_SHORT'), + '12' => Text::_('DECEMBER_SHORT') + ); + $form->monthField = HTMLHelper::_( + 'select.genericlist', + $months, + 'month', + array( + 'list.attr' => 'class="form-select"', + 'list.select' => $state->get('filter.month'), + 'option.key' => null + ) + ); + + // Year Field + $this->years = $this->getModel()->getYears(); + $years = array(); + $years[] = HTMLHelper::_('select.option', null, Text::_('JYEAR')); + + for ($i = 0, $iMax = count($this->years); $i < $iMax; $i++) { + $years[] = HTMLHelper::_('select.option', $this->years[$i], $this->years[$i]); + } + + $form->yearField = HTMLHelper::_( + 'select.genericlist', + $years, + 'year', + array('list.attr' => 'class="form-select"', 'list.select' => $state->get('filter.year')) + ); + $form->limitField = $pagination->getLimitBox(); + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + $this->filter = $state->get('list.filter'); + $this->form = &$form; + $this->items = &$items; + $this->params = &$params; + $this->user = &$user; + $this->pagination = &$pagination; + $this->pagination->setAdditionalUrlParam('month', $state->get('filter.month')); + $this->pagination->setAdditionalUrlParam('year', $state->get('filter.year')); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function _prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/code/components/com_content/src/View/Article/HtmlView.php b/code/components/com_content/src/View/Article/HtmlView.php index 1678dd3d..2678f389 100644 --- a/code/components/com_content/src/View/Article/HtmlView.php +++ b/code/components/com_content/src/View/Article/HtmlView.php @@ -1,4 +1,5 @@ getLayout() == 'pagebreak') - { - parent::display($tpl); - - return; - } - - $app = Factory::getApplication(); - $user = Factory::getUser(); - - $this->item = $this->get('Item'); - $this->print = $app->input->getBool('print', false); - $this->state = $this->get('State'); - $this->user = $user; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Create a shortcut for $item. - $item = $this->item; - $item->tagLayout = new FileLayout('joomla.content.tags'); - - // Add router helpers. - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - - // No link for ROOT category - if ($item->parent_alias === 'root') - { - $item->parent_id = null; - } - - // @todo Change based on shownoauth - $item->readmore_link = Route::_(RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language)); - - // Merge article params. If this is single-article view, menu params override article params - // Otherwise, article params override menu item params - $this->params = $this->state->get('params'); - $active = $app->getMenu()->getActive(); - $temp = clone $this->params; - - // Check to see which parameters should take priority. If the active menu item link to the current article, then - // the menu item params take priority - if ($active - && $active->component == 'com_content' - && isset($active->query['view'], $active->query['id']) - && $active->query['view'] == 'article' - && $active->query['id'] == $item->id) - { - $this->menuItemMatchArticle = true; - - // Load layout from active query (in case it is an alternative menu item) - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - // Check for alternative layout of article - elseif ($layout = $item->params->get('article_layout')) - { - $this->setLayout($layout); - } - - // $item->params are the article params, $temp are the menu item params - // Merge so that the menu item params take priority - $item->params->merge($temp); - } - else - { - // The active menu item is not linked to this article, so the article params take priority here - // Merge the menu item params with the article params so that the article params take priority - $temp->merge($item->params); - $item->params = $temp; - - // Check for alternative layouts (since we are not in a single-article menu item) - // Single-article menu item layout takes priority over alt layout for an article - if ($layout = $item->params->get('article_layout')) - { - $this->setLayout($layout); - } - } - - $offset = $this->state->get('list.offset'); - - // Check the view access to the article (the model has already computed the values). - if ($item->params->get('access-view') == false && ($item->params->get('show_noauth', '0') == '0')) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return; - } - - /** - * Check for no 'access-view' and empty fulltext, - * - Redirect guest users to login - * - Deny access to logged users with 403 code - * NOTE: we do not recheck for no access-view + show_noauth disabled ... since it was checked above - */ - if ($item->params->get('access-view') == false && !strlen($item->fulltext)) - { - if ($this->user->get('guest')) - { - $return = base64_encode(Uri::getInstance()); - $login_url_with_return = Route::_('index.php?option=com_users&view=login&return=' . $return); - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'notice'); - $app->redirect($login_url_with_return, 403); - } - else - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return; - } - } - - /** - * NOTE: The following code (usually) sets the text to contain the fulltext, but it is the - * responsibility of the layout to check 'access-view' and only use "introtext" for guests - */ - if ($item->params->get('show_intro', '1') == '1') - { - $item->text = $item->introtext . ' ' . $item->fulltext; - } - elseif ($item->fulltext) - { - $item->text = $item->fulltext; - } - else - { - $item->text = $item->introtext; - } - - $item->tags = new TagsHelper; - $item->tags->getItemTags('com_content.article', $this->item->id); - - if (Associations::isEnabled() && $item->params->get('show_associations')) - { - $item->associations = AssociationHelper::displayAssociations($item->id); - } - - // Process the content plugins. - PluginHelper::importPlugin('content'); - $this->dispatchEvent(new Event('onContentPrepare', array('com_content.article', &$item, &$item->params, $offset))); - - $item->event = new \stdClass; - $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.article', &$item, &$item->params, $offset)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.article', &$item, &$item->params, $offset)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.article', &$item, &$item->params, $offset)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->item->params->get('pageclass_sfx', '')); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - $pathway = $app->getPathway(); - - /** - * Because the application sets a default page title, - * we need to get it from the menu item itself - */ - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); - } - - $title = $this->params->get('page_title', ''); - - // If the menu item is not linked to this article - if (!$this->menuItemMatchArticle) - { - // If a browser page title is defined, use that, then fall back to the article title if set, then fall back to the page_title option - $title = $this->item->params->get('article_page_title', $this->item->title ?: $title); - - // Get ID of the category from active menu item - if ($menu && $menu->component == 'com_content' && isset($menu->query['view']) - && in_array($menu->query['view'], ['categories', 'category'])) - { - $id = $menu->query['id']; - } - else - { - $id = 0; - } - - $path = array(array('title' => $this->item->title, 'link' => '')); - $category = Categories::getInstance('Content')->get($this->item->catid); - - while ($category !== null && $category->id != $id && $category->id !== 'root') - { - $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $pathway->addItem($item['title'], $item['link']); - } - } - - if (empty($title)) - { - $title = $this->item->title; - } - - $this->setDocumentTitle($title); - - if ($this->item->metadesc) - { - $this->document->setDescription($this->item->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - if ($app->get('MetaAuthor') == '1') - { - $author = $this->item->created_by_alias ?: $this->item->author; - $this->document->setMetaData('author', $author); - } - - $mdata = $this->item->metadata->toArray(); - - foreach ($mdata as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - - // If there is a pagebreak heading or title, add it to the page title - if (!empty($this->item->page_title)) - { - $this->item->title = $this->item->title . ' - ' . $this->item->page_title; - $this->setDocumentTitle( - $this->item->page_title . ' - ' . Text::sprintf('PLG_CONTENT_PAGEBREAK_PAGE_NUM', $this->state->get('list.offset') + 1) - ); - } - - if ($this->print) - { - $this->document->setMetaData('robots', 'noindex, nofollow'); - } - } + /** + * The article object + * + * @var \stdClass + */ + protected $item; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Should the print button be displayed or not? + * + * @var boolean + */ + protected $print = false; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The user object + * + * @var \Joomla\CMS\User\User|null + */ + protected $user = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The flag to mark if the active menu item is linked to the being displayed article + * + * @var boolean + */ + protected $menuItemMatchArticle = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + if ($this->getLayout() == 'pagebreak') { + parent::display($tpl); + + return; + } + + $app = Factory::getApplication(); + $user = $this->getCurrentUser(); + + $this->item = $this->get('Item'); + $this->print = $app->input->getBool('print', false); + $this->state = $this->get('State'); + $this->user = $user; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Create a shortcut for $item. + $item = $this->item; + $item->tagLayout = new FileLayout('joomla.content.tags'); + + // Add router helpers. + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + + // No link for ROOT category + if ($item->parent_alias === 'root') { + $item->parent_id = null; + } + + // @todo Change based on shownoauth + $item->readmore_link = Route::_(RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language)); + + // Merge article params. If this is single-article view, menu params override article params + // Otherwise, article params override menu item params + $this->params = $this->state->get('params'); + $active = $app->getMenu()->getActive(); + $temp = clone $this->params; + + // Check to see which parameters should take priority. If the active menu item link to the current article, then + // the menu item params take priority + if ( + $active + && $active->component == 'com_content' + && isset($active->query['view'], $active->query['id']) + && $active->query['view'] == 'article' + && $active->query['id'] == $item->id + ) { + $this->menuItemMatchArticle = true; + + // Load layout from active query (in case it is an alternative menu item) + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } elseif ($layout = $item->params->get('article_layout')) { + // Check for alternative layout of article + $this->setLayout($layout); + } + + // $item->params are the article params, $temp are the menu item params + // Merge so that the menu item params take priority + $item->params->merge($temp); + } else { + // The active menu item is not linked to this article, so the article params take priority here + // Merge the menu item params with the article params so that the article params take priority + $temp->merge($item->params); + $item->params = $temp; + + // Check for alternative layouts (since we are not in a single-article menu item) + // Single-article menu item layout takes priority over alt layout for an article + if ($layout = $item->params->get('article_layout')) { + $this->setLayout($layout); + } + } + + $offset = $this->state->get('list.offset'); + + // Check the view access to the article (the model has already computed the values). + if ($item->params->get('access-view') == false && ($item->params->get('show_noauth', '0') == '0')) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return; + } + + /** + * Check for no 'access-view' and empty fulltext, + * - Redirect guest users to login + * - Deny access to logged users with 403 code + * NOTE: we do not recheck for no access-view + show_noauth disabled ... since it was checked above + */ + if ($item->params->get('access-view') == false && !strlen($item->fulltext)) { + if ($this->user->get('guest')) { + $return = base64_encode(Uri::getInstance()); + $login_url_with_return = Route::_('index.php?option=com_users&view=login&return=' . $return); + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'notice'); + $app->redirect($login_url_with_return, 403); + } else { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return; + } + } + + /** + * NOTE: The following code (usually) sets the text to contain the fulltext, but it is the + * responsibility of the layout to check 'access-view' and only use "introtext" for guests + */ + if ($item->params->get('show_intro', '1') == '1') { + $item->text = $item->introtext . ' ' . $item->fulltext; + } elseif ($item->fulltext) { + $item->text = $item->fulltext; + } else { + $item->text = $item->introtext; + } + + $item->tags = new TagsHelper(); + $item->tags->getItemTags('com_content.article', $this->item->id); + + if (Associations::isEnabled() && $item->params->get('show_associations')) { + $item->associations = AssociationHelper::displayAssociations($item->id); + } + + // Process the content plugins. + PluginHelper::importPlugin('content'); + $this->dispatchEvent(new Event('onContentPrepare', array('com_content.article', &$item, &$item->params, $offset))); + + $item->event = new \stdClass(); + $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.article', &$item, &$item->params, $offset)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.article', &$item, &$item->params, $offset)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.article', &$item, &$item->params, $offset)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->item->params->get('pageclass_sfx', '')); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + $pathway = $app->getPathway(); + + /** + * Because the application sets a default page title, + * we need to get it from the menu item itself + */ + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); + } + + $title = $this->params->get('page_title', ''); + + // If the menu item is not linked to this article + if (!$this->menuItemMatchArticle) { + // If a browser page title is defined, use that, then fall back to the article title if set, then fall back to the page_title option + $title = $this->item->params->get('article_page_title', $this->item->title ?: $title); + + // Get ID of the category from active menu item + if ( + $menu && $menu->component == 'com_content' && isset($menu->query['view']) + && in_array($menu->query['view'], ['categories', 'category']) + ) { + $id = $menu->query['id']; + } else { + $id = 0; + } + + $path = array(array('title' => $this->item->title, 'link' => '')); + $category = Categories::getInstance('Content')->get($this->item->catid); + + while ($category !== null && $category->id != $id && $category->id !== 'root') { + $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $pathway->addItem($item['title'], $item['link']); + } + } + + if (empty($title)) { + $title = $this->item->title; + } + + $this->setDocumentTitle($title); + + if ($this->item->metadesc) { + $this->document->setDescription($this->item->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + if ($app->get('MetaAuthor') == '1') { + $author = $this->item->created_by_alias ?: $this->item->author; + $this->document->setMetaData('author', $author); + } + + $mdata = $this->item->metadata->toArray(); + + foreach ($mdata as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + + // If there is a pagebreak heading or title, add it to the page title + if (!empty($this->item->page_title)) { + $this->item->title = $this->item->title . ' - ' . $this->item->page_title; + $this->setDocumentTitle( + $this->item->page_title . ' - ' . Text::sprintf('PLG_CONTENT_PAGEBREAK_PAGE_NUM', $this->state->get('list.offset') + 1) + ); + } + + if ($this->print) { + $this->document->setMetaData('robots', 'noindex, nofollow'); + } + } } diff --git a/code/components/com_content/src/View/Categories/HtmlView.php b/code/components/com_content/src/View/Categories/HtmlView.php index f16ff6d5..acee52a6 100644 --- a/code/components/com_content/src/View/Categories/HtmlView.php +++ b/code/components/com_content/src/View/Categories/HtmlView.php @@ -1,4 +1,5 @@ getParams(); - $item->description = ''; - $obj = json_decode($item->images); + /** + * Method to reconcile non-standard names from components to usage in this class. + * Typically overridden in the component feed view class. + * + * @param object $item The item for a feed, an element of the $items array. + * + * @return void + * + * @since 3.2 + */ + protected function reconcileNames($item) + { + // Get description, intro_image, author and date + $app = Factory::getApplication(); + $params = $app->getParams(); + $item->description = ''; + $obj = json_decode($item->images); - if (!empty($obj->image_intro)) - { - $item->description = '

    ' . HTMLHelper::_('image', $obj->image_intro, $obj->image_intro_alt) . '

    '; - } + if (!empty($obj->image_intro)) { + $item->description = '

    ' . HTMLHelper::_('image', $obj->image_intro, $obj->image_intro_alt) . '

    '; + } - $item->description .= ($params->get('feed_summary', 0) ? $item->introtext . $item->fulltext : $item->introtext); + $item->description .= ($params->get('feed_summary', 0) ? $item->introtext . $item->fulltext : $item->introtext); - // Add readmore link to description if introtext is shown, show_readmore is true and fulltext exists - if (!$item->params->get('feed_summary', 0) && $item->params->get('feed_show_readmore', 0) && $item->fulltext) - { - // Compute the article slug - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + // Add readmore link to description if introtext is shown, show_readmore is true and fulltext exists + if (!$item->params->get('feed_summary', 0) && $item->params->get('feed_show_readmore', 0) && $item->fulltext) { + // Compute the article slug + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - // URL link to article - $link = Route::_( - RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language), - true, - $app->get('force_ssl') == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE, - true - ); + // URL link to article + $link = Route::_( + RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language), + true, + $app->get('force_ssl') == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE, + true + ); - $item->description .= '

    ' - . Text::_('COM_CONTENT_FEED_READMORE') . '

    '; - } + $item->description .= '

    ' + . Text::_('COM_CONTENT_FEED_READMORE') . '

    '; + } - $item->author = $item->created_by_alias ?: $item->author; - } + $item->author = $item->created_by_alias ?: $item->author; + } } diff --git a/code/components/com_content/src/View/Category/HtmlView.php b/code/components/com_content/src/View/Category/HtmlView.php index 90e31bf2..f5910431 100644 --- a/code/components/com_content/src/View/Category/HtmlView.php +++ b/code/components/com_content/src/View/Category/HtmlView.php @@ -1,4 +1,5 @@ pagination->hideEmptyLimitstart = true; - - // Prepare the data - // Get the metrics for the structural page layout. - $params = $this->params; - $numLeading = $params->def('num_leading_articles', 1); - $numIntro = $params->def('num_intro_articles', 4); - $numLinks = $params->def('num_links', 4); - $this->vote = PluginHelper::isEnabled('content', 'vote'); - - PluginHelper::importPlugin('content'); - - $app = Factory::getApplication(); - - // Compute the article slugs and prepare introtext (runs content plugins). - foreach ($this->items as $item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - - // No link for ROOT category - if ($item->parent_alias === 'root') - { - $item->parent_id = null; - } - - $item->event = new \stdClass; - - // Old plugins: Ensure that text property is available - if (!isset($item->text)) - { - $item->text = $item->introtext; - } - - $app->triggerEvent('onContentPrepare', array('com_content.category', &$item, &$item->params, 0)); - - // Old plugins: Use processed text as introtext - $item->introtext = $item->text; - - $results = $app->triggerEvent('onContentAfterTitle', array('com_content.category', &$item, &$item->params, 0)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = $app->triggerEvent('onContentBeforeDisplay', array('com_content.category', &$item, &$item->params, 0)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = $app->triggerEvent('onContentAfterDisplay', array('com_content.category', &$item, &$item->params, 0)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - } - - // For blog layouts, preprocess the breakdown of leading, intro and linked articles. - // This makes it much easier for the designer to just interrogate the arrays. - if ($params->get('layout_type') === 'blog' || $this->getLayout() === 'blog') - { - foreach ($this->items as $i => $item) - { - if ($i < $numLeading) - { - $this->lead_items[] = $item; - } - - elseif ($i >= $numLeading && $i < $numLeading + $numIntro) - { - $this->intro_items[] = $item; - } - - elseif ($i < $numLeading + $numIntro + $numLinks) - { - $this->link_items[] = $item; - } - } - } - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $active = $app->getMenu()->getActive(); - - if ($this->menuItemMatchCategory) - { - $this->params->def('page_heading', $this->params->get('page_title', $active->title)); - $title = $this->params->get('page_title', $active->title); - } - else - { - $this->params->def('page_heading', $this->category->title); - $title = $this->category->title; - $this->params->set('page_title', $title); - } - - if (empty($title)) - { - $title = $this->category->title; - } - - $this->setDocumentTitle($title); - - if ($this->category->metadesc) - { - $this->document->setDescription($this->category->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - if (!is_object($this->category->metadata)) - { - $this->category->metadata = new Registry($this->category->metadata); - } - - if (($app->get('MetaAuthor') == '1') && $this->category->get('author', '')) - { - $this->document->setMetaData('author', $this->category->get('author', '')); - } - - $mdata = $this->category->metadata->toArray(); - - foreach ($mdata as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function prepareDocument() - { - parent::prepareDocument(); - - parent::addFeed(); - - if ($this->menuItemMatchCategory) - { - // If the active menu item is linked directly to the category being displayed, no further process is needed - return; - } - - // Get ID of the category from active menu item - $menu = $this->menu; - - if ($menu && $menu->component == 'com_content' && isset($menu->query['view']) - && in_array($menu->query['view'], ['categories', 'category'])) - { - $id = $menu->query['id']; - } - else - { - $id = 0; - } - - $path = [['title' => $this->category->title, 'link' => '']]; - $category = $this->category->getParent(); - - while ($category !== null && $category->id !== 'root' && $category->id != $id) - { - $path[] = ['title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)]; - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $this->pathway->addItem($item['title'], $item['link']); - } - } + /** + * @var array Array of leading items for blog display + * @since 3.2 + */ + protected $lead_items = array(); + + /** + * @var array Array of intro items for blog display + * @since 3.2 + */ + protected $intro_items = array(); + + /** + * @var array Array of links in blog display + * @since 3.2 + */ + protected $link_items = array(); + + /** + * @var string The name of the extension for the category + * @since 3.2 + */ + protected $extension = 'com_content'; + + /** + * @var string Default title to use for page title + * @since 3.2 + */ + protected $defaultPageTitle = 'JGLOBAL_ARTICLES'; + + /** + * @var string The name of the view to link individual items to + * @since 3.2 + */ + protected $viewName = 'article'; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + parent::commonCategoryDisplay(); + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + // Prepare the data + // Get the metrics for the structural page layout. + $params = $this->params; + $numLeading = $params->def('num_leading_articles', 1); + $numIntro = $params->def('num_intro_articles', 4); + $numLinks = $params->def('num_links', 4); + $this->vote = PluginHelper::isEnabled('content', 'vote'); + + PluginHelper::importPlugin('content'); + + $app = Factory::getApplication(); + + // Compute the article slugs and prepare introtext (runs content plugins). + foreach ($this->items as $item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + + // No link for ROOT category + if ($item->parent_alias === 'root') { + $item->parent_id = null; + } + + $item->event = new \stdClass(); + + // Old plugins: Ensure that text property is available + if (!isset($item->text)) { + $item->text = $item->introtext; + } + + $app->triggerEvent('onContentPrepare', array('com_content.category', &$item, &$item->params, 0)); + + // Old plugins: Use processed text as introtext + $item->introtext = $item->text; + + $results = $app->triggerEvent('onContentAfterTitle', array('com_content.category', &$item, &$item->params, 0)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = $app->triggerEvent('onContentBeforeDisplay', array('com_content.category', &$item, &$item->params, 0)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = $app->triggerEvent('onContentAfterDisplay', array('com_content.category', &$item, &$item->params, 0)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + } + + // For blog layouts, preprocess the breakdown of leading, intro and linked articles. + // This makes it much easier for the designer to just interrogate the arrays. + if ($params->get('layout_type') === 'blog' || $this->getLayout() === 'blog') { + foreach ($this->items as $i => $item) { + if ($i < $numLeading) { + $this->lead_items[] = $item; + } elseif ($i >= $numLeading && $i < $numLeading + $numIntro) { + $this->intro_items[] = $item; + } elseif ($i < $numLeading + $numIntro + $numLinks) { + $this->link_items[] = $item; + } + } + } + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $active = $app->getMenu()->getActive(); + + if ($this->menuItemMatchCategory) { + $this->params->def('page_heading', $this->params->get('page_title', $active->title)); + $title = $this->params->get('page_title', $active->title); + } else { + $this->params->def('page_heading', $this->category->title); + $title = $this->category->title; + $this->params->set('page_title', $title); + } + + if (empty($title)) { + $title = $this->category->title; + } + + $this->setDocumentTitle($title); + + if ($this->category->metadesc) { + $this->document->setDescription($this->category->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + if (!is_object($this->category->metadata)) { + $this->category->metadata = new Registry($this->category->metadata); + } + + if (($app->get('MetaAuthor') == '1') && $this->category->get('author', '')) { + $this->document->setMetaData('author', $this->category->get('author', '')); + } + + $mdata = $this->category->metadata->toArray(); + + foreach ($mdata as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function prepareDocument() + { + parent::prepareDocument(); + + parent::addFeed(); + + if ($this->menuItemMatchCategory) { + // If the active menu item is linked directly to the category being displayed, no further process is needed + return; + } + + // Get ID of the category from active menu item + $menu = $this->menu; + + if ( + $menu && $menu->component == 'com_content' && isset($menu->query['view']) + && in_array($menu->query['view'], ['categories', 'category']) + ) { + $id = $menu->query['id']; + } else { + $id = 0; + } + + $path = [['title' => $this->category->title, 'link' => '']]; + $category = $this->category->getParent(); + + while ($category !== null && $category->id !== 'root' && $category->id != $id) { + $path[] = ['title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)]; + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $this->pathway->addItem($item['title'], $item['link']); + } + } } diff --git a/code/components/com_content/src/View/Featured/FeedView.php b/code/components/com_content/src/View/Featured/FeedView.php index 1a037b07..8e8eeeb6 100644 --- a/code/components/com_content/src/View/Featured/FeedView.php +++ b/code/components/com_content/src/View/Featured/FeedView.php @@ -1,4 +1,5 @@ getParams(); - $feedEmail = $app->get('feed_email', 'none'); - $siteEmail = $app->get('mailfrom'); - - $this->document->link = Route::_('index.php?option=com_content&view=featured'); - - // Get some data from the model - $app->input->set('limit', $app->get('feed_limit')); - $categories = Categories::getInstance('Content'); - $rows = $this->get('Items'); - - foreach ($rows as $row) - { - // Strip html from feed item title - $title = htmlspecialchars($row->title, ENT_QUOTES, 'UTF-8'); - $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); - - // Compute the article slug - $row->slug = $row->alias ? ($row->id . ':' . $row->alias) : $row->id; - - // URL link to article - $link = RouteHelper::getArticleRoute($row->slug, $row->catid, $row->language); - - $description = ''; - $obj = json_decode($row->images); - - if (!empty($obj->image_intro)) - { - $description = '

    ' . HTMLHelper::_('image', $obj->image_intro, $obj->image_intro_alt) . '

    '; - } - - $description .= ($params->get('feed_summary', 0) ? $row->introtext . $row->fulltext : $row->introtext); - $author = $row->created_by_alias ?: $row->author; - - // Load individual item creator class - $item = new FeedItem; - $item->title = $title; - $item->link = Route::_($link); - $item->date = $row->publish_up; - $item->category = array(); - - // All featured articles are categorized as "Featured" - $item->category[] = Text::_('JFEATURED'); - - for ($item_category = $categories->get($row->catid); $item_category !== null; $item_category = $item_category->getParent()) - { - // Only add non-root categories - if ($item_category->id > 1) - { - $item->category[] = $item_category->title; - } - } - - $item->author = $author; - - if ($feedEmail === 'site') - { - $item->authorEmail = $siteEmail; - } - elseif ($feedEmail === 'author') - { - $item->authorEmail = $row->author_email; - } - - // Add readmore link to description if introtext is shown, show_readmore is true and fulltext exists - if (!$params->get('feed_summary', 0) && $params->get('feed_show_readmore', 0) && $row->fulltext) - { - $link = Route::_($link, true, $app->get('force_ssl') == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE, true); - $description .= '

    ' - . Text::_('COM_CONTENT_FEED_READMORE') . '

    '; - } - - // Load item description and add div - $item->description = '
    ' . $description . '
    '; - - // Loads item info into rss array - $this->document->addItem($item); - } - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + // Parameters + $app = Factory::getApplication(); + $params = $app->getParams(); + $feedEmail = $app->get('feed_email', 'none'); + $siteEmail = $app->get('mailfrom'); + + $this->document->link = Route::_('index.php?option=com_content&view=featured'); + + // Get some data from the model + $app->input->set('limit', $app->get('feed_limit')); + $categories = Categories::getInstance('Content'); + $rows = $this->get('Items'); + + foreach ($rows as $row) { + // Strip html from feed item title + $title = htmlspecialchars($row->title, ENT_QUOTES, 'UTF-8'); + $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); + + // Compute the article slug + $row->slug = $row->alias ? ($row->id . ':' . $row->alias) : $row->id; + + // URL link to article + $link = RouteHelper::getArticleRoute($row->slug, $row->catid, $row->language); + + $description = ''; + $obj = json_decode($row->images); + + if (!empty($obj->image_intro)) { + $description = '

    ' . HTMLHelper::_('image', $obj->image_intro, $obj->image_intro_alt) . '

    '; + } + + $description .= ($params->get('feed_summary', 0) ? $row->introtext . $row->fulltext : $row->introtext); + $author = $row->created_by_alias ?: $row->author; + + // Load individual item creator class + $item = new FeedItem(); + $item->title = $title; + $item->link = Route::_($link); + $item->date = $row->publish_up; + $item->category = array(); + + // All featured articles are categorized as "Featured" + $item->category[] = Text::_('JFEATURED'); + + for ($item_category = $categories->get($row->catid); $item_category !== null; $item_category = $item_category->getParent()) { + // Only add non-root categories + if ($item_category->id > 1) { + $item->category[] = $item_category->title; + } + } + + $item->author = $author; + + if ($feedEmail === 'site') { + $item->authorEmail = $siteEmail; + } elseif ($feedEmail === 'author') { + $item->authorEmail = $row->author_email; + } + + // Add readmore link to description if introtext is shown, show_readmore is true and fulltext exists + if (!$params->get('feed_summary', 0) && $params->get('feed_show_readmore', 0) && $row->fulltext) { + $link = Route::_($link, true, $app->get('force_ssl') == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE, true); + $description .= '

    ' + . Text::_('COM_CONTENT_FEED_READMORE') . '

    '; + } + + // Load item description and add div + $item->description = '
    ' . $description . '
    '; + + // Loads item info into rss array + $this->document->addItem($item); + } + } } diff --git a/code/components/com_content/src/View/Featured/HtmlView.php b/code/components/com_content/src/View/Featured/HtmlView.php index ef037d93..f99c6293 100644 --- a/code/components/com_content/src/View/Featured/HtmlView.php +++ b/code/components/com_content/src/View/Featured/HtmlView.php @@ -1,4 +1,5 @@ get('State'); - $items = $this->get('Items'); - $pagination = $this->get('Pagination'); - - // Flag indicates to not add limitstart=0 to URL - $pagination->hideEmptyLimitstart = true; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - /** @var \Joomla\Registry\Registry $params */ - $params = &$state->params; - - // PREPARE THE DATA - - // Get the metrics for the structural page layout. - $numLeading = (int) $params->def('num_leading_articles', 1); - $numIntro = (int) $params->def('num_intro_articles', 4); - - PluginHelper::importPlugin('content'); - - // Compute the article slugs and prepare introtext (runs content plugins). - foreach ($items as &$item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - - // No link for ROOT category - if ($item->parent_alias === 'root') - { - $item->parent_id = null; - } - - $item->event = new \stdClass; - - // Old plugins: Ensure that text property is available - if (!isset($item->text)) - { - $item->text = $item->introtext; - } - - Factory::getApplication()->triggerEvent('onContentPrepare', array('com_content.featured', &$item, &$item->params, 0)); - - // Old plugins: Use processed text as introtext - $item->introtext = $item->text; - - $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.featured', &$item, &$item->params, 0)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.featured', &$item, &$item->params, 0)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.featured', &$item, &$item->params, 0)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - } - - // Preprocess the breakdown of leading, intro and linked articles. - // This makes it much easier for the designer to just integrate the arrays. - $max = count($items); - - // The first group is the leading articles. - $limit = $numLeading; - - for ($i = 0; $i < $limit && $i < $max; $i++) - { - $this->lead_items[$i] = &$items[$i]; - } - - // The second group is the intro articles. - $limit = $numLeading + $numIntro; - - // Order articles across, then down (or single column mode) - for ($i = $numLeading; $i < $limit && $i < $max; $i++) - { - $this->intro_items[$i] = &$items[$i]; - } - - // The remainder are the links. - for ($i = $numLeading + $numIntro; $i < $max; $i++) - { - $this->link_items[$i] = &$items[$i]; - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - $this->params = &$params; - $this->items = &$items; - $this->pagination = &$pagination; - $this->user = &$user; - $this->db = Factory::getDbo(); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - */ - protected function _prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - // Add feed links - if ($this->params->get('show_feed_link', 1)) - { - $link = '&format=feed&limitstart='; - $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); - $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); - $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); - $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); - } - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state = null; + + /** + * The featured articles array + * + * @var \stdClass[] + */ + protected $items = null; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination = null; + + /** + * The featured articles to be displayed as lead items. + * + * @var \stdClass[] + */ + protected $lead_items = array(); + + /** + * The featured articles to be displayed as intro items. + * + * @var \stdClass[] + */ + protected $intro_items = array(); + + /** + * The featured articles to be displayed as link items. + * + * @var \stdClass[] + */ + protected $link_items = array(); + + /** + * @var \Joomla\Database\DatabaseDriver + * + * @since 3.6.3 + * + * @deprecated 5.0 Will be removed without replacement + */ + protected $db; + + /** + * The user object + * + * @var \Joomla\CMS\User\User|null + */ + protected $user = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + + $state = $this->get('State'); + $items = $this->get('Items'); + $pagination = $this->get('Pagination'); + + // Flag indicates to not add limitstart=0 to URL + $pagination->hideEmptyLimitstart = true; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + /** @var \Joomla\Registry\Registry $params */ + $params = &$state->params; + + // PREPARE THE DATA + + // Get the metrics for the structural page layout. + $numLeading = (int) $params->def('num_leading_articles', 1); + $numIntro = (int) $params->def('num_intro_articles', 4); + + PluginHelper::importPlugin('content'); + + // Compute the article slugs and prepare introtext (runs content plugins). + foreach ($items as &$item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + + // No link for ROOT category + if ($item->parent_alias === 'root') { + $item->parent_id = null; + } + + $item->event = new \stdClass(); + + // Old plugins: Ensure that text property is available + if (!isset($item->text)) { + $item->text = $item->introtext; + } + + Factory::getApplication()->triggerEvent('onContentPrepare', array('com_content.featured', &$item, &$item->params, 0)); + + // Old plugins: Use processed text as introtext + $item->introtext = $item->text; + + $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.featured', &$item, &$item->params, 0)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.featured', &$item, &$item->params, 0)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.featured', &$item, &$item->params, 0)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + } + + // Preprocess the breakdown of leading, intro and linked articles. + // This makes it much easier for the designer to just integrate the arrays. + $max = count($items); + + // The first group is the leading articles. + $limit = $numLeading; + + for ($i = 0; $i < $limit && $i < $max; $i++) { + $this->lead_items[$i] = &$items[$i]; + } + + // The second group is the intro articles. + $limit = $numLeading + $numIntro; + + // Order articles across, then down (or single column mode) + for ($i = $numLeading; $i < $limit && $i < $max; $i++) { + $this->intro_items[$i] = &$items[$i]; + } + + // The remainder are the links. + for ($i = $numLeading + $numIntro; $i < $max; $i++) { + $this->link_items[$i] = &$items[$i]; + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + $this->params = &$params; + $this->items = &$items; + $this->pagination = &$pagination; + $this->user = &$user; + $this->db = Factory::getDbo(); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + */ + protected function _prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + // Add feed links + if ($this->params->get('show_feed_link', 1)) { + $link = '&format=feed&limitstart='; + $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); + $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); + $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); + $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); + } + } } diff --git a/code/components/com_content/src/View/Form/HtmlView.php b/code/components/com_content/src/View/Form/HtmlView.php index eb685043..e2d33df1 100644 --- a/code/components/com_content/src/View/Form/HtmlView.php +++ b/code/components/com_content/src/View/Form/HtmlView.php @@ -1,4 +1,5 @@ getIdentity(); - - // Get model data. - $this->state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - $this->return_page = $this->get('ReturnPage'); - - if (empty($this->item->id)) - { - $catid = $this->state->params->get('catid'); - - if ($this->state->params->get('enable_category') == 1 && $catid) - { - $authorised = $user->authorise('core.create', 'com_content.category.' . $catid); - } - else - { - $authorised = $user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')); - } - } - else - { - $authorised = $this->item->params->get('access-edit'); - } - - if ($authorised !== true) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return false; - } - - $this->item->tags = new TagsHelper; - - if (!empty($this->item->id)) - { - $this->item->tags->getItemTags('com_content.article', $this->item->id); - - $this->item->images = json_decode($this->item->images); - $this->item->urls = json_decode($this->item->urls); - - $tmp = new \stdClass; - $tmp->images = $this->item->images; - $tmp->urls = $this->item->urls; - $this->form->bind($tmp); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Create a shortcut to the parameters. - $params = &$this->state->params; - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - $this->params = $params; - - // Override global params with article specific params - $this->params->merge($this->item->params); - $this->user = $user; - - // Propose current language as default when creating new article - if (empty($this->item->id) && Multilanguage::isEnabled() && $params->get('enable_category') != 1) - { - $lang = Factory::getLanguage()->getTag(); - $this->form->setFieldAttribute('language', 'default', $lang); - } - - $captchaSet = $params->get('captcha', Factory::getApplication()->get('captcha', '0')); - - foreach (PluginHelper::getPlugin('captcha') as $plugin) - { - if ($captchaSet === $plugin->name) - { - $this->captchaEnabled = true; - break; - } - } - - // If the article is being edited and the current user has permission to create article - if ($this->item->id - && ($user->authorise('core.create', 'com_content') || \count($user->getAuthorisedCategories('com_content', 'core.create')))) - { - $this->showSaveAsCopy = true; - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_CONTENT_FORM_EDIT_ARTICLE')); - } - - $title = $this->params->def('page_title', Text::_('COM_CONTENT_FORM_EDIT_ARTICLE')); - - $this->setDocumentTitle($title); - - $app->getPathway()->addItem($title); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The item being created + * + * @var \stdClass + */ + protected $item; + + /** + * The page to return to after the article is submitted + * + * @var string + */ + protected $return_page = ''; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The user object + * + * @var \Joomla\CMS\User\User + * + * @since 4.0.0 + */ + protected $user = null; + + /** + * Should we show a captcha form for the submission of the article? + * + * @var boolean + * + * @since 3.7.0 + */ + protected $captchaEnabled = false; + + /** + * Should we show Save As Copy button? + * + * @var boolean + * @since 4.1.0 + */ + protected $showSaveAsCopy = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $user = $app->getIdentity(); + + // Get model data. + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + $this->return_page = $this->get('ReturnPage'); + + if (empty($this->item->id)) { + $catid = $this->state->params->get('catid'); + + if ($this->state->params->get('enable_category') == 1 && $catid) { + $authorised = $user->authorise('core.create', 'com_content.category.' . $catid); + } else { + $authorised = $user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')); + } + } else { + $authorised = $this->item->params->get('access-edit'); + } + + if ($authorised !== true) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return false; + } + + $this->item->tags = new TagsHelper(); + + if (!empty($this->item->id)) { + $this->item->tags->getItemTags('com_content.article', $this->item->id); + + $this->item->images = json_decode($this->item->images); + $this->item->urls = json_decode($this->item->urls); + + $tmp = new \stdClass(); + $tmp->images = $this->item->images; + $tmp->urls = $this->item->urls; + $this->form->bind($tmp); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Create a shortcut to the parameters. + $params = &$this->state->params; + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + $this->params = $params; + + // Override global params with article specific params + $this->params->merge($this->item->params); + $this->user = $user; + + // Propose current language as default when creating new article + if (empty($this->item->id) && Multilanguage::isEnabled() && $params->get('enable_category') != 1) { + $lang = Factory::getLanguage()->getTag(); + $this->form->setFieldAttribute('language', 'default', $lang); + } + + $captchaSet = $params->get('captcha', Factory::getApplication()->get('captcha', '0')); + + foreach (PluginHelper::getPlugin('captcha') as $plugin) { + if ($captchaSet === $plugin->name) { + $this->captchaEnabled = true; + break; + } + } + + // If the article is being edited and the current user has permission to create article + if ( + $this->item->id + && ($user->authorise('core.create', 'com_content') || \count($user->getAuthorisedCategories('com_content', 'core.create'))) + ) { + $this->showSaveAsCopy = true; + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_CONTENT_FORM_EDIT_ARTICLE')); + } + + $title = $this->params->def('page_title', Text::_('COM_CONTENT_FORM_EDIT_ARTICLE')); + + $this->setDocumentTitle($title); + + $app->getPathway()->addItem($title); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/code/components/com_content/tmpl/archive/default.php b/code/components/com_content/tmpl/archive/default.php index 95d7a2f9..e749e93a 100644 --- a/code/components/com_content/tmpl/archive/default.php +++ b/code/components/com_content/tmpl/archive/default.php @@ -1,4 +1,5 @@
    params->get('show_page_heading')) : ?> - +
    -
    - +
    loadTemplate('items'); ?>
    diff --git a/code/components/com_content/tmpl/archive/default_items.php b/code/components/com_content/tmpl/archive/default_items.php index 5e83c2b5..e080d636 100644 --- a/code/components/com_content/tmpl/archive/default_items.php +++ b/code/components/com_content/tmpl/archive/default_items.php @@ -1,4 +1,5 @@ params; ?>
    - items as $i => $item) : ?> - params->get('info_block_position', 0); ?> -
    - + + + event->afterDisplayContent; ?> +
    +
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - -
    - pagination->getPagesLinks(); ?> -
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + +
    + pagination->getPagesLinks(); ?> +
    diff --git a/code/components/com_content/tmpl/article/default.php b/code/components/com_content/tmpl/article/default.php index 160dae0e..41e4d639 100644 --- a/code/components/com_content/tmpl/article/default.php +++ b/code/components/com_content/tmpl/article/default.php @@ -1,4 +1,5 @@ item->publish_down) && $this->item->publish_down < $currentDate; ?>
    - - params->get('show_page_heading')) : ?> - - item->pagination) && !$this->item->paginationposition && $this->item->paginationrelative) - { - echo $this->item->pagination; - } - ?> + + params->get('show_page_heading')) : ?> + + item->pagination) && !$this->item->paginationposition && $this->item->paginationrelative) { + echo $this->item->pagination; + } + ?> - get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') - || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam; ?> + get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') + || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam; ?> - get('show_title')) : ?> - - - - $params, 'item' => $this->item)); ?> - + get('show_title')) : ?> + + + + $params, 'item' => $this->item)); ?> + - - item->event->afterDisplayTitle; ?> + + item->event->afterDisplayTitle; ?> - - $this->item, 'params' => $params, 'position' => 'above')); ?> - + + $this->item, 'params' => $params, 'position' => 'above')); ?> + - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tagLayout = new FileLayout('joomla.content.tags'); ?> + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tagLayout = new FileLayout('joomla.content.tags'); ?> - item->tagLayout->render($this->item->tags->itemTags); ?> - + item->tagLayout->render($this->item->tags->itemTags); ?> + - - item->event->beforeDisplayContent; ?> + + item->event->beforeDisplayContent; ?> - get('urls_position', 0) === 0) : ?> - loadTemplate('links'); ?> - - get('access-view')) : ?> - item); ?> - item->pagination) && !$this->item->paginationposition && !$this->item->paginationrelative) : - echo $this->item->pagination; - endif; - ?> - item->toc)) : - echo $this->item->toc; - endif; ?> -
    - item->text; ?> -
    + get('urls_position', 0) === 0) : ?> + loadTemplate('links'); ?> + + get('access-view')) : ?> + item); ?> + item->pagination) && !$this->item->paginationposition && !$this->item->paginationrelative) : + echo $this->item->pagination; + endif; + ?> + item->toc)) : + echo $this->item->toc; + endif; ?> +
    + item->text; ?> +
    - - - $this->item, 'params' => $params, 'position' => 'below')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tagLayout = new FileLayout('joomla.content.tags'); ?> - item->tagLayout->render($this->item->tags->itemTags); ?> - - + + + $this->item, 'params' => $params, 'position' => 'below')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tagLayout = new FileLayout('joomla.content.tags'); ?> + item->tagLayout->render($this->item->tags->itemTags); ?> + + - item->pagination) && $this->item->paginationposition && !$this->item->paginationrelative) : - echo $this->item->pagination; - ?> - - get('urls_position', 0) === 1) : ?> - loadTemplate('links'); ?> - - - get('show_noauth') == true && $user->get('guest')) : ?> - item); ?> - item->introtext); ?> - - get('show_readmore') && $this->item->fulltext != null) : ?> - getMenu(); ?> - getActive(); ?> - id; ?> - - setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); ?> - $this->item, 'params' => $params, 'link' => $link)); ?> - - - item->pagination) && $this->item->paginationposition && $this->item->paginationrelative) : - echo $this->item->pagination; - ?> - - - item->event->afterDisplayContent; ?> + item->pagination) && $this->item->paginationposition && !$this->item->paginationrelative) : + echo $this->item->pagination; + ?> + + get('urls_position', 0) === 1) : ?> + loadTemplate('links'); ?> + + + get('show_noauth') == true && $user->get('guest')) : ?> + item); ?> + item->introtext); ?> + + get('show_readmore') && $this->item->fulltext != null) : ?> + getMenu(); ?> + getActive(); ?> + id; ?> + + setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); ?> + $this->item, 'params' => $params, 'link' => $link)); ?> + + + item->pagination) && $this->item->paginationposition && $this->item->paginationrelative) : + echo $this->item->pagination; + ?> + + + item->event->afterDisplayContent; ?>
    diff --git a/code/components/com_content/tmpl/article/default_links.php b/code/components/com_content/tmpl/article/default_links.php index 51e25ed3..a8e4c427 100644 --- a/code/components/com_content/tmpl/article/default_links.php +++ b/code/components/com_content/tmpl/article/default_links.php @@ -1,4 +1,5 @@ item->params; if ($urls && (!empty($urls->urla) || !empty($urls->urlb) || !empty($urls->urlc))) : -?> + ?>
    -
      - urla, $urls->urlatext, $urls->targeta, 'a'), - array($urls->urlb, $urls->urlbtext, $urls->targetb, 'b'), - array($urls->urlc, $urls->urlctext, $urls->targetc, 'c') - ); - foreach ($urlarray as $url) : - $link = $url[0]; - $label = $url[1]; - $target = $url[2]; - $id = $url[3]; +
        + urla, $urls->urlatext, $urls->targeta, 'a'), + array($urls->urlb, $urls->urlbtext, $urls->targetb, 'b'), + array($urls->urlc, $urls->urlctext, $urls->targetc, 'c') + ); + foreach ($urlarray as $url) : + $link = $url[0]; + $label = $url[1]; + $target = $url[2]; + $id = $url[3]; - if ( ! $link) : - continue; - endif; + if (! $link) : + continue; + endif; - // If no label is present, take the link - $label = $label ?: $link; + // If no label is present, take the link + $label = $label ?: $link; - // If no target is present, use the default - $target = $target ?: $params->get('target' . $id); - ?> -
      • - get('target' . $id); + ?> +
      • + ' . - htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ''; - break; + switch ($target) { + case 1: + // Open in a new window + echo '' . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ''; + break; - case 2: - // Open in a popup window - $attribs = 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=600'; - echo "" . - htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ''; - break; - case 3: - echo '' . - htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; - echo HTMLHelper::_( - 'bootstrap.renderModal', - 'linkModal', - array( - 'url' => $link, - 'title' => $label, - 'height' => '100%', - 'width' => '100%', - 'modalWidth' => '500', - 'bodyHeight' => '500', - 'footer' => '' - ) - ); - break; + case 2: + // Open in a popup window + $attribs = 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=600'; + echo "" . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ''; + break; + case 3: + echo '' . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; + echo HTMLHelper::_( + 'bootstrap.renderModal', + 'linkModal', + array( + 'url' => $link, + 'title' => $label, + 'height' => '100%', + 'width' => '100%', + 'modalWidth' => '500', + 'bodyHeight' => '500', + 'footer' => '' + ) + ); + break; - default: - // Open in parent window - echo '' . - htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; - break; - } - ?> -
      • - -
      + default: + // Open in parent window + echo '' . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; + break; + } + ?> + + +
    diff --git a/code/components/com_content/tmpl/categories/default.php b/code/components/com_content/tmpl/categories/default.php index 19a6961a..a1bd6b92 100644 --- a/code/components/com_content/tmpl/categories/default.php +++ b/code/components/com_content/tmpl/categories/default.php @@ -1,4 +1,5 @@
    - loadTemplate('items'); - ?> + loadTemplate('items'); + ?>
    diff --git a/code/components/com_content/tmpl/categories/default_items.php b/code/components/com_content/tmpl/categories/default_items.php index a591a4a8..2ed17e9f 100644 --- a/code/components/com_content/tmpl/categories/default_items.php +++ b/code/components/com_content/tmpl/categories/default_items.php @@ -1,4 +1,5 @@ maxLevelcat != 0 && count($this->items[$this->parent->id]) > 0) : -?> -
    - items[$this->parent->id] as $id => $item) : ?> - params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> -
    -
    -
    - - escape($item->title); ?> - params->get('show_cat_num_articles_cat') == 1) :?> - -   - numitems; ?> - - -
    - getChildren()) > 0 && $this->maxLevelcat > 1) : ?> - - -
    - params->get('show_description_image') && $item->getParams()->get('image')) : ?> - getParams()->get('image'), $item->getParams()->get('image_alt')); ?> - - params->get('show_subcat_desc_cat') == 1) : ?> - description) : ?> -
    - description, '', 'com_content.categories'); ?> -
    - - + ?> +
    + items[$this->parent->id] as $id => $item) : ?> + params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> +
    +
    +
    + + escape($item->title); ?> + params->get('show_cat_num_articles_cat') == 1) :?> + +   + numitems; ?> + + +
    + getChildren()) > 0 && $this->maxLevelcat > 1) : ?> + + +
    + params->get('show_description_image') && $item->getParams()->get('image')) : ?> + getParams()->get('image'), $item->getParams()->get('image_alt')); ?> + + params->get('show_subcat_desc_cat') == 1) : ?> + description) : ?> +
    + description, '', 'com_content.categories'); ?> +
    + + - getChildren()) > 0 && $this->maxLevelcat > 1) : ?> - - -
    - - -
    + getChildren()) > 0 && $this->maxLevelcat > 1) : ?> + + +
    + + +
    diff --git a/code/components/com_content/tmpl/category/blog.php b/code/components/com_content/tmpl/category/blog.php index cd6d6565..3dcc50da 100644 --- a/code/components/com_content/tmpl/category/blog.php +++ b/code/components/com_content/tmpl/category/blog.php @@ -1,4 +1,5 @@ diff --git a/code/components/com_content/tmpl/category/blog.xml b/code/components/com_content/tmpl/category/blog.xml index 86145c25..eba3a1f2 100644 --- a/code/components/com_content/tmpl/category/blog.xml +++ b/code/components/com_content/tmpl/category/blog.xml @@ -30,6 +30,7 @@ label="JTAG" multiple="true" mode="nested" + custom="deny" /> diff --git a/code/components/com_content/tmpl/category/blog_children.php b/code/components/com_content/tmpl/category/blog_children.php index 7f4c921b..67573300 100644 --- a/code/components/com_content/tmpl/category/blog_children.php +++ b/code/components/com_content/tmpl/category/blog_children.php @@ -1,4 +1,5 @@ getAuthorisedViewLevels(); if ($this->maxLevel != 0 && count($this->children[$this->category->id]) > 0) : ?> + children[$this->category->id] as $id => $child) : ?> + + access, $groups)) : ?> + params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> + - - - + maxLevel > 1 && count($child->getChildren()) > 0) : ?> + + + + + + format('Y-m-d H:i:s'); $isUnpublished = ($this->item->state == ContentComponent::CONDITION_UNPUBLISHED || $this->item->publish_up > $currentDate) - || ($this->item->publish_down < $currentDate && $this->item->publish_down !== null); + || ($this->item->publish_down < $currentDate && $this->item->publish_down !== null); ?> item); ?>
    - -
    - - - item); ?> - - - $params, 'item' => $this->item)); ?> - - - - get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') - || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?> - - - $this->item, 'params' => $params, 'position' => 'above')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tags->itemTags); ?> - - - get('show_intro')) : ?> - - item->event->afterDisplayTitle; ?> - - - - item->event->beforeDisplayContent; ?> - - item->introtext; ?> - - - - $this->item, 'params' => $params, 'position' => 'below')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tags->itemTags); ?> - - - - get('show_readmore') && $this->item->readmore) : - if ($params->get('access-view')) : - $link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); - else : - $menu = Factory::getApplication()->getMenu(); - $active = $menu->getActive(); - $itemId = $active->id; - $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); - $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); - endif; ?> - - $this->item, 'params' => $params, 'link' => $link)); ?> - - - - -
    - - - - item->event->afterDisplayContent; ?> + +
    + + + item); ?> + + + $params, 'item' => $this->item)); ?> + + + + get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') + || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?> + + + $this->item, 'params' => $params, 'position' => 'above')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tags->itemTags); ?> + + + get('show_intro')) : ?> + + item->event->afterDisplayTitle; ?> + + + + item->event->beforeDisplayContent; ?> + + item->introtext; ?> + + + + $this->item, 'params' => $params, 'position' => 'below')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tags->itemTags); ?> + + + + get('show_readmore') && $this->item->readmore) : + if ($params->get('access-view')) : + $link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); + else : + $menu = Factory::getApplication()->getMenu(); + $active = $menu->getActive(); + $itemId = $active->id; + $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); + $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); + endif; ?> + + $this->item, 'params' => $params, 'link' => $link)); ?> + + + + +
    + + + + item->event->afterDisplayContent; ?>
    diff --git a/code/components/com_content/tmpl/category/blog_links.php b/code/components/com_content/tmpl/category/blog_links.php index 0027a357..83250ff6 100644 --- a/code/components/com_content/tmpl/category/blog_links.php +++ b/code/components/com_content/tmpl/category/blog_links.php @@ -1,4 +1,5 @@ diff --git a/code/components/com_content/tmpl/category/default.php b/code/components/com_content/tmpl/category/default.php index ab946a6c..d2e30dc6 100644 --- a/code/components/com_content/tmpl/category/default.php +++ b/code/components/com_content/tmpl/category/default.php @@ -1,4 +1,5 @@ diff --git a/code/components/com_content/tmpl/category/default_articles.php b/code/components/com_content/tmpl/category/default_articles.php index 8d0193bd..d6c8d5b7 100644 --- a/code/components/com_content/tmpl/category/default_articles.php +++ b/code/components/com_content/tmpl/category/default_articles.php @@ -1,4 +1,5 @@ params->get('filter_field') === 'tag') && (Multilanguage::isEnabled())) -{ - $tagfilter = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter'); +if (($this->params->get('filter_field') === 'tag') && (Multilanguage::isEnabled())) { + $tagfilter = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter'); - switch ($tagfilter) - { - case 'current_language': - $langFilter = Factory::getApplication()->getLanguage()->getTag(); - break; + switch ($tagfilter) { + case 'current_language': + $langFilter = Factory::getApplication()->getLanguage()->getTag(); + break; - case 'all': - $langFilter = false; - break; + case 'all': + $langFilter = false; + break; - default: - $langFilter = $tagfilter; - } + default: + $langFilter = $tagfilter; + } } // Check for at least one editable article $isEditable = false; -if (!empty($this->items)) -{ - foreach ($this->items as $article) - { - if ($article->params->get('access-edit')) - { - $isEditable = true; - break; - } - } +if (!empty($this->items)) { + foreach ($this->items as $article) { + if ($article->params->get('access-edit')) { + $isEditable = true; + break; + } + } } $currentDate = Factory::getDate()->format('Y-m-d H:i:s'); ?> diff --git a/code/components/com_content/tmpl/category/default_children.php b/code/components/com_content/tmpl/category/default_children.php index bb8b5334..21d741a4 100644 --- a/code/components/com_content/tmpl/category/default_children.php +++ b/code/components/com_content/tmpl/category/default_children.php @@ -1,4 +1,5 @@ children[$this->category->id]) > 0) : ?> - children[$this->category->id] as $id => $child) : ?> - - access, $groups)) : ?> - params->get('show_empty_categories') || $child->getNumItems(true) || count($child->getChildren())) : ?> - - - - + + + + diff --git a/code/components/com_content/tmpl/featured/default.php b/code/components/com_content/tmpl/featured/default.php index fd46e43c..fd98ab1f 100644 --- a/code/components/com_content/tmpl/featured/default.php +++ b/code/components/com_content/tmpl/featured/default.php @@ -1,4 +1,5 @@ diff --git a/code/components/com_content/tmpl/featured/default_item.php b/code/components/com_content/tmpl/featured/default_item.php index 250764eb..1d43493d 100644 --- a/code/components/com_content/tmpl/featured/default_item.php +++ b/code/components/com_content/tmpl/featured/default_item.php @@ -1,4 +1,5 @@ item); ?>
    - -
    - - - get('show_title')) : ?> -

    - get('link_titles') && $params->get('access-view')) : ?> - - - escape($this->item->title); ?> - -

    - - - item->state == ContentComponent::CONDITION_UNPUBLISHED) : ?> - - - - - - - - - - - $params, 'item' => $this->item)); ?> - - - - item->event->afterDisplayTitle; ?> - - - get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') - || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?> - - - $this->item, 'params' => $params, 'position' => 'above')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tags->itemTags); ?> - - - - item->event->beforeDisplayContent; ?> - - item->introtext; ?> - - - - $this->item, 'params' => $params, 'position' => 'below')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tags->itemTags); ?> - - - - get('show_readmore') && $this->item->readmore) : - if ($params->get('access-view')) : - $link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); - else : - $menu = Factory::getApplication()->getMenu(); - $active = $menu->getActive(); - $itemId = $active->id; - $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); - $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); - endif; ?> - - $this->item, 'params' => $params, 'link' => $link)); ?> - - - - -
    - + +
    + + + get('show_title')) : ?> +

    + get('link_titles') && $params->get('access-view')) : ?> + + + escape($this->item->title); ?> + +

    + + + item->state == ContentComponent::CONDITION_UNPUBLISHED) : ?> + + + + + + + + + + + $params, 'item' => $this->item)); ?> + + + + item->event->afterDisplayTitle; ?> + + + get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') + || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?> + + + $this->item, 'params' => $params, 'position' => 'above')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tags->itemTags); ?> + + + + item->event->beforeDisplayContent; ?> + + item->introtext; ?> + + + + $this->item, 'params' => $params, 'position' => 'below')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tags->itemTags); ?> + + + + get('show_readmore') && $this->item->readmore) : + if ($params->get('access-view')) : + $link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); + else : + $menu = Factory::getApplication()->getMenu(); + $active = $menu->getActive(); + $itemId = $active->id; + $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); + $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); + endif; ?> + + $this->item, 'params' => $params, 'link' => $link)); ?> + + + + +
    +
    diff --git a/code/components/com_content/tmpl/featured/default_links.php b/code/components/com_content/tmpl/featured/default_links.php index 3f87a682..083f2b9f 100644 --- a/code/components/com_content/tmpl/featured/default_links.php +++ b/code/components/com_content/tmpl/featured/default_links.php @@ -1,4 +1,5 @@ diff --git a/code/components/com_content/tmpl/form/edit.php b/code/components/com_content/tmpl/form/edit.php index 5b586502..7477afd6 100644 --- a/code/components/com_content/tmpl/form/edit.php +++ b/code/components/com_content/tmpl/form/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_content.form-edit'); + ->useScript('form.validate') + ->useScript('com_content.form-edit'); $this->tab_name = 'com-content-form'; $this->ignore_fieldsets = array('image-intro', 'image-full', 'jmetadata', 'item_associations'); @@ -31,149 +32,152 @@ // This checks if the editor config options have ever been saved. If they haven't they will fall back to the original settings. $editoroptions = isset($params->show_publishing_options); -if (!$editoroptions) -{ - $params->show_urls_images_frontend = '0'; +if (!$editoroptions) { + $params->show_urls_images_frontend = '0'; } ?>
    - get('show_page_heading')) : ?> - - - -
    -
    - tab_name, ['active' => 'editor', 'recall' => true, 'breakpoint' => 768]); ?> - - tab_name, 'editor', Text::_('COM_CONTENT_ARTICLE_CONTENT')); ?> - form->renderField('title'); ?> - - item->id)) : ?> - form->renderField('alias'); ?> - - - form->renderField('articletext'); ?> - - captchaEnabled) : ?> - form->renderField('captcha'); ?> - - - - get('show_urls_images_frontend')) : ?> - tab_name, 'images', Text::_('COM_CONTENT_IMAGES_AND_URLS')); ?> - form->renderField('image_intro', 'images'); ?> - form->renderField('image_intro_alt', 'images'); ?> - form->renderField('image_intro_alt_empty', 'images'); ?> - form->renderField('image_intro_caption', 'images'); ?> - form->renderField('float_intro', 'images'); ?> - form->renderField('image_fulltext', 'images'); ?> - form->renderField('image_fulltext_alt', 'images'); ?> - form->renderField('image_fulltext_alt_empty', 'images'); ?> - form->renderField('image_fulltext_caption', 'images'); ?> - form->renderField('float_fulltext', 'images'); ?> - form->renderField('urla', 'urls'); ?> - form->renderField('urlatext', 'urls'); ?> -
    -
    - form->getInput('targeta', 'urls'); ?> -
    -
    - form->renderField('urlb', 'urls'); ?> - form->renderField('urlbtext', 'urls'); ?> -
    -
    - form->getInput('targetb', 'urls'); ?> -
    -
    - form->renderField('urlc', 'urls'); ?> - form->renderField('urlctext', 'urls'); ?> -
    -
    - form->getInput('targetc', 'urls'); ?> -
    -
    - - - - - - tab_name, 'publishing', Text::_('COM_CONTENT_PUBLISHING')); ?> - - form->renderField('transition'); ?> - form->renderField('state'); ?> - form->renderField('catid'); ?> - form->renderField('tags'); ?> - form->renderField('note'); ?> - get('save_history', 0)) : ?> - form->renderField('version_note'); ?> - - get('show_publishing_options', 1) == 1) : ?> - form->renderField('created_by_alias'); ?> - - item->params->get('access-change')) : ?> - form->renderField('featured'); ?> - get('show_publishing_options', 1) == 1) : ?> - form->renderField('featured_up'); ?> - form->renderField('featured_down'); ?> - form->renderField('publish_up'); ?> - form->renderField('publish_down'); ?> - - - form->renderField('access'); ?> - item->id)) : ?> -
    -
    -
    -
    - -
    -
    - - - - - tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL')); ?> - form->renderField('language'); ?> - - - form->renderField('language'); ?> - - - get('show_publishing_options', 1) == 1) : ?> - tab_name, 'metadata', Text::_('COM_CONTENT_METADATA')); ?> - form->renderField('metadesc'); ?> - form->renderField('metakey'); ?> - - - - - - - - -
    -
    - - showSaveAsCopy) : ?> - - - - get('save_history', 0) && $this->item->id) : ?> - form->getInput('contenthistory'); ?> - -
    -
    + get('show_page_heading')) : ?> + + + +
    +
    + tab_name, ['active' => 'editor', 'recall' => true, 'breakpoint' => 768]); ?> + + tab_name, 'editor', Text::_('COM_CONTENT_ARTICLE_CONTENT')); ?> + form->renderField('title'); ?> + + item->id)) : ?> + form->renderField('alias'); ?> + + + form->renderField('articletext'); ?> + + captchaEnabled) : ?> + form->renderField('captcha'); ?> + + + + get('show_urls_images_frontend')) : ?> + tab_name, 'images', Text::_('COM_CONTENT_IMAGES_AND_URLS')); ?> + form->renderField('image_intro', 'images'); ?> + form->renderField('image_intro_alt', 'images'); ?> + form->renderField('image_intro_alt_empty', 'images'); ?> + form->renderField('image_intro_caption', 'images'); ?> + form->renderField('float_intro', 'images'); ?> + form->renderField('image_fulltext', 'images'); ?> + form->renderField('image_fulltext_alt', 'images'); ?> + form->renderField('image_fulltext_alt_empty', 'images'); ?> + form->renderField('image_fulltext_caption', 'images'); ?> + form->renderField('float_fulltext', 'images'); ?> + form->renderField('urla', 'urls'); ?> + form->renderField('urlatext', 'urls'); ?> +
    +
    + form->getInput('targeta', 'urls'); ?> +
    +
    + form->renderField('urlb', 'urls'); ?> + form->renderField('urlbtext', 'urls'); ?> +
    +
    + form->getInput('targetb', 'urls'); ?> +
    +
    + form->renderField('urlc', 'urls'); ?> + form->renderField('urlctext', 'urls'); ?> +
    +
    + form->getInput('targetc', 'urls'); ?> +
    +
    + + + + + + tab_name, 'publishing', Text::_('COM_CONTENT_PUBLISHING')); ?> + + form->renderField('transition'); ?> + form->renderField('state'); ?> + form->renderField('catid'); ?> + form->renderField('tags'); ?> + form->renderField('note'); ?> + get('save_history', 0)) : ?> + form->renderField('version_note'); ?> + + get('show_publishing_options', 1) == 1) : ?> + form->renderField('created_by_alias'); ?> + + item->params->get('access-change')) : ?> + form->renderField('featured'); ?> + get('show_publishing_options', 1) == 1) : ?> + form->renderField('featured_up'); ?> + form->renderField('featured_down'); ?> + form->renderField('publish_up'); ?> + form->renderField('publish_down'); ?> + + + form->renderField('access'); ?> + item->id)) : ?> +
    +
    +
    +
    + +
    +
    + + + + + tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL')); ?> + form->renderField('language'); ?> + + + form->renderField('language'); ?> + + + get('show_publishing_options', 1) == 1) : ?> + tab_name, 'metadata', Text::_('COM_CONTENT_METADATA')); ?> + form->renderField('metadesc'); ?> + form->renderField('metakey'); ?> + + + + + + + + +
    +
    + + + showSaveAsCopy) : ?> + + + + get('save_history', 0) && $this->item->id) : ?> + form->getInput('contenthistory'); ?> + +
    +
    diff --git a/code/components/com_contenthistory/src/Controller/DisplayController.php b/code/components/com_contenthistory/src/Controller/DisplayController.php index 29bf90b0..de913939 100644 --- a/code/components/com_contenthistory/src/Controller/DisplayController.php +++ b/code/components/com_contenthistory/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getLanguage()->load($this->option, JPATH_ADMINISTRATOR) || - $this->app->getLanguage()->load($this->option, JPATH_SITE); - } + /** + * Load the language + * + * @since 4.0.0 + * + * @return void + */ + protected function loadLanguage() + { + // Load common and local language files. + $this->app->getLanguage()->load($this->option, JPATH_ADMINISTRATOR) || + $this->app->getLanguage()->load($this->option, JPATH_SITE); + } - /** - * Method to check component access permission - * - * @since 4.0.0 - * - * @return void - * - * @throws \Exception|NotAllowed - */ - protected function checkAccess() - { - // Check the user has permission to access this component if in the backend - if ($this->app->getIdentity()->guest) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + * + * @throws \Exception|NotAllowed + */ + protected function checkAccess() + { + // Check the user has permission to access this component if in the backend + if ($this->app->getIdentity()->guest) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } - /** - * Get a controller from the component - * - * @param string $name Controller name - * @param string $client Optional client (like Administrator, Site etc.) - * @param array $config Optional controller config - * - * @return BaseController - * - * @since 4.0.0 - */ - public function getController(string $name, string $client = '', array $config = array()): BaseController - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - $client = 'Administrator'; + /** + * Get a controller from the component + * + * @param string $name Controller name + * @param string $client Optional client (like Administrator, Site etc.) + * @param array $config Optional controller config + * + * @return BaseController + * + * @since 4.0.0 + */ + public function getController(string $name, string $client = '', array $config = array()): BaseController + { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + $client = 'Administrator'; - return parent::getController($name, $client, $config); - } + return parent::getController($name, $client, $config); + } } diff --git a/code/components/com_fields/layouts/field/render.php b/code/components/com_fields/layouts/field/render.php index 0623fd73..cd6e4144 100644 --- a/code/components/com_fields/layouts/field/render.php +++ b/code/components/com_fields/layouts/field/render.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; -if (!array_key_exists('field', $displayData)) -{ - return; +if (!array_key_exists('field', $displayData)) { + return; } $field = $displayData['field']; @@ -24,19 +25,18 @@ $labelClass = $field->params->get('label_render_class'); $valueClass = $field->params->get('value_render_class'); -if ($value == '') -{ - return; +if ($value == '') { + return; } ?> - : + : - + - + diff --git a/code/components/com_fields/layouts/fields/render.php b/code/components/com_fields/layouts/fields/render.php index c84630be..80a8a5c4 100644 --- a/code/components/com_fields/layouts/fields/render.php +++ b/code/components/com_fields/layouts/fields/render.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\Component\Fields\Administrator\Helper\FieldsHelper; // Check if we have all the data -if (!array_key_exists('item', $displayData) || !array_key_exists('context', $displayData)) -{ - return; +if (!array_key_exists('item', $displayData) || !array_key_exists('context', $displayData)) { + return; } // Setting up for display $item = $displayData['item']; -if (!$item) -{ - return; +if (!$item) { + return; } $context = $displayData['context']; -if (!$context) -{ - return; +if (!$context) { + return; } $parts = explode('.', $context); $component = $parts[0]; $fields = null; -if (array_key_exists('fields', $displayData)) -{ - $fields = $displayData['fields']; -} -else -{ - $fields = $item->jcfields ?: FieldsHelper::getFields($context, $item, true); +if (array_key_exists('fields', $displayData)) { + $fields = $displayData['fields']; +} else { + $fields = $item->jcfields ?: FieldsHelper::getFields($context, $item, true); } -if (empty($fields)) -{ - return; +if (empty($fields)) { + return; } $output = array(); -foreach ($fields as $field) -{ - // If the value is empty do nothing - if (!isset($field->value) || trim($field->value) === '') - { - continue; - } - - $class = $field->name . ' ' . $field->params->get('render_class'); - $layout = $field->params->get('layout', 'render'); - $content = FieldsHelper::render($context, 'field.' . $layout, array('field' => $field)); - - // If the content is empty do nothing - if (trim($content) === '') - { - continue; - } - - $output[] = '
  • ' . $content . '
  • '; +foreach ($fields as $field) { + // If the value is empty do nothing + if (!isset($field->value) || trim($field->value) === '') { + continue; + } + + $class = $field->name . ' ' . $field->params->get('render_class'); + $layout = $field->params->get('layout', 'render'); + $content = FieldsHelper::render($context, 'field.' . $layout, array('field' => $field)); + + // If the content is empty do nothing + if (trim($content) === '') { + continue; + } + + $output[] = '
  • ' . $content . '
  • '; } -if (empty($output)) -{ - return; +if (empty($output)) { + return; } ?>
      - +
    diff --git a/code/components/com_fields/src/Controller/DisplayController.php b/code/components/com_fields/src/Controller/DisplayController.php index d96c9257..70e5bb02 100644 --- a/code/components/com_fields/src/Controller/DisplayController.php +++ b/code/components/com_fields/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ get('view') === 'fields' && $input->get('layout') === 'modal') { + // Load the backend language file. + $app->getLanguage()->load('com_fields', JPATH_ADMINISTRATOR); - /** - * @param array $config An optional associative array of configuration settings. - * Recognized key values include 'name', 'default_task', 'model_path', and - * 'view_path' (this list is not meant to be comprehensive). - * @param MVCFactoryInterface|null $factory The factory. - * @param CMSApplication|null $app The Application for the dispatcher - * @param \Joomla\CMS\Input\Input|null $input The request's input object - * - * @since 3.7.0 - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - // Frontpage Editor Fields Button proxying. - if ($input->get('view') === 'fields' && $input->get('layout') === 'modal') - { - // Load the backend language file. - $app->getLanguage()->load('com_fields', JPATH_ADMINISTRATOR); - - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } - parent::__construct($config, $factory, $app, $input); - } + parent::__construct($config, $factory, $app, $input); + } } diff --git a/code/components/com_fields/src/Dispatcher/Dispatcher.php b/code/components/com_fields/src/Dispatcher/Dispatcher.php index af013bbc..11fbe231 100644 --- a/code/components/com_fields/src/Dispatcher/Dispatcher.php +++ b/code/components/com_fields/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->get('view') !== 'fields' || $this->input->get('layout') !== 'modal') - { - return; - } - - $context = $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'); - $parts = FieldsHelper::extract($context); - - if (!$this->app->getIdentity()->authorise('core.create', $parts[0]) - || !$this->app->getIdentity()->authorise('core.edit', $parts[0])) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR')); - } - } + /** + * Method to check component access permission + * + * @return void + * + * @since 4.0.0 + */ + protected function checkAccess() + { + parent::checkAccess(); + + if ($this->input->get('view') !== 'fields' || $this->input->get('layout') !== 'modal') { + return; + } + + $context = $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'); + $parts = FieldsHelper::extract($context); + + if ( + !$this->app->getIdentity()->authorise('core.create', $parts[0]) + || !$this->app->getIdentity()->authorise('core.edit', $parts[0]) + ) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR')); + } + } } diff --git a/code/components/com_finder/helpers/route.php b/code/components/com_finder/helpers/route.php index 4cfe9367..c32879a4 100644 --- a/code/components/com_finder/helpers/route.php +++ b/code/components/com_finder/helpers/route.php @@ -1,16 +1,21 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\Component\Finder\Site\Helper\RouteHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Finder route helper class. * diff --git a/code/components/com_finder/src/Controller/DisplayController.php b/code/components/com_finder/src/Controller/DisplayController.php index 5552cd63..f71cfb03 100644 --- a/code/components/com_finder/src/Controller/DisplayController.php +++ b/code/components/com_finder/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input; - $cachable = true; - - // Load plugin language files. - LanguageHelper::loadPluginLanguage(); - - // Set the default view name and format from the Request. - $viewName = $input->get('view', 'search', 'word'); - $input->set('view', $viewName); - - // Don't cache view for search queries - if ($input->get('q', null, 'string') || $input->get('f', null, 'int') || $input->get('t', null, 'array')) - { - $cachable = false; - } - - $safeurlparams = array( - 'f' => 'INT', - 'lang' => 'CMD' - ); - - return parent::display($cachable, $safeurlparams); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. [optional] + * @param array $urlparams An array of safe URL parameters and their variable types, + * for valid values see {@link \JFilterInput::clean()}. [optional] + * + * @return static This object is to support chaining. + * + * @since 2.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $input = $this->app->input; + $cachable = true; + + // Load plugin language files. + LanguageHelper::loadPluginLanguage(); + + // Set the default view name and format from the Request. + $viewName = $input->get('view', 'search', 'word'); + $input->set('view', $viewName); + + // Don't cache view for search queries + if ($input->get('q', null, 'string') || $input->get('f', null, 'int') || $input->get('t', null, 'array')) { + $cachable = false; + } + + $safeurlparams = array( + 'f' => 'INT', + 'lang' => 'CMD' + ); + + return parent::display($cachable, $safeurlparams); + } } diff --git a/code/components/com_finder/src/Controller/SuggestionsController.php b/code/components/com_finder/src/Controller/SuggestionsController.php index e8f04b7b..2342fb3e 100644 --- a/code/components/com_finder/src/Controller/SuggestionsController.php +++ b/code/components/com_finder/src/Controller/SuggestionsController.php @@ -1,4 +1,5 @@ app; - $app->mimeType = 'application/json'; - - // Ensure caching is disabled as it depends on the query param in the model - $app->allowCache(false); - - $suggestions = $this->getSuggestions(); - - // Send the response. - $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); - $app->sendHeaders(); - echo '{ "suggestions": ' . json_encode($suggestions) . ' }'; - } - - /** - * Method to find search query suggestions for OpenSearch - * - * @return void - * - * @since 4.0.0 - */ - public function opensearchsuggest() - { - $app = $this->app; - $app->mimeType = 'application/json'; - $result = array(); - $result[] = $app->input->request->get('q', '', 'string'); - - $result[] = $this->getSuggestions(); - - // Ensure caching is disabled as it depends on the query param in the model - $app->allowCache(false); - - // Send the response. - $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); - $app->sendHeaders(); - echo json_encode($result); - } - - /** - * Method to retrieve the data from the database - * - * @return array The suggested words - * - * @since 3.4 - */ - protected function getSuggestions() - { - $return = array(); - - $params = ComponentHelper::getParams('com_finder'); - - if ($params->get('show_autosuggest', 1)) - { - // Get the suggestions. - $model = $this->getModel('Suggestions'); - $return = $model->getItems(); - } - - // Check the data. - if (empty($return)) - { - $return = array(); - } - - return $return; - } + /** + * Method to find search query suggestions. Uses awesomplete + * + * @return void + * + * @since 3.4 + */ + public function suggest() + { + $app = $this->app; + $app->mimeType = 'application/json'; + + // Ensure caching is disabled as it depends on the query param in the model + $app->allowCache(false); + + $suggestions = $this->getSuggestions(); + + // Send the response. + $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); + $app->sendHeaders(); + echo '{ "suggestions": ' . json_encode($suggestions) . ' }'; + } + + /** + * Method to find search query suggestions for OpenSearch + * + * @return void + * + * @since 4.0.0 + */ + public function opensearchsuggest() + { + $app = $this->app; + $app->mimeType = 'application/json'; + $result = array(); + $result[] = $app->input->request->get('q', '', 'string'); + + $result[] = $this->getSuggestions(); + + // Ensure caching is disabled as it depends on the query param in the model + $app->allowCache(false); + + // Send the response. + $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); + $app->sendHeaders(); + echo json_encode($result); + } + + /** + * Method to retrieve the data from the database + * + * @return array The suggested words + * + * @since 3.4 + */ + protected function getSuggestions() + { + $return = array(); + + $params = ComponentHelper::getParams('com_finder'); + + if ($params->get('show_autosuggest', 1)) { + // Get the suggestions. + $model = $this->getModel('Suggestions'); + $return = $model->getItems(); + } + + // Check the data. + if (empty($return)) { + $return = array(); + } + + return $return; + } } diff --git a/code/components/com_finder/src/Helper/FinderHelper.php b/code/components/com_finder/src/Helper/FinderHelper.php index e32f6cd0..85051528 100644 --- a/code/components/com_finder/src/Helper/FinderHelper.php +++ b/code/components/com_finder/src/Helper/FinderHelper.php @@ -1,4 +1,5 @@ get('gather_search_statistics', 0)) - { - return; - } + /** + * Method to log searches to the database + * + * @param Query $searchquery The search query + * @param integer $resultCount The number of results for this search + * + * @return void + * + * @since 4.0.0 + */ + public static function logSearch(Query $searchquery, $resultCount = 0) + { + if (!ComponentHelper::getParams('com_finder')->get('gather_search_statistics', 0)) { + return; + } - if (trim($searchquery->input) == '' && !$searchquery->empty) - { - return; - } + if (trim($searchquery->input) == '' && !$searchquery->empty) { + return; + } - // Initialise our variables - $db = Factory::getDbo(); - $query = $db->getQuery(true); + // Initialise our variables + $db = Factory::getDbo(); + $query = $db->getQuery(true); - // Sanitise the term for the database - $temp = unserialize(serialize($searchquery)); - $temp->input = trim(strtolower($searchquery->input)); - $entry = new \stdClass; - $entry->searchterm = $temp->input; - $entry->query = serialize($temp); - $entry->md5sum = md5($entry->query); - $entry->hits = 1; - $entry->results = $resultCount; + // Sanitise the term for the database + $temp = new \stdClass(); + $temp->input = trim(strtolower((string) $searchquery->input)); + $entry = new \stdClass(); + $entry->searchterm = $temp->input; + $entry->query = serialize($temp); + $entry->md5sum = md5($entry->query); + $entry->hits = 1; + $entry->results = $resultCount; - // Query the table to determine if the term has been searched previously - $query->select($db->quoteName('hits')) - ->from($db->quoteName('#__finder_logging')) - ->where($db->quoteName('md5sum') . ' = ' . $db->quote($entry->md5sum)); - $db->setQuery($query); - $hits = (int) $db->loadResult(); + // Query the table to determine if the term has been searched previously + $query->select($db->quoteName('hits')) + ->from($db->quoteName('#__finder_logging')) + ->where($db->quoteName('md5sum') . ' = ' . $db->quote($entry->md5sum)); + $db->setQuery($query); + $hits = (int) $db->loadResult(); - // Reset the $query object - $query->clear(); + // Reset the $query object + $query->clear(); - // Update the table based on the results - if ($hits) - { - $query->update($db->quoteName('#__finder_logging')) - ->set('hits = (hits + 1)') - ->where($db->quoteName('md5sum') . ' = ' . $db->quote($entry->md5sum)); - $db->setQuery($query); - $db->execute(); - } - else - { - $query->insert($db->quoteName('#__finder_logging')) - ->columns( - [ - $db->quoteName('searchterm'), - $db->quoteName('query'), - $db->quoteName('md5sum'), - $db->quoteName('hits'), - $db->quoteName('results'), - ] - ) - ->values('?, ?, ?, ?, ?') - ->bind(1, $entry->searchterm) - ->bind(2, $entry->query, ParameterType::LARGE_OBJECT) - ->bind(3, $entry->md5sum) - ->bind(4, $entry->hits, ParameterType::INTEGER) - ->bind(5, $entry->results, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } - } + // Update the table based on the results + if ($hits) { + $query->update($db->quoteName('#__finder_logging')) + ->set('hits = (hits + 1)') + ->where($db->quoteName('md5sum') . ' = ' . $db->quote($entry->md5sum)); + $db->setQuery($query); + $db->execute(); + } else { + $query->insert($db->quoteName('#__finder_logging')) + ->columns( + [ + $db->quoteName('searchterm'), + $db->quoteName('query'), + $db->quoteName('md5sum'), + $db->quoteName('hits'), + $db->quoteName('results'), + ] + ) + ->values('?, ?, ?, ?, ?') + ->bind(1, $entry->searchterm) + ->bind(2, $entry->query, ParameterType::LARGE_OBJECT) + ->bind(3, $entry->md5sum) + ->bind(4, $entry->hits, ParameterType::INTEGER) + ->bind(5, $entry->results, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + } + } } diff --git a/code/components/com_finder/src/Helper/RouteHelper.php b/code/components/com_finder/src/Helper/RouteHelper.php index 275df8c4..2aa268df 100644 --- a/code/components/com_finder/src/Helper/RouteHelper.php +++ b/code/components/com_finder/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ 'search', 'q' => $q, 'f' => $f); - $item = self::getItemid($query); - - // Get the base route. - $uri = clone Uri::getInstance('index.php?option=com_finder&view=search'); - - // Add the pre-defined search filter if present. - if ($f !== null) - { - $uri->setVar('f', $f); - } - - // Add the search query string if present. - if ($q !== null) - { - $uri->setVar('q', $q); - } - - // Add the menu item id if present. - if ($item !== null) - { - $uri->setVar('Itemid', $item); - } - - return $uri->toString(array('path', 'query')); - } - - /** - * Method to get the route for an advanced search page. - * - * @param integer $f The search filter id. [optional] - * @param string $q The search query string. [optional] - * - * @return string The advanced search route. - * - * @since 2.5 - */ - public static function getAdvancedRoute($f = null, $q = null) - { - // Get the menu item id. - $query = array('view' => 'advanced', 'q' => $q, 'f' => $f); - $item = self::getItemid($query); - - // Get the base route. - $uri = clone Uri::getInstance('index.php?option=com_finder&view=advanced'); - - // Add the pre-defined search filter if present. - if ($q !== null) - { - $uri->setVar('f', $f); - } - - // Add the search query string if present. - if ($q !== null) - { - $uri->setVar('q', $q); - } - - // Add the menu item id if present. - if ($item !== null) - { - $uri->setVar('Itemid', $item); - } - - return $uri->toString(array('path', 'query')); - } - - /** - * Method to get the most appropriate menu item for the route based on the - * supplied query needles. - * - * @param array $query An array of URL parameters. - * - * @return mixed An integer on success, null otherwise. - * - * @since 2.5 - */ - public static function getItemid($query) - { - static $items, $active; - - // Get the menu items for com_finder. - if (!$items || !$active) - { - $app = Factory::getApplication(); - $com = ComponentHelper::getComponent('com_finder'); - $menu = $app->getMenu(); - $active = $menu->getActive(); - $items = $menu->getItems('component_id', $com->id); - $items = is_array($items) ? $items : array(); - } - - // Try to match the active view and filter. - if ($active && @$active->query['view'] == @$query['view'] && @$active->query['f'] == @$query['f']) - { - return $active->id; - } - - // Try to match the view, query, and filter. - foreach ($items as $item) - { - if (@$item->query['view'] == @$query['view'] && @$item->query['q'] == @$query['q'] && @$item->query['f'] == @$query['f']) - { - return $item->id; - } - } - - // Try to match the view and filter. - foreach ($items as $item) - { - if (@$item->query['view'] == @$query['view'] && @$item->query['f'] == @$query['f']) - { - return $item->id; - } - } - - // Try to match the view. - foreach ($items as $item) - { - if (@$item->query['view'] == @$query['view']) - { - return $item->id; - } - } - - return null; - } + /** + * Method to get the route for a search page. + * + * @param integer $f The search filter id. [optional] + * @param string $q The search query string. [optional] + * + * @return string The search route. + * + * @since 2.5 + */ + public static function getSearchRoute($f = null, $q = null) + { + // Get the menu item id. + $query = array('view' => 'search', 'q' => $q, 'f' => $f); + $item = self::getItemid($query); + + // Get the base route. + $uri = clone Uri::getInstance('index.php?option=com_finder&view=search'); + + // Add the pre-defined search filter if present. + if ($f !== null) { + $uri->setVar('f', $f); + } + + // Add the search query string if present. + if ($q !== null) { + $uri->setVar('q', $q); + } + + // Add the menu item id if present. + if ($item !== null) { + $uri->setVar('Itemid', $item); + } + + return $uri->toString(array('path', 'query')); + } + + /** + * Method to get the route for an advanced search page. + * + * @param integer $f The search filter id. [optional] + * @param string $q The search query string. [optional] + * + * @return string The advanced search route. + * + * @since 2.5 + */ + public static function getAdvancedRoute($f = null, $q = null) + { + // Get the menu item id. + $query = array('view' => 'advanced', 'q' => $q, 'f' => $f); + $item = self::getItemid($query); + + // Get the base route. + $uri = clone Uri::getInstance('index.php?option=com_finder&view=advanced'); + + // Add the pre-defined search filter if present. + if ($q !== null) { + $uri->setVar('f', $f); + } + + // Add the search query string if present. + if ($q !== null) { + $uri->setVar('q', $q); + } + + // Add the menu item id if present. + if ($item !== null) { + $uri->setVar('Itemid', $item); + } + + return $uri->toString(array('path', 'query')); + } + + /** + * Method to get the most appropriate menu item for the route based on the + * supplied query needles. + * + * @param array $query An array of URL parameters. + * + * @return mixed An integer on success, null otherwise. + * + * @since 2.5 + */ + public static function getItemid($query) + { + static $items, $active; + + // Get the menu items for com_finder. + if (!$items || !$active) { + $app = Factory::getApplication(); + $com = ComponentHelper::getComponent('com_finder'); + $menu = $app->getMenu(); + $active = $menu->getActive(); + $items = $menu->getItems('component_id', $com->id); + $items = is_array($items) ? $items : array(); + } + + // Try to match the active view and filter. + if ($active && @$active->query['view'] == @$query['view'] && @$active->query['f'] == @$query['f']) { + return $active->id; + } + + // Try to match the view, query, and filter. + foreach ($items as $item) { + if (@$item->query['view'] == @$query['view'] && @$item->query['q'] == @$query['q'] && @$item->query['f'] == @$query['f']) { + return $item->id; + } + } + + // Try to match the view and filter. + foreach ($items as $item) { + if (@$item->query['view'] == @$query['view'] && @$item->query['f'] == @$query['f']) { + return $item->id; + } + } + + // Try to match the view. + foreach ($items as $item) { + if (@$item->query['view'] == @$query['view']) { + return $item->id; + } + } + + return null; + } } diff --git a/code/components/com_finder/src/Model/SearchModel.php b/code/components/com_finder/src/Model/SearchModel.php index 3092b2e5..8ec8c7a2 100644 --- a/code/components/com_finder/src/Model/SearchModel.php +++ b/code/components/com_finder/src/Model/SearchModel.php @@ -1,4 +1,5 @@ $row) - { - // Build the result object. - if (is_resource($row->object)) - { - $result = unserialize(stream_get_contents($row->object)); - } - else - { - $result = unserialize($row->object); - } - - $result->cleanURL = $result->route; - - // Add the result back to the stack. - $results[] = $result; - } - - // Return the results. - return $results; - } - - /** - * Method to get the query object. - * - * @return Query A query object. - * - * @since 2.5 - */ - public function getQuery() - { - // Return the query object. - return $this->searchquery; - } - - /** - * Method to build a database query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery A database query. - * - * @since 2.5 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'l.link_id, l.object' - ) - ); - - $query->from('#__finder_links AS l'); - - $user = Factory::getUser(); - $groups = $this->getState('user.groups', $user->getAuthorisedViewLevels()); - $query->whereIn($db->quoteName('l.access'), $groups) - ->where('l.state = 1') - ->where('l.published = 1'); - - // Get the current date, minus seconds. - $nowDate = $db->quote(substr_replace(Factory::getDate()->toSql(), '00', -2)); - - // Add the publish up and publish down filters. - $query->where('(l.publish_start_date IS NULL OR l.publish_start_date <= ' . $nowDate . ')') - ->where('(l.publish_end_date IS NULL OR l.publish_end_date >= ' . $nowDate . ')'); - - $query->group('l.link_id'); - $query->group('l.object'); - - /* - * Add the taxonomy filters to the query. We have to join the taxonomy - * map table for each group so that we can use AND clauses across - * groups. Within each group there can be an array of values that will - * use OR clauses. - */ - if (!empty($this->searchquery->filters)) - { - // Convert the associative array to a numerically indexed array. - $groups = array_values($this->searchquery->filters); - $taxonomies = call_user_func_array('array_merge', array_values($this->searchquery->filters)); - - $query->join('INNER', $db->quoteName('#__finder_taxonomy_map') . ' AS t ON t.link_id = l.link_id') - ->where('t.node_id IN (' . implode(',', array_unique($taxonomies)) . ')'); - - // Iterate through each taxonomy group. - for ($i = 0, $c = count($groups); $i < $c; $i++) - { - $query->having('SUM(CASE WHEN t.node_id IN (' . implode(',', $groups[$i]) . ') THEN 1 ELSE 0 END) > 0'); - } - } - - // Add the start date filter to the query. - if (!empty($this->searchquery->date1)) - { - // Escape the date. - $date1 = $db->quote($this->searchquery->date1); - - // Add the appropriate WHERE condition. - if ($this->searchquery->when1 === 'before') - { - $query->where($db->quoteName('l.start_date') . ' <= ' . $date1); - } - elseif ($this->searchquery->when1 === 'after') - { - $query->where($db->quoteName('l.start_date') . ' >= ' . $date1); - } - else - { - $query->where($db->quoteName('l.start_date') . ' = ' . $date1); - } - } - - // Add the end date filter to the query. - if (!empty($this->searchquery->date2)) - { - // Escape the date. - $date2 = $db->quote($this->searchquery->date2); - - // Add the appropriate WHERE condition. - if ($this->searchquery->when2 === 'before') - { - $query->where($db->quoteName('l.start_date') . ' <= ' . $date2); - } - elseif ($this->searchquery->when2 === 'after') - { - $query->where($db->quoteName('l.start_date') . ' >= ' . $date2); - } - else - { - $query->where($db->quoteName('l.start_date') . ' = ' . $date2); - } - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->where('l.language IN (' . $db->quote(Factory::getLanguage()->getTag()) . ', ' . $db->quote('*') . ')'); - } - - // Get the result ordering and direction. - $ordering = $this->getState('list.ordering', 'm.weight'); - $direction = $this->getState('list.direction', 'DESC'); - - /* - * If we are ordering by relevance we have to add up the relevance - * scores that are contained in the ordering field. - */ - if ($ordering === 'm.weight') - { - // Get the base query and add the ordering information. - $query->select('SUM(' . $db->escape($ordering) . ') AS ordering'); - } - /* - * If we are not ordering by relevance, we just have to add - * the unique items to the set. - */ - else - { - // Get the base query and add the ordering information. - $query->select($db->escape($ordering) . ' AS ordering'); - } - - $query->order('ordering ' . $db->escape($direction)); - - /* - * If there are no optional or required search terms in the query, we - * can get the results in one relatively simple database query. - */ - if (empty($this->includedTerms) && $this->searchquery->empty && $this->searchquery->input == '') - { - // Return the results. - return $query; - } - - /* - * If there are no optional or required search terms in the query and - * empty searches are not allowed, we return an empty query. - * If the search term is not empty and empty searches are allowed, - * but no terms were found, we return an empty query as well. - */ - if (empty($this->includedTerms) - && (!$this->searchquery->empty || ($this->searchquery->empty && $this->searchquery->input != ''))) - { - // Since we need to return a query, we simplify this one. - $query->clear('join') - ->clear('where') - ->clear('bounded') - ->clear('having') - ->clear('group') - ->where('false'); - - return $query; - } - - $included = call_user_func_array('array_merge', array_values($this->includedTerms)); - $query->join('INNER', $this->_db->quoteName('#__finder_links_terms') . ' AS m ON m.link_id = l.link_id') - ->where('m.term_id IN (' . implode(',', $included) . ')'); - - // Check if there are any excluded terms to deal with. - if (count($this->excludedTerms)) - { - $query2 = $db->getQuery(true); - $query2->select('e.link_id') - ->from($this->_db->quoteName('#__finder_links_terms', 'e')) - ->where('e.term_id IN (' . implode(',', $this->excludedTerms) . ')'); - $query->where('l.link_id NOT IN (' . $query2 . ')'); - } - - /* - * The query contains required search terms. - */ - if (count($this->requiredTerms)) - { - foreach ($this->requiredTerms as $terms) - { - if (count($terms)) - { - $query->having('SUM(CASE WHEN m.term_id IN (' . implode(',', $terms) . ') THEN 1 ELSE 0 END) > 0'); - } - else - { - $query->where('false'); - break; - } - } - } - - return $query; - } - - /** - * Method to get a store id based on model the configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. [optional] - * @param boolean $page True to store the data paged, false to store all data. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '', $page = true) - { - // Get the query object. - $query = $this->getQuery(); - - // Add the search query state. - $id .= ':' . $query->input; - $id .= ':' . $query->language; - $id .= ':' . $query->filter; - $id .= ':' . serialize($query->filters); - $id .= ':' . $query->date1; - $id .= ':' . $query->date2; - $id .= ':' . $query->when1; - $id .= ':' . $query->when2; - - if ($page) - { - // Add the list state for page specific data. - $id .= ':' . $this->getState('list.start'); - $id .= ':' . $this->getState('list.limit'); - $id .= ':' . $this->getState('list.ordering'); - $id .= ':' . $this->getState('list.direction'); - } - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. [optional] - * @param string $direction An optional direction. [optional] - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = null, $direction = null) - { - // Get the configuration options. - $app = Factory::getApplication(); - $input = $app->input; - $params = $app->getParams(); - $user = Factory::getUser(); - $language = Factory::getLanguage(); - - $this->setState('filter.language', Multilanguage::isEnabled()); - - $request = $input->request; - $options = array(); - - // Get the empty query setting. - $options['empty'] = $params->get('allow_empty_query', 0); - - // Get the static taxonomy filters. - $options['filter'] = $request->getInt('f', $params->get('f', '')); - - // Get the dynamic taxonomy filters. - $options['filters'] = $request->get('t', $params->get('t', array()), 'array'); - - // Get the query string. - $options['input'] = $request->getString('q', $params->get('q', '')); - - // Get the query language. - $options['language'] = $request->getCmd('l', $params->get('l', $language->getTag())); - - // Get the start date and start date modifier filters. - $options['date1'] = $request->getString('d1', $params->get('d1', '')); - $options['when1'] = $request->getString('w1', $params->get('w1', '')); - - // Get the end date and end date modifier filters. - $options['date2'] = $request->getString('d2', $params->get('d2', '')); - $options['when2'] = $request->getString('w2', $params->get('w2', '')); - - // Load the query object. - $this->searchquery = new Query($options); - - // Load the query token data. - $this->excludedTerms = $this->searchquery->getExcludedTermIds(); - $this->includedTerms = $this->searchquery->getIncludedTermIds(); - $this->requiredTerms = $this->searchquery->getRequiredTermIds(); - - // Load the list state. - $this->setState('list.start', $input->get('limitstart', 0, 'uint')); - $this->setState('list.limit', $input->get('limit', $params->get('list_limit', $app->get('list_limit', 20)), 'uint')); - - /* - * Load the sort ordering. - * Currently this is 'hard' coded via menu item parameter but may not satisfy a users need. - * More flexibility was way more user friendly. So we allow the user to pass a custom value - * from the pool of fields that are indexed like the 'title' field. - * Also, we allow this parameter to be passed in either case (lower/upper). - */ - $order = $input->getWord('o', $params->get('sort_order', 'relevance')); - $order = StringHelper::strtolower($order); - $this->setState('list.raworder', $order); - - switch ($order) - { - case 'date': - $this->setState('list.ordering', 'l.start_date'); - break; - - case 'price': - $this->setState('list.ordering', 'l.list_price'); - break; - - case ($order === 'relevance' && !empty($this->includedTerms)) : - $this->setState('list.ordering', 'm.weight'); - break; - - case 'title': - $this->setState('list.ordering', 'l.title'); - break; - - default: - $this->setState('list.ordering', 'l.link_id'); - $this->setState('list.raworder'); - break; - } - - /* - * Load the sort direction. - * Currently this is 'hard' coded via menu item parameter but may not satisfy a users need. - * More flexibility was way more user friendly. So we allow to be inverted. - * Also, we allow this parameter to be passed in either case (lower/upper). - */ - $dirn = $input->getWord('od', $params->get('sort_direction', 'desc')); - $dirn = StringHelper::strtolower($dirn); - - switch ($dirn) - { - case 'asc': - $this->setState('list.direction', 'ASC'); - break; - - default: - $this->setState('list.direction', 'DESC'); - break; - } - - // Set the match limit. - $this->setState('match.limit', 1000); - - // Load the parameters. - $this->setState('params', $params); - - // Load the user state. - $this->setState('user.id', (int) $user->get('id')); - $this->setState('user.groups', $user->getAuthorisedViewLevels()); - } - - /** - * Method to retrieve data from cache. - * - * @param string $id The cache store id. - * @param boolean $persistent Flag to enable the use of external cache. [optional] - * - * @return mixed The cached data if found, null otherwise. - * - * @since 2.5 - */ - protected function retrieve($id, $persistent = true) - { - $data = null; - - // Use the internal cache if possible. - if (isset($this->cache[$id])) - { - return $this->cache[$id]; - } - - // Use the external cache if data is persistent. - if ($persistent) - { - $data = Factory::getCache($this->context, 'output')->get($id); - $data = $data ? unserialize($data) : null; - } - - // Store the data in internal cache. - if ($data) - { - $this->cache[$id] = $data; - } - - return $data; - } - - /** - * Method to store data in cache. - * - * @param string $id The cache store id. - * @param mixed $data The data to cache. - * @param boolean $persistent Flag to enable the use of external cache. [optional] - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - */ - protected function store($id, $data, $persistent = true) - { - // Store the data in internal cache. - $this->cache[$id] = $data; - - // Store the data in external cache if data is persistent. - if ($persistent) - { - return Factory::getCache($this->context, 'output')->store(serialize($data), $id); - } - - return true; - } + /** + * Context string for the model type + * + * @var string + * @since 2.5 + */ + protected $context = 'com_finder.search'; + + /** + * The query object is an instance of Query which contains and + * models the entire search query including the text input; static and + * dynamic taxonomy filters; date filters; etc. + * + * @var Query + * @since 2.5 + */ + protected $searchquery; + + /** + * An array of all excluded terms ids. + * + * @var array + * @since 2.5 + */ + protected $excludedTerms = array(); + + /** + * An array of all included terms ids. + * + * @var array + * @since 2.5 + */ + protected $includedTerms = array(); + + /** + * An array of all required terms ids. + * + * @var array + * @since 2.5 + */ + protected $requiredTerms = array(); + + /** + * Method to get the results of the query. + * + * @return array An array of Result objects. + * + * @since 2.5 + * @throws \Exception on database error. + */ + public function getItems() + { + $items = parent::getItems(); + + // Check the data. + if (empty($items)) { + return null; + } + + $results = array(); + + // Convert the rows to result objects. + foreach ($items as $rk => $row) { + // Build the result object. + if (is_resource($row->object)) { + $result = unserialize(stream_get_contents($row->object)); + } else { + $result = unserialize($row->object); + } + + $result->cleanURL = $result->route; + + // Add the result back to the stack. + $results[] = $result; + } + + // Return the results. + return $results; + } + + /** + * Method to get the query object. + * + * @return Query A query object. + * + * @since 2.5 + */ + public function getQuery() + { + // Return the query object. + return $this->searchquery; + } + + /** + * Method to build a database query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery A database query. + * + * @since 2.5 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'l.link_id, l.object' + ) + ); + + $query->from('#__finder_links AS l'); + + $user = Factory::getUser(); + $groups = $this->getState('user.groups', $user->getAuthorisedViewLevels()); + $query->whereIn($db->quoteName('l.access'), $groups) + ->where('l.state = 1') + ->where('l.published = 1'); + + // Get the current date, minus seconds. + $nowDate = $db->quote(substr_replace(Factory::getDate()->toSql(), '00', -2)); + + // Add the publish up and publish down filters. + $query->where('(l.publish_start_date IS NULL OR l.publish_start_date <= ' . $nowDate . ')') + ->where('(l.publish_end_date IS NULL OR l.publish_end_date >= ' . $nowDate . ')'); + + $query->group('l.link_id'); + $query->group('l.object'); + + /* + * Add the taxonomy filters to the query. We have to join the taxonomy + * map table for each group so that we can use AND clauses across + * groups. Within each group there can be an array of values that will + * use OR clauses. + */ + if (!empty($this->searchquery->filters)) { + // Convert the associative array to a numerically indexed array. + $groups = array_values($this->searchquery->filters); + $taxonomies = call_user_func_array('array_merge', array_values($this->searchquery->filters)); + + $query->join('INNER', $db->quoteName('#__finder_taxonomy_map') . ' AS t ON t.link_id = l.link_id') + ->where('t.node_id IN (' . implode(',', array_unique($taxonomies)) . ')'); + + // Iterate through each taxonomy group. + for ($i = 0, $c = count($groups); $i < $c; $i++) { + $query->having('SUM(CASE WHEN t.node_id IN (' . implode(',', $groups[$i]) . ') THEN 1 ELSE 0 END) > 0'); + } + } + + // Add the start date filter to the query. + if (!empty($this->searchquery->date1)) { + // Escape the date. + $date1 = $db->quote($this->searchquery->date1); + + // Add the appropriate WHERE condition. + if ($this->searchquery->when1 === 'before') { + $query->where($db->quoteName('l.start_date') . ' <= ' . $date1); + } elseif ($this->searchquery->when1 === 'after') { + $query->where($db->quoteName('l.start_date') . ' >= ' . $date1); + } else { + $query->where($db->quoteName('l.start_date') . ' = ' . $date1); + } + } + + // Add the end date filter to the query. + if (!empty($this->searchquery->date2)) { + // Escape the date. + $date2 = $db->quote($this->searchquery->date2); + + // Add the appropriate WHERE condition. + if ($this->searchquery->when2 === 'before') { + $query->where($db->quoteName('l.start_date') . ' <= ' . $date2); + } elseif ($this->searchquery->when2 === 'after') { + $query->where($db->quoteName('l.start_date') . ' >= ' . $date2); + } else { + $query->where($db->quoteName('l.start_date') . ' = ' . $date2); + } + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->where('l.language IN (' . $db->quote(Factory::getLanguage()->getTag()) . ', ' . $db->quote('*') . ')'); + } + + // Get the result ordering and direction. + $ordering = $this->getState('list.ordering', 'm.weight'); + $direction = $this->getState('list.direction', 'DESC'); + + /* + * If we are ordering by relevance we have to add up the relevance + * scores that are contained in the ordering field. + */ + if ($ordering === 'm.weight') { + // Get the base query and add the ordering information. + $query->select('SUM(' . $db->escape($ordering) . ') AS ordering'); + } else { + /** + * If we are not ordering by relevance, we just have to add + * the unique items to the set. + */ + // Get the base query and add the ordering information. + $query->select($db->escape($ordering) . ' AS ordering'); + } + + $query->order('ordering ' . $db->escape($direction)); + + /* + * If there are no optional or required search terms in the query, we + * can get the results in one relatively simple database query. + */ + if (empty($this->includedTerms) && $this->searchquery->empty && $this->searchquery->input == '') { + // Return the results. + return $query; + } + + /* + * If there are no optional or required search terms in the query and + * empty searches are not allowed, we return an empty query. + * If the search term is not empty and empty searches are allowed, + * but no terms were found, we return an empty query as well. + */ + if ( + empty($this->includedTerms) + && (!$this->searchquery->empty || ($this->searchquery->empty && $this->searchquery->input != '')) + ) { + // Since we need to return a query, we simplify this one. + $query->clear('join') + ->clear('where') + ->clear('bounded') + ->clear('having') + ->clear('group') + ->where('false'); + + return $query; + } + + $included = call_user_func_array('array_merge', array_values($this->includedTerms)); + $query->join('INNER', $db->quoteName('#__finder_links_terms') . ' AS m ON m.link_id = l.link_id') + ->where('m.term_id IN (' . implode(',', $included) . ')'); + + // Check if there are any excluded terms to deal with. + if (count($this->excludedTerms)) { + $query2 = $db->getQuery(true); + $query2->select('e.link_id') + ->from($db->quoteName('#__finder_links_terms', 'e')) + ->where('e.term_id IN (' . implode(',', $this->excludedTerms) . ')'); + $query->where('l.link_id NOT IN (' . $query2 . ')'); + } + + /* + * The query contains required search terms. + */ + if (count($this->requiredTerms)) { + foreach ($this->requiredTerms as $terms) { + if (count($terms)) { + $query->having('SUM(CASE WHEN m.term_id IN (' . implode(',', $terms) . ') THEN 1 ELSE 0 END) > 0'); + } else { + $query->where('false'); + break; + } + } + } + + return $query; + } + + /** + * Method to get a store id based on model the configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. [optional] + * @param boolean $page True to store the data paged, false to store all data. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '', $page = true) + { + // Get the query object. + $query = $this->getQuery(); + + // Add the search query state. + $id .= ':' . $query->input; + $id .= ':' . $query->language; + $id .= ':' . $query->filter; + $id .= ':' . serialize($query->filters); + $id .= ':' . $query->date1; + $id .= ':' . $query->date2; + $id .= ':' . $query->when1; + $id .= ':' . $query->when2; + + if ($page) { + // Add the list state for page specific data. + $id .= ':' . $this->getState('list.start'); + $id .= ':' . $this->getState('list.limit'); + $id .= ':' . $this->getState('list.ordering'); + $id .= ':' . $this->getState('list.direction'); + } + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. [optional] + * @param string $direction An optional direction. [optional] + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = null, $direction = null) + { + // Get the configuration options. + $app = Factory::getApplication(); + $input = $app->input; + $params = $app->getParams(); + $user = Factory::getUser(); + $language = Factory::getLanguage(); + + $this->setState('filter.language', Multilanguage::isEnabled()); + + $request = $input->request; + $options = array(); + + // Get the empty query setting. + $options['empty'] = $params->get('allow_empty_query', 0); + + // Get the static taxonomy filters. + $options['filter'] = $request->getInt('f', $params->get('f', '')); + + // Get the dynamic taxonomy filters. + $options['filters'] = $request->get('t', $params->get('t', array()), 'array'); + + // Get the query string. + $options['input'] = $request->getString('q', $params->get('q', '')); + + // Get the query language. + $options['language'] = $request->getCmd('l', $params->get('l', $language->getTag())); + + // Set the word match mode + $options['word_match'] = $params->get('word_match', 'exact'); + + // Get the start date and start date modifier filters. + $options['date1'] = $request->getString('d1', $params->get('d1', '')); + $options['when1'] = $request->getString('w1', $params->get('w1', '')); + + // Get the end date and end date modifier filters. + $options['date2'] = $request->getString('d2', $params->get('d2', '')); + $options['when2'] = $request->getString('w2', $params->get('w2', '')); + + // Load the query object. + $this->searchquery = new Query($options, $this->getDatabase()); + + // Load the query token data. + $this->excludedTerms = $this->searchquery->getExcludedTermIds(); + $this->includedTerms = $this->searchquery->getIncludedTermIds(); + $this->requiredTerms = $this->searchquery->getRequiredTermIds(); + + // Load the list state. + $this->setState('list.start', $input->get('limitstart', 0, 'uint')); + $this->setState('list.limit', $input->get('limit', $params->get('list_limit', $app->get('list_limit', 20)), 'uint')); + + /* + * Load the sort ordering. + * Currently this is 'hard' coded via menu item parameter but may not satisfy a users need. + * More flexibility was way more user friendly. So we allow the user to pass a custom value + * from the pool of fields that are indexed like the 'title' field. + * Also, we allow this parameter to be passed in either case (lower/upper). + */ + $order = $input->getWord('o', $params->get('sort_order', 'relevance')); + $order = StringHelper::strtolower($order); + $this->setState('list.raworder', $order); + + switch ($order) { + case 'date': + $this->setState('list.ordering', 'l.start_date'); + break; + + case 'price': + $this->setState('list.ordering', 'l.list_price'); + break; + + case ($order === 'relevance' && !empty($this->includedTerms)): + $this->setState('list.ordering', 'm.weight'); + break; + + case 'title': + $this->setState('list.ordering', 'l.title'); + break; + + default: + $this->setState('list.ordering', 'l.link_id'); + $this->setState('list.raworder'); + break; + } + + /* + * Load the sort direction. + * Currently this is 'hard' coded via menu item parameter but may not satisfy a users need. + * More flexibility was way more user friendly. So we allow to be inverted. + * Also, we allow this parameter to be passed in either case (lower/upper). + */ + $dirn = $input->getWord('od', $params->get('sort_direction', 'desc')); + $dirn = StringHelper::strtolower($dirn); + + switch ($dirn) { + case 'asc': + $this->setState('list.direction', 'ASC'); + break; + + default: + $this->setState('list.direction', 'DESC'); + break; + } + + // Set the match limit. + $this->setState('match.limit', 1000); + + // Load the parameters. + $this->setState('params', $params); + + // Load the user state. + $this->setState('user.id', (int) $user->get('id')); + $this->setState('user.groups', $user->getAuthorisedViewLevels()); + } } diff --git a/code/components/com_finder/src/Model/SuggestionsModel.php b/code/components/com_finder/src/Model/SuggestionsModel.php index 4ee28d09..3f7ca3f6 100644 --- a/code/components/com_finder/src/Model/SuggestionsModel.php +++ b/code/components/com_finder/src/Model/SuggestionsModel.php @@ -1,4 +1,5 @@ $v) - { - $items[$k] = $v->term; - } - - return $items; - } - - /** - * Method to build a database query to load the list data. - * - * @return DatabaseQuery A database query - * - * @since 2.5 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - $groups = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); - $lang = Helper::getPrimaryLanguage($this->getState('language')); - - // Create a new query object. - $db = $this->getDbo(); - $termIdQuery = $db->getQuery(true); - $termQuery = $db->getQuery(true); - - // Limit term count to a reasonable number of results to reduce main query join size - $termIdQuery->select('ti.term_id') - ->from($db->quoteName('#__finder_terms', 'ti')) - ->where('ti.term LIKE ' . $db->quote($db->escape(StringHelper::strtolower($this->getState('input')), true) . '%', false)) - ->where('ti.common = 0') - ->where('ti.language IN (' . $db->quote($lang) . ', ' . $db->quote('*') . ')') - ->order('ti.links DESC') - ->order('ti.weight DESC'); - - $termIds = $db->setQuery($termIdQuery, 0, 100)->loadColumn(); - - // Early return on term mismatch - if (!count($termIds)) - { - return $termIdQuery; - } - - // Select required fields - $termQuery->select('DISTINCT(t.term)') - ->from($db->quoteName('#__finder_terms', 't')) - ->whereIn('t.term_id', $termIds) - ->order('t.links DESC') - ->order('t.weight DESC'); - - // Join mapping table for term <-> link relation - $mappingTable = $db->quoteName('#__finder_links_terms', 'tm'); - $termQuery->join('INNER', $mappingTable . ' ON tm.term_id = t.term_id'); - - // Join links table - $termQuery->join('INNER', $db->quoteName('#__finder_links', 'l') . ' ON (tm.link_id = l.link_id)') - ->where('l.access IN (' . implode(',', $groups) . ')') - ->where('l.state = 1') - ->where('l.published = 1'); - - return $termQuery; - } - - /** - * Method to get a store id based on model the configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Add the search query state. - $id .= ':' . $this->getState('input'); - $id .= ':' . $this->getState('language'); - - // Add the list state. - $id .= ':' . $this->getState('list.start'); - $id .= ':' . $this->getState('list.limit'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = null, $direction = null) - { - // Get the configuration options. - $app = Factory::getApplication(); - $input = $app->input; - $params = ComponentHelper::getParams('com_finder'); - $user = Factory::getUser(); - - // Get the query input. - $this->setState('input', $input->request->get('q', '', 'string')); - - // Set the query language - if (Multilanguage::isEnabled()) - { - $lang = Factory::getLanguage()->getTag(); - } - else - { - $lang = Helper::getDefaultLanguage(); - } - - $this->setState('language', $lang); - - // Load the list state. - $this->setState('list.start', 0); - $this->setState('list.limit', 10); - - // Load the parameters. - $this->setState('params', $params); - - // Load the user state. - $this->setState('user.id', (int) $user->get('id')); - } + /** + * Context string for the model type. + * + * @var string + * @since 2.5 + */ + protected $context = 'com_finder.suggestions'; + + /** + * Method to get an array of data items. + * + * @return array An array of data items. + * + * @since 2.5 + */ + public function getItems() + { + // Get the items. + $items = parent::getItems(); + + // Convert them to a simple array. + foreach ($items as $k => $v) { + $items[$k] = $v->term; + } + + return $items; + } + + /** + * Method to build a database query to load the list data. + * + * @return DatabaseQuery A database query + * + * @since 2.5 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + $groups = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); + $lang = Helper::getPrimaryLanguage($this->getState('language')); + + // Create a new query object. + $db = $this->getDatabase(); + $termIdQuery = $db->getQuery(true); + $termQuery = $db->getQuery(true); + + // Limit term count to a reasonable number of results to reduce main query join size + $termIdQuery->select('ti.term_id') + ->from($db->quoteName('#__finder_terms', 'ti')) + ->where('ti.term LIKE ' . $db->quote($db->escape(StringHelper::strtolower($this->getState('input')), true) . '%', false)) + ->where('ti.common = 0') + ->where('ti.language IN (' . $db->quote($lang) . ', ' . $db->quote('*') . ')') + ->order('ti.links DESC') + ->order('ti.weight DESC'); + + $termIds = $db->setQuery($termIdQuery, 0, 100)->loadColumn(); + + // Early return on term mismatch + if (!count($termIds)) { + return $termIdQuery; + } + + // Select required fields + $termQuery->select('DISTINCT(t.term)') + ->from($db->quoteName('#__finder_terms', 't')) + ->whereIn('t.term_id', $termIds) + ->order('t.links DESC') + ->order('t.weight DESC'); + + // Join mapping table for term <-> link relation + $mappingTable = $db->quoteName('#__finder_links_terms', 'tm'); + $termQuery->join('INNER', $mappingTable . ' ON tm.term_id = t.term_id'); + + // Join links table + $termQuery->join('INNER', $db->quoteName('#__finder_links', 'l') . ' ON (tm.link_id = l.link_id)') + ->where('l.access IN (' . implode(',', $groups) . ')') + ->where('l.state = 1') + ->where('l.published = 1'); + + return $termQuery; + } + + /** + * Method to get a store id based on model the configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Add the search query state. + $id .= ':' . $this->getState('input'); + $id .= ':' . $this->getState('language'); + + // Add the list state. + $id .= ':' . $this->getState('list.start'); + $id .= ':' . $this->getState('list.limit'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = null, $direction = null) + { + // Get the configuration options. + $app = Factory::getApplication(); + $input = $app->input; + $params = ComponentHelper::getParams('com_finder'); + $user = Factory::getUser(); + + // Get the query input. + $this->setState('input', $input->request->get('q', '', 'string')); + + // Set the query language + if (Multilanguage::isEnabled()) { + $lang = Factory::getLanguage()->getTag(); + } else { + $lang = Helper::getDefaultLanguage(); + } + + $this->setState('language', $lang); + + // Load the list state. + $this->setState('list.start', 0); + $this->setState('list.limit', 10); + + // Load the parameters. + $this->setState('params', $params); + + // Load the user state. + $this->setState('user.id', (int) $user->get('id')); + } } diff --git a/code/components/com_finder/src/Service/Router.php b/code/components/com_finder/src/Service/Router.php index 024fa4b3..81c35ca2 100644 --- a/code/components/com_finder/src/Service/Router.php +++ b/code/components/com_finder/src/Service/Router.php @@ -1,4 +1,5 @@ registerView($search); + /** + * Finder Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + */ + public function __construct(SiteApplication $app, AbstractMenu $menu) + { + $search = new RouterViewConfiguration('search'); + $this->registerView($search); - parent::__construct($app, $menu); + parent::__construct($app, $menu); - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } } diff --git a/code/components/com_finder/src/View/Search/FeedView.php b/code/components/com_finder/src/View/Search/FeedView.php index 4e642595..dd437da2 100644 --- a/code/components/com_finder/src/View/Search/FeedView.php +++ b/code/components/com_finder/src/View/Search/FeedView.php @@ -1,4 +1,5 @@ input->set('limit', $app->get('feed_limit')); + // Adjust the list limit to the feed limit. + $app->input->set('limit', $app->get('feed_limit')); - // Get view data. - $state = $this->get('State'); - $params = $state->get('params'); - $query = $this->get('Query'); - $results = $this->get('Items'); - $total = $this->get('Total'); + // Get view data. + $state = $this->get('State'); + $params = $state->get('params'); + $query = $this->get('Query'); + $results = $this->get('Items'); + $total = $this->get('Total'); - // Push out the query data. - $explained = HTMLHelper::_('query.explained', $query); + // Push out the query data. + $explained = HTMLHelper::_('query.explained', $query); - // Set the document title. - $this->setDocumentTitle($params->get('page_title', '')); + // Set the document title. + $this->setDocumentTitle($params->get('page_title', '')); - // Configure the document description. - if (!empty($explained)) - { - $this->document->setDescription(html_entity_decode(strip_tags($explained), ENT_QUOTES, 'UTF-8')); - } + // Configure the document description. + if (!empty($explained)) { + $this->document->setDescription(html_entity_decode(strip_tags($explained), ENT_QUOTES, 'UTF-8')); + } - // Set the document link. - $this->document->link = Route::_($query->toUri()); + // Set the document link. + $this->document->link = Route::_($query->toUri()); - // If we don't have any results, we are done. - if (empty($results)) - { - return; - } + // If we don't have any results, we are done. + if (empty($results)) { + return; + } - // Convert the results to feed entries. - foreach ($results as $result) - { - // Convert the result to a feed entry. - $item = new FeedItem; - $item->title = $result->title; - $item->link = Route::_($result->route); - $item->description = $result->description; + // Convert the results to feed entries. + foreach ($results as $result) { + // Convert the result to a feed entry. + $item = new FeedItem(); + $item->title = $result->title; + $item->link = Route::_($result->route); + $item->description = $result->description; - // Use Unix date to cope for non-english languages - $item->date = (int) $result->start_date ? HTMLHelper::_('date', $result->start_date, 'U') : $result->indexdate; + // Use Unix date to cope for non-english languages + $item->date = (int) $result->start_date ? HTMLHelper::_('date', $result->start_date, 'U') : $result->indexdate; - // Loads item info into RSS array - $this->document->addItem($item); - } - } + // Loads item info into RSS array + $this->document->addItem($item); + } + } } diff --git a/code/components/com_finder/src/View/Search/HtmlView.php b/code/components/com_finder/src/View/Search/HtmlView.php index 9b20a7be..24a6e760 100644 --- a/code/components/com_finder/src/View/Search/HtmlView.php +++ b/code/components/com_finder/src/View/Search/HtmlView.php @@ -1,4 +1,5 @@ params = $app->getParams(); - - // Get view data. - $this->state = $this->get('State'); - $this->query = $this->get('Query'); - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderQuery') : null; - $this->results = $this->get('Items'); - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderResults') : null; - $this->total = $this->get('Total'); - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderTotal') : null; - $this->pagination = $this->get('Pagination'); - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderPagination') : null; - - // Flag indicates to not add limitstart=0 to URL - $this->pagination->hideEmptyLimitstart = true; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Configure the pathway. - if (!empty($this->query->input)) - { - $app->getPathway()->addItem($this->escape($this->query->input)); - } - - // Check for a double quote in the query string. - if (strpos($this->query->input, '"')) - { - // Get the application router. - $router = $app->getRouter(); - - // Fix the q variable in the URL. - if ($router->getVar('q') !== $this->query->input) - { - $router->setVar('q', $this->query->input); - } - } - - // Run an event on each result item - if (is_array($this->results)) - { - // Import Finder plugins - PluginHelper::importPlugin('finder'); - - foreach ($this->results as $result) - { - $app->triggerEvent('onFinderResult', array(&$result, &$this->query)); - } - } - - // Log the search - FinderHelper::logSearch($this->query, $this->total); - - // Push out the query data. - $this->suggested = HTMLHelper::_('query.suggested', $this->query); - $this->explained = HTMLHelper::_('query.explained', $this->query); - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); - - // Check for layout override only if this is not the active menu item - // If it is the active menu item, then the view and category id will match - $active = $app->getMenu()->getActive(); - - if (isset($active->query['layout'])) - { - // We need to set the layout in case this is an alternative menu item (with an alternative layout) - $this->setLayout($active->query['layout']); - } - - $this->prepareDocument(); - - \JDEBUG ? Profiler::getInstance('Application')->mark('beforeFinderLayout') : null; - - parent::display($tpl); - - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderLayout') : null; - } - - /** - * Method to get hidden input fields for a get form so that control variables - * are not lost upon form submission - * - * @return string A string of hidden input form fields - * - * @since 2.5 - */ - protected function getFields() - { - $fields = null; - - // Get the URI. - $uri = Uri::getInstance(Route::_($this->query->toUri())); - $uri->delVar('q'); - $uri->delVar('o'); - $uri->delVar('t'); - $uri->delVar('d1'); - $uri->delVar('d2'); - $uri->delVar('w1'); - $uri->delVar('w2'); - $elements = $uri->getQuery(true); - - // Create hidden input elements for each part of the URI. - foreach ($elements as $n => $v) - { - if (is_scalar($v)) - { - $fields .= ''; - } - } - - return $fields; - } - - /** - * Method to get the layout file for a search result object. - * - * @param string $layout The layout file to check. [optional] - * - * @return string The layout file to use. - * - * @since 2.5 - */ - protected function getLayoutFile($layout = null) - { - // Create and sanitize the file name. - $file = $this->_layout . '_' . preg_replace('/[^A-Z0-9_\.-]/i', '', $layout); - - // Check if the file exists. - $filetofind = $this->_createFileName('template', array('name' => $file)); - $exists = Path::find($this->_path['template'], $filetofind); - - return ($exists ? $layout : 'result'); - } - - /** - * Prepares the document - * - * @return void - * - * @since 2.5 - */ - protected function prepareDocument() - { - $app = Factory::getApplication(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_FINDER_DEFAULT_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($layout = $this->params->get('article_layout')) - { - $this->setLayout($layout); - } - - // Configure the document meta-description. - if (!empty($this->explained)) - { - $explained = $this->escape(html_entity_decode(strip_tags($this->explained), ENT_QUOTES, 'UTF-8')); - $this->document->setDescription($explained); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - // Check for OpenSearch - if ($this->params->get('opensearch', 1)) - { - $ostitle = $this->params->get('opensearch_name', - Text::_('COM_FINDER_OPENSEARCH_NAME') . ' ' . $app->get('sitename') - ); - $this->document->addHeadLink( - Uri::getInstance()->toString(array('scheme', 'host', 'port')) . Route::_('index.php?option=com_finder&view=search&format=opensearch'), - 'search', 'rel', array('title' => $ostitle, 'type' => 'application/opensearchdescription+xml') - ); - } - - // Add feed link to the document head. - if ($this->params->get('show_feed_link', 1) == 1) - { - // Add the RSS link. - $props = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); - $route = Route::_($this->query->toUri() . '&format=feed&type=rss'); - $this->document->addHeadLink($route, 'alternate', 'rel', $props); - - // Add the ATOM link. - $props = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); - $route = Route::_($this->query->toUri() . '&format=feed&type=atom'); - $this->document->addHeadLink($route, 'alternate', 'rel', $props); - } - } + use SiteRouterAwareTrait; + + /** + * The query indexer object + * + * @var Query + * + * @since 4.0.0 + */ + protected $query; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params = null; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The logged in user + * + * @var \Joomla\CMS\User\User|null + */ + protected $user = null; + + /** + * The suggested search query + * + * @var string|false + * + * @since 4.0.0 + */ + protected $suggested = false; + + /** + * The explained (human-readable) search query + * + * @var string|null + * + * @since 4.0.0 + */ + protected $explained = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * An array of results + * + * @var array + * + * @since 3.8.0 + */ + protected $results; + + /** + * The total number of items + * + * @var integer + * + * @since 3.8.0 + */ + protected $total; + + /** + * The pagination object + * + * @var Pagination + * + * @since 3.8.0 + */ + protected $pagination; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $this->params = $app->getParams(); + + // Get view data. + $this->state = $this->get('State'); + $this->query = $this->get('Query'); + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderQuery') : null; + $this->results = $this->get('Items'); + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderResults') : null; + $this->total = $this->get('Total'); + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderTotal') : null; + $this->pagination = $this->get('Pagination'); + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderPagination') : null; + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Configure the pathway. + if (!empty($this->query->input)) { + $app->getPathway()->addItem($this->escape($this->query->input)); + } + + // Check for a double quote in the query string. + if (strpos($this->query->input, '"')) { + $router = $this->getSiteRouter(); + + // Fix the q variable in the URL. + if ($router->getVar('q') !== $this->query->input) { + $router->setVar('q', $this->query->input); + } + } + + // Run an event on each result item + if (is_array($this->results)) { + // Import Finder plugins + PluginHelper::importPlugin('finder'); + + foreach ($this->results as $result) { + $app->triggerEvent('onFinderResult', array(&$result, &$this->query)); + } + } + + // Log the search + FinderHelper::logSearch($this->query, $this->total); + + // Push out the query data. + $this->suggested = HTMLHelper::_('query.suggested', $this->query); + $this->explained = HTMLHelper::_('query.explained', $this->query); + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); + + // Check for layout override only if this is not the active menu item + // If it is the active menu item, then the view and category id will match + $active = $app->getMenu()->getActive(); + + if (isset($active->query['layout'])) { + // We need to set the layout in case this is an alternative menu item (with an alternative layout) + $this->setLayout($active->query['layout']); + } + + $this->prepareDocument(); + + \JDEBUG ? Profiler::getInstance('Application')->mark('beforeFinderLayout') : null; + + parent::display($tpl); + + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderLayout') : null; + } + + /** + * Method to get hidden input fields for a get form so that control variables + * are not lost upon form submission + * + * @return string A string of hidden input form fields + * + * @since 2.5 + */ + protected function getFields() + { + $fields = null; + + // Get the URI. + $uri = Uri::getInstance(Route::_($this->query->toUri())); + $uri->delVar('q'); + $uri->delVar('o'); + $uri->delVar('t'); + $uri->delVar('d1'); + $uri->delVar('d2'); + $uri->delVar('w1'); + $uri->delVar('w2'); + $elements = $uri->getQuery(true); + + // Create hidden input elements for each part of the URI. + foreach ($elements as $n => $v) { + if (is_scalar($v)) { + $fields .= ''; + } + } + + return $fields; + } + + /** + * Method to get the layout file for a search result object. + * + * @param string $layout The layout file to check. [optional] + * + * @return string The layout file to use. + * + * @since 2.5 + */ + protected function getLayoutFile($layout = null) + { + // Create and sanitize the file name. + $file = $this->_layout . '_' . preg_replace('/[^A-Z0-9_\.-]/i', '', $layout); + + // Check if the file exists. + $filetofind = $this->_createFileName('template', array('name' => $file)); + $exists = Path::find($this->_path['template'], $filetofind); + + return ($exists ? $layout : 'result'); + } + + /** + * Prepares the document + * + * @return void + * + * @since 2.5 + */ + protected function prepareDocument() + { + $app = Factory::getApplication(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_FINDER_DEFAULT_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($layout = $this->params->get('article_layout')) { + $this->setLayout($layout); + } + + // Configure the document meta-description. + if (!empty($this->explained)) { + $explained = $this->escape(html_entity_decode(strip_tags($this->explained), ENT_QUOTES, 'UTF-8')); + $this->document->setDescription($explained); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + // Check for OpenSearch + if ($this->params->get('opensearch', 1)) { + $ostitle = $this->params->get( + 'opensearch_name', + Text::_('COM_FINDER_OPENSEARCH_NAME') . ' ' . $app->get('sitename') + ); + $this->document->addHeadLink( + Uri::getInstance()->toString(array('scheme', 'host', 'port')) . Route::_('index.php?option=com_finder&view=search&format=opensearch'), + 'search', + 'rel', + array('title' => $ostitle, 'type' => 'application/opensearchdescription+xml') + ); + } + + // Add feed link to the document head. + if ($this->params->get('show_feed_link', 1) == 1) { + // Add the RSS link. + $props = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); + $route = Route::_($this->query->toUri() . '&format=feed&type=rss'); + $this->document->addHeadLink($route, 'alternate', 'rel', $props); + + // Add the ATOM link. + $props = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); + $route = Route::_($this->query->toUri() . '&format=feed&type=atom'); + $this->document->addHeadLink($route, 'alternate', 'rel', $props); + } + } } diff --git a/code/components/com_finder/src/View/Search/OpensearchView.php b/code/components/com_finder/src/View/Search/OpensearchView.php index b8eac3f9..d765a0e1 100644 --- a/code/components/com_finder/src/View/Search/OpensearchView.php +++ b/code/components/com_finder/src/View/Search/OpensearchView.php @@ -1,4 +1,5 @@ document->setShortName($params->get('opensearch_name', $app->get('sitename'))); - $this->document->setDescription($params->get('opensearch_description', $app->get('MetaDesc'))); + $params = ComponentHelper::getParams('com_finder'); + $this->document->setShortName($params->get('opensearch_name', $app->get('sitename', ''))); + $this->document->setDescription($params->get('opensearch_description', $app->get('MetaDesc', ''))); - // Prevent any output when OpenSearch Support is disabled - if (!$params->get('opensearch', 1)) - { - return; - } + // Prevent any output when OpenSearch Support is disabled + if (!$params->get('opensearch', 1)) { + return; + } - // Add the URL for the search - $searchUri = 'index.php?option=com_finder&view=search&q={searchTerms}'; - $suggestionsUri = 'index.php?option=com_finder&task=suggestions.opensearchsuggest&format=json&q={searchTerms}'; - $baseUrl = Uri::getInstance()->toString(array('host', 'port', 'scheme')); - $active = $app->getMenu()->getActive(); + // Add the URL for the search + $searchUri = 'index.php?option=com_finder&view=search&q={searchTerms}'; + $suggestionsUri = 'index.php?option=com_finder&task=suggestions.opensearchsuggest&format=json&q={searchTerms}'; + $baseUrl = Uri::getInstance()->toString(array('host', 'port', 'scheme')); + $active = $app->getMenu()->getActive(); - if ($active->component == 'com_finder') - { - $searchUri .= '&Itemid=' . $active->id; - $suggestionsUri .= '&Itemid=' . $active->id; - } + if ($active->component == 'com_finder') { + $searchUri .= '&Itemid=' . $active->id; + $suggestionsUri .= '&Itemid=' . $active->id; + } - // Add the HTML result view - $htmlSearch = new OpensearchUrl; - $htmlSearch->template = $baseUrl . Route::_($searchUri, false); - $this->document->addUrl($htmlSearch); + // Add the HTML result view + $htmlSearch = new OpensearchUrl(); + $htmlSearch->template = $baseUrl . Route::_($searchUri, false); + $this->document->addUrl($htmlSearch); - // Add the RSS result view - $htmlSearch = new OpensearchUrl; - $htmlSearch->template = $baseUrl . Route::_($searchUri . '&format=feed&type=rss', false); - $htmlSearch->type = 'application/rss+xml'; - $this->document->addUrl($htmlSearch); + // Add the RSS result view + $htmlSearch = new OpensearchUrl(); + $htmlSearch->template = $baseUrl . Route::_($searchUri . '&format=feed&type=rss', false); + $htmlSearch->type = 'application/rss+xml'; + $this->document->addUrl($htmlSearch); - // Add the Atom result view - $htmlSearch = new OpensearchUrl; - $htmlSearch->template = $baseUrl . Route::_($searchUri . '&format=feed&type=atom', false); - $htmlSearch->type = 'application/atom+xml'; - $this->document->addUrl($htmlSearch); + // Add the Atom result view + $htmlSearch = new OpensearchUrl(); + $htmlSearch->template = $baseUrl . Route::_($searchUri . '&format=feed&type=atom', false); + $htmlSearch->type = 'application/atom+xml'; + $this->document->addUrl($htmlSearch); - // Add suggestions URL - if ($params->get('show_autosuggest', 1)) - { - $htmlSearch = new OpensearchUrl; - $htmlSearch->template = $baseUrl . Route::_($suggestionsUri, false); - $htmlSearch->type = 'application/x-suggestions+json'; - $this->document->addUrl($htmlSearch); - } - } + // Add suggestions URL + if ($params->get('show_autosuggest', 1)) { + $htmlSearch = new OpensearchUrl(); + $htmlSearch->template = $baseUrl . Route::_($suggestionsUri, false); + $htmlSearch->type = 'application/x-suggestions+json'; + $this->document->addUrl($htmlSearch); + } + } } diff --git a/code/components/com_finder/tmpl/search/default.php b/code/components/com_finder/tmpl/search/default.php index c83d4b72..419f1443 100644 --- a/code/components/com_finder/tmpl/search/default.php +++ b/code/components/com_finder/tmpl/search/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager() - ->useStyle('com_finder.finder') - ->useScript('com_finder.finder'); + ->useStyle('com_finder.finder') + ->useScript('com_finder.finder'); ?>
    - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading'))) : ?> - escape($this->params->get('page_heading')); ?> - - escape($this->params->get('page_title')); ?> - -

    - -
    - loadTemplate('form'); ?> -
    - - query->search === true) : ?> -
    - loadTemplate('results'); ?> -
    - + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading'))) : ?> + escape($this->params->get('page_heading')); ?> + + escape($this->params->get('page_title')); ?> + +

    + +
    + loadTemplate('form'); ?> +
    + + query->search === true) : ?> +
    + loadTemplate('results'); ?> +
    +
    diff --git a/code/components/com_finder/tmpl/search/default.xml b/code/components/com_finder/tmpl/search/default.xml index fd5097bc..2a0f363c 100644 --- a/code/components/com_finder/tmpl/search/default.xml +++ b/code/components/com_finder/tmpl/search/default.xml @@ -14,12 +14,12 @@ diff --git a/code/components/com_finder/tmpl/search/default_form.php b/code/components/com_finder/tmpl/search/default_form.php index cfba0b5a..5c767739 100644 --- a/code/components/com_finder/tmpl/search/default_form.php +++ b/code/components/com_finder/tmpl/search/default_form.php @@ -1,4 +1,5 @@ params->get('show_autosuggest', 1)) -{ - $this->document->getWebAssetManager()->usePreset('awesomplete'); - $this->document->addScriptOptions('finder-search', array('url' => Route::_('index.php?option=com_finder&task=suggestions.suggest&format=json&tmpl=component', false))); +if ($this->params->get('show_autosuggest', 1)) { + $this->document->getWebAssetManager()->usePreset('awesomplete'); + $this->document->addScriptOptions('finder-search', array('url' => Route::_('index.php?option=com_finder&task=suggestions.suggest&format=json&tmpl=component', false))); } ?>
    - getFields(); ?> - + getFields(); ?> + - params->get('show_advanced', 1)) : ?> -
    - - - - params->get('show_advanced_tips', 1)) : ?> -
    -
    - - - - - params->get('tuplecount', 1) > 1) : ?> - - - -
    -
    - -
    - query, $this->params); ?> -
    -
    - + params->get('show_advanced', 1)) : ?> +
    + + + + params->get('show_advanced_tips', 1)) : ?> +
    +
    + + + + + params->get('tuplecount', 1) > 1) : ?> + + + +
    +
    + +
    + query, $this->params); ?> +
    +
    +
    diff --git a/code/components/com_finder/tmpl/search/default_result.php b/code/components/com_finder/tmpl/search/default_result.php index 8338a981..c04d30a3 100644 --- a/code/components/com_finder/tmpl/search/default_result.php +++ b/code/components/com_finder/tmpl/search/default_result.php @@ -1,4 +1,5 @@ getIdentity(); $show_description = $this->params->get('show_description', 1); -if ($show_description) -{ - // Calculate number of characters to display around the result - $term_length = StringHelper::strlen($this->query->input); - $desc_length = $this->params->get('description_length', 255); - $pad_length = $term_length < $desc_length ? (int) floor(($desc_length - $term_length) / 2) : 0; +if ($show_description) { + // Calculate number of characters to display around the result + $term_length = StringHelper::strlen($this->query->input); + $desc_length = $this->params->get('description_length', 255); + $pad_length = $term_length < $desc_length ? (int) floor(($desc_length - $term_length) / 2) : 0; - // Make sure we highlight term both in introtext and fulltext - $full_description = $this->result->description; - if (!empty($this->result->summary) && !empty($this->result->body)) - { - $full_description = Helper::parse($this->result->summary . $this->result->body); - } + // Make sure we highlight term both in introtext and fulltext + $full_description = $this->result->description; + if (!empty($this->result->summary) && !empty($this->result->body)) { + $full_description = Helper::parse($this->result->summary . $this->result->body); + } - // Find the position of the search term - $pos = $term_length ? StringHelper::strpos(StringHelper::strtolower($full_description), StringHelper::strtolower($this->query->input)) : false; + // Find the position of the search term + $pos = $term_length ? StringHelper::strpos(StringHelper::strtolower($full_description), StringHelper::strtolower($this->query->input)) : false; - // Find a potential start point - $start = ($pos && $pos > $pad_length) ? $pos - $pad_length : 0; + // Find a potential start point + $start = ($pos && $pos > $pad_length) ? $pos - $pad_length : 0; - // Find a space between $start and $pos, start right after it. - $space = StringHelper::strpos($full_description, ' ', $start > 0 ? $start - 1 : 0); - $start = ($space && $space < $pos) ? $space + 1 : $start; + // Find a space between $start and $pos, start right after it. + $space = StringHelper::strpos($full_description, ' ', $start > 0 ? $start - 1 : 0); + $start = ($space && $space < $pos) ? $space + 1 : $start; - $description = HTMLHelper::_('string.truncate', StringHelper::substr($full_description, $start), $desc_length, true); + $description = HTMLHelper::_('string.truncate', StringHelper::substr($full_description, $start), $desc_length, true); } $showImage = $this->params->get('show_image', 0); $imageClass = $this->params->get('image_class', ''); $extraAttr = []; -if ($showImage && !empty($this->result->imageUrl) && $imageClass !== '') -{ - $extraAttr['class'] = $imageClass; +if ($showImage && !empty($this->result->imageUrl) && $imageClass !== '') { + $extraAttr['class'] = $imageClass; } $icon = ''; -if (!empty($this->result->mime)) -{ - $icon = ' '; +if (!empty($this->result->mime)) { + $icon = ' '; } $show_url = ''; -if ($this->params->get('show_url', 1)) -{ - $show_url = '' . $this->baseUrl . Route::_($this->result->cleanURL) . ''; +if ($this->params->get('show_url', 1)) { + $show_url = '' . $this->baseUrl . Route::_($this->result->cleanURL) . ''; } ?>
  • - result->imageUrl)) : ?> -
    - params->get('link_image') && $this->result->route) : ?> - - result->imageUrl, $this->result->imageAlt, $extraAttr); ?> - - - result->imageUrl, $this->result->imageAlt, $extraAttr); ?> - -
    - -

    - result->route) : ?> - result->route), - '' . $icon . $this->result->title . '' . $show_url, - [ - 'class' => 'result__title-link' - ] - ); ?> - - result->title; ?> - -

    - -

    - result->start_date && $this->params->get('show_date', 1)) : ?> - - - -

    - - result->getTaxonomy(); ?> - params->get('show_taxonomy', 1)) : ?> -
      - $taxonomy) : ?> - - state == 1 && in_array($branch->access, $user->getAuthorisedViewLevels())) : ?> - - - state == 1 && in_array($node->access, $user->getAuthorisedViewLevels())) : ?> - title; ?> - - - -
    • - : -
    • - - - -
    - + result->imageUrl)) : ?> +
    + params->get('link_image') && $this->result->route) : ?> + + result->imageUrl, $this->result->imageAlt, $extraAttr); ?> + + + result->imageUrl, $this->result->imageAlt, $extraAttr); ?> + +
    + +

    + result->route) : ?> + result->route), + '' . $icon . $this->result->title . '' . $show_url, + [ + 'class' => 'result__title-link' + ] + ); ?> + + result->title; ?> + +

    + +

    + result->start_date && $this->params->get('show_date', 1)) : ?> + + + +

    + + result->getTaxonomy(); ?> + params->get('show_taxonomy', 1)) : ?> +
      + $taxonomy) : ?> + + state == 1 && in_array($branch->access, $user->getAuthorisedViewLevels())) : ?> + + + state == 1 && in_array($node->access, $user->getAuthorisedViewLevels())) : ?> + title; ?> + + + +
    • + : +
    • + + + +
    +
  • diff --git a/code/components/com_finder/tmpl/search/default_results.php b/code/components/com_finder/tmpl/search/default_results.php index 691fe722..7e283441 100644 --- a/code/components/com_finder/tmpl/search/default_results.php +++ b/code/components/com_finder/tmpl/search/default_results.php @@ -1,4 +1,5 @@ suggested && $this->params->get('show_suggested_query', 1)) || ($this->explained && $this->params->get('show_explained_query', 1))) : ?> -
    - - suggested && $this->params->get('show_suggested_query', 1)) : ?> - - query->toUri()); ?> - setVar('q', $this->suggested); ?> - - toString(array('path', 'query'))); ?> - ' . $this->escape($this->suggested) . ''; ?> - - explained && $this->params->get('show_explained_query', 1)) : ?> - -

    - total, $this->explained); ?> -

    - -
    +
    + + suggested && $this->params->get('show_suggested_query', 1)) : ?> + + query->toUri()); ?> + setVar('q', $this->suggested); ?> + + toString(array('path', 'query'))); ?> + ' . $this->escape($this->suggested) . ''; ?> + + explained && $this->params->get('show_explained_query', 1)) : ?> + +

    + total, $this->explained); ?> +

    + +
    total === 0) || ($this->total === null)) : ?> -
    -

    - getLanguageFilter() ? '_MULTILANG' : ''; ?> -

    escape($this->query->input)); ?>

    -
    - - +
    +

    + getLanguageFilter() ? '_MULTILANG' : ''; ?> +

    escape($this->query->input)); ?>

    +
    + + query->highlight) && $this->params->get('highlight_terms', 1)) : ?> - document->getWebAssetManager()->useScript('highlight'); - $this->document->addScriptOptions( - 'highlight', - [[ - 'class' => 'js-highlight', - 'highLight' => $this->query->highlight, - ]] - ); - ?> + document->getWebAssetManager()->useScript('highlight'); + $this->document->addScriptOptions( + 'highlight', + [[ + 'class' => 'js-highlight', + 'highLight' => $this->query->highlight, + ]] + ); + ?>
      - baseUrl = Uri::getInstance()->toString(array('scheme', 'host', 'port')); ?> - results as $i => $result) : ?> - result = &$result; ?> - result->counter = $i + 1; ?> - getLayoutFile($this->result->layout); ?> - loadTemplate($layout); ?> - + baseUrl = Uri::getInstance()->toString(array('scheme', 'host', 'port')); ?> + results as $i => $result) : ?> + result = &$result; ?> + result->counter = $i + 1; ?> + getLayoutFile($this->result->layout); ?> + loadTemplate($layout); ?> +
    - params->get('show_pagination', 1) > 0) : ?> -
    - pagination->getPagesLinks(); ?> -
    - - params->get('show_pagination_results', 1) > 0) : ?> -
    - - pagination->limitstart + 1; ?> - pagination->total; ?> - pagination->limit * $this->pagination->pagesCurrent; ?> - $total ? $total : $limit); ?> - -
    - + params->get('show_pagination', 1) > 0) : ?> +
    + pagination->getPagesLinks(); ?> +
    + + params->get('show_pagination_results', 1) > 0) : ?> +
    + + pagination->limitstart + 1; ?> + pagination->total; ?> + pagination->limit * $this->pagination->pagesCurrent; ?> + $total ? $total : $limit); ?> + +
    +
    diff --git a/code/components/com_media/src/Dispatcher/Dispatcher.php b/code/components/com_media/src/Dispatcher/Dispatcher.php index 893d1347..da10efef 100644 --- a/code/components/com_media/src/Dispatcher/Dispatcher.php +++ b/code/components/com_media/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getLanguage()->load('', JPATH_ADMINISTRATOR); - $this->app->getLanguage()->load($this->option, JPATH_ADMINISTRATOR); + /** + * Load the language + * + * @since 4.0.0 + * + * @return void + */ + protected function loadLanguage() + { + // Load the administrator languages needed for the media manager + $this->app->getLanguage()->load('', JPATH_ADMINISTRATOR); + $this->app->getLanguage()->load($this->option, JPATH_ADMINISTRATOR); - parent::loadLanguage(); - } + parent::loadLanguage(); + } - /** - * Method to check component access permission - * - * @since 4.0.0 - * - * @return void - */ - protected function checkAccess() - { - $user = $this->app->getIdentity(); + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + */ + protected function checkAccess() + { + $user = $this->app->getIdentity(); - // Access check - if (!$user->authorise('core.manage', 'com_media') - && !$user->authorise('core.create', 'com_media')) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + // Access check + if ( + !$user->authorise('core.manage', 'com_media') + && !$user->authorise('core.create', 'com_media') + ) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } - /** - * Get a controller from the component - * - * @param string $name Controller name - * @param string $client Optional client (like Administrator, Site etc.) - * @param array $config Optional controller config - * - * @return BaseController - * - * @since 4.0.0 - */ - public function getController(string $name, string $client = '', array $config = array()): BaseController - { - $config['base_path'] = JPATH_ADMINISTRATOR . '/components/com_media'; + /** + * Get a controller from the component + * + * @param string $name Controller name + * @param string $client Optional client (like Administrator, Site etc.) + * @param array $config Optional controller config + * + * @return BaseController + * + * @since 4.0.0 + */ + public function getController(string $name, string $client = '', array $config = array()): BaseController + { + $config['base_path'] = JPATH_ADMINISTRATOR . '/components/com_media'; - // Force to load the admin controller - return parent::getController($name, 'Administrator', $config); - } + // Force to load the admin controller + return parent::getController($name, 'Administrator', $config); + } } diff --git a/code/components/com_menus/layouts/joomla/searchtools/default.php b/code/components/com_menus/layouts/joomla/searchtools/default.php index 456ea927..0d7cd07e 100644 --- a/code/components/com_menus/layouts/joomla/searchtools/default.php +++ b/code/components/com_menus/layouts/joomla/searchtools/default.php @@ -1,4 +1,5 @@ filterForm) && !empty($data['view']->filterForm)) -{ - // Checks if a selector (e.g. client_id) exists. - if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) - { - $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; - - // Checks if a selector should be shown in the current layout. - if (isset($data['view']->layout)) - { - $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; - } - - // Unset the selector field from active filters group. - unset($data['view']->activeFilters[$selectorFieldName]); - } - - if ($data['view'] instanceof \Joomla\Component\Menus\Administrator\View\Items\HtmlView) : - unset($data['view']->activeFilters['client_id']); - endif; - - // Checks if the filters button should exist. - $filters = $data['view']->filterForm->getGroup('filter'); - $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; - - // Checks if it should show the be hidden. - $hideActiveFilters = empty($data['view']->activeFilters); - - // Check if the no results message should appear. - if (isset($data['view']->total) && (int) $data['view']->total === 0) - { - $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); - if (!empty($noResults)) - { - $noResultsText = Text::_($noResults); - } - } +if (isset($data['view']->filterForm) && !empty($data['view']->filterForm)) { + // Checks if a selector (e.g. client_id) exists. + if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) { + $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; + + // Checks if a selector should be shown in the current layout. + if (isset($data['view']->layout)) { + $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; + } + + // Unset the selector field from active filters group. + unset($data['view']->activeFilters[$selectorFieldName]); + } + + if ($data['view'] instanceof \Joomla\Component\Menus\Administrator\View\Items\HtmlView) : + unset($data['view']->activeFilters['client_id']); + endif; + + // Checks if the filters button should exist. + $filters = $data['view']->filterForm->getGroup('filter'); + $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; + + // Checks if it should show the be hidden. + $hideActiveFilters = empty($data['view']->activeFilters); + + // Check if the no results message should appear. + if (isset($data['view']->total) && (int) $data['view']->total === 0) { + $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); + if (!empty($noResults)) { + $noResultsText = Text::_($noResults); + } + } } // Set some basic options. $customOptions = array( - 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, - 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, - 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), - 'searchFieldSelector' => '#filter_search', - 'selectorFieldName' => $selectorFieldName, - 'showSelector' => $showSelector, - 'orderFieldSelector' => '#list_fullordering', - 'showNoResults' => !empty($noResultsText), - 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', - 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', + 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, + 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, + 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), + 'searchFieldSelector' => '#filter_search', + 'selectorFieldName' => $selectorFieldName, + 'showSelector' => $showSelector, + 'orderFieldSelector' => '#list_fullordering', + 'showNoResults' => !empty($noResultsText), + 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', + 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', ); // Merge custom options in the options array. @@ -89,39 +85,39 @@ HTMLHelper::_('searchtools.form', $data['options']['formSelector'], $data['options']); ?> - sublayout('noitems', $data); ?> + sublayout('noitems', $data); ?> diff --git a/code/components/com_menus/src/Dispatcher/Dispatcher.php b/code/components/com_menus/src/Dispatcher/Dispatcher.php index 4a87d0ac..828e043c 100644 --- a/code/components/com_menus/src/Dispatcher/Dispatcher.php +++ b/code/components/com_menus/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getLanguage()->load('com_menus', JPATH_ADMINISTRATOR); - } + /** + * Load the language + * + * @since 4.0.0 + * + * @return void + */ + protected function loadLanguage() + { + $this->app->getLanguage()->load('com_menus', JPATH_ADMINISTRATOR); + } - /** - * Dispatch a controller task. Redirecting the user if appropriate. - * - * @return void - * - * @since 4.0.0 - */ - public function checkAccess() - { - parent::checkAccess(); + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since 4.0.0 + */ + public function checkAccess() + { + parent::checkAccess(); - if ($this->input->get('view') !== 'items' - || $this->input->get('layout') !== 'modal' - || !$this->app->getIdentity()->authorise('core.create', 'com_menus')) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + if ( + $this->input->get('view') !== 'items' + || $this->input->get('layout') !== 'modal' + || !$this->app->getIdentity()->authorise('core.create', 'com_menus') + ) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } - /** - * Get a controller from the component - * - * @param string $name Controller name - * @param string $client Optional client (like Administrator, Site etc.) - * @param array $config Optional controller config - * - * @return \Joomla\CMS\MVC\Controller\BaseController - * - * @since 4.0.0 - */ - public function getController(string $name, string $client = '', array $config = array()): BaseController - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - $client = 'Administrator'; + /** + * Get a controller from the component + * + * @param string $name Controller name + * @param string $client Optional client (like Administrator, Site etc.) + * @param array $config Optional controller config + * + * @return \Joomla\CMS\MVC\Controller\BaseController + * + * @since 4.0.0 + */ + public function getController(string $name, string $client = '', array $config = array()): BaseController + { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + $client = 'Administrator'; - return parent::getController($name, $client, $config); - } + return parent::getController($name, $client, $config); + } } diff --git a/code/components/com_modules/src/Controller/DisplayController.php b/code/components/com_modules/src/Controller/DisplayController.php index e0f00834..d8018850 100644 --- a/code/components/com_modules/src/Controller/DisplayController.php +++ b/code/components/com_modules/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input = Factory::getApplication()->input; + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param Input|null $input The Input object for the request + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + $this->input = Factory::getApplication()->input; - // Modules frontpage Editor Module proxying. - if ($this->input->get('view') === 'modules' && $this->input->get('layout') === 'modal') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } + // Modules frontpage Editor Module proxying. + if ($this->input->get('view') === 'modules' && $this->input->get('layout') === 'modal') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } - parent::__construct($config, $factory, $app, $input); - } + parent::__construct($config, $factory, $app, $input); + } } diff --git a/code/components/com_modules/src/Dispatcher/Dispatcher.php b/code/components/com_modules/src/Dispatcher/Dispatcher.php index daead79a..c7a06dbb 100644 --- a/code/components/com_modules/src/Dispatcher/Dispatcher.php +++ b/code/components/com_modules/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getLanguage()->load('com_modules', JPATH_ADMINISTRATOR); - } + /** + * Load the language + * + * @since 4.0.0 + * + * @return void + */ + protected function loadLanguage() + { + $this->app->getLanguage()->load('com_modules', JPATH_ADMINISTRATOR); + } - /** - * Dispatch a controller task. Redirecting the user if appropriate. - * - * @return void - * - * @since 4.0.0 - */ - public function checkAccess() - { - parent::checkAccess(); + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since 4.0.0 + */ + public function checkAccess() + { + parent::checkAccess(); - if ($this->input->get('view') === 'modules' - && $this->input->get('layout') === 'modal' - && !$this->app->getIdentity()->authorise('core.create', 'com_modules')) - { - throw new NotAllowed; - } - } + if ( + $this->input->get('view') === 'modules' + && $this->input->get('layout') === 'modal' + && !$this->app->getIdentity()->authorise('core.create', 'com_modules') + ) { + throw new NotAllowed(); + } + } - /** - * Get a controller from the component - * - * @param string $name Controller name - * @param string $client Optional client (like Administrator, Site etc.) - * @param array $config Optional controller config - * - * @return \Joomla\CMS\MVC\Controller\BaseController - * - * @since 4.0.0 - */ - public function getController(string $name, string $client = '', array $config = array()): BaseController - { - if ($this->input->get('task') === 'orderPosition') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - $client = 'Administrator'; - } + /** + * Get a controller from the component + * + * @param string $name Controller name + * @param string $client Optional client (like Administrator, Site etc.) + * @param array $config Optional controller config + * + * @return \Joomla\CMS\MVC\Controller\BaseController + * + * @since 4.0.0 + */ + public function getController(string $name, string $client = '', array $config = array()): BaseController + { + if ($this->input->get('task') === 'orderPosition') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + $client = 'Administrator'; + } - return parent::getController($name, $client, $config); - } + return parent::getController($name, $client, $config); + } } diff --git a/code/components/com_newsfeeds/helpers/route.php b/code/components/com_newsfeeds/helpers/route.php index 6ca8fecb..919b4a17 100644 --- a/code/components/com_newsfeeds/helpers/route.php +++ b/code/components/com_newsfeeds/helpers/route.php @@ -1,16 +1,21 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\Component\Newsfeeds\Site\Helper\RouteHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Newsfeeds Component Route Helper * diff --git a/code/components/com_newsfeeds/src/Controller/DisplayController.php b/code/components/com_newsfeeds/src/Controller/DisplayController.php index 16de07e9..0c5d8766 100644 --- a/code/components/com_newsfeeds/src/Controller/DisplayController.php +++ b/code/components/com_newsfeeds/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'categories'); - $this->input->set('view', $vName); - - if ($this->app->getIdentity()->get('id') || ($this->input->getMethod() === 'POST' && $vName === 'category' )) - { - $cachable = false; - } - - $safeurlparams = array('id' => 'INT', 'limit' => 'UINT', 'limitstart' => 'UINT', - 'filter_order' => 'CMD', 'filter_order_Dir' => 'CMD', 'lang' => 'CMD'); - - return parent::display($cachable, $safeurlparams); - } + /** + * Method to show a newsfeeds view + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $cachable = true; + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'categories'); + $this->input->set('view', $vName); + + if ($this->app->getIdentity()->get('id') || ($this->input->getMethod() === 'POST' && $vName === 'category' )) { + $cachable = false; + } + + $safeurlparams = array('id' => 'INT', 'limit' => 'UINT', 'limitstart' => 'UINT', + 'filter_order' => 'CMD', 'filter_order_Dir' => 'CMD', 'lang' => 'CMD'); + + return parent::display($cachable, $safeurlparams); + } } diff --git a/code/components/com_newsfeeds/src/Helper/AssociationHelper.php b/code/components/com_newsfeeds/src/Helper/AssociationHelper.php index c7515826..3be17675 100644 --- a/code/components/com_newsfeeds/src/Helper/AssociationHelper.php +++ b/code/components/com_newsfeeds/src/Helper/AssociationHelper.php @@ -1,4 +1,5 @@ input; - $view = $view ?? $jinput->get('view'); - $id = empty($id) ? $jinput->getInt('id') : $id; + /** + * Method to get the associations for a given item + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 3.0 + */ + public static function getAssociations($id = 0, $view = null) + { + $jinput = Factory::getApplication()->input; + $view = $view ?? $jinput->get('view'); + $id = empty($id) ? $jinput->getInt('id') : $id; - if ($view === 'newsfeed') - { - if ($id) - { - $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $id); + if ($view === 'newsfeed') { + if ($id) { + $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $id); - $return = array(); + $return = array(); - foreach ($associations as $tag => $item) - { - $return[$tag] = RouteHelper::getNewsfeedRoute($item->id, (int) $item->catid, $item->language); - } + foreach ($associations as $tag => $item) { + $return[$tag] = RouteHelper::getNewsfeedRoute($item->id, (int) $item->catid, $item->language); + } - return $return; - } - } + return $return; + } + } - if ($view === 'category' || $view === 'categories') - { - return self::getCategoryAssociations($id, 'com_newsfeeds'); - } + if ($view === 'category' || $view === 'categories') { + return self::getCategoryAssociations($id, 'com_newsfeeds'); + } - return array(); - } + return array(); + } } diff --git a/code/components/com_newsfeeds/src/Helper/RouteHelper.php b/code/components/com_newsfeeds/src/Helper/RouteHelper.php index 6f3be804..e2363102 100644 --- a/code/components/com_newsfeeds/src/Helper/RouteHelper.php +++ b/code/components/com_newsfeeds/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ 1) - { - $link .= '&catid=' . $catid; - } + if ((int) $catid > 1) { + $link .= '&catid=' . $catid; + } - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } - return $link; - } + return $link; + } - /** - * getCategoryRoute - * - * @param int $catid category id - * @param int $language language - * - * @return string - */ - public static function getCategoryRoute($catid, $language = 0) - { - if ($catid instanceof CategoryNode) - { - $id = $catid->id; - } - else - { - $id = (int) $catid; - } + /** + * getCategoryRoute + * + * @param int $catid category id + * @param int $language language + * + * @return string + */ + public static function getCategoryRoute($catid, $language = 0) + { + if ($catid instanceof CategoryNode) { + $id = $catid->id; + } else { + $id = (int) $catid; + } - if ($id < 1) - { - $link = ''; - } - else - { - // Create the link - $link = 'index.php?option=com_newsfeeds&view=category&id=' . $id; + if ($id < 1) { + $link = ''; + } else { + // Create the link + $link = 'index.php?option=com_newsfeeds&view=category&id=' . $id; - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } + } - return $link; - } + return $link; + } } diff --git a/code/components/com_newsfeeds/src/Model/CategoriesModel.php b/code/components/com_newsfeeds/src/Model/CategoriesModel.php index 964ab8b3..06a3fb39 100644 --- a/code/components/com_newsfeeds/src/Model/CategoriesModel.php +++ b/code/components/com_newsfeeds/src/Model/CategoriesModel.php @@ -1,4 +1,5 @@ setState('filter.extension', $this->_extension); - - // Get the parent id if defined. - $parentId = $app->input->getInt('id'); - $this->setState('filter.parentId', $parentId); - - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('filter.published', 1); - $this->setState('filter.access', true); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.parentId'); - - return parent::getStoreId($id); - } - - /** - * redefine the function and add some properties to make the styling easier - * - * @return mixed An array of data items on success, false on failure. - */ - public function getItems() - { - if ($this->_items === null) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_items_cat', 1) || !$params->get('show_empty_categories_cat', 0); - $categories = Categories::getInstance('Newsfeeds', $options); - $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); - - if (is_object($this->_parent)) - { - $this->_items = $this->_parent->getChildren(); - } - else - { - $this->_items = false; - } - } - - return $this->_items; - } - - /** - * get the Parent - * - * @return null - */ - public function getParent() - { - if (!is_object($this->_parent)) - { - $this->getItems(); - } - - return $this->_parent; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_newsfeeds.categories'; + + /** + * The category context (allows other extensions to derived from this model). + * + * @var string + */ + protected $_extension = 'com_newsfeeds'; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + private $_parent = null; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + private $_items = null; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field + * @param string $direction An optional direction [asc|desc] + * + * @return void + * + * @throws \Exception + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $this->setState('filter.extension', $this->_extension); + + // Get the parent id if defined. + $parentId = $app->input->getInt('id'); + $this->setState('filter.parentId', $parentId); + + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('filter.published', 1); + $this->setState('filter.access', true); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.parentId'); + + return parent::getStoreId($id); + } + + /** + * redefine the function and add some properties to make the styling easier + * + * @return mixed An array of data items on success, false on failure. + */ + public function getItems() + { + if ($this->_items === null) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_items_cat', 1) || !$params->get('show_empty_categories_cat', 0); + $categories = Categories::getInstance('Newsfeeds', $options); + $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); + + if (is_object($this->_parent)) { + $this->_items = $this->_parent->getChildren(); + } else { + $this->_items = false; + } + } + + return $this->_items; + } + + /** + * get the Parent + * + * @return null + */ + public function getParent() + { + if (!is_object($this->_parent)) { + $this->getItems(); + } + + return $this->_parent; + } } diff --git a/code/components/com_newsfeeds/src/Model/CategoryModel.php b/code/components/com_newsfeeds/src/Model/CategoryModel.php index a8616ebd..9fc0173a 100644 --- a/code/components/com_newsfeeds/src/Model/CategoryModel.php +++ b/code/components/com_newsfeeds/src/Model/CategoryModel.php @@ -1,4 +1,5 @@ _params)) - { - $params = new Registry; - $item->params = $params; - $params->loadString($item->params); - } - - // Some contexts may not use tags data at all, so we allow callers to disable loading tag data - if ($this->getState('load_tags', true)) - { - $item->tags = new TagsHelper; - $item->tags->getItemTags('com_newsfeeds.newsfeed', $item->id); - } - } - - return $items; - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select required fields from the categories. - $query->select($this->getState('list.select', $db->quoteName('a') . '.*')) - ->from($db->quoteName('#__newsfeeds', 'a')) - ->whereIn($db->quoteName('a.access'), $groups); - - // Filter by category. - if ($categoryId = (int) $this->getState('category.id')) - { - $query->where($db->quoteName('a.catid') . ' = :categoryId') - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->whereIn($db->quoteName('c.access'), $groups) - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - - // Filter by state - $state = $this->getState('filter.published'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.published') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - else - { - $query->where($db->quoteName('a.published') . ' IN (0,1,2)'); - } - - // Filter by start and end dates. - if ($this->getState('filter.publish_date')) - { - $nowDate = Factory::getDate()->toSql(); - - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_up') . ' IS NULL', - $db->quoteName('a.publish_up') . ' <= :nowDate1', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_down') . ' IS NULL', - $db->quoteName('a.publish_down') . ' >= :nowDate2', - ], - 'OR' - ) - ->bind([':nowDate1', ':nowDate2'], $nowDate); - } - - // Filter by search in title - if ($search = $this->getState('list.filter')) - { - $search = '%' . $search . '%'; - $query->where($db->quoteName('a.name') . ' LIKE :search') - ->bind(':search', $search); - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field - * @param string $direction An optional direction [asc|desc] - * - * @return void - * - * @since 1.6 - * - * @throws \Exception - */ - protected function populateState($ordering = null, $direction = null) - { - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_newsfeeds'); - - // List state information - $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); - $this->setState('list.limit', $limit); - - $limitstart = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $limitstart); - - // Optional filter text - $this->setState('list.filter', $app->input->getString('filter-search')); - - $orderCol = $app->input->get('filter_order', 'ordering'); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->input->get('filter_order_Dir', 'ASC'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $id = $app->input->get('id', 0, 'int'); - $this->setState('category.id', $id); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_newsfeeds')) && (!$user->authorise('core.edit', 'com_newsfeeds'))) - { - // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); - - // Filter by start and end dates. - $this->setState('filter.publish_date', true); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to get category data for the current category - * - * @return object - * - * @since 1.5 - */ - public function getCategory() - { - if (!is_object($this->_item)) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); - $categories = Categories::getInstance('Newsfeeds', $options); - $this->_item = $categories->get($this->getState('category.id', 'root')); - - if (is_object($this->_item)) - { - $this->_children = $this->_item->getChildren(); - $this->_parent = false; - - if ($this->_item->getParent()) - { - $this->_parent = $this->_item->getParent(); - } - - $this->_rightsibling = $this->_item->getSibling(); - $this->_leftsibling = $this->_item->getSibling(false); - } - else - { - $this->_children = false; - $this->_parent = false; - } - } - - return $this->_item; - } - - /** - * Get the parent category. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function getParent() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_parent; - } - - /** - * Get the sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getLeftSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_leftsibling; - } - - /** - * Get the sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getRightSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_rightsibling; - } - - /** - * Get the child categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getChildren() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_children; - } - - /** - * Increment the hit counter for the category. - * - * @param int $pk Optional primary key of the category to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); - $table = Table::getInstance('Category', 'JTable'); - $table->hit($pk); - } - - return true; - } + /** + * Category items data + * + * @var array + */ + protected $_item; + + /** + * Array of newsfeeds in the category + * + * @var \stdClass[] + */ + protected $_articles; + + /** + * Category left and right of this one + * + * @var CategoryNode[]|null + */ + protected $_siblings; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + protected $_children; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + protected $_parent; + + /** + * The category that applies. + * + * @var object + */ + protected $_category; + + /** + * The list of other newsfeed categories. + * + * @var array + */ + protected $_categories; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'numarticles', 'a.numarticles', + 'link', 'a.link', + 'ordering', 'a.ordering', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to get a list of items. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + // Invoke the parent getItems method to get the main list + $items = parent::getItems(); + + $taggedItems = []; + + // Convert the params field into an object, saving original in _params + foreach ($items as $item) { + if (!isset($this->_params)) { + $item->params = new Registry($item->params); + } + + // Some contexts may not use tags data at all, so we allow callers to disable loading tag data + if ($this->getState('load_tags', true)) { + $item->tags = new TagsHelper(); + $taggedItems[$item->id] = $item; + } + } + + // Load tags of all items. + if ($taggedItems) { + $tagsHelper = new TagsHelper(); + $itemIds = \array_keys($taggedItems); + + foreach ($tagsHelper->getMultipleItemTags('com_newsfeeds.newsfeed', $itemIds) as $id => $tags) { + $taggedItems[$id]->tags->itemTags = $tags; + } + } + + return $items; + } + + /** + * Method to build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + // Create a new query object. + $db = $this->getDatabase(); + + /** @var \Joomla\Database\DatabaseQuery $query */ + $query = $db->getQuery(true); + + // Select required fields from the categories. + $query->select($this->getState('list.select', $db->quoteName('a') . '.*')) + ->from($db->quoteName('#__newsfeeds', 'a')) + ->whereIn($db->quoteName('a.access'), $groups); + + // Filter by category. + if ($categoryId = (int) $this->getState('category.id')) { + $query->where($db->quoteName('a.catid') . ' = :categoryId') + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->whereIn($db->quoteName('c.access'), $groups) + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + + // Filter by state + $state = $this->getState('filter.published'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.published') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } else { + $query->where($db->quoteName('a.published') . ' IN (0,1,2)'); + } + + // Filter by start and end dates. + if ($this->getState('filter.publish_date')) { + $nowDate = Factory::getDate()->toSql(); + + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_up') . ' IS NULL', + $db->quoteName('a.publish_up') . ' <= :nowDate1', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_down') . ' IS NULL', + $db->quoteName('a.publish_down') . ' >= :nowDate2', + ], + 'OR' + ) + ->bind([':nowDate1', ':nowDate2'], $nowDate); + } + + // Filter by search in title + if ($search = $this->getState('list.filter')) { + $search = '%' . $search . '%'; + $query->where($db->quoteName('a.name') . ' LIKE :search') + ->bind(':search', $search); + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field + * @param string $direction An optional direction [asc|desc] + * + * @return void + * + * @since 1.6 + * + * @throws \Exception + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_newsfeeds'); + + // List state information + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); + $this->setState('list.limit', $limit); + + $limitstart = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $limitstart); + + // Optional filter text + $this->setState('list.filter', $app->input->getString('filter-search')); + + $orderCol = $app->input->get('filter_order', 'ordering'); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->input->get('filter_order_Dir', 'ASC'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $id = $app->input->get('id', 0, 'int'); + $this->setState('category.id', $id); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_newsfeeds')) && (!$user->authorise('core.edit', 'com_newsfeeds'))) { + // Limit to published for people who can't edit or edit.state. + $this->setState('filter.published', 1); + + // Filter by start and end dates. + $this->setState('filter.publish_date', true); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to get category data for the current category + * + * @return object + * + * @since 1.5 + */ + public function getCategory() + { + if (!is_object($this->_item)) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); + $categories = Categories::getInstance('Newsfeeds', $options); + $this->_item = $categories->get($this->getState('category.id', 'root')); + + if (is_object($this->_item)) { + $this->_children = $this->_item->getChildren(); + $this->_parent = false; + + if ($this->_item->getParent()) { + $this->_parent = $this->_item->getParent(); + } + + $this->_rightsibling = $this->_item->getSibling(); + $this->_leftsibling = $this->_item->getSibling(false); + } else { + $this->_children = false; + $this->_parent = false; + } + } + + return $this->_item; + } + + /** + * Get the parent category. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function getParent() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_parent; + } + + /** + * Get the sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getLeftSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_leftsibling; + } + + /** + * Get the sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getRightSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_rightsibling; + } + + /** + * Get the child categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getChildren() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_children; + } + + /** + * Increment the hit counter for the category. + * + * @param int $pk Optional primary key of the category to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); + $table = Table::getInstance('Category', 'JTable'); + $table->hit($pk); + } + + return true; + } } diff --git a/code/components/com_newsfeeds/src/Model/NewsfeedModel.php b/code/components/com_newsfeeds/src/Model/NewsfeedModel.php index 1f332f19..d3365afb 100644 --- a/code/components/com_newsfeeds/src/Model/NewsfeedModel.php +++ b/code/components/com_newsfeeds/src/Model/NewsfeedModel.php @@ -1,4 +1,5 @@ input->getInt('id'); - $this->setState('newsfeed.id', $pk); - - $offset = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.offset', $offset); - - // Load the parameters. - $params = $app->getParams(); - $this->setState('params', $params); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_newsfeeds')) && (!$user->authorise('core.edit', 'com_newsfeeds'))) - { - $this->setState('filter.published', 1); - $this->setState('filter.archived', 2); - } - } - - /** - * Method to get newsfeed data. - * - * @param integer $pk The id of the newsfeed. - * - * @return mixed Menu item data object on success, false on failure. - * - * @since 1.6 - */ - public function &getItem($pk = null) - { - $pk = (int) $pk ?: (int) $this->getState('newsfeed.id'); - - if ($this->_item === null) - { - $this->_item = array(); - } - - if (!isset($this->_item[$pk])) - { - try - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $this->getState('item.select', $db->quoteName('a') . '.*'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('c.alias', 'category_alias'), - $db->quoteName('c.access', 'category_access'), - $db->quoteName('u.name', 'author'), - $db->quoteName('parent.title', 'parent_title'), - $db->quoteName('parent.id', 'parent_id'), - $db->quoteName('parent.path', 'parent_route'), - $db->quoteName('parent.alias', 'parent_alias'), - ] - ) - ->from($db->quoteName('#__newsfeeds', 'a')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by')) - ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) - ->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - // Filter by published state. - $published = $this->getState('filter.published'); - $archived = $this->getState('filter.archived'); - - if (is_numeric($published)) - { - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - $published = (int) $published; - $archived = (int) $archived; - - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.published') . ' = :published1', - $db->quoteName('a.published') . ' = :archived1', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_up') . ' IS NULL', - $db->quoteName('a.publish_up') . ' <= :nowDate1', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_down') . ' IS NULL', - $db->quoteName('a.publish_down') . ' >= :nowDate2', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('c.published') . ' = :published2', - $db->quoteName('c.published') . ' = :archived2', - ], - 'OR' - ) - ->bind([':published1', ':published2'], $published, ParameterType::INTEGER) - ->bind([':archived1', ':archived2'], $archived, ParameterType::INTEGER) - ->bind([':nowDate1', ':nowDate2'], $nowDate); - } - - $db->setQuery($query); - - $data = $db->loadObject(); - - if ($data === null) - { - throw new \Exception(Text::_('COM_NEWSFEEDS_ERROR_FEED_NOT_FOUND'), 404); - } - - // Check for published state if filter set. - - if ((is_numeric($published) || is_numeric($archived)) && $data->published != $published && $data->published != $archived) - { - throw new \Exception(Text::_('COM_NEWSFEEDS_ERROR_FEED_NOT_FOUND'), 404); - } - - // Convert parameter fields to objects. - $registry = new Registry($data->params); - $data->params = clone $this->getState('params'); - $data->params->merge($registry); - - $data->metadata = new Registry($data->metadata); - - // Compute access permissions. - - if ($access = $this->getState('filter.access')) - { - // If the access filter has been set, we already know this user can view. - $data->params->set('access-view', true); - } - else - { - // If no access filter is set, the layout takes some responsibility for display of limited information. - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); - } - - $this->_item[$pk] = $data; - } - catch (\Exception $e) - { - $this->setError($e); - $this->_item[$pk] = false; - } - } - - return $this->_item[$pk]; - } - - /** - * Increment the hit counter for the newsfeed. - * - * @param int $pk Optional primary key of the item to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - * - * @since 3.0 - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('newsfeed.id'); - - $table = $this->getTable('Newsfeed', 'Administrator'); - $table->hit($pk); - } - - return true; - } + /** + * Model context string. + * + * @var string + * @since 1.6 + */ + protected $_context = 'com_newsfeeds.newsfeed'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load state from the request. + $pk = $app->input->getInt('id'); + $this->setState('newsfeed.id', $pk); + + $offset = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.offset', $offset); + + // Load the parameters. + $params = $app->getParams(); + $this->setState('params', $params); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_newsfeeds')) && (!$user->authorise('core.edit', 'com_newsfeeds'))) { + $this->setState('filter.published', 1); + $this->setState('filter.archived', 2); + } + } + + /** + * Method to get newsfeed data. + * + * @param integer $pk The id of the newsfeed. + * + * @return mixed Menu item data object on success, false on failure. + * + * @since 1.6 + */ + public function &getItem($pk = null) + { + $pk = (int) $pk ?: (int) $this->getState('newsfeed.id'); + + if ($this->_item === null) { + $this->_item = array(); + } + + if (!isset($this->_item[$pk])) { + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $this->getState('item.select', $db->quoteName('a') . '.*'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('c.alias', 'category_alias'), + $db->quoteName('c.access', 'category_access'), + $db->quoteName('u.name', 'author'), + $db->quoteName('parent.title', 'parent_title'), + $db->quoteName('parent.id', 'parent_id'), + $db->quoteName('parent.path', 'parent_route'), + $db->quoteName('parent.alias', 'parent_alias'), + ] + ) + ->from($db->quoteName('#__newsfeeds', 'a')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by')) + ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) + ->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + // Filter by published state. + $published = $this->getState('filter.published'); + $archived = $this->getState('filter.archived'); + + if (is_numeric($published)) { + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + $published = (int) $published; + $archived = (int) $archived; + + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.published') . ' = :published1', + $db->quoteName('a.published') . ' = :archived1', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_up') . ' IS NULL', + $db->quoteName('a.publish_up') . ' <= :nowDate1', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_down') . ' IS NULL', + $db->quoteName('a.publish_down') . ' >= :nowDate2', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('c.published') . ' = :published2', + $db->quoteName('c.published') . ' = :archived2', + ], + 'OR' + ) + ->bind([':published1', ':published2'], $published, ParameterType::INTEGER) + ->bind([':archived1', ':archived2'], $archived, ParameterType::INTEGER) + ->bind([':nowDate1', ':nowDate2'], $nowDate); + } + + $db->setQuery($query); + + $data = $db->loadObject(); + + if ($data === null) { + throw new \Exception(Text::_('COM_NEWSFEEDS_ERROR_FEED_NOT_FOUND'), 404); + } + + // Check for published state if filter set. + + if ((is_numeric($published) || is_numeric($archived)) && $data->published != $published && $data->published != $archived) { + throw new \Exception(Text::_('COM_NEWSFEEDS_ERROR_FEED_NOT_FOUND'), 404); + } + + // Convert parameter fields to objects. + $registry = new Registry($data->params); + $data->params = clone $this->getState('params'); + $data->params->merge($registry); + + $data->metadata = new Registry($data->metadata); + + // Compute access permissions. + + if ($access = $this->getState('filter.access')) { + // If the access filter has been set, we already know this user can view. + $data->params->set('access-view', true); + } else { + // If no access filter is set, the layout takes some responsibility for display of limited information. + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); + } + + $this->_item[$pk] = $data; + } catch (\Exception $e) { + $this->setError($e); + $this->_item[$pk] = false; + } + } + + return $this->_item[$pk]; + } + + /** + * Increment the hit counter for the newsfeed. + * + * @param int $pk Optional primary key of the item to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + * + * @since 3.0 + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('newsfeed.id'); + + $table = $this->getTable('Newsfeed', 'Administrator'); + $table->hit($pk); + } + + return true; + } } diff --git a/code/components/com_newsfeeds/src/Service/Category.php b/code/components/com_newsfeeds/src/Service/Category.php index 2c2ae1c0..9b7f1802 100644 --- a/code/components/com_newsfeeds/src/Service/Category.php +++ b/code/components/com_newsfeeds/src/Service/Category.php @@ -1,4 +1,5 @@ categoryFactory = $categoryFactory; - $this->db = $db; - - $params = ComponentHelper::getParams('com_newsfeeds'); - $this->noIDs = (bool) $params->get('sef_ids'); - $categories = new RouterViewConfiguration('categories'); - $categories->setKey('id'); - $this->registerView($categories); - $category = new RouterViewConfiguration('category'); - $category->setKey('id')->setParent($categories, 'catid')->setNestable(); - $this->registerView($category); - $newsfeed = new RouterViewConfiguration('newsfeed'); - $newsfeed->setKey('id')->setParent($category, 'catid'); - $this->registerView($newsfeed); - - parent::__construct($app, $menu); - - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategorySegment($id, $query) - { - $category = $this->getCategories()->get($id); - - if ($category) - { - $path = array_reverse($category->getPath(), true); - $path[0] = '1:root'; - - if ($this->noIDs) - { - foreach ($path as &$segment) - { - list($id, $segment) = explode(':', $segment, 2); - } - } - - return $path; - } - - return array(); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategoriesSegment($id, $query) - { - return $this->getCategorySegment($id, $query); - } - - /** - * Method to get the segment(s) for a newsfeed - * - * @param string $id ID of the newsfeed to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getNewsfeedSegment($id, $query) - { - if (!strpos($id, ':')) - { - $id = (int) $id; - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('alias')) - ->from($this->db->quoteName('#__newsfeeds')) - ->where($this->db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - $id .= ':' . $this->db->loadResult(); - } - - if ($this->noIDs) - { - list($void, $segment) = explode(':', $id, 2); - - return array($void => $segment); - } - - return array((int) $id => $id); - } - - /** - * Method to get the id for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoryId($segment, $query) - { - if (isset($query['id'])) - { - $category = $this->getCategories(['access' => false])->get($query['id']); - - if ($category) - { - foreach ($category->getChildren() as $child) - { - if ($this->noIDs) - { - if ($child->alias === $segment) - { - return $child->id; - } - } - else - { - if ($child->id == (int) $segment) - { - return $child->id; - } - } - } - } - } - - return false; - } - - /** - * Method to get the segment(s) for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoriesId($segment, $query) - { - return $this->getCategoryId($segment, $query); - } - - /** - * Method to get the segment(s) for a newsfeed - * - * @param string $segment Segment of the newsfeed to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getNewsfeedId($segment, $query) - { - if ($this->noIDs) - { - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__newsfeeds')) - ->where( - [ - $this->db->quoteName('alias') . ' = :segment', - $this->db->quoteName('catid') . ' = :id', - ] - ) - ->bind(':segment', $segment) - ->bind(':id', $query['id'], ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - return (int) $this->db->loadResult(); - } - - return (int) $segment; - } - - /** - * Method to get categories from cache - * - * @param array $options The options for retrieving categories - * - * @return CategoryInterface The object containing categories - * - * @since 4.0.0 - */ - private function getCategories(array $options = []): CategoryInterface - { - $key = serialize($options); - - if (!isset($this->categoryCache[$key])) - { - $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); - } - - return $this->categoryCache[$key]; - } + /** + * Flag to remove IDs + * + * @var boolean + */ + protected $noIDs = false; + + /** + * The category factory + * + * @var CategoryFactoryInterface + * + * @since 4.0.0 + */ + private $categoryFactory; + + /** + * The category cache + * + * @var array + * + * @since 4.0.0 + */ + private $categoryCache = []; + + /** + * The db + * + * @var DatabaseInterface + * + * @since 4.0.0 + */ + private $db; + + /** + * Newsfeeds Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + */ + public function __construct(SiteApplication $app, AbstractMenu $menu, CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) + { + $this->categoryFactory = $categoryFactory; + $this->db = $db; + + $params = ComponentHelper::getParams('com_newsfeeds'); + $this->noIDs = (bool) $params->get('sef_ids'); + $categories = new RouterViewConfiguration('categories'); + $categories->setKey('id'); + $this->registerView($categories); + $category = new RouterViewConfiguration('category'); + $category->setKey('id')->setParent($categories, 'catid')->setNestable(); + $this->registerView($category); + $newsfeed = new RouterViewConfiguration('newsfeed'); + $newsfeed->setKey('id')->setParent($category, 'catid'); + $this->registerView($newsfeed); + + parent::__construct($app, $menu); + + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategorySegment($id, $query) + { + $category = $this->getCategories()->get($id); + + if ($category) { + $path = array_reverse($category->getPath(), true); + $path[0] = '1:root'; + + if ($this->noIDs) { + foreach ($path as &$segment) { + list($id, $segment) = explode(':', $segment, 2); + } + } + + return $path; + } + + return array(); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategoriesSegment($id, $query) + { + return $this->getCategorySegment($id, $query); + } + + /** + * Method to get the segment(s) for a newsfeed + * + * @param string $id ID of the newsfeed to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getNewsfeedSegment($id, $query) + { + if (!strpos($id, ':')) { + $id = (int) $id; + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('alias')) + ->from($this->db->quoteName('#__newsfeeds')) + ->where($this->db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + $id .= ':' . $this->db->loadResult(); + } + + if ($this->noIDs) { + list($void, $segment) = explode(':', $id, 2); + + return array($void => $segment); + } + + return array((int) $id => $id); + } + + /** + * Method to get the id for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoryId($segment, $query) + { + if (isset($query['id'])) { + $category = $this->getCategories(['access' => false])->get($query['id']); + + if ($category) { + foreach ($category->getChildren() as $child) { + if ($this->noIDs) { + if ($child->alias === $segment) { + return $child->id; + } + } else { + if ($child->id == (int) $segment) { + return $child->id; + } + } + } + } + } + + return false; + } + + /** + * Method to get the segment(s) for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoriesId($segment, $query) + { + return $this->getCategoryId($segment, $query); + } + + /** + * Method to get the segment(s) for a newsfeed + * + * @param string $segment Segment of the newsfeed to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getNewsfeedId($segment, $query) + { + if ($this->noIDs) { + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__newsfeeds')) + ->where( + [ + $this->db->quoteName('alias') . ' = :segment', + $this->db->quoteName('catid') . ' = :id', + ] + ) + ->bind(':segment', $segment) + ->bind(':id', $query['id'], ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + return (int) $this->db->loadResult(); + } + + return (int) $segment; + } + + /** + * Method to get categories from cache + * + * @param array $options The options for retrieving categories + * + * @return CategoryInterface The object containing categories + * + * @since 4.0.0 + */ + private function getCategories(array $options = []): CategoryInterface + { + $key = serialize($options); + + if (!isset($this->categoryCache[$key])) { + $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); + } + + return $this->categoryCache[$key]; + } } diff --git a/code/components/com_newsfeeds/src/View/Categories/HtmlView.php b/code/components/com_newsfeeds/src/View/Categories/HtmlView.php index e8076475..6d68c869 100644 --- a/code/components/com_newsfeeds/src/View/Categories/HtmlView.php +++ b/code/components/com_newsfeeds/src/View/Categories/HtmlView.php @@ -1,4 +1,5 @@ commonCategoryDisplay(); - - // Flag indicates to not add limitstart=0 to URL - $this->pagination->hideEmptyLimitstart = true; - - // Prepare the data. - // Compute the newsfeed slug. - foreach ($this->items as $item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - $temp = $item->params; - $item->params = clone $this->params; - $item->params->merge($temp); - } - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function prepareDocument() - { - parent::prepareDocument(); - - $menu = $this->menu; - $id = (int) @$menu->query['id']; - - if ($menu && (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' - || $id != $this->category->id)) - { - $path = array(array('title' => $this->category->title, 'link' => '')); - $category = $this->category->getParent(); - - while ((!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' - || $id != $category->id) && $category->id > 1) - { - $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $this->pathway->addItem($item['title'], $item['link']); - } - } - } + /** + * @var string Default title to use for page title + * @since 3.2 + */ + protected $defaultPageTitle = 'COM_NEWSFEEDS_DEFAULT_PAGE_TITLE'; + + /** + * @var string The name of the extension for the category + * @since 3.2 + */ + protected $extension = 'com_newsfeeds'; + + /** + * @var string The name of the view to link individual items to + * @since 3.2 + */ + protected $viewName = 'newsfeed'; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->commonCategoryDisplay(); + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + // Prepare the data. + // Compute the newsfeed slug. + foreach ($this->items as $item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + $temp = $item->params; + $item->params = clone $this->params; + $item->params->merge($temp); + } + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function prepareDocument() + { + parent::prepareDocument(); + + $menu = $this->menu; + $id = (int) @$menu->query['id']; + + if ( + $menu && (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' + || $id != $this->category->id) + ) { + $path = array(array('title' => $this->category->title, 'link' => '')); + $category = $this->category->getParent(); + + while ( + (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' + || $id != $category->id) && $category->id > 1 + ) { + $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $this->pathway->addItem($item['title'], $item['link']); + } + } + } } diff --git a/code/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php b/code/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php index 6355bddb..c1d76049 100644 --- a/code/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php +++ b/code/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php @@ -1,4 +1,5 @@ input->getBool('print'); - - // Get model data. - $state = $this->get('State'); - $item = $this->get('Item'); - - // Check for errors. - // @TODO: Maybe this could go into ComponentHelper::raiseErrors($this->get('Errors')) - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Add router helpers. - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - $item->catslug = $item->category_alias ? ($item->catid . ':' . $item->category_alias) : $item->catid; - $item->parent_slug = $item->category_alias ? ($item->parent_id . ':' . $item->parent_alias) : $item->parent_id; - - // Merge newsfeed params. If this is single-newsfeed view, menu params override newsfeed params - // Otherwise, newsfeed params override menu item params - $params = $state->get('params'); - $newsfeed_params = clone $item->params; - $active = $app->getMenu()->getActive(); - $temp = clone $params; - - // Check to see which parameters should take priority - if ($active) - { - $currentLink = $active->link; - - // If the current view is the active item and a newsfeed view for this feed, then the menu item params take priority - if (strpos($currentLink, 'view=newsfeed') && strpos($currentLink, '&id=' . (string) $item->id)) - { - // $item->params are the newsfeed params, $temp are the menu item params - // Merge so that the menu item params take priority - $newsfeed_params->merge($temp); - $item->params = $newsfeed_params; - - // Load layout from active query (in case it is an alternative menu item) - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - } - else - { - // Current view is not a single newsfeed, so the newsfeed params take priority here - // Merge the menu item params with the newsfeed params so that the newsfeed params take priority - $temp->merge($newsfeed_params); - $item->params = $temp; - - // Check for alternative layouts (since we are not in a single-newsfeed menu item) - if ($layout = $item->params->get('newsfeed_layout')) - { - $this->setLayout($layout); - } - } - } - else - { - // Merge so that newsfeed params take priority - $temp->merge($newsfeed_params); - $item->params = $temp; - - // Check for alternative layouts (since we are not in a single-newsfeed menu item) - if ($layout = $item->params->get('newsfeed_layout')) - { - $this->setLayout($layout); - } - } - - // Check the access to the newsfeed - $levels = $user->getAuthorisedViewLevels(); - - if (!in_array($item->access, $levels) || (in_array($item->access, $levels) && (!in_array($item->category_access, $levels)))) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return; - } - - // Get the current menu item - $params = $app->getParams(); - - $params->merge($item->params); - - try - { - $feed = new FeedFactory; - $this->rssDoc = $feed->getFeed($item->link); - } - catch (\InvalidArgumentException $e) - { - $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); - } - catch (\RuntimeException $e) - { - $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); - } - - if (empty($this->rssDoc)) - { - $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); - } - - $feed_display_order = $params->get('feed_display_order', 'des'); - - if ($feed_display_order === 'asc') - { - $this->rssDoc->reverseItems(); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - $this->params = $params; - $this->state = $state; - $this->item = $item; - $this->user = $user; - - if (!empty($msg)) - { - $this->msg = $msg; - } - - $this->print = $print; - - $item->tags = new TagsHelper; - $item->tags->getItemTags('com_newsfeeds.newsfeed', $item->id); - - // Increment the hit counter of the newsfeed. - $model = $this->getModel(); - $model->hit(); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - $pathway = $app->getPathway(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_NEWSFEEDS_DEFAULT_PAGE_TITLE')); - } - - $title = $this->params->get('page_title', ''); - - $id = (int) @$menu->query['id']; - - // If the menu item does not concern this newsfeed - if ($menu && (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] !== 'newsfeed' - || $id != $this->item->id)) - { - // If this is not a single newsfeed menu item, set the page title to the newsfeed title - if ($this->item->name) - { - $title = $this->item->name; - } - - $path = array(array('title' => $this->item->name, 'link' => '')); - $category = Categories::getInstance('Newsfeeds')->get($this->item->catid); - - while ((!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' - || $id != $category->id) && $category->id > 1) - { - $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id)); - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $pathway->addItem($item['title'], $item['link']); - } - } - - if (empty($title)) - { - $title = $this->item->name; - } - - $this->setDocumentTitle($title); - - if ($this->item->metadesc) - { - $this->document->setDescription($this->item->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - if ($app->get('MetaAuthor') == '1') - { - $this->document->setMetaData('author', $this->item->author); - } - - $mdata = $this->item->metadata->toArray(); - - foreach ($mdata as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - } + /** + * The model state + * + * @var object + * + * @since 1.6 + */ + protected $state; + + /** + * The newsfeed item + * + * @var object + * + * @since 1.6 + */ + protected $item; + + /** + * UNUSED? + * + * @var boolean + * + * @since 1.6 + */ + protected $print; + + /** + * The current user instance + * + * @var \Joomla\CMS\User\User|null + * + * @since 4.0.0 + */ + protected $user = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $user = $this->getCurrentUser(); + + // Get view related request variables. + $print = $app->input->getBool('print'); + + // Get model data. + $state = $this->get('State'); + $item = $this->get('Item'); + + // Check for errors. + // @TODO: Maybe this could go into ComponentHelper::raiseErrors($this->get('Errors')) + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Add router helpers. + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + $item->catslug = $item->category_alias ? ($item->catid . ':' . $item->category_alias) : $item->catid; + $item->parent_slug = $item->category_alias ? ($item->parent_id . ':' . $item->parent_alias) : $item->parent_id; + + // Merge newsfeed params. If this is single-newsfeed view, menu params override newsfeed params + // Otherwise, newsfeed params override menu item params + $params = $state->get('params'); + $newsfeed_params = clone $item->params; + $active = $app->getMenu()->getActive(); + $temp = clone $params; + + // Check to see which parameters should take priority + if ($active) { + $currentLink = $active->link; + + // If the current view is the active item and a newsfeed view for this feed, then the menu item params take priority + if (strpos($currentLink, 'view=newsfeed') && strpos($currentLink, '&id=' . (string) $item->id)) { + // $item->params are the newsfeed params, $temp are the menu item params + // Merge so that the menu item params take priority + $newsfeed_params->merge($temp); + $item->params = $newsfeed_params; + + // Load layout from active query (in case it is an alternative menu item) + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + } else { + // Current view is not a single newsfeed, so the newsfeed params take priority here + // Merge the menu item params with the newsfeed params so that the newsfeed params take priority + $temp->merge($newsfeed_params); + $item->params = $temp; + + // Check for alternative layouts (since we are not in a single-newsfeed menu item) + if ($layout = $item->params->get('newsfeed_layout')) { + $this->setLayout($layout); + } + } + } else { + // Merge so that newsfeed params take priority + $temp->merge($newsfeed_params); + $item->params = $temp; + + // Check for alternative layouts (since we are not in a single-newsfeed menu item) + if ($layout = $item->params->get('newsfeed_layout')) { + $this->setLayout($layout); + } + } + + // Check the access to the newsfeed + $levels = $user->getAuthorisedViewLevels(); + + if (!in_array($item->access, $levels) || (in_array($item->access, $levels) && (!in_array($item->category_access, $levels)))) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return; + } + + // Get the current menu item + $params = $app->getParams(); + + $params->merge($item->params); + + try { + $feed = new FeedFactory(); + $this->rssDoc = $feed->getFeed($item->link); + } catch (\InvalidArgumentException $e) { + $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); + } catch (\RuntimeException $e) { + $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); + } + + if (empty($this->rssDoc)) { + $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); + } + + $feed_display_order = $params->get('feed_display_order', 'des'); + + if ($feed_display_order === 'asc') { + $this->rssDoc->reverseItems(); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + $this->params = $params; + $this->state = $state; + $this->item = $item; + $this->user = $user; + + if (!empty($msg)) { + $this->msg = $msg; + } + + $this->print = $print; + + $item->tags = new TagsHelper(); + $item->tags->getItemTags('com_newsfeeds.newsfeed', $item->id); + + // Increment the hit counter of the newsfeed. + $model = $this->getModel(); + $model->hit(); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + $pathway = $app->getPathway(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_NEWSFEEDS_DEFAULT_PAGE_TITLE')); + } + + $title = $this->params->get('page_title', ''); + + $id = (int) @$menu->query['id']; + + // If the menu item does not concern this newsfeed + if ( + $menu && (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] !== 'newsfeed' + || $id != $this->item->id) + ) { + // If this is not a single newsfeed menu item, set the page title to the newsfeed title + if ($this->item->name) { + $title = $this->item->name; + } + + $path = array(array('title' => $this->item->name, 'link' => '')); + $category = Categories::getInstance('Newsfeeds')->get($this->item->catid); + + while ( + (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' + || $id != $category->id) && $category->id > 1 + ) { + $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id)); + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $pathway->addItem($item['title'], $item['link']); + } + } + + if (empty($title)) { + $title = $this->item->name; + } + + $this->setDocumentTitle($title); + + if ($this->item->metadesc) { + $this->document->setDescription($this->item->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + if ($app->get('MetaAuthor') == '1') { + $this->document->setMetaData('author', $this->item->author); + } + + $mdata = $this->item->metadata->toArray(); + + foreach ($mdata as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + } } diff --git a/code/components/com_newsfeeds/tmpl/categories/default.php b/code/components/com_newsfeeds/tmpl/categories/default.php index 1a90ac23..3335f606 100644 --- a/code/components/com_newsfeeds/tmpl/categories/default.php +++ b/code/components/com_newsfeeds/tmpl/categories/default.php @@ -1,4 +1,5 @@
    - - loadTemplate('items'); ?> + + loadTemplate('items'); ?>
    diff --git a/code/components/com_newsfeeds/tmpl/categories/default_items.php b/code/components/com_newsfeeds/tmpl/categories/default_items.php index a7624750..0b3d6aff 100644 --- a/code/components/com_newsfeeds/tmpl/categories/default_items.php +++ b/code/components/com_newsfeeds/tmpl/categories/default_items.php @@ -1,4 +1,5 @@ maxLevelcat != 0 && count($this->items[$this->parent->id]) > 0) : ?> - items[$this->parent->id] as $id => $item) : ?> - params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> -
    - - params->get('show_subcat_desc_cat') == 1) : ?> - description) : ?> -
    - description, '', 'com_newsfeeds.categories'); ?> -
    - - - getChildren()) > 0 && $this->maxLevelcat > 1) : ?> -
    - items[$item->id] = $item->getChildren(); ?> - parent = $item; ?> - maxLevelcat--; ?> - loadTemplate('items'); ?> - parent = $item->getParent(); ?> - maxLevelcat++; ?> -
    - -
    - - + items[$this->parent->id] as $id => $item) : ?> + params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> +
    + + params->get('show_subcat_desc_cat') == 1) : ?> + description) : ?> +
    + description, '', 'com_newsfeeds.categories'); ?> +
    + + + getChildren()) > 0 && $this->maxLevelcat > 1) : ?> +
    + items[$item->id] = $item->getChildren(); ?> + parent = $item; ?> + maxLevelcat--; ?> + loadTemplate('items'); ?> + parent = $item->getParent(); ?> + maxLevelcat++; ?> +
    + +
    + + diff --git a/code/components/com_newsfeeds/tmpl/category/default.php b/code/components/com_newsfeeds/tmpl/category/default.php index 12ce5faf..f3852a4e 100644 --- a/code/components/com_newsfeeds/tmpl/category/default.php +++ b/code/components/com_newsfeeds/tmpl/category/default.php @@ -1,4 +1,5 @@ params->get('show_page_heading') ? 'h2' : 'h1'; ?>
    - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - - params->get('show_category_title', 1)) : ?> - <> - category->title, '', 'com_newsfeeds.category.title'); ?> - > - - params->get('show_tags', 1) && !empty($this->category->tags->itemTags)) : ?> - category->tagLayout = new FileLayout('joomla.content.tags'); ?> - category->tagLayout->render($this->category->tags->itemTags); ?> - - params->get('show_description', 1) || $this->params->def('show_description_image', 1)) : ?> -
    - params->get('show_description_image') && $this->category->getParams()->get('image')) : ?> - $this->category->getParams()->get('image'), - 'alt' => empty($this->category->getParams()->get('image_alt')) && empty($this->category->getParams()->get('image_alt_empty')) ? false : $this->category->getParams()->get('image_alt'), - ] - ); ?> - - params->get('show_description') && $this->category->description) : ?> - category->description, '', 'com_newsfeeds.category'); ?> - -
    -
    - - loadTemplate('items'); ?> - maxLevel != 0 && !empty($this->children[$this->category->id])) : ?> -
    -

    - -

    - loadTemplate('children'); ?> -
    - + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + + params->get('show_category_title', 1)) : ?> + <> + category->title, '', 'com_newsfeeds.category.title'); ?> + > + + params->get('show_tags', 1) && !empty($this->category->tags->itemTags)) : ?> + category->tagLayout = new FileLayout('joomla.content.tags'); ?> + category->tagLayout->render($this->category->tags->itemTags); ?> + + params->get('show_description', 1) || $this->params->def('show_description_image', 1)) : ?> +
    + params->get('show_description_image') && $this->category->getParams()->get('image')) : ?> + $this->category->getParams()->get('image'), + 'alt' => empty($this->category->getParams()->get('image_alt')) && empty($this->category->getParams()->get('image_alt_empty')) ? false : $this->category->getParams()->get('image_alt'), + ] + ); ?> + + params->get('show_description') && $this->category->description) : ?> + category->description, '', 'com_newsfeeds.category'); ?> + +
    +
    + + loadTemplate('items'); ?> + maxLevel != 0 && !empty($this->children[$this->category->id])) : ?> +
    +

    + +

    + loadTemplate('children'); ?> +
    +
    diff --git a/code/components/com_newsfeeds/tmpl/category/default_children.php b/code/components/com_newsfeeds/tmpl/category/default_children.php index 7c7e0749..09b81be9 100644 --- a/code/components/com_newsfeeds/tmpl/category/default_children.php +++ b/code/components/com_newsfeeds/tmpl/category/default_children.php @@ -1,4 +1,5 @@ maxLevel != 0 && count($this->children[$this->category->id]) > 0) : ?> -
      - children[$this->category->id] as $id => $child) : ?> - params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> -
    • - - - escape($child->title); ?> - - - params->get('show_subcat_desc') == 1) : ?> - description) : ?> -
      - description, '', 'com_newsfeeds.category'); ?> -
      - - - params->get('show_cat_items') == 1) : ?> - -   - numitems; ?> - - - getChildren()) > 0) : ?> - children[$child->id] = $child->getChildren(); ?> - category = $child; ?> - maxLevel--; ?> - loadTemplate('children'); ?> - category = $child->getParent(); ?> - maxLevel++; ?> - -
    • - - -
    +
      + children[$this->category->id] as $id => $child) : ?> + params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> +
    • + + + escape($child->title); ?> + + + params->get('show_subcat_desc') == 1) : ?> + description) : ?> +
      + description, '', 'com_newsfeeds.category'); ?> +
      + + + params->get('show_cat_items') == 1) : ?> + +   + numitems; ?> + + + getChildren()) > 0) : ?> + children[$child->id] = $child->getChildren(); ?> + category = $child; ?> + maxLevel--; ?> + loadTemplate('children'); ?> + category = $child->getParent(); ?> + maxLevel++; ?> + +
    • + + +
    - items)) : ?> -

    - -
    - params->get('filter_field') !== 'hide' || $this->params->get('show_pagination_limit')) : ?> -
    - params->get('filter_field') !== 'hide' && $this->params->get('filter_field') == '1') : ?> -
    - - -
    - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - -
    - -
      - items as $item) : ?> -
    • - params->get('show_articles')) : ?> - - numarticles); ?> - - - - - - published == 0) : ?> - - - - -
      - params->get('show_link')) : ?> - link); ?> - - - - - -
      - -
    • - -
    - - items)) : ?> - params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - pagination->getPagesLinks(); ?> -
    - - -
    - + items)) : ?> +

    + +
    + params->get('filter_field') !== 'hide' || $this->params->get('show_pagination_limit')) : ?> +
    + params->get('filter_field') !== 'hide' && $this->params->get('filter_field') == '1') : ?> +
    + + +
    + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + +
    + +
      + items as $item) : ?> +
    • + params->get('show_articles')) : ?> + + numarticles); ?> + + + + + + published == 0) : ?> + + + + +
      + params->get('show_link')) : ?> + link); ?> + + + + + +
      + +
    • + +
    + + items)) : ?> + params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + pagination->getPagesLinks(); ?> +
    + + +
    +
    diff --git a/code/components/com_newsfeeds/tmpl/newsfeed/default.php b/code/components/com_newsfeeds/tmpl/newsfeed/default.php index 99cf1315..6ee290ee 100644 --- a/code/components/com_newsfeeds/tmpl/newsfeed/default.php +++ b/code/components/com_newsfeeds/tmpl/newsfeed/default.php @@ -1,4 +1,5 @@ msg)) : ?> - msg; ?> + msg; ?> - - item->rtl; ?> - - isRtl(); ?> - - - - - - - - - - - - - - item->images); ?> -
    - params->get('display_num')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - -

    - item->published == 0) : ?> - - - - item->name); ?> - -

    + + item->rtl; ?> + + isRtl(); ?> + + + + + + + + + + + + + + item->images); ?> +
    + params->get('display_num')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + +

    + item->published == 0) : ?> + + + + item->name); ?> + +

    - params->get('show_tags', 1)) : ?> - item->tagLayout = new FileLayout('joomla.content.tags'); ?> - item->tagLayout->render($this->item->tags->itemTags); ?> - + params->get('show_tags', 1)) : ?> + item->tagLayout = new FileLayout('joomla.content.tags'); ?> + item->tagLayout->render($this->item->tags->itemTags); ?> + - - image_first) && !empty($images->image_first)) : ?> - float_first) ? $this->params->get('float_first') : $images->float_first; ?> -
    -
    - $images->image_first, - 'alt' => empty($images->image_first_alt) && empty($images->image_first_alt_empty) ? false : $images->image_first_alt, - ] - ); ?> - image_first_caption) : ?> -
    escape($images->image_first_caption); ?>
    - -
    -
    - + + image_first) && !empty($images->image_first)) : ?> + float_first) ? $this->params->get('float_first') : $images->float_first; ?> +
    +
    + $images->image_first, + 'alt' => empty($images->image_first_alt) && empty($images->image_first_alt_empty) ? false : $images->image_first_alt, + ] + ); ?> + image_first_caption) : ?> +
    escape($images->image_first_caption); ?>
    + +
    +
    + - image_second) and !empty($images->image_second)) : ?> - float_second) ? $this->params->get('float_second') : $images->float_second; ?> -
    -
    - $images->image_second, - 'alt' => empty($images->image_second_alt) && empty($images->image_second_alt_empty) ? false : $images->image_second_alt, - ] - ); ?> - image_second_caption) : ?> -
    escape($images->image_second_caption); ?>
    - -
    -
    - - - item->description; ?> - + image_second) and !empty($images->image_second)) : ?> + float_second) ? $this->params->get('float_second') : $images->float_second; ?> +
    +
    + $images->image_second, + 'alt' => empty($images->image_second_alt) && empty($images->image_second_alt_empty) ? false : $images->image_second_alt, + ] + ); ?> + image_second_caption) : ?> +
    escape($images->image_second_caption); ?>
    + +
    +
    + + + item->description; ?> + - params->get('show_feed_description')) : ?> -
    - rssDoc->description); ?> -
    - + params->get('show_feed_description')) : ?> +
    + rssDoc->description); ?> +
    + - - rssDoc->image && $this->params->get('show_feed_image')) : ?> -
    - $this->rssDoc->image->uri, - 'alt' => $this->rssDoc->image->title, - ] - ); ?> -
    - + + rssDoc->image && $this->params->get('show_feed_image')) : ?> +
    + $this->rssDoc->image->uri, + 'alt' => $this->rssDoc->image->title, + ] + ); ?> +
    + - - rssDoc[0])) : ?> -
      - item->numarticles; $i++) : ?> - rssDoc[$i])) : ?> - - - rssDoc[$i]->uri || !$this->rssDoc[$i]->isPermaLink ? trim($this->rssDoc[$i]->uri) : trim($this->rssDoc[$i]->guid); ?> - item->link : $uri; ?> - rssDoc[$i]->content !== '' ? trim($this->rssDoc[$i]->content) : ''; ?> -
    1. - - - - - + + rssDoc[0])) : ?> +
        + item->numarticles; $i++) : ?> + rssDoc[$i])) : ?> + + + rssDoc[$i]->uri || !$this->rssDoc[$i]->isPermaLink ? trim($this->rssDoc[$i]->uri) : trim($this->rssDoc[$i]->guid); ?> + item->link : $uri; ?> + rssDoc[$i]->content !== '' ? trim($this->rssDoc[$i]->content) : ''; ?> +
      1. + + + + + - params->get('show_item_description') && $text !== '') : ?> -
        - params->get('show_feed_image', 0) == 0) : ?> - - - params->get('feed_character_count')); ?> - -
        - -
      2. - -
      - -
    + params->get('show_item_description') && $text !== '') : ?> +
    + params->get('show_feed_image', 0) == 0) : ?> + + + params->get('feed_character_count')); ?> + +
    + + + + + +
    diff --git a/code/components/com_privacy/src/Controller/DisplayController.php b/code/components/com_privacy/src/Controller/DisplayController.php index a428c60f..03d5c30d 100644 --- a/code/components/com_privacy/src/Controller/DisplayController.php +++ b/code/components/com_privacy/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', $this->default_view); - - // Submitting information requests and confirmation through the frontend is restricted to authenticated users at this time - if (in_array($view, ['confirm', 'request']) && $this->app->getIdentity()->guest) - { - $this->setRedirect( - Route::_('index.php?option=com_users&view=login&return=' . base64_encode('index.php?option=com_privacy&view=' . $view), false) - ); - - return $this; - } - - // Set a Referrer-Policy header for views which require it - if (in_array($view, ['confirm', 'remind'])) - { - $this->app->setHeader('Referrer-Policy', 'no-referrer', true); - } - - return parent::display($cachable, $urlparams); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return $this + * + * @since 3.9.0 + */ + public function display($cachable = false, $urlparams = []) + { + $view = $this->input->get('view', $this->default_view); + + // Submitting information requests and confirmation through the frontend is restricted to authenticated users at this time + if (in_array($view, ['confirm', 'request']) && $this->app->getIdentity()->guest) { + $this->setRedirect( + Route::_('index.php?option=com_users&view=login&return=' . base64_encode('index.php?option=com_privacy&view=' . $view), false) + ); + + return $this; + } + + // Set a Referrer-Policy header for views which require it + if (in_array($view, ['confirm', 'remind'])) { + $this->app->setHeader('Referrer-Policy', 'no-referrer', true); + } + + return parent::display($cachable, $urlparams); + } } diff --git a/code/components/com_privacy/src/Controller/RequestController.php b/code/components/com_privacy/src/Controller/RequestController.php index 8147528a..1a899676 100644 --- a/code/components/com_privacy/src/Controller/RequestController.php +++ b/code/components/com_privacy/src/Controller/RequestController.php @@ -1,4 +1,5 @@ checkToken('post'); - - /** @var ConfirmModel $model */ - $model = $this->getModel('Confirm', 'Site'); - $data = $this->input->post->get('jform', [], 'array'); - - $return = $model->confirmRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if (Factory::getApplication()->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_PRIVACY_ERROR_CONFIRMING_REQUEST'); - } - - // Go back to the confirm form. - $this->setRedirect(Route::_('index.php?option=com_privacy&view=confirm', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Confirm failed. - // Go back to the confirm form. - $message = Text::sprintf('COM_PRIVACY_ERROR_CONFIRMING_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_privacy&view=confirm', false), $message, 'notice'); - - return false; - } - else - { - // Confirm succeeded. - $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CONFIRM_REQUEST_SUCCEEDED'), 'info'); - - return true; - } - } - - /** - * Method to submit an information request. - * - * @return boolean - * - * @since 3.9.0 - */ - public function submit() - { - // Check the request token. - $this->checkToken('post'); - - /** @var RequestModel $model */ - $model = $this->getModel('Request', 'Site'); - $data = $this->input->post->get('jform', [], 'array'); - - $return = $model->createRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if (Factory::getApplication()->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_PRIVACY_ERROR_CREATING_REQUEST'); - } - - // Go back to the confirm form. - $this->setRedirect(Route::_('index.php?option=com_privacy&view=request', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Confirm failed. - // Go back to the confirm form. - $message = Text::sprintf('COM_PRIVACY_ERROR_CREATING_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_privacy&view=request', false), $message, 'notice'); - - return false; - } - else - { - // Confirm succeeded. - $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CREATE_REQUEST_SUCCEEDED'), 'info'); - - return true; - } - } - - /** - * Method to extend the privacy consent. - * - * @return boolean - * - * @since 3.9.0 - */ - public function remind() - { - // Check the request token. - $this->checkToken('post'); - - /** @var ConfirmModel $model */ - $model = $this->getModel('Remind', 'Site'); - $data = $this->input->post->get('jform', [], 'array'); - - $return = $model->remindRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if (Factory::getApplication()->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_PRIVACY_ERROR_REMIND_REQUEST'); - } - - // Go back to the confirm form. - $this->setRedirect(Route::_('index.php?option=com_privacy&view=remind', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Confirm failed. - // Go back to the confirm form. - $message = Text::sprintf('COM_PRIVACY_ERROR_CONFIRMING_REMIND_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_privacy&view=remind', false), $message, 'notice'); - - return false; - } - else - { - // Confirm succeeded. - $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CONFIRM_REMIND_SUCCEEDED'), 'info'); - - return true; - } - } + /** + * Method to confirm the information request. + * + * @return boolean + * + * @since 3.9.0 + */ + public function confirm() + { + // Check the request token. + $this->checkToken('post'); + + /** @var ConfirmModel $model */ + $model = $this->getModel('Confirm', 'Site'); + $data = $this->input->post->get('jform', [], 'array'); + + $return = $model->confirmRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if ($this->app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_PRIVACY_ERROR_CONFIRMING_REQUEST'); + } + + // Go back to the confirm form. + $this->setRedirect(Route::_('index.php?option=com_privacy&view=confirm', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Confirm failed. + // Go back to the confirm form. + $message = Text::sprintf('COM_PRIVACY_ERROR_CONFIRMING_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_privacy&view=confirm', false), $message, 'notice'); + + return false; + } else { + // Confirm succeeded. + $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CONFIRM_REQUEST_SUCCEEDED'), 'info'); + + return true; + } + } + + /** + * Method to submit an information request. + * + * @return boolean + * + * @since 3.9.0 + */ + public function submit() + { + // Check the request token. + $this->checkToken('post'); + + /** @var RequestModel $model */ + $model = $this->getModel('Request', 'Site'); + $data = $this->input->post->get('jform', [], 'array'); + + $return = $model->createRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if ($this->app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_PRIVACY_ERROR_CREATING_REQUEST'); + } + + // Go back to the confirm form. + $this->setRedirect(Route::_('index.php?option=com_privacy&view=request', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Confirm failed. + // Go back to the confirm form. + $message = Text::sprintf('COM_PRIVACY_ERROR_CREATING_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_privacy&view=request', false), $message, 'notice'); + + return false; + } else { + // Confirm succeeded. + $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CREATE_REQUEST_SUCCEEDED'), 'info'); + + return true; + } + } + + /** + * Method to extend the privacy consent. + * + * @return boolean + * + * @since 3.9.0 + */ + public function remind() + { + // Check the request token. + $this->checkToken('post'); + + /** @var ConfirmModel $model */ + $model = $this->getModel('Remind', 'Site'); + $data = $this->input->post->get('jform', [], 'array'); + + $return = $model->remindRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if ($this->app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_PRIVACY_ERROR_REMIND_REQUEST'); + } + + // Go back to the confirm form. + $this->setRedirect(Route::_('index.php?option=com_privacy&view=remind', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Confirm failed. + // Go back to the confirm form. + $message = Text::sprintf('COM_PRIVACY_ERROR_CONFIRMING_REMIND_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_privacy&view=remind', false), $message, 'notice'); + + return false; + } else { + // Confirm succeeded. + $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CONFIRM_REMIND_SUCCEEDED'), 'info'); + + return true; + } + } } diff --git a/code/components/com_privacy/src/Model/ConfirmModel.php b/code/components/com_privacy/src/Model/ConfirmModel.php index d47cb8fc..59cdf876 100644 --- a/code/components/com_privacy/src/Model/ConfirmModel.php +++ b/code/components/com_privacy/src/Model/ConfirmModel.php @@ -1,4 +1,5 @@ getForm(); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Get the user email address - $email = Factory::getUser()->email; - - // Search for the information request - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load(['email' => $email, 'status' => 0])) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); - - return false; - } - - // A request can only be confirmed if it is in a pending status and has a confirmation token - if ($table->status != '0' || !$table->confirm_token || $table->confirm_token_created_at === null) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); - - return false; - } - - // A request can only be confirmed if the token is less than 24 hours old - $confirmTokenCreatedAt = new Date($table->confirm_token_created_at); - $confirmTokenCreatedAt->add(new \DateInterval('P1D')); - - $now = new Date('now'); - - if ($now > $confirmTokenCreatedAt) - { - // Invalidate the request - $table->status = -1; - $table->confirm_token = ''; - $table->confirm_token_created_at = null; - - try - { - $table->store(); - } - catch (ExecutionFailureException $exception) - { - // The error will be logged in the database API, we just need to catch it here to not let things fatal out - } - - $this->setError(Text::_('COM_PRIVACY_ERROR_CONFIRM_TOKEN_EXPIRED')); - - return false; - } - - // Verify the token - if (!UserHelper::verifyPassword($data['confirm_token'], $table->confirm_token)) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); - - return false; - } - - // Everything is good to go, transition the request to confirmed - $saved = $this->save( - [ - 'id' => $table->id, - 'status' => 1, - 'confirm_token' => '', - ] - ); - - if (!$saved) - { - // Error was set by the save method - return false; - } - - // Push a notification to the site's super users, deliberately ignoring if this process fails so the below message goes out - /** @var MessageModel $messageModel */ - $messageModel = Factory::getApplication()->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); - - $messageModel->notifySuperUsers( - Text::_('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CONFIRMED_REQUEST_SUBJECT'), - Text::sprintf('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CONFIRMED_REQUEST_MESSAGE', $table->email) - ); - - $message = [ - 'action' => 'request-confirmed', - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_CONFIRMED_REQUEST', 'com_privacy.request'); - - return true; - } - - /** - * Method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 3.9.0 - */ - public function getForm($data = [], $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_privacy.confirm', 'confirm', ['control' => 'jform']); - - if (empty($form)) - { - return false; - } - - $input = Factory::getApplication()->input; - - if ($input->getMethod() === 'GET') - { - $form->setValue('confirm_token', '', $input->get->getAlnum('confirm_token')); - } - - return $form; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.9.0 - * @throws \Exception - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_privacy'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + /** + * Confirms the information request. + * + * @param array $data The data expected for the form. + * + * @return mixed Exception | boolean + * + * @since 3.9.0 + */ + public function confirmRequest($data) + { + // Get the form. + $form = $this->getForm(); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Get the user email address + $email = Factory::getUser()->email; + + // Search for the information request + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load(['email' => $email, 'status' => 0])) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); + + return false; + } + + // A request can only be confirmed if it is in a pending status and has a confirmation token + if ($table->status != '0' || !$table->confirm_token || $table->confirm_token_created_at === null) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); + + return false; + } + + // A request can only be confirmed if the token is less than 24 hours old + $confirmTokenCreatedAt = new Date($table->confirm_token_created_at); + $confirmTokenCreatedAt->add(new \DateInterval('P1D')); + + $now = new Date('now'); + + if ($now > $confirmTokenCreatedAt) { + // Invalidate the request + $table->status = -1; + $table->confirm_token = ''; + $table->confirm_token_created_at = null; + + try { + $table->store(); + } catch (ExecutionFailureException $exception) { + // The error will be logged in the database API, we just need to catch it here to not let things fatal out + } + + $this->setError(Text::_('COM_PRIVACY_ERROR_CONFIRM_TOKEN_EXPIRED')); + + return false; + } + + // Verify the token + if (!UserHelper::verifyPassword($data['confirm_token'], $table->confirm_token)) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); + + return false; + } + + // Everything is good to go, transition the request to confirmed + $saved = $this->save( + [ + 'id' => $table->id, + 'status' => 1, + 'confirm_token' => '', + ] + ); + + if (!$saved) { + // Error was set by the save method + return false; + } + + // Push a notification to the site's super users, deliberately ignoring if this process fails so the below message goes out + /** @var MessageModel $messageModel */ + $messageModel = Factory::getApplication()->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); + + $messageModel->notifySuperUsers( + Text::_('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CONFIRMED_REQUEST_SUBJECT'), + Text::sprintf('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CONFIRMED_REQUEST_MESSAGE', $table->email) + ); + + $message = [ + 'action' => 'request-confirmed', + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_CONFIRMED_REQUEST', 'com_privacy.request'); + + return true; + } + + /** + * Method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 3.9.0 + */ + public function getForm($data = [], $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_privacy.confirm', 'confirm', ['control' => 'jform']); + + if (empty($form)) { + return false; + } + + $input = Factory::getApplication()->input; + + if ($input->getMethod() === 'GET') { + $form->setValue('confirm_token', '', $input->get->getAlnum('confirm_token')); + } + + return $form; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.9.0 + * @throws \Exception + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_privacy'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/code/components/com_privacy/src/Model/RemindModel.php b/code/components/com_privacy/src/Model/RemindModel.php index 94b56772..7ed5881c 100644 --- a/code/components/com_privacy/src/Model/RemindModel.php +++ b/code/components/com_privacy/src/Model/RemindModel.php @@ -1,4 +1,5 @@ getForm(); - $data['email'] = PunycodeHelper::emailToPunycode($data['email']); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - /** @var ConsentTable $table */ - $table = $this->getTable(); - - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName(['r.id', 'r.user_id', 'r.token'])); - $query->from($db->quoteName('#__privacy_consents', 'r')); - $query->join('LEFT', $db->quoteName('#__users', 'u'), - $db->quoteName('u.id') . ' = ' . $db->quoteName('r.user_id') - ); - $query->where($db->quoteName('u.email') . ' = :email') - ->bind(':email', $data['email']); - $query->where($db->quoteName('r.remind') . ' = 1'); - $db->setQuery($query); - - try - { - $remind = $db->loadObject(); - } - catch (ExecutionFailureException $e) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REMIND')); - - return false; - } - - if (!$remind) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REMIND')); - - return false; - } - - // Verify the token - if (!UserHelper::verifyPassword($data['remind_token'], $remind->token)) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_REMIND_REQUESTS')); - - return false; - } - - // Everything is good to go, transition the request to extended - $saved = $this->save( - [ - 'id' => $remind->id, - 'remind' => 0, - 'token' => '', - 'created' => Factory::getDate()->toSql(), - ] - ); - - if (!$saved) - { - // Error was set by the save method - return false; - } - - return true; - } - - /** - * Method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 3.9.0 - */ - public function getForm($data = [], $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_privacy.remind', 'remind', ['control' => 'jform']); - - if (empty($form)) - { - return false; - } - - $input = Factory::getApplication()->input; - - if ($input->getMethod() === 'GET') - { - $form->setValue('remind_token', '', $input->get->getAlnum('remind_token')); - } - - return $form; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Consent', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_privacy'); - - // Load the parameters. - $this->setState('params', $params); - } + /** + * Confirms the remind request. + * + * @param array $data The data expected for the form. + * + * @return mixed \Exception | JException | boolean + * + * @since 3.9.0 + */ + public function remindRequest($data) + { + // Get the form. + $form = $this->getForm(); + $data['email'] = PunycodeHelper::emailToPunycode($data['email']); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + /** @var ConsentTable $table */ + $table = $this->getTable(); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['r.id', 'r.user_id', 'r.token'])); + $query->from($db->quoteName('#__privacy_consents', 'r')); + $query->join( + 'LEFT', + $db->quoteName('#__users', 'u'), + $db->quoteName('u.id') . ' = ' . $db->quoteName('r.user_id') + ); + $query->where($db->quoteName('u.email') . ' = :email') + ->bind(':email', $data['email']); + $query->where($db->quoteName('r.remind') . ' = 1'); + $db->setQuery($query); + + try { + $remind = $db->loadObject(); + } catch (ExecutionFailureException $e) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REMIND')); + + return false; + } + + if (!$remind) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REMIND')); + + return false; + } + + // Verify the token + if (!UserHelper::verifyPassword($data['remind_token'], $remind->token)) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_REMIND_REQUESTS')); + + return false; + } + + // Everything is good to go, transition the request to extended + $saved = $this->save( + [ + 'id' => $remind->id, + 'remind' => 0, + 'token' => '', + 'created' => Factory::getDate()->toSql(), + ] + ); + + if (!$saved) { + // Error was set by the save method + return false; + } + + return true; + } + + /** + * Method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 3.9.0 + */ + public function getForm($data = [], $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_privacy.remind', 'remind', ['control' => 'jform']); + + if (empty($form)) { + return false; + } + + $input = Factory::getApplication()->input; + + if ($input->getMethod() === 'GET') { + $form->setValue('remind_token', '', $input->get->getAlnum('remind_token')); + } + + return $form; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Consent', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_privacy'); + + // Load the parameters. + $this->setState('params', $params); + } } diff --git a/code/components/com_privacy/src/Model/RequestModel.php b/code/components/com_privacy/src/Model/RequestModel.php index 8048f1fc..e42117a8 100644 --- a/code/components/com_privacy/src/Model/RequestModel.php +++ b/code/components/com_privacy/src/Model/RequestModel.php @@ -1,4 +1,5 @@ get('mailonline', 1)) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED')); - - return false; - } - - // Get the form. - $form = $this->getForm(); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - $data['email'] = Factory::getUser()->email; - - // Search for an open information request matching the email and type - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('COUNT(id)') - ->from($db->quoteName('#__privacy_requests')) - ->where($db->quoteName('email') . ' = :email') - ->where($db->quoteName('request_type') . ' = :requesttype') - ->whereIn($db->quoteName('status'), [0, 1]) - ->bind(':email', $data['email']) - ->bind(':requesttype', $data['request_type']); - - try - { - $result = (int) $db->setQuery($query)->loadResult(); - } - catch (ExecutionFailureException $exception) - { - // Can't check for existing requests, so don't create a new one - $this->setError(Text::_('COM_PRIVACY_ERROR_CHECKING_FOR_EXISTING_REQUESTS')); - - return false; - } - - if ($result > 0) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_PENDING_REQUEST_OPEN')); - - return false; - } - - // Everything is good to go, create the request - $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $hashedToken = UserHelper::hashPassword($token); - - $data['confirm_token'] = $hashedToken; - $data['confirm_token_created_at'] = Factory::getDate()->toSql(); - - if (!$this->save($data)) - { - // The save function will set the error message, so just return here - return false; - } - - // Push a notification to the site's super users, deliberately ignoring if this process fails so the below message goes out - /** @var MessageModel $messageModel */ - $messageModel = $app->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); - - $messageModel->notifySuperUsers( - Text::_('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CREATED_REQUEST_SUBJECT'), - Text::sprintf('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CREATED_REQUEST_MESSAGE', $data['email']) - ); - - // The mailer can be set to either throw Exceptions or return boolean false, account for both - try - { - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'url' => Uri::root(), - 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm&confirm_token=' . $token, false, $linkMode, true), - 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm', false, $linkMode, true), - 'token' => $token, - ]; - - switch ($data['request_type']) - { - case 'export': - $mailer = new MailTemplate('com_privacy.notification.export', $app->getLanguage()->getTag()); - - break; - - case 'remove': - $mailer = new MailTemplate('com_privacy.notification.remove', $app->getLanguage()->getTag()); - - break; - - default: - $this->setError(Text::_('COM_PRIVACY_ERROR_UNKNOWN_REQUEST_TYPE')); - - return false; - } - - $mailer->addTemplateData($templateData); - $mailer->addRecipient($data['email']); - - $mailer->send(); - - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($this->getState($this->getName() . '.id'))) - { - $this->setError($table->getError()); - - return false; - } - - // Log the request's creation - $message = [ - 'action' => 'request-created', - 'requesttype' => $table->request_type, - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_CREATED_REQUEST', 'com_privacy.request'); - - // The email sent and the record is saved, everything is good to go from here - return true; - } - catch (MailDisabledException | phpmailerException $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - } - - /** - * Method for getting the form from the model. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 3.9.0 - */ - public function getForm($data = [], $loadData = true) - { - return $this->loadForm('com_privacy.request', 'request', ['control' => 'jform']); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_privacy'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + /** + * Creates an information request. + * + * @param array $data The data expected for the form. + * + * @return mixed Exception | boolean + * + * @since 3.9.0 + */ + public function createRequest($data) + { + $app = Factory::getApplication(); + + // Creating requests requires the site's email sending be enabled + if (!$app->get('mailonline', 1)) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED')); + + return false; + } + + // Get the form. + $form = $this->getForm(); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + $data['email'] = Factory::getUser()->email; + + // Search for an open information request matching the email and type + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(id)') + ->from($db->quoteName('#__privacy_requests')) + ->where($db->quoteName('email') . ' = :email') + ->where($db->quoteName('request_type') . ' = :requesttype') + ->whereIn($db->quoteName('status'), [0, 1]) + ->bind(':email', $data['email']) + ->bind(':requesttype', $data['request_type']); + + try { + $result = (int) $db->setQuery($query)->loadResult(); + } catch (ExecutionFailureException $exception) { + // Can't check for existing requests, so don't create a new one + $this->setError(Text::_('COM_PRIVACY_ERROR_CHECKING_FOR_EXISTING_REQUESTS')); + + return false; + } + + if ($result > 0) { + $this->setError(Text::_('COM_PRIVACY_ERROR_PENDING_REQUEST_OPEN')); + + return false; + } + + // Everything is good to go, create the request + $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $hashedToken = UserHelper::hashPassword($token); + + $data['confirm_token'] = $hashedToken; + $data['confirm_token_created_at'] = Factory::getDate()->toSql(); + + if (!$this->save($data)) { + // The save function will set the error message, so just return here + return false; + } + + // Push a notification to the site's super users, deliberately ignoring if this process fails so the below message goes out + /** @var MessageModel $messageModel */ + $messageModel = $app->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); + + $messageModel->notifySuperUsers( + Text::_('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CREATED_REQUEST_SUBJECT'), + Text::sprintf('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CREATED_REQUEST_MESSAGE', $data['email']) + ); + + // The mailer can be set to either throw Exceptions or return boolean false, account for both + try { + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'url' => Uri::root(), + 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm&confirm_token=' . $token, false, $linkMode, true), + 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm', false, $linkMode, true), + 'token' => $token, + ]; + + switch ($data['request_type']) { + case 'export': + $mailer = new MailTemplate('com_privacy.notification.export', $app->getLanguage()->getTag()); + + break; + + case 'remove': + $mailer = new MailTemplate('com_privacy.notification.remove', $app->getLanguage()->getTag()); + + break; + + default: + $this->setError(Text::_('COM_PRIVACY_ERROR_UNKNOWN_REQUEST_TYPE')); + + return false; + } + + $mailer->addTemplateData($templateData); + $mailer->addRecipient($data['email']); + + $mailer->send(); + + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($this->getState($this->getName() . '.id'))) { + $this->setError($table->getError()); + + return false; + } + + // Log the request's creation + $message = [ + 'action' => 'request-created', + 'requesttype' => $table->request_type, + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_CREATED_REQUEST', 'com_privacy.request'); + + // The email sent and the record is saved, everything is good to go from here + return true; + } catch (MailDisabledException | phpmailerException $exception) { + $this->setError($exception->getMessage()); + + return false; + } + } + + /** + * Method for getting the form from the model. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 3.9.0 + */ + public function getForm($data = [], $loadData = true) + { + return $this->loadForm('com_privacy.request', 'request', ['control' => 'jform']); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_privacy'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/code/components/com_privacy/src/Service/Router.php b/code/components/com_privacy/src/Service/Router.php index 6b6a7ddb..0e4243b9 100644 --- a/code/components/com_privacy/src/Service/Router.php +++ b/code/components/com_privacy/src/Service/Router.php @@ -1,4 +1,5 @@ registerView(new RouterViewConfiguration('confirm')); - $this->registerView(new RouterViewConfiguration('request')); - $this->registerView(new RouterViewConfiguration('remind')); + /** + * Privacy Component router constructor + * + * @param CMSApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * + * @since 3.9.0 + */ + public function __construct($app = null, $menu = null) + { + $this->registerView(new RouterViewConfiguration('confirm')); + $this->registerView(new RouterViewConfiguration('request')); + $this->registerView(new RouterViewConfiguration('remind')); - parent::__construct($app, $menu); + parent::__construct($app, $menu); - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } } diff --git a/code/components/com_privacy/src/View/Confirm/HtmlView.php b/code/components/com_privacy/src/View/Confirm/HtmlView.php index 1d3fc2fe..636d8e33 100644 --- a/code/components/com_privacy/src/View/Confirm/HtmlView.php +++ b/code/components/com_privacy/src/View/Confirm/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->params; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 3.9.0 - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_CONFIRM_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The form object + * + * @var Form + * @since 3.9.0 + */ + protected $form; + + /** + * The CSS class suffix to append to the view container + * + * @var string + * @since 3.9.0 + */ + protected $pageclass_sfx; + + /** + * The view parameters + * + * @var Registry + * @since 3.9.0 + */ + protected $params; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise variables. + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->params; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 3.9.0 + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_CONFIRM_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/code/components/com_privacy/src/View/Remind/HtmlView.php b/code/components/com_privacy/src/View/Remind/HtmlView.php index 2664844f..9428da82 100644 --- a/code/components/com_privacy/src/View/Remind/HtmlView.php +++ b/code/components/com_privacy/src/View/Remind/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->params; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 3.9.0 - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_REMIND_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The form object + * + * @var Form + * @since 3.9.0 + */ + protected $form; + + /** + * The CSS class suffix to append to the view container + * + * @var string + * @since 3.9.0 + */ + protected $pageclass_sfx; + + /** + * The view parameters + * + * @var Registry + * @since 3.9.0 + */ + protected $params; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise variables. + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->params; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 3.9.0 + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_REMIND_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/code/components/com_privacy/src/View/Request/HtmlView.php b/code/components/com_privacy/src/View/Request/HtmlView.php index 23c97e2a..ade63031 100644 --- a/code/components/com_privacy/src/View/Request/HtmlView.php +++ b/code/components/com_privacy/src/View/Request/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->params; - $this->sendMailEnabled = (bool) Factory::getApplication()->get('mailonline', 1); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 3.9.0 - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_REQUEST_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The form object + * + * @var Form + * @since 3.9.0 + */ + protected $form; + + /** + * The CSS class suffix to append to the view container + * + * @var string + * @since 3.9.0 + */ + protected $pageclass_sfx; + + /** + * The view parameters + * + * @var Registry + * @since 3.9.0 + */ + protected $params; + + /** + * Flag indicating the site supports sending email + * + * @var boolean + * @since 3.9.0 + */ + protected $sendMailEnabled; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise variables. + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->params; + $this->sendMailEnabled = (bool) Factory::getApplication()->get('mailonline', 1); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 3.9.0 + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_REQUEST_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/code/components/com_privacy/tmpl/confirm/default.php b/code/components/com_privacy/tmpl/confirm/default.php index 2f80026c..7a74c0c8 100644 --- a/code/components/com_privacy/tmpl/confirm/default.php +++ b/code/components/com_privacy/tmpl/confirm/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/code/components/com_privacy/tmpl/remind/default.php b/code/components/com_privacy/tmpl/remind/default.php index aa311d36..6e501f6f 100644 --- a/code/components/com_privacy/tmpl/remind/default.php +++ b/code/components/com_privacy/tmpl/remind/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/code/components/com_privacy/tmpl/request/default.php b/code/components/com_privacy/tmpl/request/default.php index 44afcbb8..d203a87d 100644 --- a/code/components/com_privacy/tmpl/request/default.php +++ b/code/components/com_privacy/tmpl/request/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - - sendMailEnabled) : ?> -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    - -
    - - -
    - + params->get('show_page_heading')) : ?> + + + sendMailEnabled) : ?> +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    + +
    + + +
    +
    diff --git a/code/components/com_tags/helpers/route.php b/code/components/com_tags/helpers/route.php index 55cb605e..030b3b18 100644 --- a/code/components/com_tags/helpers/route.php +++ b/code/components/com_tags/helpers/route.php @@ -1,16 +1,21 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\Component\Tags\Site\Helper\RouteHelper; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Tags Component Route Helper. * diff --git a/code/components/com_tags/src/Controller/DisplayController.php b/code/components/com_tags/src/Controller/DisplayController.php index 562624c2..8f314a36 100644 --- a/code/components/com_tags/src/Controller/DisplayController.php +++ b/code/components/com_tags/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getIdentity(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'tags'); - $this->input->set('view', $vName); - - if ($user->get('id') || ($this->input->getMethod() === 'POST' && $vName === 'tags')) - { - $cachable = false; - } - - $safeurlparams = array( - 'id' => 'ARRAY', - 'type' => 'ARRAY', - 'limit' => 'UINT', - 'limitstart' => 'UINT', - 'filter_order' => 'CMD', - 'filter_order_Dir' => 'CMD', - 'lang' => 'CMD' - ); - - return parent::display($cachable, $safeurlparams); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param mixed|boolean $urlparams An array of safe URL parameters and their + * variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 3.1 + */ + public function display($cachable = false, $urlparams = false) + { + $user = $this->app->getIdentity(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'tags'); + $this->input->set('view', $vName); + + if ($user->get('id') || ($this->input->getMethod() === 'POST' && $vName === 'tags')) { + $cachable = false; + } + + $safeurlparams = array( + 'id' => 'ARRAY', + 'type' => 'ARRAY', + 'limit' => 'UINT', + 'limitstart' => 'UINT', + 'filter_order' => 'CMD', + 'filter_order_Dir' => 'CMD', + 'lang' => 'CMD' + ); + + return parent::display($cachable, $safeurlparams); + } } diff --git a/code/components/com_tags/src/Controller/TagsController.php b/code/components/com_tags/src/Controller/TagsController.php index 293227bc..2e5f9763 100644 --- a/code/components/com_tags/src/Controller/TagsController.php +++ b/code/components/com_tags/src/Controller/TagsController.php @@ -1,4 +1,5 @@ app->getIdentity(); - - // Receive request data - $filters = array( - 'like' => trim($this->input->get('like', null, 'string')), - 'title' => trim($this->input->get('title', null, 'string')), - 'flanguage' => $this->input->get('flanguage', null, 'word'), - 'published' => $this->input->get('published', 1, 'int'), - 'parent_id' => $this->input->get('parent_id', 0, 'int'), - 'access' => $user->getAuthorisedViewLevels(), - ); - - if ((!$user->authorise('core.edit.state', 'com_tags')) && (!$user->authorise('core.edit', 'com_tags'))) - { - // Filter on published for those who do not have edit or edit.state rights. - $filters['published'] = 1; - } - - $results = TagsHelper::searchTags($filters); - - if ($results) - { - // Output a JSON object - echo json_encode($results); - } - - $this->app->close(); - } + /** + * Method to search tags with AJAX + * + * @return void + */ + public function searchAjax() + { + $user = $this->app->getIdentity(); + + // Receive request data + $filters = array( + 'like' => trim($this->input->get('like', null, 'string')), + 'title' => trim($this->input->get('title', null, 'string')), + 'flanguage' => $this->input->get('flanguage', null, 'word'), + 'published' => $this->input->get('published', 1, 'int'), + 'parent_id' => $this->input->get('parent_id', 0, 'int'), + 'access' => $user->getAuthorisedViewLevels(), + ); + + if ((!$user->authorise('core.edit.state', 'com_tags')) && (!$user->authorise('core.edit', 'com_tags'))) { + // Filter on published for those who do not have edit or edit.state rights. + $filters['published'] = 1; + } + + $results = TagsHelper::searchTags($filters); + + if ($results) { + // Output a JSON object + echo json_encode($results); + } + + $this->app->close(); + } } diff --git a/code/components/com_tags/src/Helper/RouteHelper.php b/code/components/com_tags/src/Helper/RouteHelper.php index 981ac0f0..0a130fb8 100644 --- a/code/components/com_tags/src/Helper/RouteHelper.php +++ b/code/components/com_tags/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ getRoute($contentItemId, $typeAlias, $link, $language, $contentCatId); - } - - return $link; - } - - /** - * Tries to load the router for the component and calls it. Otherwise calls getRoute. - * - * @param integer $id The ID of the tag - * - * @return string URL link to pass to the router - * - * @since 3.1 - */ - public static function getTagRoute($id) - { - $needles = array( - 'tag' => array((int) $id) - ); - - if ($id < 1) - { - $link = ''; - } - else - { - $link = 'index.php?option=com_tags&view=tag&id=' . $id; - - if ($item = self::_findItem($needles)) - { - $link .= '&Itemid=' . $item; - } - else - { - $needles = array('tags' => array(1, 0)); - - if ($item = self::_findItem($needles)) - { - $link .= '&Itemid=' . $item; - } - } - } - - return $link; - } - - /** - * Tries to load the router for the tags view. - * - * @return string URL link to pass to the router - * - * @since 3.7 - */ - public static function getTagsRoute() - { - $needles = array( - 'tags' => array(0) - ); - - $link = 'index.php?option=com_tags&view=tags'; - - if ($item = self::_findItem($needles)) - { - $link .= '&Itemid=' . $item; - } - - return $link; - } - - /** - * Find Item static function - * - * @param array $needles Array used to get the language value - * - * @return null - * - * @throws \Exception - */ - protected static function _findItem($needles = null) - { - $menus = AbstractMenu::getInstance('site'); - $language = $needles['language'] ?? '*'; - - // Prepare the reverse lookup array. - if (self::$lookup === null) - { - self::$lookup = array(); - - $component = ComponentHelper::getComponent('com_tags'); - $items = $menus->getItems('component_id', $component->id); - - if ($items) - { - foreach ($items as $item) - { - if (isset($item->query, $item->query['view'])) - { - $lang = ($item->language != '' ? $item->language : '*'); - - if (!isset(self::$lookup[$lang])) - { - self::$lookup[$lang] = array(); - } - - $view = $item->query['view']; - - if (!isset(self::$lookup[$lang][$view])) - { - self::$lookup[$lang][$view] = array(); - } - - // Only match menu items that list one tag - if (isset($item->query['id']) && is_array($item->query['id'])) - { - foreach ($item->query['id'] as $position => $tagId) - { - if (!isset(self::$lookup[$lang][$view][$item->query['id'][$position]]) || count($item->query['id']) == 1) - { - self::$lookup[$lang][$view][$item->query['id'][$position]] = $item->id; - } - } - } - elseif ($view == 'tags') - { - self::$lookup[$lang]['tags'][] = $item->id; - } - } - } - } - } - - if ($needles) - { - foreach ($needles as $view => $ids) - { - if (isset(self::$lookup[$language][$view])) - { - foreach ($ids as $id) - { - if (isset(self::$lookup[$language][$view][(int) $id])) - { - return self::$lookup[$language][$view][(int) $id]; - } - } - } - } - } - else - { - $active = $menus->getActive(); - - if ($active) - { - return $active->id; - } - } - - return null; - } + /** + * Lookup-table for menu items + * + * @var array + */ + protected static $lookup; + + /** + * Tries to load the router for the component and calls it. Otherwise uses getTagRoute. + * + * @param integer $contentItemId Component item id + * @param string $contentItemAlias Component item alias + * @param integer $contentCatId Component item category id + * @param string $language Component item language + * @param string $typeAlias Component type alias + * @param string $routerName Component router + * + * @return string URL link to pass to the router + * + * @since 3.1 + */ + public static function getItemRoute($contentItemId, $contentItemAlias, $contentCatId, $language, $typeAlias, $routerName) + { + $link = ''; + $explodedAlias = explode('.', $typeAlias); + $explodedRouter = explode('::', $routerName); + + if (file_exists($routerFile = JPATH_BASE . '/components/' . $explodedAlias[0] . '/helpers/route.php')) { + \JLoader::register($explodedRouter[0], $routerFile); + $routerClass = $explodedRouter[0]; + $routerMethod = $explodedRouter[1]; + + if (class_exists($routerClass) && method_exists($routerClass, $routerMethod)) { + if ($routerMethod === 'getCategoryRoute') { + $link = $routerClass::$routerMethod($contentItemId, $language); + } else { + $link = $routerClass::$routerMethod($contentItemId . ':' . $contentItemAlias, $contentCatId, $language); + } + } + } + + if ($link === '') { + // Create a fallback link in case we can't find the component router + $router = new CMSRouteHelper(); + $link = $router->getRoute($contentItemId, $typeAlias, $link, $language, $contentCatId); + } + + return $link; + } + + /** + * Tries to load the router for the component and calls it. Otherwise calls getRoute. + * + * @param integer $id The ID of the tag + * + * @return string URL link to pass to the router + * + * @since 3.1 + * @throws Exception + * @deprecated 5.0.0 Use getComponentTagRoute() instead + */ + public static function getTagRoute($id) + { + @trigger_error('This function is replaced by the getComponentTagRoute()', E_USER_DEPRECATED); + + return self::getComponentTagRoute($id); + } + + /** + * Tries to load the router for the component and calls it. Otherwise calls getRoute. + * + * @param string $id The ID of the tag in the format TAG_ID:TAG_ALIAS + * @param string $language The language of the tag + * + * @return string URL link to pass to the router + * + * @since 4.2.0 + * @throws Exception + */ + public static function getComponentTagRoute(string $id, string $language = '*'): string + { + $needles = [ + 'tag' => [(int) $id], + 'language' => $language, + ]; + + if ($id < 1) { + $link = ''; + } else { + $link = 'index.php?option=com_tags&view=tag&id=' . $id; + + if ($item = self::_findItem($needles)) { + $link .= '&Itemid=' . $item; + } else { + $needles = [ + 'tags' => [1, 0], + 'language' => $language, + ]; + + if ($item = self::_findItem($needles)) { + $link .= '&Itemid=' . $item; + } + } + } + + return $link; + } + + /** + * Tries to load the router for the tags view. + * + * @return string URL link to pass to the router + * + * @since 3.7 + * @throws Exception + * @deprecated 5.0.0 + */ + public static function getTagsRoute() + { + @trigger_error('This function is replaced by the getComponentTagsRoute()', E_USER_DEPRECATED); + + return self::getComponentTagsRoute(); + } + + /** + * Tries to load the router for the tags view. + * + * @param string $language The language of the tag + * + * @return string URL link to pass to the router + * + * @since 4.2.0 + * @throws Exception + */ + public static function getComponentTagsRoute(string $language = '*'): string + { + $needles = [ + 'tags' => [0], + 'language' => $language, + ]; + + $link = 'index.php?option=com_tags&view=tags'; + + if ($item = self::_findItem($needles)) { + $link .= '&Itemid=' . $item; + } + + return $link; + } + + /** + * Find Item static function + * + * @param array $needles Array used to get the language value + * + * @return null + * + * @throws Exception + */ + protected static function _findItem($needles = null) + { + $menus = AbstractMenu::getInstance('site'); + $language = $needles['language'] ?? '*'; + + // Prepare the reverse lookup array. + if (self::$lookup === null) { + self::$lookup = array(); + + $component = ComponentHelper::getComponent('com_tags'); + $items = $menus->getItems('component_id', $component->id); + + if ($items) { + foreach ($items as $item) { + if (isset($item->query, $item->query['view'])) { + $lang = ($item->language != '' ? $item->language : '*'); + + if (!isset(self::$lookup[$lang])) { + self::$lookup[$lang] = array(); + } + + $view = $item->query['view']; + + if (!isset(self::$lookup[$lang][$view])) { + self::$lookup[$lang][$view] = array(); + } + + // Only match menu items that list one tag + if (isset($item->query['id']) && is_array($item->query['id'])) { + foreach ($item->query['id'] as $position => $tagId) { + if (!isset(self::$lookup[$lang][$view][$item->query['id'][$position]]) || count($item->query['id']) == 1) { + self::$lookup[$lang][$view][$item->query['id'][$position]] = $item->id; + } + } + } elseif ($view == 'tags') { + self::$lookup[$lang]['tags'][] = $item->id; + } + } + } + } + } + + if ($needles) { + foreach ($needles as $view => $ids) { + if (isset(self::$lookup[$language][$view])) { + foreach ($ids as $id) { + if (isset(self::$lookup[$language][$view][(int) $id])) { + return self::$lookup[$language][$view][(int) $id]; + } + } + } + } + } else { + $active = $menus->getActive(); + + if ($active) { + return $active->id; + } + } + + return null; + } } diff --git a/code/components/com_tags/src/Model/TagModel.php b/code/components/com_tags/src/Model/TagModel.php index bf5f43a9..37ee4a06 100644 --- a/code/components/com_tags/src/Model/TagModel.php +++ b/code/components/com_tags/src/Model/TagModel.php @@ -1,4 +1,5 @@ link = RouteHelper::getItemRoute( - $item->content_item_id, - $item->core_alias, - $item->core_catid, - $item->core_language, - $item->type_alias, - $item->router - ); - - // Get display date - switch ($this->state->params->get('tag_list_show_date')) - { - case 'modified': - $item->displayDate = $item->core_modified_time; - break; - - case 'created': - $item->displayDate = $item->core_created_time; - break; - - default: - $item->displayDate = ($item->core_publish_up == 0) ? $item->core_created_time : $item->core_publish_up; - break; - } - } - } - - return $items; - } - - /** - * Method to build an SQL query to load the list data of all items with a given tag. - * - * @return string An SQL query - * - * @since 3.1 - */ - protected function getListQuery() - { - $tagId = $this->getState('tag.id') ? : ''; - - $typesr = $this->getState('tag.typesr'); - $orderByOption = $this->getState('list.ordering', 'c.core_title'); - $includeChildren = $this->state->params->get('include_children', 0); - $orderDir = $this->getState('list.direction', 'ASC'); - $matchAll = $this->getState('params')->get('return_any_or_all', 1); - $language = $this->getState('tag.language'); - $stateFilter = $this->getState('tag.state'); - - // Optionally filter on language - if (empty($language)) - { - $language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all'); - } - - $query = (new TagsHelper)->getTagItemsQuery($tagId, $typesr, $includeChildren, $orderByOption, $orderDir, $matchAll, $language, $stateFilter); - - if ($this->state->get('list.filter')) - { - $query->where($this->_db->quoteName('c.core_title') . ' LIKE ' . $this->_db->quote('%' . $this->state->get('list.filter') . '%')); - } - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.1 - */ - protected function populateState($ordering = 'c.core_title', $direction = 'ASC') - { - $app = Factory::getApplication(); - - // Load the parameters. - $params = $app->isClient('administrator') ? ComponentHelper::getParams('com_tags') : $app->getParams(); - - $this->setState('params', $params); - - // Load state from the request. - $ids = (array) $app->input->get('id', array(), 'string'); - - if (count($ids) == 1) - { - $ids = explode(',', $ids[0]); - } - - $ids = ArrayHelper::toInteger($ids); - - // Remove zero values resulting from bad input - $ids = array_filter($ids); - - $pkString = implode(',', $ids); - - $this->setState('tag.id', $pkString); - - // Get the selected list of types from the request. If none are specified all are used. - $typesr = $app->input->get('types', array(), 'array'); - - if ($typesr) - { - // Implode is needed because the array can contain a string with a coma separated list of ids - $typesr = implode(',', $typesr); - - // Sanitise - $typesr = explode(',', $typesr); - $typesr = ArrayHelper::toInteger($typesr); - - $this->setState('tag.typesr', $typesr); - } - - $language = $app->input->getString('tag_list_language_filter'); - $this->setState('tag.language', $language); - - // List state information - $format = $app->input->getWord('format'); - - if ($format === 'feed') - { - $limit = $app->get('feed_limit'); - } - else - { - $limit = $params->get('display_num', $app->get('list_limit', 20)); - $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $limit, 'uint'); - } - - $this->setState('list.limit', $limit); - - $offset = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $offset); - - $itemid = $pkString . ':' . $app->input->get('Itemid', 0, 'int'); - $orderCol = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); - $orderCol = !$orderCol ? $this->state->params->get('tag_list_orderby', 'c.core_title') : $orderCol; - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'c.core_title'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_order_direction', 'filter_order_Dir', '', 'string'); - $listOrder = !$listOrder ? $this->state->params->get('tag_list_orderby_direction', 'ASC') : $listOrder; - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $this->setState('tag.state', 1); - - // Optional filter text - $filterSearch = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_search', 'filter-search', '', 'string'); - $this->setState('list.filter', $filterSearch); - } - - /** - * Method to get tag data for the current tag or tags - * - * @param integer $pk An optional ID - * - * @return array - * - * @since 3.1 - */ - public function getItem($pk = null) - { - if (!isset($this->item)) - { - $this->item = []; - - if (empty($pk)) - { - $pk = $this->getState('tag.id'); - } - - // Get a level row instance. - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - $table = $this->getTable(); - - $idsArray = explode(',', $pk); - - // Attempt to load the rows into an array. - foreach ($idsArray as $id) - { - try - { - $table->load($id); - - // Check published state. - if ($published = $this->getState('tag.state')) - { - if ($table->published != $published) - { - continue; - } - } - - if (!in_array($table->access, Factory::getUser()->getAuthorisedViewLevels())) - { - continue; - } - - // Convert the Table to a clean CMSObject. - $properties = $table->getProperties(1); - $this->item[] = ArrayHelper::toObject($properties, CMSObject::class); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - } - - if (!$this->item) - { - throw new \Exception(Text::_('COM_TAGS_TAG_NOT_FOUND'), 404); - } - - return $this->item; - } - - /** - * Increment the hit counter. - * - * @param integer $pk Optional primary key of the article to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - * - * @since 3.2 - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('tag.id'); - - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - $table = $this->getTable(); - $table->hit($pk); - - // Load the table data for later - $table->load($pk); - - if (!$table->hasPrimaryKey()) - { - throw new \Exception(Text::_('COM_TAGS_TAG_NOT_FOUND'), 404); - } - } - - return true; - } + /** + * The tags that apply. + * + * @var object + * @since 3.1 + */ + protected $tag = null; + + /** + * The list of items associated with the tags. + * + * @var array + * @since 3.1 + */ + protected $items = null; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'core_content_id', 'c.core_content_id', + 'core_title', 'c.core_title', + 'core_type_alias', 'c.core_type_alias', + 'core_checked_out_user_id', 'c.core_checked_out_user_id', + 'core_checked_out_time', 'c.core_checked_out_time', + 'core_catid', 'c.core_catid', + 'core_state', 'c.core_state', + 'core_access', 'c.core_access', + 'core_created_user_id', 'c.core_created_user_id', + 'core_created_time', 'c.core_created_time', + 'core_modified_time', 'c.core_modified_time', + 'core_ordering', 'c.core_ordering', + 'core_featured', 'c.core_featured', + 'core_language', 'c.core_language', + 'core_hits', 'c.core_hits', + 'core_publish_up', 'c.core_publish_up', + 'core_publish_down', 'c.core_publish_down', + 'core_images', 'c.core_images', + 'core_urls', 'c.core_urls', + 'match_count', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to get a list of items for a list of tags. + * + * @return mixed An array of objects on success, false on failure. + * + * @since 3.1 + */ + public function getItems() + { + // Invoke the parent getItems method to get the main list + $items = parent::getItems(); + + if (!empty($items)) { + foreach ($items as $item) { + $item->link = RouteHelper::getItemRoute( + $item->content_item_id, + $item->core_alias, + $item->core_catid, + $item->core_language, + $item->type_alias, + $item->router + ); + + // Get display date + switch ($this->state->params->get('tag_list_show_date')) { + case 'modified': + $item->displayDate = $item->core_modified_time; + break; + + case 'created': + $item->displayDate = $item->core_created_time; + break; + + default: + $item->displayDate = ($item->core_publish_up == 0) ? $item->core_created_time : $item->core_publish_up; + break; + } + } + } + + return $items; + } + + /** + * Method to build an SQL query to load the list data of all items with a given tag. + * + * @return string An SQL query + * + * @since 3.1 + */ + protected function getListQuery() + { + $tagId = $this->getState('tag.id') ? : ''; + + $typesr = $this->getState('tag.typesr'); + $orderByOption = $this->getState('list.ordering', 'c.core_title'); + $includeChildren = $this->state->params->get('include_children', 0); + $orderDir = $this->getState('list.direction', 'ASC'); + $matchAll = $this->getState('params')->get('return_any_or_all', 1); + $language = $this->getState('tag.language'); + $stateFilter = $this->getState('tag.state'); + + // Optionally filter on language + if (empty($language)) { + $language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all'); + } + + $query = (new TagsHelper())->getTagItemsQuery($tagId, $typesr, $includeChildren, $orderByOption, $orderDir, $matchAll, $language, $stateFilter); + + if ($this->state->get('list.filter')) { + $db = $this->getDatabase(); + $query->where($db->quoteName('c.core_title') . ' LIKE ' . $db->quote('%' . $this->state->get('list.filter') . '%')); + } + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.1 + */ + protected function populateState($ordering = 'c.core_title', $direction = 'ASC') + { + $app = Factory::getApplication(); + + // Load the parameters. + $params = $app->isClient('administrator') ? ComponentHelper::getParams('com_tags') : $app->getParams(); + + $this->setState('params', $params); + + // Load state from the request. + $ids = (array) $app->input->get('id', array(), 'string'); + + if (count($ids) == 1) { + $ids = explode(',', $ids[0]); + } + + $ids = ArrayHelper::toInteger($ids); + + // Remove zero values resulting from bad input + $ids = array_filter($ids); + + $pkString = implode(',', $ids); + + $this->setState('tag.id', $pkString); + + // Get the selected list of types from the request. If none are specified all are used. + $typesr = $app->input->get('types', array(), 'array'); + + if ($typesr) { + // Implode is needed because the array can contain a string with a coma separated list of ids + $typesr = implode(',', $typesr); + + // Sanitise + $typesr = explode(',', $typesr); + $typesr = ArrayHelper::toInteger($typesr); + + $this->setState('tag.typesr', $typesr); + } + + $language = $app->input->getString('tag_list_language_filter'); + $this->setState('tag.language', $language); + + // List state information + $format = $app->input->getWord('format'); + + if ($format === 'feed') { + $limit = $app->get('feed_limit'); + } else { + $limit = $params->get('display_num', $app->get('list_limit', 20)); + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $limit, 'uint'); + } + + $this->setState('list.limit', $limit); + + $offset = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $offset); + + $itemid = $pkString . ':' . $app->input->get('Itemid', 0, 'int'); + $orderCol = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); + $orderCol = !$orderCol ? $this->state->params->get('tag_list_orderby', 'c.core_title') : $orderCol; + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'c.core_title'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_order_direction', 'filter_order_Dir', '', 'string'); + $listOrder = !$listOrder ? $this->state->params->get('tag_list_orderby_direction', 'ASC') : $listOrder; + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $this->setState('tag.state', 1); + + // Optional filter text + $filterSearch = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_search', 'filter-search', '', 'string'); + $this->setState('list.filter', $filterSearch); + } + + /** + * Method to get tag data for the current tag or tags + * + * @param integer $pk An optional ID + * + * @return array + * + * @since 3.1 + */ + public function getItem($pk = null) + { + if (!isset($this->item)) { + $this->item = []; + + if (empty($pk)) { + $pk = $this->getState('tag.id'); + } + + // Get a level row instance. + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + $table = $this->getTable(); + + $idsArray = explode(',', $pk); + + // Attempt to load the rows into an array. + foreach ($idsArray as $id) { + try { + $table->load($id); + + // Check published state. + if ($published = $this->getState('tag.state')) { + if ($table->published != $published) { + continue; + } + } + + if (!in_array($table->access, Factory::getUser()->getAuthorisedViewLevels())) { + continue; + } + + // Convert the Table to a clean CMSObject. + $properties = $table->getProperties(1); + $this->item[] = ArrayHelper::toObject($properties, CMSObject::class); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + } + + if (!$this->item) { + throw new \Exception(Text::_('COM_TAGS_TAG_NOT_FOUND'), 404); + } + + return $this->item; + } + + /** + * Increment the hit counter. + * + * @param integer $pk Optional primary key of the article to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + * + * @since 3.2 + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('tag.id'); + + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + $table = $this->getTable(); + $table->hit($pk); + + // Load the table data for later + $table->load($pk); + + if (!$table->hasPrimaryKey()) { + throw new \Exception(Text::_('COM_TAGS_TAG_NOT_FOUND'), 404); + } + } + + return true; + } } diff --git a/code/components/com_tags/src/Model/TagsModel.php b/code/components/com_tags/src/Model/TagsModel.php index e0929d71..734ce591 100644 --- a/code/components/com_tags/src/Model/TagsModel.php +++ b/code/components/com_tags/src/Model/TagsModel.php @@ -1,4 +1,5 @@ input->getInt('parent_id'); - $this->setState('tag.parent_id', $pid); - - $language = $app->input->getString('tag_list_language_filter'); - $this->setState('tag.language', $language); - - $offset = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.offset', $offset); - $app = Factory::getApplication(); - - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('list.limit', $params->get('maximum', 200)); - - $this->setState('filter.published', 1); - $this->setState('filter.access', true); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_tags')) && (!$user->authorise('core.edit', 'com_tags'))) - { - $this->setState('filter.published', 1); - } - - // Optional filter text - $itemid = $pid . ':' . $app->input->getInt('Itemid', 0); - $filterSearch = $app->getUserStateFromRequest('com_tags.tags.list.' . $itemid . '.filter_search', 'filter-search', '', 'string'); - $this->setState('list.filter', $filterSearch); - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - $app = Factory::getApplication(); - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - $pid = (int) $this->getState('tag.parent_id'); - $orderby = $this->state->params->get('all_tags_orderby', 'title'); - $published = (int) $this->state->params->get('published', 1); - $orderDirection = $this->state->params->get('all_tags_orderby_direction', 'ASC'); - $language = $this->getState('tag.language'); - - // Create a new query object. - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Select required fields from the tags. - $query->select('a.*, u.name as created_by_user_name, u.email') - ->from($db->quoteName('#__tags', 'a')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('a.created_user_id') . ' = ' . $db->quoteName('u.id')) - ->whereIn($db->quoteName('a.access'), $groups); - - if (!empty($pid)) - { - $query->where($db->quoteName('a.parent_id') . ' = :pid') - ->bind(':pid', $pid, ParameterType::INTEGER); - } - - // Exclude the root. - $query->where($db->quoteName('a.parent_id') . ' <> 0'); - - // Optionally filter on language - if (empty($language)) - { - $language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all'); - } - - if ($language !== 'all') - { - if ($language === 'current_language') - { - $language = ContentHelper::getCurrentLanguage(); - } - - $query->whereIn($db->quoteName('language'), [$language, '*'], ParameterType::STRING); - } - - // List state information - $format = $app->input->getWord('format'); - - if ($format === 'feed') - { - $limit = $app->get('feed_limit'); - } - else - { - if ($this->state->params->get('show_pagination_limit')) - { - $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); - } - else - { - $limit = $this->state->params->get('maximum', 20); - } - } - - $this->setState('list.limit', $limit); - - $offset = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $offset); - - // Optionally filter on entered value - if ($this->state->get('list.filter')) - { - $title = '%' . $this->state->get('list.filter') . '%'; - $query->where($db->quoteName('a.title') . ' LIKE :title') - ->bind(':title', $title); - } - - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - - $query->order($db->quoteName($orderby) . ' ' . $orderDirection . ', a.title ASC'); - - return $query; - } + /** + * Model context string. + * + * @var string + * @since 3.1 + */ + public $_context = 'com_tags.tags'; + + /** + * Method to auto-populate the model state. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @note Calling getState in this method will result in recursion. + * + * @since 3.1 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + + // Load state from the request. + $pid = $app->input->getInt('parent_id'); + $this->setState('tag.parent_id', $pid); + + $language = $app->input->getString('tag_list_language_filter'); + $this->setState('tag.language', $language); + + $offset = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.offset', $offset); + $app = Factory::getApplication(); + + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('list.limit', $params->get('maximum', 200)); + + $this->setState('filter.published', 1); + $this->setState('filter.access', true); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_tags')) && (!$user->authorise('core.edit', 'com_tags'))) { + $this->setState('filter.published', 1); + } + + // Optional filter text + $itemid = $pid . ':' . $app->input->getInt('Itemid', 0); + $filterSearch = $app->getUserStateFromRequest('com_tags.tags.list.' . $itemid . '.filter_search', 'filter-search', '', 'string'); + $this->setState('list.filter', $filterSearch); + } + + /** + * Method to build an SQL query to load the list data. + * + * @return string An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + $app = Factory::getApplication(); + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + $pid = (int) $this->getState('tag.parent_id'); + $orderby = $this->state->params->get('all_tags_orderby', 'title'); + $published = (int) $this->state->params->get('published', 1); + $orderDirection = $this->state->params->get('all_tags_orderby_direction', 'ASC'); + $language = $this->getState('tag.language'); + + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select required fields from the tags. + $query->select('a.*, u.name as created_by_user_name, u.email') + ->from($db->quoteName('#__tags', 'a')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('a.created_user_id') . ' = ' . $db->quoteName('u.id')) + ->whereIn($db->quoteName('a.access'), $groups); + + if (!empty($pid)) { + $query->where($db->quoteName('a.parent_id') . ' = :pid') + ->bind(':pid', $pid, ParameterType::INTEGER); + } + + // Exclude the root. + $query->where($db->quoteName('a.parent_id') . ' <> 0'); + + // Optionally filter on language + if (empty($language)) { + $language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all'); + } + + if ($language !== 'all') { + if ($language === 'current_language') { + $language = ContentHelper::getCurrentLanguage(); + } + + $query->whereIn($db->quoteName('language'), [$language, '*'], ParameterType::STRING); + } + + // List state information + $format = $app->input->getWord('format'); + + if ($format === 'feed') { + $limit = $app->get('feed_limit'); + } else { + if ($this->state->params->get('show_pagination_limit')) { + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); + } else { + $limit = $this->state->params->get('maximum', 20); + } + } + + $this->setState('list.limit', $limit); + + $offset = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $offset); + + // Optionally filter on entered value + if ($this->state->get('list.filter')) { + $title = '%' . $this->state->get('list.filter') . '%'; + $query->where($db->quoteName('a.title') . ' LIKE :title') + ->bind(':title', $title); + } + + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + + $query->order($db->quoteName($orderby) . ' ' . $orderDirection . ', a.title ASC'); + + return $query; + } } diff --git a/code/components/com_tags/src/Service/Router.php b/code/components/com_tags/src/Service/Router.php index d840c208..545e1d1f 100644 --- a/code/components/com_tags/src/Service/Router.php +++ b/code/components/com_tags/src/Service/Router.php @@ -1,4 +1,5 @@ db = $db; - - parent::__construct($app, $menu); - } - - /** - * Build the route for the com_tags component - * - * @param array &$query An array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.3 - */ - public function build(&$query) - { - $segments = array(); - - // Get a menu item based on Itemid or currently active - - // We need a menu item. Either the one specified in the query, or the current active one if none specified - if (empty($query['Itemid'])) - { - $menuItem = $this->menu->getActive(); - } - else - { - $menuItem = $this->menu->getItem($query['Itemid']); - } - - $mView = empty($menuItem->query['view']) ? null : $menuItem->query['view']; - $mId = empty($menuItem->query['id']) ? null : $menuItem->query['id']; - - if (is_array($mId)) - { - $mId = ArrayHelper::toInteger($mId); - } - - $view = ''; - - if (isset($query['view'])) - { - $view = $query['view']; - - if (empty($query['Itemid'])) - { - $segments[] = $view; - } - - unset($query['view']); - } - - // Are we dealing with a tag that is attached to a menu item? - if ($mView == $view && isset($query['id']) && $mId == $query['id']) - { - unset($query['id']); - - return $segments; - } - - if ($view === 'tag') - { - $notActiveTag = is_array($mId) ? (count($mId) > 1 || $mId[0] != (int) $query['id']) : ($mId != (int) $query['id']); - - if ($notActiveTag || $mView != $view) - { - // ID in com_tags can be either an integer, a string or an array of IDs - $id = is_array($query['id']) ? implode(',', $query['id']) : $query['id']; - $segments[] = $id; - } - - unset($query['id']); - } - - if (isset($query['layout'])) - { - if ((!empty($query['Itemid']) && isset($menuItem->query['layout']) - && $query['layout'] == $menuItem->query['layout']) - || $query['layout'] === 'default') - { - unset($query['layout']); - } - } - - $total = count($segments); - - for ($i = 0; $i < $total; $i++) - { - $segments[$i] = str_replace(':', '-', $segments[$i]); - $position = strpos($segments[$i], '-'); - - if ($position) - { - // Remove id from segment - $segments[$i] = substr($segments[$i], $position + 1); - } - } - - return $segments; - } - - /** - * Parse the segments of a URL. - * - * @param array &$segments The segments of the URL to parse. - * - * @return array The URL attributes to be used by the application. - * - * @since 3.3 - */ - public function parse(&$segments) - { - $total = count($segments); - $vars = array(); - - for ($i = 0; $i < $total; $i++) - { - $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1); - } - - // Get the active menu item. - $item = $this->menu->getActive(); - - // Count route segments - $count = count($segments); - - // Standard routing for tags. - if (!isset($item)) - { - $vars['view'] = $segments[0]; - $vars['id'] = $this->fixSegment($segments[$count - 1]); - unset($segments[0]); - unset($segments[$count - 1]); - - return $vars; - } - - $vars['id'] = $this->fixSegment($segments[0]); - $vars['view'] = 'tag'; - unset($segments[0]); - - return $vars; - } - - /** - * Try to add missing id to segment - * - * @param string $segment One piece of segment of the URL to parse - * - * @return string The segment with founded id - * - * @since 3.7 - */ - protected function fixSegment($segment) - { - // Try to find tag id - $alias = str_replace(':', '-', $segment); - - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__tags')) - ->where($this->db->quoteName('alias') . ' = :alias') - ->bind(':alias', $alias); - - $id = $this->db->setQuery($query)->loadResult(); - - if ($id) - { - $segment = "$id:$alias"; - } - - return $segment; - } + /** + * The db + * + * @var DatabaseInterface + * + * @since 4.0.0 + */ + private $db; + + /** + * Tags Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + * + * @since 4.0.0 + */ + public function __construct(SiteApplication $app, AbstractMenu $menu, ?CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) + { + $this->db = $db; + + parent::__construct($app, $menu); + } + + /** + * Build the route for the com_tags component + * + * @param array &$query An array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.3 + */ + public function build(&$query) + { + $segments = array(); + + // Get a menu item based on Itemid or currently active + + // We need a menu item. Either the one specified in the query, or the current active one if none specified + if (empty($query['Itemid'])) { + $menuItem = $this->menu->getActive(); + } else { + $menuItem = $this->menu->getItem($query['Itemid']); + } + + $mView = empty($menuItem->query['view']) ? null : $menuItem->query['view']; + $mId = empty($menuItem->query['id']) ? null : $menuItem->query['id']; + + if (is_array($mId)) { + $mId = ArrayHelper::toInteger($mId); + } + + $view = ''; + + if (isset($query['view'])) { + $view = $query['view']; + + if (empty($query['Itemid'])) { + $segments[] = $view; + } + + unset($query['view']); + } + + // Are we dealing with a tag that is attached to a menu item? + if ($mView == $view && isset($query['id']) && $mId == $query['id']) { + unset($query['id']); + + return $segments; + } + + if ($view === 'tag') { + $notActiveTag = is_array($mId) ? (count($mId) > 1 || $mId[0] != (int) $query['id']) : ($mId != (int) $query['id']); + + if ($notActiveTag || $mView != $view) { + // ID in com_tags can be either an integer, a string or an array of IDs + $id = is_array($query['id']) ? implode(',', $query['id']) : $query['id']; + $segments[] = $id; + } + + unset($query['id']); + } + + if (isset($query['layout'])) { + if ( + (!empty($query['Itemid']) && isset($menuItem->query['layout']) + && $query['layout'] == $menuItem->query['layout']) + || $query['layout'] === 'default' + ) { + unset($query['layout']); + } + } + + $total = count($segments); + + for ($i = 0; $i < $total; $i++) { + $segments[$i] = str_replace(':', '-', $segments[$i]); + $position = strpos($segments[$i], '-'); + + if ($position) { + // Remove id from segment + $segments[$i] = substr($segments[$i], $position + 1); + } + } + + return $segments; + } + + /** + * Parse the segments of a URL. + * + * @param array &$segments The segments of the URL to parse. + * + * @return array The URL attributes to be used by the application. + * + * @since 3.3 + */ + public function parse(&$segments) + { + $total = count($segments); + $vars = array(); + + for ($i = 0; $i < $total; $i++) { + $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1); + } + + // Get the active menu item. + $item = $this->menu->getActive(); + + // Count route segments + $count = count($segments); + + // Standard routing for tags. + if (!isset($item)) { + $vars['view'] = $segments[0]; + $vars['id'] = $this->fixSegment($segments[$count - 1]); + unset($segments[0]); + unset($segments[$count - 1]); + + return $vars; + } + + $vars['id'] = $this->fixSegment($segments[0]); + $vars['view'] = 'tag'; + unset($segments[0]); + + return $vars; + } + + /** + * Try to add missing id to segment + * + * @param string $segment One piece of segment of the URL to parse + * + * @return string The segment with founded id + * + * @since 3.7 + */ + protected function fixSegment($segment) + { + // Try to find tag id + $alias = str_replace(':', '-', $segment); + + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__tags')) + ->where($this->db->quoteName('alias') . ' = :alias') + ->bind(':alias', $alias); + + $id = $this->db->setQuery($query)->loadResult(); + + if ($id) { + $segment = "$id:$alias"; + } + + return $segment; + } } diff --git a/code/components/com_tags/src/View/Tag/FeedView.php b/code/components/com_tags/src/View/Tag/FeedView.php index 7fa154cd..14be73a6 100644 --- a/code/components/com_tags/src/View/Tag/FeedView.php +++ b/code/components/com_tags/src/View/Tag/FeedView.php @@ -1,4 +1,5 @@ input->get('id', array(), 'int'); - $i = 0; - $tagIds = ''; - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - foreach ($ids as $id) - { - if ($i !== 0) - { - $tagIds .= '&'; - } - - $tagIds .= 'id[' . $i . ']=' . $id; - - $i++; - } - - $this->document->link = Route::_('index.php?option=com_tags&view=tag&' . $tagIds); - - $app->input->set('limit', $app->get('feed_limit')); - $siteEmail = $app->get('mailfrom'); - $fromName = $app->get('fromname'); - $feedEmail = $app->get('feed_email', 'none'); - - $this->document->editor = $fromName; - - if ($feedEmail !== 'none') - { - $this->document->editorEmail = $siteEmail; - } - - // Get some data from the model - $items = $this->get('Items'); - - if ($items !== false) - { - foreach ($items as $item) - { - // Strip HTML from feed item title - $title = $this->escape($item->core_title); - $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); - - // Strip HTML from feed item description text - $description = $item->core_body; - $author = $item->core_created_by_alias ?: $item->author; - $date = ($item->displayDate ? date('r', strtotime($item->displayDate)) : ''); - - // Load individual item creator class - $feeditem = new FeedItem; - $feeditem->title = $title; - $feeditem->link = Route::_($item->link); - $feeditem->description = $description; - $feeditem->date = $date; - $feeditem->category = $title; - $feeditem->author = $author; - - if ($feedEmail === 'site') - { - $item->authorEmail = $siteEmail; - } - elseif ($feedEmail === 'author') - { - $item->authorEmail = $item->author_email; - } - - // Loads item info into RSS array - $this->document->addItem($feeditem); - } - } - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $ids = (array) $app->input->get('id', array(), 'int'); + $i = 0; + $tagIds = ''; + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + foreach ($ids as $id) { + if ($i !== 0) { + $tagIds .= '&'; + } + + $tagIds .= 'id[' . $i . ']=' . $id; + + $i++; + } + + $this->document->link = Route::_('index.php?option=com_tags&view=tag&' . $tagIds); + + $app->input->set('limit', $app->get('feed_limit')); + $siteEmail = $app->get('mailfrom'); + $fromName = $app->get('fromname'); + $feedEmail = $app->get('feed_email', 'none'); + + $this->document->editor = $fromName; + + if ($feedEmail !== 'none') { + $this->document->editorEmail = $siteEmail; + } + + // Get some data from the model + $items = $this->get('Items'); + + if ($items !== false) { + foreach ($items as $item) { + // Strip HTML from feed item title + $title = $this->escape($item->core_title); + $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); + + // Strip HTML from feed item description text + $description = $item->core_body; + $author = $item->core_created_by_alias ?: $item->author; + $date = ($item->displayDate ? date('r', strtotime($item->displayDate)) : ''); + + // Load individual item creator class + $feeditem = new FeedItem(); + $feeditem->title = $title; + $feeditem->link = Route::_($item->link); + $feeditem->description = $description; + $feeditem->date = $date; + $feeditem->category = $title; + $feeditem->author = $author; + + if ($feedEmail === 'site') { + $item->authorEmail = $siteEmail; + } elseif ($feedEmail === 'author') { + $item->authorEmail = $item->author_email; + } + + // Loads item info into RSS array + $this->document->addItem($feeditem); + } + } + } } diff --git a/code/components/com_tags/src/View/Tag/HtmlView.php b/code/components/com_tags/src/View/Tag/HtmlView.php index 7fde0d3c..a5f7541c 100644 --- a/code/components/com_tags/src/View/Tag/HtmlView.php +++ b/code/components/com_tags/src/View/Tag/HtmlView.php @@ -1,4 +1,5 @@ getParams(); - - // Get some data from the models - $state = $this->get('State'); - $items = $this->get('Items'); - $item = $this->get('Item'); - $children = $this->get('Children'); - $parent = $this->get('Parent'); - $pagination = $this->get('Pagination'); - - // Flag indicates to not add limitstart=0 to URL - $pagination->hideEmptyLimitstart = true; - - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check whether access level allows access. - // @TODO: Should already be computed in $item->params->get('access-view') - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - foreach ($item as $itemElement) - { - if (!in_array($itemElement->access, $groups)) - { - unset($itemElement); - } - - // Prepare the data. - if (!empty($itemElement)) - { - $temp = new Registry($itemElement->params); - $itemElement->params = clone $params; - $itemElement->params->merge($temp); - $itemElement->params = (array) json_decode($itemElement->params); - $itemElement->metadata = new Registry($itemElement->metadata); - } - } - - if ($items !== false) - { - PluginHelper::importPlugin('content'); - - foreach ($items as $itemElement) - { - $itemElement->event = new \stdClass; - - // For some plugins. - !empty($itemElement->core_body) ? $itemElement->text = $itemElement->core_body : $itemElement->text = null; - - $itemElement->core_params = new Registry($itemElement->core_params); - - Factory::getApplication()->triggerEvent('onContentPrepare', ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0]); - - $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', - ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] - ); - $itemElement->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', - ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] - ); - $itemElement->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', - ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] - ); - $itemElement->event->afterDisplayContent = trim(implode("\n", $results)); - - // Write the results back into the body - if (!empty($itemElement->core_body)) - { - $itemElement->core_body = $itemElement->text; - } - - // Categories store the images differently so lets re-map it so the display is correct - if ($itemElement->type_alias === 'com_content.category') - { - $itemElement->core_images = json_encode( - array( - 'image_intro' => $itemElement->core_params->get('image', ''), - 'image_intro_alt' => $itemElement->core_params->get('image_alt', '') - ) - ); - } - } - } - - $this->state = $state; - $this->items = $items; - $this->children = $children; - $this->parent = $parent; - $this->pagination = $pagination; - $this->user = $user; - $this->item = $item; - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - // Merge tag params. If this is single-tag view, menu params override tag params - // Otherwise, article params override menu item params - $this->params = $this->state->get('params'); - $active = $app->getMenu()->getActive(); - $temp = clone $this->params; - - // Convert item params to a Registry object - $item[0]->params = new Registry($item[0]->params); - - // Check to see which parameters should take priority - if ($active) - { - $currentLink = $active->link; - - // If the current view is the active item and a tag view for one tag, then the menu item params take priority - if (strpos($currentLink, 'view=tag') && strpos($currentLink, '&id[0]=' . (string) $item[0]->id)) - { - // $item[0]->params are the tag params, $temp are the menu item params - // Merge so that the menu item params take priority - $item[0]->params->merge($temp); - - // Load layout from active query (in case it is an alternative menu item) - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - } - else - { - // Current menuitem is not a single tag view, so the tag params take priority. - // Merge the menu item params with the tag params so that the tag params take priority - $temp->merge($item[0]->params); - $item[0]->params = $temp; - - // Check for alternative layouts (since we are not in a single-article menu item) - // Single-article menu item layout takes priority over alt layout for an article - if ($layout = $item[0]->params->get('tag_layout')) - { - $this->setLayout($layout); - } - } - } - else - { - // Merge so that item params take priority - $temp->merge($item[0]->params); - $item[0]->params = $temp; - - // Check for alternative layouts (since we are not in a single-tag menu item) - // Single-tag menu item layout takes priority over alt layout for an article - if ($layout = $item[0]->params->get('tag_layout')) - { - $this->setLayout($layout); - } - } - - // Increment the hit counter - $model = $this->getModel(); - $model->hit(); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - $menu = $app->getMenu()->getActive(); - $this->tags_title = $this->getTagsTitle(); - $pathway = $app->getPathway(); - $title = ''; - - // Highest priority for "Browser Page Title". - if ($menu) - { - $title = $menu->getParams()->get('page_title', ''); - } - - if ($this->tags_title) - { - $this->params->def('page_heading', $this->tags_title); - $title = $title ?: $this->tags_title; - } - elseif ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - $title = $title ?: $this->params->get('page_title', $menu->title); - } - - $this->setDocumentTitle($title); - $pathway->addItem($title); - - foreach ($this->item as $itemElement) - { - if ($itemElement->metadesc) - { - $this->document->setDescription($itemElement->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } - - if (count($this->item) === 1) - { - foreach ($this->item[0]->metadata->toArray() as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - } - - if ($this->params->get('show_feed_link', 1) == 1) - { - $link = '&format=feed&limitstart='; - $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); - $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); - $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); - $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); - } - } - - /** - * Creates the tags title for the output - * - * @return string - * - * @since 3.1 - */ - protected function getTagsTitle() - { - $tags_title = array(); - - if (!empty($this->item)) - { - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - foreach ($this->item as $item) - { - if (in_array($item->access, $groups)) - { - $tags_title[] = $item->title; - } - } - } - - return implode(' ', $tags_title); - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.1 + */ + protected $state; + + /** + * List of items associated with the tag + * + * @var \stdClass[]|false + * + * @since 3.1 + */ + protected $items; + + /** + * Tag data for the current tag or tags (on success, false on failure) + * + * @var \Joomla\CMS\Object\CMSObject|boolean + * + * @since 3.1 + */ + protected $item; + + /** + * UNUSED + * + * @var null + * + * @since 3.1 + */ + protected $children; + + /** + * UNUSED + * + * @var null + * + * @since 3.1 + */ + protected $parent; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.1 + */ + protected $pagination; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 3.1 + */ + protected $params; + + /** + * Array of tags title + * + * @var array + * + * @since 3.1 + */ + protected $tags_title; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The logged in user + * + * @var User|null + * + * @since 4.0.0 + */ + protected $user = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.1 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $params = $app->getParams(); + + // Get some data from the models + $state = $this->get('State'); + $items = $this->get('Items'); + $item = $this->get('Item'); + $children = $this->get('Children'); + $parent = $this->get('Parent'); + $pagination = $this->get('Pagination'); + + // Flag indicates to not add limitstart=0 to URL + $pagination->hideEmptyLimitstart = true; + + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check whether access level allows access. + // @TODO: Should already be computed in $item->params->get('access-view') + $user = $this->getCurrentUser(); + $groups = $user->getAuthorisedViewLevels(); + + foreach ($item as $itemElement) { + if (!in_array($itemElement->access, $groups)) { + unset($itemElement); + } + + // Prepare the data. + if (!empty($itemElement)) { + $temp = new Registry($itemElement->params); + $itemElement->params = clone $params; + $itemElement->params->merge($temp); + $itemElement->params = (array) json_decode($itemElement->params); + $itemElement->metadata = new Registry($itemElement->metadata); + } + } + + if ($items !== false) { + PluginHelper::importPlugin('content'); + + foreach ($items as $itemElement) { + $itemElement->event = new \stdClass(); + + // For some plugins. + !empty($itemElement->core_body) ? $itemElement->text = $itemElement->core_body : $itemElement->text = null; + + $itemElement->core_params = new Registry($itemElement->core_params); + + Factory::getApplication()->triggerEvent('onContentPrepare', ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0]); + + $results = Factory::getApplication()->triggerEvent( + 'onContentAfterTitle', + ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] + ); + $itemElement->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent( + 'onContentBeforeDisplay', + ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] + ); + $itemElement->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent( + 'onContentAfterDisplay', + ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] + ); + $itemElement->event->afterDisplayContent = trim(implode("\n", $results)); + + // Write the results back into the body + if (!empty($itemElement->core_body)) { + $itemElement->core_body = $itemElement->text; + } + + // Categories store the images differently so lets re-map it so the display is correct + if ($itemElement->type_alias === 'com_content.category') { + $itemElement->core_images = json_encode( + array( + 'image_intro' => $itemElement->core_params->get('image', ''), + 'image_intro_alt' => $itemElement->core_params->get('image_alt', '') + ) + ); + } + } + } + + $this->state = $state; + $this->items = $items; + $this->children = $children; + $this->parent = $parent; + $this->pagination = $pagination; + $this->user = $user; + $this->item = $item; + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + // Merge tag params. If this is single-tag view, menu params override tag params + // Otherwise, article params override menu item params + $this->params = $this->state->get('params'); + $active = $app->getMenu()->getActive(); + $temp = clone $this->params; + + // Convert item params to a Registry object + $item[0]->params = new Registry($item[0]->params); + + // Check to see which parameters should take priority + if ($active) { + $currentLink = $active->link; + + // If the current view is the active item and a tag view for one tag, then the menu item params take priority + if (strpos($currentLink, 'view=tag') && strpos($currentLink, '&id[0]=' . (string) $item[0]->id)) { + // $item[0]->params are the tag params, $temp are the menu item params + // Merge so that the menu item params take priority + $item[0]->params->merge($temp); + + // Load layout from active query (in case it is an alternative menu item) + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + } else { + // Current menuitem is not a single tag view, so the tag params take priority. + // Merge the menu item params with the tag params so that the tag params take priority + $temp->merge($item[0]->params); + $item[0]->params = $temp; + + // Check for alternative layouts (since we are not in a single-article menu item) + // Single-article menu item layout takes priority over alt layout for an article + if ($layout = $item[0]->params->get('tag_layout')) { + $this->setLayout($layout); + } + } + } else { + // Merge so that item params take priority + $temp->merge($item[0]->params); + $item[0]->params = $temp; + + // Check for alternative layouts (since we are not in a single-tag menu item) + // Single-tag menu item layout takes priority over alt layout for an article + if ($layout = $item[0]->params->get('tag_layout')) { + $this->setLayout($layout); + } + } + + // Increment the hit counter + $model = $this->getModel(); + $model->hit(); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + $menu = $app->getMenu()->getActive(); + $this->tags_title = $this->getTagsTitle(); + $pathway = $app->getPathway(); + $title = ''; + + // Highest priority for "Browser Page Title". + if ($menu) { + $title = $menu->getParams()->get('page_title', ''); + } + + if ($this->tags_title) { + $this->params->def('page_heading', $this->tags_title); + $title = $title ?: $this->tags_title; + } elseif ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + $title = $title ?: $this->params->get('page_title', $menu->title); + } + + $this->setDocumentTitle($title); + $pathway->addItem($title); + + foreach ($this->item as $itemElement) { + if ($itemElement->metadesc) { + $this->document->setDescription($itemElement->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } + + if (count($this->item) === 1) { + foreach ($this->item[0]->metadata->toArray() as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + } + + if ($this->params->get('show_feed_link', 1) == 1) { + $link = '&format=feed&limitstart='; + $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); + $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); + $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); + $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); + } + } + + /** + * Creates the tags title for the output + * + * @return string + * + * @since 3.1 + */ + protected function getTagsTitle() + { + $tags_title = array(); + + if (!empty($this->item)) { + $user = $this->getCurrentUser(); + $groups = $user->getAuthorisedViewLevels(); + + foreach ($this->item as $item) { + if (in_array($item->access, $groups)) { + $tags_title[] = $item->title; + } + } + } + + return implode(' ', $tags_title); + } } diff --git a/code/components/com_tags/src/View/Tags/FeedView.php b/code/components/com_tags/src/View/Tags/FeedView.php index 48fdbb38..b6460ee4 100644 --- a/code/components/com_tags/src/View/Tags/FeedView.php +++ b/code/components/com_tags/src/View/Tags/FeedView.php @@ -1,4 +1,5 @@ document->link = Route::_('index.php?option=com_tags&view=tags'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $this->document->link = Route::_('index.php?option=com_tags&view=tags'); - $app->input->set('limit', $app->get('feed_limit')); - $siteEmail = $app->get('mailfrom'); - $fromName = $app->get('fromname'); - $feedEmail = $app->get('feed_email', 'none'); + $app->input->set('limit', $app->get('feed_limit')); + $siteEmail = $app->get('mailfrom'); + $fromName = $app->get('fromname'); + $feedEmail = $app->get('feed_email', 'none'); - $this->document->editor = $fromName; + $this->document->editor = $fromName; - if ($feedEmail !== 'none') - { - $this->document->editorEmail = $siteEmail; - } + if ($feedEmail !== 'none') { + $this->document->editorEmail = $siteEmail; + } - // Get some data from the model - $items = $this->get('Items'); + // Get some data from the model + $items = $this->get('Items'); - foreach ($items as $item) - { - // Strip HTML from feed item title - $title = $this->escape($item->title); - $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); + foreach ($items as $item) { + // Strip HTML from feed item title + $title = $this->escape($item->title); + $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); - // Strip HTML from feed item description text - $description = $item->description; - $author = $item->created_by_alias ?: $item->created_by_user_name; - $date = $item->created_time ? date('r', strtotime($item->created_time)) : ''; + // Strip HTML from feed item description text + $description = $item->description; + $author = $item->created_by_alias ?: $item->created_by_user_name; + $date = $item->created_time ? date('r', strtotime($item->created_time)) : ''; - // Load individual item creator class - $feeditem = new FeedItem; - $feeditem->title = $title; - $feeditem->link = '/index.php?option=com_tags&view=tag&id=' . (int) $item->id; - $feeditem->description = $description; - $feeditem->date = $date; - $feeditem->category = 'All Tags'; - $feeditem->author = $author; + // Load individual item creator class + $feeditem = new FeedItem(); + $feeditem->title = $title; + $feeditem->link = '/index.php?option=com_tags&view=tag&id=' . (int) $item->id; + $feeditem->description = $description; + $feeditem->date = $date; + $feeditem->category = 'All Tags'; + $feeditem->author = $author; - if ($feedEmail === 'site') - { - $feeditem->authorEmail = $siteEmail; - } + if ($feedEmail === 'site') { + $feeditem->authorEmail = $siteEmail; + } - if ($feedEmail === 'author') - { - $feeditem->authorEmail = $item->email; - } + if ($feedEmail === 'author') { + $feeditem->authorEmail = $item->email; + } - // Loads item info into RSS array - $this->document->addItem($feeditem); - } - } + // Loads item info into RSS array + $this->document->addItem($feeditem); + } + } } diff --git a/code/components/com_tags/src/View/Tags/HtmlView.php b/code/components/com_tags/src/View/Tags/HtmlView.php index 8cab3fbf..5e6d40df 100644 --- a/code/components/com_tags/src/View/Tags/HtmlView.php +++ b/code/components/com_tags/src/View/Tags/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->params = $this->state->get('params'); - $this->user = Factory::getUser(); - - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Flag indicates to not add limitstart=0 to URL - $this->pagination->hideEmptyLimitstart = true; - - if (!empty($this->items)) - { - foreach ($this->items as $itemElement) - { - // Prepare the data. - $temp = new Registry($itemElement->params); - $itemElement->params = clone $this->params; - $itemElement->params->merge($temp); - $itemElement->params = (array) json_decode($itemElement->params); - } - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); - - $active = Factory::getApplication()->getMenu()->getActive(); - - // Load layout from active query (in case it is an alternative menu item) - if ($active && isset($active->query['option']) && $active->query['option'] === 'com_tags' && $active->query['view'] === 'tags') - { - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - } - else - { - // Load default All Tags layout from component - if ($layout = $this->params->get('tags_layout')) - { - $this->setLayout($layout); - } - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function _prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_TAGS_DEFAULT_PAGE_TITLE')); - } - - // Set metadata for all tags menu item - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - // Respect configuration Sitename Before/After for TITLE in views All Tags. - $this->setDocumentTitle($this->document->getTitle()); - - // Add alternative feed link - if ($this->params->get('show_feed_link', 1) == 1) - { - $link = '&format=feed&limitstart='; - $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); - $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); - $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); - $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); - } - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.1 + */ + protected $state; + + /** + * The list of tags + * + * @var array|false + * @since 3.1 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 3.1 + */ + protected $pagination; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * @since 3.1 + */ + protected $params = null; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The logged in user + * + * @var \Joomla\CMS\User\User|null + * @since 4.0.0 + */ + protected $user = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + // Get some data from the models + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->params = $this->state->get('params'); + $this->user = $this->getCurrentUser(); + + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + if (!empty($this->items)) { + foreach ($this->items as $itemElement) { + // Prepare the data. + $temp = new Registry($itemElement->params); + $itemElement->params = clone $this->params; + $itemElement->params->merge($temp); + $itemElement->params = (array) json_decode($itemElement->params); + } + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); + + $active = Factory::getApplication()->getMenu()->getActive(); + + // Load layout from active query (in case it is an alternative menu item) + if ($active && isset($active->query['option']) && $active->query['option'] === 'com_tags' && $active->query['view'] === 'tags') { + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + } else { + // Load default All Tags layout from component + if ($layout = $this->params->get('tags_layout')) { + $this->setLayout($layout); + } + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function _prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_TAGS_DEFAULT_PAGE_TITLE')); + } + + // Set metadata for all tags menu item + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + // Respect configuration Sitename Before/After for TITLE in views All Tags. + $this->setDocumentTitle($this->document->getTitle()); + + // Add alternative feed link + if ($this->params->get('show_feed_link', 1) == 1) { + $link = '&format=feed&limitstart='; + $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); + $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); + $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); + $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); + } + } } diff --git a/code/components/com_tags/tmpl/tag/default.php b/code/components/com_tags/tmpl/tag/default.php index 5e17225a..fabeaa51 100644 --- a/code/components/com_tags/tmpl/tag/default.php +++ b/code/components/com_tags/tmpl/tag/default.php @@ -1,4 +1,5 @@ - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + - params->get('show_tag_title', 1)) : ?> - <> - tags_title, '', 'com_tag.tag'); ?> - > - + params->get('show_tag_title', 1)) : ?> + <> + tags_title, '', 'com_tag.tag'); ?> + > + - - item) === 1 && ($this->params->get('tag_list_show_tag_image', 1) || $this->params->get('tag_list_show_tag_description', 1))) : ?> -
    - item[0]->images); ?> - params->get('tag_list_show_tag_image', 1) == 1 && !empty($images->image_fulltext)) : ?> - image_fulltext, $images->image_fulltext_alt); ?> - - params->get('tag_list_show_tag_description') == 1 && $this->item[0]->description) : ?> - item[0]->description, '', 'com_tags.tag'); ?> - -
    - + + item) === 1 && ($this->params->get('tag_list_show_tag_image', 1) || $this->params->get('tag_list_show_tag_description', 1))) : ?> +
    + item[0]->images); ?> + params->get('tag_list_show_tag_image', 1) == 1 && !empty($images->image_fulltext)) : ?> + image_fulltext, $images->image_fulltext_alt); ?> + + params->get('tag_list_show_tag_description') == 1 && $this->item[0]->description) : ?> + item[0]->description, '', 'com_tags.tag'); ?> + +
    + - - params->get('tag_list_show_tag_description', 1) || $this->params->get('show_description_image', 1)) : ?> - params->get('show_description_image', 1) == 1 && $this->params->get('tag_list_image')) : ?> - params->get('tag_list_image'), empty($this->params->get('tag_list_image_alt')) && empty($this->params->get('tag_list_image_alt_empty')) ? false : $this->params->get('tag_list_image_alt')); ?> - - params->get('tag_list_description', '') > '') : ?> - params->get('tag_list_description'), '', 'com_tags.tag'); ?> - - - loadTemplate('items'); ?> + + params->get('tag_list_show_tag_description', 1) || $this->params->get('show_description_image', 1)) : ?> + params->get('show_description_image', 1) == 1 && $this->params->get('tag_list_image')) : ?> + params->get('tag_list_image'), empty($this->params->get('tag_list_image_alt')) && empty($this->params->get('tag_list_image_alt_empty')) ? false : $this->params->get('tag_list_image_alt')); ?> + + params->get('tag_list_description', '') > '') : ?> + params->get('tag_list_description'), '', 'com_tags.tag'); ?> + + + loadTemplate('items'); ?> - params->def('show_pagination', 1) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - pagination->getPagesLinks(); ?> -
    - + params->def('show_pagination', 1) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + pagination->getPagesLinks(); ?> +
    + diff --git a/code/components/com_tags/tmpl/tag/default_items.php b/code/components/com_tags/tmpl/tag/default_items.php index ce51281c..62e51858 100644 --- a/code/components/com_tags/tmpl/tag/default_items.php +++ b/code/components/com_tags/tmpl/tag/default_items.php @@ -1,4 +1,5 @@ authorise('core.edit.state', 'com_tags'); ?>
    -
    - params->get('filter_field') || $this->params->get('show_pagination_limit')) : ?> - params->get('filter_field')) : ?> -
    - - - - -
    - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - + + params->get('filter_field') || $this->params->get('show_pagination_limit')) : ?> + params->get('filter_field')) : ?> +
    + + + + +
    + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + - - - -
    + + + + - items)) : ?> -
    - - -
    - -
      - items as $i => $item) : ?> - core_state == 0) : ?> -
    • - -
    • - - type_alias === 'com_users.category') || ($item->type_alias === 'com_banners.category')) : ?> -

      - escape($item->core_title); ?> -

      - -

      - - escape($item->core_title); ?> - -

      - - - event->afterDisplayTitle; ?> - core_images); ?> - params->get('tag_list_show_item_image', 1) == 1 && !empty($images->image_intro)) : ?> - - image_intro, $images->image_intro_alt); ?> - - - params->get('tag_list_show_item_description', 1)) : ?> - - event->beforeDisplayContent; ?> - - core_body, $this->params->get('tag_list_item_maximum_characters')); ?> - - - event->afterDisplayContent; ?> - -
    • - -
    - + items)) : ?> +
    + + +
    + +
      + items as $i => $item) : ?> + core_state == 0) : ?> +
    • + +
    • + + type_alias === 'com_users.category') || ($item->type_alias === 'com_banners.category')) : ?> +

      + escape($item->core_title); ?> +

      + +

      + + escape($item->core_title); ?> + +

      + + + event->afterDisplayTitle; ?> + core_images); ?> + params->get('tag_list_show_item_image', 1) == 1 && !empty($images->image_intro)) : ?> + + image_intro, $images->image_intro_alt); ?> + + + params->get('tag_list_show_item_description', 1)) : ?> + + event->beforeDisplayContent; ?> + + core_body, $this->params->get('tag_list_item_maximum_characters')); ?> + + + event->afterDisplayContent; ?> + +
    • + +
    +
    diff --git a/code/components/com_tags/tmpl/tag/list.php b/code/components/com_tags/tmpl/tag/list.php index 05bbafc2..c60afcfa 100644 --- a/code/components/com_tags/tmpl/tag/list.php +++ b/code/components/com_tags/tmpl/tag/list.php @@ -1,4 +1,5 @@ - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - - - params->get('show_tag_title', 1)) : ?> - <> - tags_title, '', 'com_tag.tag'); ?> - > - - - - item) === 1 && ($this->params->get('tag_list_show_tag_image', 1) || $this->params->get('tag_list_show_tag_description', 1))) : ?> -
    - item[0]->images); ?> - params->get('tag_list_show_tag_image', 1) == 1 && !empty($images->image_fulltext)) : ?> - image_fulltext, ''); ?> - - params->get('tag_list_show_tag_description') == 1 && $this->item[0]->description) : ?> - item[0]->description, '', 'com_tags.tag'); ?> - -
    - - - - params->get('tag_list_show_tag_description', 1) || $this->params->get('show_description_image', 1)) : ?> - params->get('show_description_image', 1) == 1 && $this->params->get('tag_list_image')) : ?> - params->get('tag_list_image'), empty($this->params->get('tag_list_image_alt')) && empty($this->params->get('tag_list_image_alt_empty')) ? false : $this->params->get('tag_list_image_alt')); ?> - - params->get('tag_list_description', '') > '') : ?> - params->get('tag_list_description'), '', 'com_tags.tag'); ?> - - - loadTemplate('items'); ?> + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + + + params->get('show_tag_title', 1)) : ?> + <> + tags_title, '', 'com_tag.tag'); ?> + > + + + + item) === 1 && ($this->params->get('tag_list_show_tag_image', 1) || $this->params->get('tag_list_show_tag_description', 1))) : ?> +
    + item[0]->images); ?> + params->get('tag_list_show_tag_image', 1) == 1 && !empty($images->image_fulltext)) : ?> + image_fulltext, ''); ?> + + params->get('tag_list_show_tag_description') == 1 && $this->item[0]->description) : ?> + item[0]->description, '', 'com_tags.tag'); ?> + +
    + + + + params->get('tag_list_show_tag_description', 1) || $this->params->get('show_description_image', 1)) : ?> + params->get('show_description_image', 1) == 1 && $this->params->get('tag_list_image')) : ?> + params->get('tag_list_image'), empty($this->params->get('tag_list_image_alt')) && empty($this->params->get('tag_list_image_alt_empty')) ? false : $this->params->get('tag_list_image_alt')); ?> + + params->get('tag_list_description', '') > '') : ?> + params->get('tag_list_description'), '', 'com_tags.tag'); ?> + + + loadTemplate('items'); ?> diff --git a/code/components/com_tags/tmpl/tag/list_items.php b/code/components/com_tags/tmpl/tag/list_items.php index 3d84e9fb..777091a9 100644 --- a/code/components/com_tags/tmpl/tag/list_items.php +++ b/code/components/com_tags/tmpl/tag/list_items.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
    -
    - params->get('filter_field')) : ?> -
    - - - - -
    - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - + + params->get('filter_field')) : ?> +
    + + + + +
    + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + - items)) : ?> -
    - - -
    - - - params->get('show_headings')) : ?> - - - - params->get('tag_list_show_date')) : ?> - - - - - - - items as $i => $item) : ?> - core_state == 0) : ?> - - - - - - params->get('tag_list_show_date')) : ?> - - - - - -
    - - - - - - - - - -
    - type_alias === 'com_users.category') || ($item->type_alias === 'com_banners.category')) : ?> - escape($item->core_title); ?> - - - escape($item->core_title); ?> - - - core_state == 0) : ?> - - - - - - displayDate, - $this->escape($this->params->get('date_format', Text::_('DATE_FORMAT_LC3'))) - ); ?> -
    - + items)) : ?> +
    + + +
    + + + params->get('show_headings')) : ?> + + + + params->get('tag_list_show_date')) : ?> + + + + + + + items as $i => $item) : ?> + core_state == 0) : ?> + + + + + + params->get('tag_list_show_date')) : ?> + + + + + +
    + + + + + + + + + +
    + type_alias === 'com_users.category') || ($item->type_alias === 'com_banners.category')) : ?> + escape($item->core_title); ?> + + + escape($item->core_title); ?> + + + core_state == 0) : ?> + + + + + + displayDate, + $this->escape($this->params->get('date_format', Text::_('DATE_FORMAT_LC3'))) + ); ?> +
    + - - params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - pagination->getPagesLinks(); ?> -
    - - - - - -
    + + params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + pagination->getPagesLinks(); ?> +
    + + + + + +
    diff --git a/code/components/com_tags/tmpl/tags/default.php b/code/components/com_tags/tmpl/tags/default.php index fe98a64e..c45171f4 100644 --- a/code/components/com_tags/tmpl/tags/default.php +++ b/code/components/com_tags/tmpl/tags/default.php @@ -1,4 +1,5 @@ params->get('all_tags_description_image'); ?>
    - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - - params->get('all_tags_show_description_image') && !empty($descriptionImage)) : ?> -
    - params->get('all_tags_description_image_alt')) && empty($this->params->get('all_tags_description_image_alt_empty')) ? false : $this->params->get('all_tags_description_image_alt')); ?> -
    - - -
    - -
    - - loadTemplate('items'); ?> + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + + params->get('all_tags_show_description_image') && !empty($descriptionImage)) : ?> +
    + params->get('all_tags_description_image_alt')) && empty($this->params->get('all_tags_description_image_alt_empty')) ? false : $this->params->get('all_tags_description_image_alt')); ?> +
    + + +
    + +
    + + loadTemplate('items'); ?>
    diff --git a/code/components/com_tags/tmpl/tags/default_items.php b/code/components/com_tags/tmpl/tags/default_items.php index ff99c487..4d115e68 100644 --- a/code/components/com_tags/tmpl/tags/default_items.php +++ b/code/components/com_tags/tmpl/tags/default_items.php @@ -1,4 +1,5 @@ params->get('tag_columns', 1); // Avoid division by 0 and negative columns. -if ($columns < 1) -{ - $columns = 1; +if ($columns < 1) { + $columns = 1; } $bsspans = floor(12 / $columns); -if ($bsspans < 1) -{ - $bsspans = 1; +if ($bsspans < 1) { + $bsspans = 1; } $bscolumns = min($columns, floor(12 / $bsspans)); @@ -48,110 +47,110 @@ ?>
    -
    - params->get('filter_field') || $this->params->get('show_pagination_limit')) : ?> - params->get('filter_field')) : ?> -
    - - - - -
    - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - - - - - -
    - - items == false || $n === 0) : ?> -
    - - -
    - - items as $i => $item) : ?> - -
      - - -
    • - access)) && in_array($item->access, $this->user->getAuthorisedViewLevels())) : ?> -

      - - escape($item->title); ?> - -

      - - - params->get('all_tags_show_tag_image') && !empty($item->images)) : ?> - images); ?> - - image_intro)) : ?> - float_intro) ? $this->params->get('float_intro') : $images->float_intro; ?> -
      - - image_intro_caption) : ?> - image_intro_caption; ?> - - - image_intro, $images->image_intro_alt, $imageOptions); ?> -
      - -
      - - - params->get('all_tags_show_tag_description', 1) && !empty($item->description)) || $this->params->get('all_tags_show_tag_hits')) : ?> -
      - params->get('all_tags_show_tag_description', 1) && !empty($item->description)) : ?> - - description, $this->params->get('all_tags_tag_maximum_characters')); ?> - - - params->get('all_tags_show_tag_hits')) : ?> - - hits); ?> - - -
      - -
    • - - -
    - - - - - - - items)) : ?> - params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - pagination->getPagesLinks(); ?> -
    - - +
    + params->get('filter_field') || $this->params->get('show_pagination_limit')) : ?> + params->get('filter_field')) : ?> +
    + + + + +
    + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + + + + + +
    + + items == false || $n === 0) : ?> +
    + + +
    + + items as $i => $item) : ?> + +
      + + +
    • + access)) && in_array($item->access, $this->user->getAuthorisedViewLevels())) : ?> +

      + + escape($item->title); ?> + +

      + + + params->get('all_tags_show_tag_image') && !empty($item->images)) : ?> + images); ?> + + image_intro)) : ?> + float_intro) ? $this->params->get('float_intro') : $images->float_intro; ?> +
      + + image_intro_caption) : ?> + image_intro_caption; ?> + + + image_intro, $images->image_intro_alt, $imageOptions); ?> +
      + +
      + + + params->get('all_tags_show_tag_description', 1) && !empty($item->description)) || $this->params->get('all_tags_show_tag_hits')) : ?> +
      + params->get('all_tags_show_tag_description', 1) && !empty($item->description)) : ?> + + description, $this->params->get('all_tags_tag_maximum_characters')); ?> + + + params->get('all_tags_show_tag_hits')) : ?> + + hits); ?> + + +
      + +
    • + + +
    + + + + + + + items)) : ?> + params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + pagination->getPagesLinks(); ?> +
    + +
    diff --git a/code/components/com_users/forms/login.xml b/code/components/com_users/forms/login.xml index 1b13a74e..1e9f1541 100644 --- a/code/components/com_users/forms/login.xml +++ b/code/components/com_users/forms/login.xml @@ -25,16 +25,6 @@ /> - -
    - - diff --git a/code/components/com_users/src/Controller/CallbackController.php b/code/components/com_users/src/Controller/CallbackController.php new file mode 100644 index 00000000..55ccbc9b --- /dev/null +++ b/code/components/com_users/src/Controller/CallbackController.php @@ -0,0 +1,26 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Site\Controller; + +use Joomla\Component\Users\Administrator\Controller\CallbackController as AdminCallbackController; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Multi-factor Authentication plugins' AJAX callback controller + * + * @since 4.2.0 + */ +class CallbackController extends AdminCallbackController +{ +} diff --git a/code/components/com_users/src/Controller/CaptiveController.php b/code/components/com_users/src/Controller/CaptiveController.php new file mode 100644 index 00000000..b1f6d2e4 --- /dev/null +++ b/code/components/com_users/src/Controller/CaptiveController.php @@ -0,0 +1,55 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Site\Controller; + +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Administrator\Controller\CaptiveController as AdminCaptiveController; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Captive Multi-factor Authentication page controller + * + * @since 4.2.0 + */ +class CaptiveController extends AdminCaptiveController +{ + /** + * Execute a task by triggering a Method in the derived class. + * + * @param string $task The task to perform. + * + * @return mixed The value returned by the called Method. + * + * @throws \Exception + * @since 4.2.0 + */ + public function execute($task) + { + try { + return parent::execute($task); + } catch (\Exception $e) { + if ($e->getCode() !== 403) { + throw $e; + } + + if ($this->app->getIdentity()->guest) { + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return null; + } + } + + return null; + } +} diff --git a/code/components/com_users/src/Controller/DisplayController.php b/code/components/com_users/src/Controller/DisplayController.php index 0d29349d..4365a6b5 100644 --- a/code/components/com_users/src/Controller/DisplayController.php +++ b/code/components/com_users/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->getCmd('view', 'login'); - $vFormat = $document->getType(); - $lName = $this->input->getCmd('layout', 'default'); - - if ($view = $this->getView($vName, $vFormat)) - { - // Do any specific processing by view. - switch ($vName) - { - case 'registration': - // If the user is already logged in, redirect to the profile page. - $user = $this->app->getIdentity(); - - if ($user->get('guest') != 1) - { - // Redirect to profile page. - $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); - - return; - } - - // Check if user registration is enabled - if (ComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0) - { - // Registration is disabled - Redirect to login page. - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return; - } - - // The user is a guest, load the registration model and show the registration page. - $model = $this->getModel('Registration'); - break; - - // Handle view specific models. - case 'profile': - - // If the user is a guest, redirect to the login page. - $user = $this->app->getIdentity(); - - if ($user->get('guest') == 1) - { - // Redirect to login page. - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return; - } - - $model = $this->getModel($vName); - break; - - // Handle the default views. - case 'login': - $model = $this->getModel($vName); - break; - - case 'remind': - case 'reset': - // If the user is already logged in, redirect to the profile page. - $user = $this->app->getIdentity(); - - if ($user->get('guest') != 1) - { - // Redirect to profile page. - $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); - - return; - } - - $model = $this->getModel($vName); - break; - - default: - $model = $this->getModel('Login'); - break; - } - - // Make sure we don't send a referer - if (in_array($vName, array('remind', 'reset'))) - { - $this->app->setHeader('Referrer-Policy', 'no-referrer', true); - } - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - - $view->display(); - } - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array|boolean $urlparams An array of safe URL parameters and their variable types, + * for valid values see {@link \Joomla\CMS\Filter\InputFilter::clean()}. + * + * @return void + * + * @since 1.5 + * @throws \Exception + */ + public function display($cachable = false, $urlparams = false) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->getCmd('view', 'login'); + $vFormat = $document->getType(); + $lName = $this->input->getCmd('layout', 'default'); + + if ($view = $this->getView($vName, $vFormat)) { + // Do any specific processing by view. + switch ($vName) { + case 'registration': + // If the user is already logged in, redirect to the profile page. + $user = $this->app->getIdentity(); + + if ($user->get('guest') != 1) { + // Redirect to profile page. + $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); + + return; + } + + // Check if user registration is enabled + if (ComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0) { + // Registration is disabled - Redirect to login page. + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return; + } + + // The user is a guest, load the registration model and show the registration page. + $model = $this->getModel('Registration'); + break; + + // Handle view specific models. + case 'profile': + // If the user is a guest, redirect to the login page. + $user = $this->app->getIdentity(); + + if ($user->get('guest') == 1) { + // Redirect to login page. + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return; + } + + $model = $this->getModel($vName); + break; + + // Handle the default views. + case 'login': + $model = $this->getModel($vName); + break; + + case 'remind': + case 'reset': + // If the user is already logged in, redirect to the profile page. + $user = $this->app->getIdentity(); + + if ($user->get('guest') != 1) { + // Redirect to profile page. + $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); + + return; + } + + $model = $this->getModel($vName); + break; + + case 'captive': + case 'methods': + case 'method': + $controller = $this->factory->createController($vName, 'Site', [], $this->app, $this->input); + $task = $this->input->get('task', ''); + + return $controller->execute($task); + + break; + + default: + $model = $this->getModel('Login'); + break; + } + + // Make sure we don't send a referer + if (in_array($vName, array('remind', 'reset'))) { + $this->app->setHeader('Referrer-Policy', 'no-referrer', true); + } + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + + $view->display(); + } + } } diff --git a/code/components/com_users/src/Controller/MethodController.php b/code/components/com_users/src/Controller/MethodController.php new file mode 100644 index 00000000..f24c0b2a --- /dev/null +++ b/code/components/com_users/src/Controller/MethodController.php @@ -0,0 +1,55 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Site\Controller; + +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Administrator\Controller\MethodController as AdminMethodController; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Multi-factor Authentication method controller + * + * @since 4.2.0 + */ +class MethodController extends AdminMethodController +{ + /** + * Execute a task by triggering a Method in the derived class. + * + * @param string $task The task to perform. + * + * @return mixed The value returned by the called Method. + * + * @throws \Exception + * @since 4.2.0 + */ + public function execute($task) + { + try { + return parent::execute($task); + } catch (\Exception $e) { + if ($e->getCode() !== 403) { + throw $e; + } + + if ($this->app->getIdentity()->guest) { + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return null; + } + } + + return null; + } +} diff --git a/code/components/com_users/src/Controller/MethodsController.php b/code/components/com_users/src/Controller/MethodsController.php new file mode 100644 index 00000000..d49b1b95 --- /dev/null +++ b/code/components/com_users/src/Controller/MethodsController.php @@ -0,0 +1,55 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Site\Controller; + +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Administrator\Controller\MethodsController as AdminMethodsController; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Multi-factor Authentication methods selection and management controller + * + * @since 4.2.0 + */ +class MethodsController extends AdminMethodsController +{ + /** + * Execute a task by triggering a Method in the derived class. + * + * @param string $task The task to perform. + * + * @return mixed The value returned by the called Method. + * + * @throws \Exception + * @since 4.2.0 + */ + public function execute($task) + { + try { + return parent::execute($task); + } catch (\Exception $e) { + if ($e->getCode() !== 403) { + throw $e; + } + + if ($this->app->getIdentity()->guest) { + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return null; + } + } + + return null; + } +} diff --git a/code/components/com_users/src/Controller/ProfileController.php b/code/components/com_users/src/Controller/ProfileController.php index e53fd9ef..7db89ab9 100644 --- a/code/components/com_users/src/Controller/ProfileController.php +++ b/code/components/com_users/src/Controller/ProfileController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Users\Site\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Users\Site\Controller; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\Router\Route; use Joomla\CMS\Uri\Uri; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Profile controller class for Users. @@ -23,215 +26,201 @@ */ class ProfileController extends BaseController { - /** - * Method to check out a user for editing and redirect to the edit form. - * - * @return boolean - * - * @since 1.6 - */ - public function edit() - { - $app = $this->app; - $user = $this->app->getIdentity(); - $loginUserId = (int) $user->get('id'); - - // Get the current user id. - $userId = $this->input->getInt('user_id'); - - // Check if the user is trying to edit another users profile. - if ($userId != $loginUserId) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return false; - } - - $cookieLogin = $user->get('cookieLogin'); - - // Check if the user logged in with a cookie - if (!empty($cookieLogin)) - { - // If so, the user must login to edit the password and other data. - $app->enqueueMessage(Text::_('JGLOBAL_REMEMBER_MUST_LOGIN'), 'message'); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return false; - } - - // Set the user id for the user to edit in the session. - $app->setUserState('com_users.edit.profile.id', $userId); - - // Redirect to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit', false)); - - return true; - } - - /** - * Method to save a user's profile data. - * - * @return void|boolean - * - * @since 1.6 - * @throws \Exception - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\ProfileModel $model */ - $model = $this->getModel('Profile', 'Site'); - $user = $this->app->getIdentity(); - $userId = (int) $user->get('id'); - - // Get the user data. - $requestData = $app->input->post->get('jform', array(), 'array'); - - // Force the ID to this user. - $requestData['id'] = $userId; - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - // Send an object which can be modified through the plugin event - $objData = (object) $requestData; - $app->triggerEvent( - 'onContentNormaliseRequestData', - array('com_users.user', $objData, $form) - ); - $requestData = (array) $objData; - - // Validate the posted data. - $data = $model->validate($form, $requestData); - - // Check for errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Unset the passwords. - unset($requestData['password1'], $requestData['password2']); - - // Save the data in the session. - $app->setUserState('com_users.edit.profile.data', $requestData); - - // Redirect back to the edit screen. - $userId = (int) $app->getUserState('com_users.edit.profile.id'); - $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit&user_id=' . $userId, false)); - - return false; - } - - // Attempt to save the data. - $return = $model->save($data); - - // Check for errors. - if ($return === false) - { - // Save the data in the session. - $app->setUserState('com_users.edit.profile.data', $data); - - // Redirect back to the edit screen. - $userId = (int) $app->getUserState('com_users.edit.profile.id'); - $this->setMessage(Text::sprintf('COM_USERS_PROFILE_SAVE_FAILED', $model->getError()), 'warning'); - $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit&user_id=' . $userId, false)); - - return false; - } - - // Redirect the user and adjust session state based on the chosen task. - switch ($this->getTask()) - { - case 'apply': - // Check out the profile. - $app->setUserState('com_users.edit.profile.id', $return); - - // Redirect back to the edit screen. - $this->setMessage(Text::_('COM_USERS_PROFILE_SAVE_SUCCESS')); - - $redirect = $app->getUserState('com_users.edit.profile.redirect'); - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = null; - } - - if (!$redirect) - { - $redirect = 'index.php?option=com_users&view=profile&layout=edit&hidemainmenu=1'; - } - - $this->setRedirect(Route::_($redirect, false)); - break; - - default: - // Clear the profile id from the session. - $app->setUserState('com_users.edit.profile.id', null); - - $redirect = $app->getUserState('com_users.edit.profile.redirect'); - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = null; - } - - if (!$redirect) - { - $redirect = 'index.php?option=com_users&view=profile&user_id=' . $return; - } - - // Redirect to the list screen. - $this->setMessage(Text::_('COM_USERS_PROFILE_SAVE_SUCCESS')); - $this->setRedirect(Route::_($redirect, false)); - break; - } - - // Flush the data from the session. - $app->setUserState('com_users.edit.profile.data', null); - } - - /** - * Method to cancel an edit. - * - * @return void - * - * @since 4.0.0 - */ - public function cancel() - { - // Check for request forgeries. - $this->checkToken(); - - // Flush the data from the session. - $this->app->setUserState('com_users.edit.profile', null); - - // Redirect to user profile. - $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); - } + /** + * Method to check out a user for editing and redirect to the edit form. + * + * @return boolean + * + * @since 1.6 + */ + public function edit() + { + $app = $this->app; + $user = $this->app->getIdentity(); + $loginUserId = (int) $user->get('id'); + + // Get the current user id. + $userId = $this->input->getInt('user_id'); + + // Check if the user is trying to edit another users profile. + if ($userId != $loginUserId) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return false; + } + + $cookieLogin = $user->get('cookieLogin'); + + // Check if the user logged in with a cookie + if (!empty($cookieLogin)) { + // If so, the user must login to edit the password and other data. + $app->enqueueMessage(Text::_('JGLOBAL_REMEMBER_MUST_LOGIN'), 'message'); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return false; + } + + // Set the user id for the user to edit in the session. + $app->setUserState('com_users.edit.profile.id', $userId); + + // Redirect to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit', false)); + + return true; + } + + /** + * Method to save a user's profile data. + * + * @return void|boolean + * + * @since 1.6 + * @throws \Exception + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\ProfileModel $model */ + $model = $this->getModel('Profile', 'Site'); + $user = $this->app->getIdentity(); + $userId = (int) $user->get('id'); + + // Get the user data. + $requestData = $app->input->post->get('jform', array(), 'array'); + + // Force the ID to this user. + $requestData['id'] = $userId; + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + // Send an object which can be modified through the plugin event + $objData = (object) $requestData; + $app->triggerEvent( + 'onContentNormaliseRequestData', + array('com_users.user', $objData, $form) + ); + $requestData = (array) $objData; + + // Validate the posted data. + $data = $model->validate($form, $requestData); + + // Check for errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Unset the passwords. + unset($requestData['password1'], $requestData['password2']); + + // Save the data in the session. + $app->setUserState('com_users.edit.profile.data', $requestData); + + // Redirect back to the edit screen. + $userId = (int) $app->getUserState('com_users.edit.profile.id'); + $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit&user_id=' . $userId, false)); + + return false; + } + + // Attempt to save the data. + $return = $model->save($data); + + // Check for errors. + if ($return === false) { + // Save the data in the session. + $app->setUserState('com_users.edit.profile.data', $data); + + // Redirect back to the edit screen. + $userId = (int) $app->getUserState('com_users.edit.profile.id'); + $this->setMessage(Text::sprintf('COM_USERS_PROFILE_SAVE_FAILED', $model->getError()), 'warning'); + $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit&user_id=' . $userId, false)); + + return false; + } + + // Redirect the user and adjust session state based on the chosen task. + switch ($this->getTask()) { + case 'apply': + // Check out the profile. + $app->setUserState('com_users.edit.profile.id', $return); + + // Redirect back to the edit screen. + $this->setMessage(Text::_('COM_USERS_PROFILE_SAVE_SUCCESS')); + + $redirect = $app->getUserState('com_users.edit.profile.redirect'); + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = null; + } + + if (!$redirect) { + $redirect = 'index.php?option=com_users&view=profile&layout=edit&hidemainmenu=1'; + } + + $this->setRedirect(Route::_($redirect, false)); + break; + + default: + // Clear the profile id from the session. + $app->setUserState('com_users.edit.profile.id', null); + + $redirect = $app->getUserState('com_users.edit.profile.redirect'); + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = null; + } + + if (!$redirect) { + $redirect = 'index.php?option=com_users&view=profile&user_id=' . $return; + } + + // Redirect to the list screen. + $this->setMessage(Text::_('COM_USERS_PROFILE_SAVE_SUCCESS')); + $this->setRedirect(Route::_($redirect, false)); + break; + } + + // Flush the data from the session. + $app->setUserState('com_users.edit.profile.data', null); + } + + /** + * Method to cancel an edit. + * + * @return void + * + * @since 4.0.0 + */ + public function cancel() + { + // Check for request forgeries. + $this->checkToken(); + + // Flush the data from the session. + $this->app->setUserState('com_users.edit.profile', null); + + // Redirect to user profile. + $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); + } } diff --git a/code/components/com_users/src/Controller/RegistrationController.php b/code/components/com_users/src/Controller/RegistrationController.php index 7328f0df..c2bd56f0 100644 --- a/code/components/com_users/src/Controller/RegistrationController.php +++ b/code/components/com_users/src/Controller/RegistrationController.php @@ -1,4 +1,5 @@ app->getIdentity(); - $input = $this->input; - $uParams = ComponentHelper::getParams('com_users'); - - // Check for admin activation. Don't allow non-super-admin to delete a super admin - if ($uParams->get('useractivation') != 2 && $user->get('id')) - { - $this->setRedirect('index.php'); - - return true; - } - - // If user registration or account activation is disabled, throw a 403. - if ($uParams->get('useractivation') == 0 || $uParams->get('allowUserRegistration') == 0) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - /** @var \Joomla\Component\Users\Site\Model\RegistrationModel $model */ - $model = $this->getModel('Registration', 'Site'); - $token = $input->getAlnum('token'); - - // Check that the token is in a valid format. - if ($token === null || strlen($token) !== 32) - { - throw new \Exception(Text::_('JINVALID_TOKEN'), 403); - } - - // Get the User ID - $userIdToActivate = $model->getUserIdFromToken($token); - - if (!$userIdToActivate) - { - $this->setMessage(Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND')); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return false; - } - - // Get the user we want to activate - $userToActivate = Factory::getUser($userIdToActivate); - - // Admin activation is on and admin is activating the account - if (($uParams->get('useractivation') == 2) && $userToActivate->getParam('activate', 0)) - { - // If a user admin is not logged in, redirect them to the login page with an error message - if (!$user->authorise('core.create', 'com_users') || !$user->authorise('core.manage', 'com_users')) - { - $activationUrl = 'index.php?option=com_users&task=registration.activate&token=' . $token; - $loginUrl = 'index.php?option=com_users&view=login&return=' . base64_encode($activationUrl); - - // In case we still run into this in the second step the user does not have the right permissions - $message = Text::_('COM_USERS_REGISTRATION_ACL_ADMIN_ACTIVATION_PERMISSIONS'); - - // When we are not logged in we should login - if ($user->guest) - { - $message = Text::_('COM_USERS_REGISTRATION_ACL_ADMIN_ACTIVATION'); - } - - $this->setMessage($message); - $this->setRedirect(Route::_($loginUrl, false)); - - return false; - } - } - - // Attempt to activate the user. - $return = $model->activate($token); - - // Check for errors. - if ($return === false) - { - // Redirect back to the home page. - $this->setMessage(Text::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect('index.php'); - - return false; - } - - $useractivation = $uParams->get('useractivation'); - - // Redirect to the login screen. - if ($useractivation == 0) - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_SAVE_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - } - elseif ($useractivation == 1) - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_ACTIVATE_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - } - elseif ($return->getParam('activate')) - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_VERIFY_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); - } - else - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_ADMINACTIVATE_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); - } - - return true; - } - - /** - * Method to register a user. - * - * @return boolean True on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function register() - { - // Check for request forgeries. - $this->checkToken(); - - // If registration is disabled - Redirect to login page. - if (ComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0) - { - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return false; - } - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\RegistrationModel $model */ - $model = $this->getModel('Registration', 'Site'); - - // Get the user data. - $requestData = $this->input->post->get('jform', array(), 'array'); - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - $data = $model->validate($form, $requestData); - - // Check for validation errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'error'); - } - else - { - $app->enqueueMessage($errors[$i], 'error'); - } - } - - // Save the data in the session. - $app->setUserState('com_users.registration.data', $requestData); - - // Redirect back to the registration screen. - $this->setRedirect(Route::_('index.php?option=com_users&view=registration', false)); - - return false; - } - - // Attempt to save the data. - $return = $model->register($data); - - // Check for errors. - if ($return === false) - { - // Save the data in the session. - $app->setUserState('com_users.registration.data', $data); - - // Redirect back to the edit screen. - $this->setMessage($model->getError(), 'error'); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration', false)); - - return false; - } - - // Flush the data from the session. - $app->setUserState('com_users.registration.data', null); - - // Redirect to the profile screen. - if ($return === 'adminactivate') - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_COMPLETE_VERIFY')); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); - } - elseif ($return === 'useractivate') - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_COMPLETE_ACTIVATE')); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); - } - else - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_SAVE_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - } - - return true; - } + /** + * Method to activate a user. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function activate() + { + $user = $this->app->getIdentity(); + $input = $this->input; + $uParams = ComponentHelper::getParams('com_users'); + + // Check for admin activation. Don't allow non-super-admin to delete a super admin + if ($uParams->get('useractivation') != 2 && $user->get('id')) { + $this->setRedirect('index.php'); + + return true; + } + + // If user registration or account activation is disabled, throw a 403. + if ($uParams->get('useractivation') == 0 || $uParams->get('allowUserRegistration') == 0) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + /** @var \Joomla\Component\Users\Site\Model\RegistrationModel $model */ + $model = $this->getModel('Registration', 'Site'); + $token = $input->getAlnum('token'); + + // Check that the token is in a valid format. + if ($token === null || strlen($token) !== 32) { + throw new \Exception(Text::_('JINVALID_TOKEN'), 403); + } + + // Get the User ID + $userIdToActivate = $model->getUserIdFromToken($token); + + if (!$userIdToActivate) { + $this->setMessage(Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND')); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return false; + } + + // Get the user we want to activate + $userToActivate = Factory::getUser($userIdToActivate); + + // Admin activation is on and admin is activating the account + if (($uParams->get('useractivation') == 2) && $userToActivate->getParam('activate', 0)) { + // If a user admin is not logged in, redirect them to the login page with an error message + if (!$user->authorise('core.create', 'com_users') || !$user->authorise('core.manage', 'com_users')) { + $activationUrl = 'index.php?option=com_users&task=registration.activate&token=' . $token; + $loginUrl = 'index.php?option=com_users&view=login&return=' . base64_encode($activationUrl); + + // In case we still run into this in the second step the user does not have the right permissions + $message = Text::_('COM_USERS_REGISTRATION_ACL_ADMIN_ACTIVATION_PERMISSIONS'); + + // When we are not logged in we should login + if ($user->guest) { + $message = Text::_('COM_USERS_REGISTRATION_ACL_ADMIN_ACTIVATION'); + } + + $this->setMessage($message); + $this->setRedirect(Route::_($loginUrl, false)); + + return false; + } + } + + // Attempt to activate the user. + $return = $model->activate($token); + + // Check for errors. + if ($return === false) { + // Redirect back to the home page. + $this->setMessage(Text::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect('index.php'); + + return false; + } + + $useractivation = $uParams->get('useractivation'); + + // Redirect to the login screen. + if ($useractivation == 0) { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_SAVE_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + } elseif ($useractivation == 1) { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_ACTIVATE_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + } elseif ($return->getParam('activate')) { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_VERIFY_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); + } else { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_ADMINACTIVATE_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); + } + + return true; + } + + /** + * Method to register a user. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function register() + { + // Check for request forgeries. + $this->checkToken(); + + // If registration is disabled - Redirect to login page. + if (ComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0) { + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return false; + } + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\RegistrationModel $model */ + $model = $this->getModel('Registration', 'Site'); + + // Get the user data. + $requestData = $this->input->post->get('jform', array(), 'array'); + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + $data = $model->validate($form, $requestData); + + // Check for validation errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'error'); + } else { + $app->enqueueMessage($errors[$i], 'error'); + } + } + + // Save the data in the session. + $app->setUserState('com_users.registration.data', $requestData); + + // Redirect back to the registration screen. + $this->setRedirect(Route::_('index.php?option=com_users&view=registration', false)); + + return false; + } + + // Attempt to save the data. + $return = $model->register($data); + + // Check for errors. + if ($return === false) { + // Save the data in the session. + $app->setUserState('com_users.registration.data', $data); + + // Redirect back to the edit screen. + $this->setMessage($model->getError(), 'error'); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration', false)); + + return false; + } + + // Flush the data from the session. + $app->setUserState('com_users.registration.data', null); + + // Redirect to the profile screen. + if ($return === 'adminactivate') { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_COMPLETE_VERIFY')); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); + } elseif ($return === 'useractivate') { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_COMPLETE_ACTIVATE')); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); + } else { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_SAVE_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + } + + return true; + } } diff --git a/code/components/com_users/src/Controller/RemindController.php b/code/components/com_users/src/Controller/RemindController.php index f3d4aff8..895607b5 100644 --- a/code/components/com_users/src/Controller/RemindController.php +++ b/code/components/com_users/src/Controller/RemindController.php @@ -1,4 +1,5 @@ checkToken('post'); - - /** @var \Joomla\Component\Users\Site\Model\RemindModel $model */ - $model = $this->getModel('Remind', 'Site'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Submit the password reset request. - $return = $model->processRemindRequest($data); - - // Check for a hard error. - if ($return == false && JDEBUG) - { - // The request failed. - // Go back to the request form. - $message = Text::sprintf('COM_USERS_REMIND_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'notice'); - - return false; - } - - // To not expose if the user exists or not we send a generic message. - $message = Text::_('COM_USERS_REMIND_REQUEST'); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message, 'notice'); - - return true; - } + /** + * Method to request a username reminder. + * + * @return boolean + * + * @since 1.6 + */ + public function remind() + { + // Check the request token. + $this->checkToken('post'); + + /** @var \Joomla\Component\Users\Site\Model\RemindModel $model */ + $model = $this->getModel('Remind', 'Site'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Submit the password reset request. + $return = $model->processRemindRequest($data); + + // Check for a hard error. + if ($return == false && JDEBUG) { + // The request failed. + // Go back to the request form. + $message = Text::sprintf('COM_USERS_REMIND_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'notice'); + + return false; + } + + // To not expose if the user exists or not we send a generic message. + $message = Text::_('COM_USERS_REMIND_REQUEST'); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message, 'notice'); + + return true; + } } diff --git a/code/components/com_users/src/Controller/ResetController.php b/code/components/com_users/src/Controller/ResetController.php index f8566765..b32e3606 100644 --- a/code/components/com_users/src/Controller/ResetController.php +++ b/code/components/com_users/src/Controller/ResetController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Users\Site\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Users\Site\Controller; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\Router\Route; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + /** * Reset controller class for Users. * @@ -21,177 +25,155 @@ */ class ResetController extends BaseController { - /** - * Method to request a password reset. - * - * @return boolean - * - * @since 1.6 - */ - public function request() - { - // Check the request token. - $this->checkToken('post'); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ - $model = $this->getModel('Reset', 'Site'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Submit the password reset request. - $return = $model->processResetRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception && JDEBUG) - { - // Get the error message to display. - if ($app->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_USERS_RESET_REQUEST_ERROR'); - } - - // Go back to the request form. - $this->setRedirect(Route::_('index.php?option=com_users&view=reset', false), $message, 'error'); - - return false; - } - elseif ($return === false && JDEBUG) - { - // The request failed. - // Go back to the request form. - $message = Text::sprintf('COM_USERS_RESET_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=reset', false), $message, 'notice'); - - return false; - } - - // To not expose if the user exists or not we send a generic message. - $message = Text::_('COM_USERS_RESET_REQUEST'); - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice'); - - return true; - } - - /** - * Method to confirm the password request. - * - * @return boolean - * - * @access public - * @since 1.6 - */ - public function confirm() - { - // Check the request token. - $this->checkToken('request'); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ - $model = $this->getModel('Reset', 'Site'); - $data = $this->input->get('jform', array(), 'array'); - - // Confirm the password reset request. - $return = $model->processResetConfirm($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if ($app->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_USERS_RESET_CONFIRM_ERROR'); - } - - // Go back to the confirm form. - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Confirm failed. - // Go back to the confirm form. - $message = Text::sprintf('COM_USERS_RESET_CONFIRM_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice'); - - return false; - } - else - { - // Confirm succeeded. - // Proceed to step three. - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false)); - - return true; - } - } - - /** - * Method to complete the password reset process. - * - * @return boolean - * - * @since 1.6 - */ - public function complete() - { - // Check for request forgeries - $this->checkToken('post'); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ - $model = $this->getModel('Reset', 'Site'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Complete the password reset request. - $return = $model->processResetComplete($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if ($app->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_USERS_RESET_COMPLETE_ERROR'); - } - - // Go back to the complete form. - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Complete failed. - // Go back to the complete form. - $message = Text::sprintf('COM_USERS_RESET_COMPLETE_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false), $message, 'notice'); - - return false; - } - else - { - // Complete succeeded. - // Proceed to the login form. - $message = Text::_('COM_USERS_RESET_COMPLETE_SUCCESS'); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message); - - return true; - } - } + /** + * Method to request a password reset. + * + * @return boolean + * + * @since 1.6 + */ + public function request() + { + // Check the request token. + $this->checkToken('post'); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ + $model = $this->getModel('Reset', 'Site'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Submit the password reset request. + $return = $model->processResetRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception && JDEBUG) { + // Get the error message to display. + if ($app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_USERS_RESET_REQUEST_ERROR'); + } + + // Go back to the request form. + $this->setRedirect(Route::_('index.php?option=com_users&view=reset', false), $message, 'error'); + + return false; + } elseif ($return === false && JDEBUG) { + // The request failed. + // Go back to the request form. + $message = Text::sprintf('COM_USERS_RESET_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=reset', false), $message, 'notice'); + + return false; + } + + // To not expose if the user exists or not we send a generic message. + $message = Text::_('COM_USERS_RESET_REQUEST'); + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice'); + + return true; + } + + /** + * Method to confirm the password request. + * + * @return boolean + * + * @access public + * @since 1.6 + */ + public function confirm() + { + // Check the request token. + $this->checkToken('request'); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ + $model = $this->getModel('Reset', 'Site'); + $data = $this->input->get('jform', array(), 'array'); + + // Confirm the password reset request. + $return = $model->processResetConfirm($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if ($app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_USERS_RESET_CONFIRM_ERROR'); + } + + // Go back to the confirm form. + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Confirm failed. + // Go back to the confirm form. + $message = Text::sprintf('COM_USERS_RESET_CONFIRM_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice'); + + return false; + } else { + // Confirm succeeded. + // Proceed to step three. + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false)); + + return true; + } + } + + /** + * Method to complete the password reset process. + * + * @return boolean + * + * @since 1.6 + */ + public function complete() + { + // Check for request forgeries + $this->checkToken('post'); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ + $model = $this->getModel('Reset', 'Site'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Complete the password reset request. + $return = $model->processResetComplete($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if ($app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_USERS_RESET_COMPLETE_ERROR'); + } + + // Go back to the complete form. + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Complete failed. + // Go back to the complete form. + $message = Text::sprintf('COM_USERS_RESET_COMPLETE_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false), $message, 'notice'); + + return false; + } else { + // Complete succeeded. + // Proceed to the login form. + $message = Text::_('COM_USERS_RESET_COMPLETE_SUCCESS'); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message); + + return true; + } + } } diff --git a/code/components/com_users/src/Controller/UserController.php b/code/components/com_users/src/Controller/UserController.php index c5c81f61..df9861e3 100644 --- a/code/components/com_users/src/Controller/UserController.php +++ b/code/components/com_users/src/Controller/UserController.php @@ -1,4 +1,5 @@ checkToken('post'); - - $input = $this->input->getInputForRequestMethod(); - - // Populate the data array: - $data = array(); - - $data['return'] = base64_decode($input->get('return', '', 'BASE64')); - $data['username'] = $input->get('username', '', 'USERNAME'); - $data['password'] = $input->get('password', '', 'RAW'); - $data['secretkey'] = $input->get('secretkey', '', 'RAW'); - - // Check for a simple menu item id - if (is_numeric($data['return'])) - { - if (Multilanguage::isEnabled()) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('language')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('client_id') . ' = 0') - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $data['return'], ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $language = $db->loadResult(); - } - catch (\RuntimeException $e) - { - return; - } - - if ($language !== '*') - { - $lang = '&lang=' . $language; - } - else - { - $lang = ''; - } - } - else - { - $lang = ''; - } - - $data['return'] = 'index.php?Itemid=' . $data['return'] . $lang; - } - else - { - // Don't redirect to an external URL. - if (!Uri::isInternal($data['return'])) - { - $data['return'] = ''; - } - } - - // Set the return URL if empty. - if (empty($data['return'])) - { - $data['return'] = 'index.php?option=com_users&view=profile'; - } - - // Set the return URL in the user state to allow modification by plugins - $this->app->setUserState('users.login.form.return', $data['return']); - - // Get the log in options. - $options = array(); - $options['remember'] = $this->input->getBool('remember', false); - $options['return'] = $data['return']; - - // Get the log in credentials. - $credentials = array(); - $credentials['username'] = $data['username']; - $credentials['password'] = $data['password']; - $credentials['secretkey'] = $data['secretkey']; - - // Perform the log in. - if (true !== $this->app->login($credentials, $options)) - { - // Login failed ! - // Clear user name, password and secret key before sending the login form back to the user. - $data['remember'] = (int) $options['remember']; - $data['username'] = ''; - $data['password'] = ''; - $data['secretkey'] = ''; - $this->app->setUserState('users.login.form.data', $data); - $this->app->redirect(Route::_('index.php?option=com_users&view=login', false)); - } - - // Success - if ($options['remember'] == true) - { - $this->app->setUserState('rememberLogin', true); - } - - $this->app->setUserState('users.login.form.data', array()); - $this->app->redirect(Route::_($this->app->getUserState('users.login.form.return'), false)); - } - - /** - * Method to log out a user. - * - * @return void - * - * @since 1.6 - */ - public function logout() - { - $this->checkToken('request'); - - $app = $this->app; - - // Prepare the logout options. - $options = array( - 'clientid' => $app->get('shared_session', '0') ? null : 0, - ); - - // Perform the log out. - $error = $app->logout(null, $options); - $input = $app->input->getInputForRequestMethod(); - - // Check if the log out succeeded. - if ($error instanceof \Exception) - { - $app->redirect(Route::_('index.php?option=com_users&view=login', false)); - } - - // Get the return URL from the request and validate that it is internal. - $return = $input->get('return', '', 'BASE64'); - $return = base64_decode($return); - - // Check for a simple menu item id - if (is_numeric($return)) - { - if (Multilanguage::isEnabled()) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('language')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('client_id') . ' = 0') - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $return, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $language = $db->loadResult(); - } - catch (\RuntimeException $e) - { - return; - } - - if ($language !== '*') - { - $lang = '&lang=' . $language; - } - else - { - $lang = ''; - } - } - else - { - $lang = ''; - } - - $return = 'index.php?Itemid=' . $return . $lang; - } - else - { - // Don't redirect to an external URL. - if (!Uri::isInternal($return)) - { - $return = ''; - } - } - - // In case redirect url is not set, redirect user to homepage - if (empty($return)) - { - $return = Uri::root(); - } - - // Redirect the user. - $app->redirect(Route::_($return, false)); - } - - /** - * Method to logout directly and redirect to page. - * - * @return void - * - * @since 3.5 - */ - public function menulogout() - { - // Get the ItemID of the page to redirect after logout - $app = $this->app; - $active = $app->getMenu()->getActive(); - $itemid = $active ? $active->getParams()->get('logout') : 0; - - // Get the language of the page when multilang is on - if (Multilanguage::isEnabled()) - { - if ($itemid) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('language')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('client_id') . ' = 0') - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $itemid, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $language = $db->loadResult(); - } - catch (\RuntimeException $e) - { - return; - } - - if ($language !== '*') - { - $lang = '&lang=' . $language; - } - else - { - $lang = ''; - } - - // URL to redirect after logout - $url = 'index.php?Itemid=' . $itemid . $lang; - } - else - { - // Logout is set to default. Get the home page ItemID - $lang_code = $app->input->cookie->getString(ApplicationHelper::getHash('language')); - $item = $app->getMenu()->getDefault($lang_code); - $itemid = $item->id; - - // Redirect to Home page after logout - $url = 'index.php?Itemid=' . $itemid; - } - } - else - { - // URL to redirect after logout, default page if no ItemID is set - $url = $itemid ? 'index.php?Itemid=' . $itemid : Uri::root(); - } - - // Logout and redirect - $this->setRedirect('index.php?option=com_users&task=user.logout&' . Session::getFormToken() . '=1&return=' . base64_encode($url)); - } - - /** - * Method to request a username reminder. - * - * @return boolean - * - * @since 1.6 - */ - public function remind() - { - // Check the request token. - $this->checkToken('post'); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\RemindModel $model */ - $model = $this->getModel('Remind', 'Site'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Submit the username remind request. - $return = $model->processRemindRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - $message = $app->get('error_reporting') - ? $return->getMessage() - : Text::_('COM_USERS_REMIND_REQUEST_ERROR'); - - // Go back to the complete form. - $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'error'); - - return false; - } - - if ($return === false) - { - // Go back to the complete form. - $message = Text::sprintf('COM_USERS_REMIND_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'notice'); - - return false; - } - - // Proceed to the login form. - $message = Text::_('COM_USERS_REMIND_REQUEST_SUCCESS'); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message); - - return true; - } - - /** - * Method to resend a user. - * - * @return void - * - * @since 1.6 - */ - public function resend() - { - // Check for request forgeries - // $this->checkToken('post'); - } + /** + * Method to log in a user. + * + * @return void + * + * @since 1.6 + */ + public function login() + { + $this->checkToken('post'); + + $input = $this->input->getInputForRequestMethod(); + + // Populate the data array: + $data = array(); + + $data['return'] = base64_decode($input->get('return', '', 'BASE64')); + $data['username'] = $input->get('username', '', 'USERNAME'); + $data['password'] = $input->get('password', '', 'RAW'); + $data['secretkey'] = $input->get('secretkey', '', 'RAW'); + + // Check for a simple menu item id + if (is_numeric($data['return'])) { + $itemId = (int) $data['return']; + $data['return'] = 'index.php?Itemid=' . $itemId; + + if (Multilanguage::isEnabled()) { + $language = $this->getModel('Login', 'Site')->getMenuLanguage($itemId); + + if ($language !== '*') { + $data['return'] .= '&lang=' . $language; + } + } + } elseif (!Uri::isInternal($data['return'])) { + // Don't redirect to an external URL. + $data['return'] = ''; + } + + // Set the return URL if empty. + if (empty($data['return'])) { + $data['return'] = 'index.php?option=com_users&view=profile'; + } + + // Set the return URL in the user state to allow modification by plugins + $this->app->setUserState('users.login.form.return', $data['return']); + + // Get the log in options. + $options = array(); + $options['remember'] = $this->input->getBool('remember', false); + $options['return'] = $data['return']; + + // Get the log in credentials. + $credentials = array(); + $credentials['username'] = $data['username']; + $credentials['password'] = $data['password']; + $credentials['secretkey'] = $data['secretkey']; + + // Perform the log in. + if (true !== $this->app->login($credentials, $options)) { + // Login failed ! + // Clear user name, password and secret key before sending the login form back to the user. + $data['remember'] = (int) $options['remember']; + $data['username'] = ''; + $data['password'] = ''; + $data['secretkey'] = ''; + $this->app->setUserState('users.login.form.data', $data); + $this->app->redirect(Route::_('index.php?option=com_users&view=login', false)); + } + + // Success + if ($options['remember'] == true) { + $this->app->setUserState('rememberLogin', true); + } + + $this->app->setUserState('users.login.form.data', array()); + $this->app->redirect(Route::_($this->app->getUserState('users.login.form.return'), false)); + } + + /** + * Method to log out a user. + * + * @return void + * + * @since 1.6 + */ + public function logout() + { + $this->checkToken('request'); + + $app = $this->app; + + // Prepare the logout options. + $options = array( + 'clientid' => $app->get('shared_session', '0') ? null : 0, + ); + + // Perform the log out. + $error = $app->logout(null, $options); + $input = $app->input->getInputForRequestMethod(); + + // Check if the log out succeeded. + if ($error instanceof \Exception) { + $app->redirect(Route::_('index.php?option=com_users&view=login', false)); + } + + // Get the return URL from the request and validate that it is internal. + $return = $input->get('return', '', 'BASE64'); + $return = base64_decode($return); + + // Check for a simple menu item id + if (is_numeric($return)) { + $return = 'index.php?Itemid=' . $return; + + if (Multilanguage::isEnabled()) { + $language = $this->getModel('Login', 'Site')->getMenuLanguage($return); + + if ($language !== '*') { + $return .= '&lang=' . $language; + } + } + } elseif (!Uri::isInternal($return)) { + $return = ''; + } + + // In case redirect url is not set, redirect user to homepage + if (empty($return)) { + $return = Uri::root(); + } + + // Redirect the user. + $app->redirect(Route::_($return, false)); + } + + /** + * Method to logout directly and redirect to page. + * + * @return void + * + * @since 3.5 + */ + public function menulogout() + { + // Get the ItemID of the page to redirect after logout + $app = $this->app; + $active = $app->getMenu()->getActive(); + $itemid = $active ? $active->getParams()->get('logout') : 0; + + // Get the language of the page when multilang is on + if (Multilanguage::isEnabled()) { + if ($itemid) { + $language = $this->getModel('Login', 'Site')->getMenuLanguage($itemid); + + // URL to redirect after logout + $url = 'index.php?Itemid=' . $itemid . ($language !== '*' ? '&lang=' . $language : ''); + } else { + // Logout is set to default. Get the home page ItemID + $lang_code = $app->input->cookie->getString(ApplicationHelper::getHash('language')); + $item = $app->getMenu()->getDefault($lang_code); + $itemid = $item->id; + + // Redirect to Home page after logout + $url = 'index.php?Itemid=' . $itemid; + } + } else { + // URL to redirect after logout, default page if no ItemID is set + $url = $itemid ? 'index.php?Itemid=' . $itemid : Uri::root(); + } + + // Logout and redirect + $this->setRedirect('index.php?option=com_users&task=user.logout&' . Session::getFormToken() . '=1&return=' . base64_encode($url)); + } + + /** + * Method to request a username reminder. + * + * @return boolean + * + * @since 1.6 + */ + public function remind() + { + // Check the request token. + $this->checkToken('post'); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\RemindModel $model */ + $model = $this->getModel('Remind', 'Site'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Submit the username remind request. + $return = $model->processRemindRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + $message = $app->get('error_reporting') + ? $return->getMessage() + : Text::_('COM_USERS_REMIND_REQUEST_ERROR'); + + // Go back to the complete form. + $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'error'); + + return false; + } + + if ($return === false) { + // Go back to the complete form. + $message = Text::sprintf('COM_USERS_REMIND_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'notice'); + + return false; + } + + // Proceed to the login form. + $message = Text::_('COM_USERS_REMIND_REQUEST_SUCCESS'); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message); + + return true; + } + + /** + * Method to resend a user. + * + * @return void + * + * @since 1.6 + */ + public function resend() + { + // Check for request forgeries + // $this->checkToken('post'); + } } diff --git a/code/components/com_users/src/Model/BackupcodesModel.php b/code/components/com_users/src/Model/BackupcodesModel.php new file mode 100644 index 00000000..48b737e3 --- /dev/null +++ b/code/components/com_users/src/Model/BackupcodesModel.php @@ -0,0 +1,24 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Site\Model; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Model for managing backup codes + * + * @since 4.2.0 + */ +class BackupcodesModel extends \Joomla\Component\Users\Administrator\Model\BackupcodesModel +{ +} diff --git a/code/components/com_users/src/Model/CaptiveModel.php b/code/components/com_users/src/Model/CaptiveModel.php new file mode 100644 index 00000000..77570d08 --- /dev/null +++ b/code/components/com_users/src/Model/CaptiveModel.php @@ -0,0 +1,24 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Site\Model; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Captive Multi-factor Authentication page's model + * + * @since 4.2.0 + */ +class CaptiveModel extends \Joomla\Component\Users\Administrator\Model\CaptiveModel +{ +} diff --git a/code/components/com_users/src/Model/LoginModel.php b/code/components/com_users/src/Model/LoginModel.php index e9ddf6af..23329ba2 100644 --- a/code/components/com_users/src/Model/LoginModel.php +++ b/code/components/com_users/src/Model/LoginModel.php @@ -1,4 +1,5 @@ loadForm('com_users.login', 'login', array('load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered login form data. - $app = Factory::getApplication(); - $data = $app->getUserState('users.login.form.data', array()); - - $input = $app->input->getInputForRequestMethod(); - - // Check for return URL from the request first - if ($return = $input->get('return', '', 'BASE64')) - { - $data['return'] = base64_decode($return); - - if (!Uri::isInternal($data['return'])) - { - $data['return'] = ''; - } - } - - $app->setUserState('users.login.form.data', $data); - - $this->preprocessData('com_users.login', $data); - - return $data; - } - - /** - * Method to auto-populate the model state. - * - * Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_users'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, $group); - } + /** + * Method to get the login form. + * + * The base form is loaded from XML and then an event is fired + * for users plugins to extend the form with extra fields. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.login', 'login', array('load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered login form data. + $app = Factory::getApplication(); + $data = $app->getUserState('users.login.form.data', array()); + + $input = $app->input->getInputForRequestMethod(); + + // Check for return URL from the request first + if ($return = $input->get('return', '', 'BASE64')) { + $data['return'] = base64_decode($return); + + if (!Uri::isInternal($data['return'])) { + $data['return'] = ''; + } + } + + $app->setUserState('users.login.form.data', $data); + + $this->preprocessData('com_users.login', $data); + + return $data; + } + + /** + * Method to auto-populate the model state. + * + * Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_users'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, $group); + } + + /** + * Returns the language for the given menu id. + * + * @param int $id The menu id + * + * @return string + * + * @since 4.2.0 + */ + public function getMenuLanguage(int $id): string + { + if (!Multilanguage::isEnabled()) { + return ''; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('language')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('client_id') . ' = 0') + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + return $db->loadResult(); + } catch (\RuntimeException $e) { + return ''; + } + } } diff --git a/code/components/com_users/src/Model/MethodModel.php b/code/components/com_users/src/Model/MethodModel.php new file mode 100644 index 00000000..52005590 --- /dev/null +++ b/code/components/com_users/src/Model/MethodModel.php @@ -0,0 +1,24 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Site\Model; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Multi-factor Authentication Method management model + * + * @since 4.2.0 + */ +class MethodModel extends \Joomla\Component\Users\Administrator\Model\MethodModel +{ +} diff --git a/code/components/com_users/src/Model/MethodsModel.php b/code/components/com_users/src/Model/MethodsModel.php new file mode 100644 index 00000000..1300acc0 --- /dev/null +++ b/code/components/com_users/src/Model/MethodsModel.php @@ -0,0 +1,24 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Site\Model; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Multi-factor Authentication Methods list page's model + * + * @since 4.2.0 + */ +class MethodsModel extends \Joomla\Component\Users\Administrator\Model\MethodsModel +{ +} diff --git a/code/components/com_users/src/Model/ProfileModel.php b/code/components/com_users/src/Model/ProfileModel.php index 6742e8a6..c265ce7a 100644 --- a/code/components/com_users/src/Model/ProfileModel.php +++ b/code/components/com_users/src/Model/ProfileModel.php @@ -1,4 +1,5 @@ array('validate' => 'user') - ), $config - ); - - parent::__construct($config, $factory, $formFactory); - } - - /** - * Method to get the profile form data. - * - * The base form data is loaded and then an event is fired - * for users plugins to extend the data. - * - * @return User - * - * @since 1.6 - * @throws \Exception - */ - public function getData() - { - if ($this->data === null) - { - $userId = $this->getState('user.id'); - - // Initialise the table with Joomla\CMS\User\User. - $this->data = new User($userId); - - // Set the base user data. - $this->data->email1 = $this->data->get('email'); - - // Override the base user data with any data in the session. - $temp = (array) Factory::getApplication()->getUserState('com_users.edit.profile.data', array()); - - foreach ($temp as $k => $v) - { - $this->data->$k = $v; - } - - // Unset the passwords. - unset($this->data->password1, $this->data->password2); - - $registry = new Registry($this->data->params); - $this->data->params = $registry->toArray(); - } - - return $this->data; - } - - /** - * Method to get the profile form. - * - * The base form is loaded from XML and then an event is fired - * for users plugins to extend the form with extra fields. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.profile', 'profile', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Check for username compliance and parameter set - $isUsernameCompliant = true; - $username = $loadData ? $form->getValue('username') : $this->loadFormData()->username; - - if ($username) - { - $isUsernameCompliant = !(preg_match('#[<>"\'%;()&\\\\]|\\.\\./#', $username) || strlen(utf8_decode($username)) < 2 - || trim($username) !== $username); - } - - $this->setState('user.username.compliant', $isUsernameCompliant); - - if ($isUsernameCompliant && !ComponentHelper::getParams('com_users')->get('change_login_name')) - { - $form->setFieldAttribute('username', 'class', ''); - $form->setFieldAttribute('username', 'filter', ''); - $form->setFieldAttribute('username', 'description', 'COM_USERS_PROFILE_NOCHANGE_USERNAME_DESC'); - $form->setFieldAttribute('username', 'validate', ''); - $form->setFieldAttribute('username', 'message', ''); - $form->setFieldAttribute('username', 'readonly', 'true'); - $form->setFieldAttribute('username', 'required', 'false'); - } - - // When multilanguage is set, a user's default site language should also be a Content Language - if (Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); - } - - // If the user needs to change their password, mark the password fields as required - if (Factory::getUser()->requireReset) - { - $form->setFieldAttribute('password1', 'required', 'true'); - $form->setFieldAttribute('password2', 'required', 'true'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $data = $this->getData(); - - $this->preprocessData('com_users.profile', $data, 'user'); - - return $data; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @throws \Exception if there is an error in the form event. - * - * @since 1.6 - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - if (ComponentHelper::getParams('com_users')->get('frontend_userparams')) - { - $form->loadFile('frontend', false); - - if (Factory::getUser()->authorise('core.login.admin')) - { - $form->loadFile('frontend_admin', false); - } - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_users'); - - // Get the user id. - $userId = Factory::getApplication()->getUserState('com_users.edit.profile.id'); - $userId = !empty($userId) ? $userId : (int) Factory::getUser()->get('id'); - - // Set the user id. - $this->setState('user.id', $userId); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return mixed The user id on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function save($data) - { - $userId = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id'); - - $user = new User($userId); - - // Prepare the data for the user object. - $data['email'] = PunycodeHelper::emailToPunycode($data['email1']); - $data['password'] = $data['password1']; - - // Unset the username if it should not be overwritten - $isUsernameCompliant = $this->getState('user.username.compliant'); - - if ($isUsernameCompliant && !ComponentHelper::getParams('com_users')->get('change_login_name')) - { - unset($data['username']); - } - - // Unset block and sendEmail so they do not get overwritten - unset($data['block'], $data['sendEmail']); - - // Handle the two factor authentication setup - if (array_key_exists('twofactor', $data)) - { - $model = $this->bootComponent('com_users')->getMVCFactory() - ->createModel('User', 'Administrator'); - - $twoFactorMethod = $data['twofactor']['method']; - - // Get the current One Time Password (two factor auth) configuration - $otpConfig = $model->getOtpConfig($userId); - - if ($twoFactorMethod !== 'none') - { - // Run the plugins - PluginHelper::importPlugin('twofactorauth'); - $otpConfigReplies = Factory::getApplication()->triggerEvent('onUserTwofactorApplyConfiguration', array($twoFactorMethod)); - - // Look for a valid reply - foreach ($otpConfigReplies as $reply) - { - if (!is_object($reply) || empty($reply->method) || ($reply->method != $twoFactorMethod)) - { - continue; - } - - $otpConfig->method = $reply->method; - $otpConfig->config = $reply->config; - - break; - } - - // Save OTP configuration. - $model->setOtpConfig($userId, $otpConfig); - - // Generate one time emergency passwords if required (depleted or not set) - if (empty($otpConfig->otep)) - { - $model->generateOteps($userId); - } - } - else - { - $otpConfig->method = 'none'; - $otpConfig->config = array(); - $model->setOtpConfig($userId, $otpConfig); - } - - // Unset the raw data - unset($data['twofactor']); - - // Reload the user record with the updated OTP configuration - $user->load($userId); - } - - // Bind the data. - if (!$user->bind($data)) - { - $this->setError($user->getError()); - - return false; - } - - // Load the users plugin group. - PluginHelper::importPlugin('user'); - - // Retrieve the user groups so they don't get overwritten - unset($user->groups); - $user->groups = Access::getGroupsByUser($user->id, false); - - // Store the data. - if (!$user->save()) - { - $this->setError($user->getError()); - - return false; - } - - // Destroy all active sessions for the user after changing the password - if ($data['password']) - { - UserHelper::destroyUserSessions($user->id, true); - } - - return $user->id; - } - - /** - * Gets the configuration forms for all two-factor authentication methods - * in an array. - * - * @param integer $userId The user ID to load the forms for (optional) - * - * @return array - * - * @since 3.2 - */ - public function getTwofactorform($userId = null) - { - $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id'); - - $model = $this->bootComponent('com_users')->getMVCFactory() - ->createModel('User', 'Administrator'); - - $otpConfig = $model->getOtpConfig($userId); - - PluginHelper::importPlugin('twofactorauth'); - - return Factory::getApplication()->triggerEvent('onUserTwofactorShowConfiguration', array($otpConfig, $userId)); - } - - /** - * Returns the one time password (OTP) – a.k.a. two factor authentication – - * configuration for a particular user. - * - * @param integer $userId The numeric ID of the user - * - * @return \stdClass An object holding the OTP configuration for this user - * - * @since 3.2 - */ - public function getOtpConfig($userId = null) - { - $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id'); - - $model = $this->bootComponent('com_users') - ->getMVCFactory()->createModel('User', 'Administrator'); - - return $model->getOtpConfig($userId); - } + /** + * @var object The user profile data. + * @since 1.6 + */ + protected $data; + + /** + * Constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * @param FormFactoryInterface $formFactory The form factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config = array_merge( + array( + 'events_map' => array('validate' => 'user') + ), + $config + ); + + parent::__construct($config, $factory, $formFactory); + } + + /** + * Method to get the profile form data. + * + * The base form data is loaded and then an event is fired + * for users plugins to extend the data. + * + * @return User + * + * @since 1.6 + * @throws \Exception + */ + public function getData() + { + if ($this->data === null) { + $userId = $this->getState('user.id'); + + // Initialise the table with Joomla\CMS\User\User. + $this->data = new User($userId); + + // Set the base user data. + $this->data->email1 = $this->data->get('email'); + + // Override the base user data with any data in the session. + $temp = (array) Factory::getApplication()->getUserState('com_users.edit.profile.data', array()); + + foreach ($temp as $k => $v) { + $this->data->$k = $v; + } + + // Unset the passwords. + unset($this->data->password1, $this->data->password2); + + $registry = new Registry($this->data->params); + $this->data->params = $registry->toArray(); + } + + return $this->data; + } + + /** + * Method to get the profile form. + * + * The base form is loaded from XML and then an event is fired + * for users plugins to extend the form with extra fields. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.profile', 'profile', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Check for username compliance and parameter set + $isUsernameCompliant = true; + $username = $loadData ? $form->getValue('username') : $this->loadFormData()->username; + + if ($username) { + $isUsernameCompliant = !(preg_match('#[<>"\'%;()&\\\\]|\\.\\./#', $username) || strlen(utf8_decode($username)) < 2 + || trim($username) !== $username); + } + + $this->setState('user.username.compliant', $isUsernameCompliant); + + if ($isUsernameCompliant && !ComponentHelper::getParams('com_users')->get('change_login_name')) { + $form->setFieldAttribute('username', 'class', ''); + $form->setFieldAttribute('username', 'filter', ''); + $form->setFieldAttribute('username', 'description', 'COM_USERS_PROFILE_NOCHANGE_USERNAME_DESC'); + $form->setFieldAttribute('username', 'validate', ''); + $form->setFieldAttribute('username', 'message', ''); + $form->setFieldAttribute('username', 'readonly', 'true'); + $form->setFieldAttribute('username', 'required', 'false'); + } + + // When multilanguage is set, a user's default site language should also be a Content Language + if (Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); + } + + // If the user needs to change their password, mark the password fields as required + if (Factory::getUser()->requireReset) { + $form->setFieldAttribute('password1', 'required', 'true'); + $form->setFieldAttribute('password2', 'required', 'true'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $data = $this->getData(); + + $this->preprocessData('com_users.profile', $data, 'user'); + + return $data; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @throws \Exception if there is an error in the form event. + * + * @since 1.6 + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + if (ComponentHelper::getParams('com_users')->get('frontend_userparams')) { + $form->loadFile('frontend', false); + + if (Factory::getUser()->authorise('core.login.admin')) { + $form->loadFile('frontend_admin', false); + } + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_users'); + + // Get the user id. + $userId = Factory::getApplication()->getUserState('com_users.edit.profile.id'); + $userId = !empty($userId) ? $userId : (int) Factory::getUser()->get('id'); + + // Set the user id. + $this->setState('user.id', $userId); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return mixed The user id on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function save($data) + { + $userId = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id'); + + $user = new User($userId); + + // Prepare the data for the user object. + $data['email'] = PunycodeHelper::emailToPunycode($data['email1']); + $data['password'] = $data['password1']; + + // Unset the username if it should not be overwritten + $isUsernameCompliant = $this->getState('user.username.compliant'); + + if ($isUsernameCompliant && !ComponentHelper::getParams('com_users')->get('change_login_name')) { + unset($data['username']); + } + + // Unset block and sendEmail so they do not get overwritten + unset($data['block'], $data['sendEmail']); + + // Bind the data. + if (!$user->bind($data)) { + $this->setError($user->getError()); + + return false; + } + + // Load the users plugin group. + PluginHelper::importPlugin('user'); + + // Retrieve the user groups so they don't get overwritten + unset($user->groups); + $user->groups = Access::getGroupsByUser($user->id, false); + + // Store the data. + if (!$user->save()) { + $this->setError($user->getError()); + + return false; + } + + // Destroy all active sessions for the user after changing the password + if ($data['password']) { + UserHelper::destroyUserSessions($user->id, true); + } + + return $user->id; + } + + /** + * Gets the configuration forms for all two-factor authentication methods + * in an array. + * + * @param integer $userId The user ID to load the forms for (optional) + * + * @return array + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0. + */ + public function getTwofactorform($userId = null) + { + return []; + } + + /** + * No longer used + * + * @param integer $userId Ignored + * + * @return \stdClass + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function getOtpConfig($userId = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getUserMfaRecords() instead.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + /** @var UserModel $model */ + $model = $this->bootComponent('com_users') + ->getMVCFactory()->createModel('User', 'Administrator'); + + return $model->getOtpConfig(); + } } diff --git a/code/components/com_users/src/Model/RegistrationModel.php b/code/components/com_users/src/Model/RegistrationModel.php index b5304824..9bb9ced5 100644 --- a/code/components/com_users/src/Model/RegistrationModel.php +++ b/code/components/com_users/src/Model/RegistrationModel.php @@ -1,4 +1,5 @@ array('validate' => 'user') - ), $config - ); - - parent::__construct($config, $factory, $formFactory); - } - - /** - * Method to get the user ID from the given token - * - * @param string $token The activation token. - * - * @return mixed False on failure, id of the user on success - * - * @since 3.8.13 - */ - public function getUserIdFromToken($token) - { - $db = $this->getDbo(); - - // Get the user id based on the token. - $query = $db->getQuery(true); - $query->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('activation') . ' = :activation') - ->where($db->quoteName('block') . ' = 1') - ->where($db->quoteName('lastvisitDate') . ' IS NULL') - ->bind(':activation', $token); - $db->setQuery($query); - - try - { - return (int) $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - } - - /** - * Method to activate a user account. - * - * @param string $token The activation token. - * - * @return mixed False on failure, user object on success. - * - * @since 1.6 - */ - public function activate($token) - { - $app = Factory::getApplication(); - $userParams = ComponentHelper::getParams('com_users'); - $userId = $this->getUserIdFromToken($token); - - // Check for a valid user id. - if (!$userId) - { - $this->setError(Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND')); - - return false; - } - - // Load the users plugin group. - PluginHelper::importPlugin('user'); - - // Activate the user. - $user = Factory::getUser($userId); - - // Admin activation is on and user is verifying their email - if (($userParams->get('useractivation') == 2) && !$user->getParam('activate', 0)) - { - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - // Compile the admin notification mail values. - $data = $user->getProperties(); - $data['activation'] = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $user->set('activation', $data['activation']); - $data['siteurl'] = Uri::base(); - $data['activate'] = Route::link( - 'site', - 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], - false, - $linkMode, - true - ); - - $data['fromname'] = $app->get('fromname'); - $data['mailfrom'] = $app->get('mailfrom'); - $data['sitename'] = $app->get('sitename'); - $user->setParam('activate', 1); - - // Get all admin users - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName(array('name', 'email', 'sendEmail', 'id'))) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('sendEmail') . ' = 1') - ->where($db->quoteName('block') . ' = 0'); - - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - // Send mail to all users with users creating permissions and receiving system emails - foreach ($rows as $row) - { - $usercreator = Factory::getUser($row->id); - - if ($usercreator->authorise('core.create', 'com_users') && $usercreator->authorise('core.manage', 'com_users')) - { - try - { - $mailer = new MailTemplate('com_users.registration.admin.verification_request', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($row->email); - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); - - return false; - } - } - } - } - // Admin activation is on and admin is activating the account - elseif (($userParams->get('useractivation') == 2) && $user->getParam('activate', 0)) - { - $user->set('activation', ''); - $user->set('block', '0'); - - // Compile the user activated notification mail values. - $data = $user->getProperties(); - $user->setParam('activate', 0); - $data['fromname'] = $app->get('fromname'); - $data['mailfrom'] = $app->get('mailfrom'); - $data['sitename'] = $app->get('sitename'); - $data['siteurl'] = Uri::base(); - $mailer = new MailTemplate('com_users.registration.user.admin_activated', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($data['email']); - - try - { - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); - - return false; - } - } - else - { - $user->set('activation', ''); - $user->set('block', '0'); - } - - // Store the user object. - if (!$user->save()) - { - $this->setError(Text::sprintf('COM_USERS_REGISTRATION_ACTIVATION_SAVE_FAILED', $user->getError())); - - return false; - } - - return $user; - } - - /** - * Method to get the registration form data. - * - * The base form data is loaded and then an event is fired - * for users plugins to extend the data. - * - * @return mixed Data object on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function getData() - { - if ($this->data === null) - { - $this->data = new \stdClass; - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_users'); - - // Override the base user data with any data in the session. - $temp = (array) $app->getUserState('com_users.registration.data', array()); - - // Don't load the data in this getForm call, or we'll call ourself - $form = $this->getForm(array(), false); - - foreach ($temp as $k => $v) - { - // Here we could have a grouped field, let's check it - if (is_array($v)) - { - $this->data->$k = new \stdClass; - - foreach ($v as $key => $val) - { - if ($form->getField($key, $k) !== false) - { - $this->data->$k->$key = $val; - } - } - } - // Only merge the field if it exists in the form. - elseif ($form->getField($k) !== false) - { - $this->data->$k = $v; - } - } - - // Get the groups the user should be added to after registration. - $this->data->groups = array(); - - // Get the default new user group, guest or public group if not specified. - $system = $params->get('new_usertype', $params->get('guest_usergroup', 1)); - - $this->data->groups[] = $system; - - // Unset the passwords. - unset($this->data->password1, $this->data->password2); - - // Get the dispatcher and load the users plugins. - PluginHelper::importPlugin('user'); - - // Trigger the data preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.registration', $this->data)); - } - - return $this->data; - } - - /** - * Method to get the registration form. - * - * The base form is loaded from XML and then an event is fired - * for users plugins to extend the form with extra fields. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.registration', 'registration', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // When multilanguage is set, a user's default site language should also be a Content Language - if (Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $data = $this->getData(); - - if (Multilanguage::isEnabled() && empty($data->language)) - { - $data->language = Factory::getLanguage()->getTag(); - } - - $this->preprocessData('com_users.registration', $data); - - return $data; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - $userParams = ComponentHelper::getParams('com_users'); - - // Add the choice for site language at registration time - if ($userParams->get('site_language') == 1 && $userParams->get('frontend_userparams') == 1) - { - $form->loadFile('sitelang', false); - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $app = Factory::getApplication(); - $params = $app->getParams('com_users'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to save the form data. - * - * @param array $temp The form data. - * - * @return mixed The user id on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function register($temp) - { - $params = ComponentHelper::getParams('com_users'); - - // Initialise the table with Joomla\CMS\User\User. - $user = new User; - $data = (array) $this->getData(); - - // Merge in the registration data. - foreach ($temp as $k => $v) - { - $data[$k] = $v; - } - - // Prepare the data for the user object. - $data['email'] = PunycodeHelper::emailToPunycode($data['email1']); - $data['password'] = $data['password1']; - $useractivation = $params->get('useractivation'); - $sendpassword = $params->get('sendpassword', 1); - - // Check if the user needs to activate their account. - if (($useractivation == 1) || ($useractivation == 2)) - { - $data['activation'] = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $data['block'] = 1; - } - - // Bind the data. - if (!$user->bind($data)) - { - $this->setError($user->getError()); - - return false; - } - - // Load the users plugin group. - PluginHelper::importPlugin('user'); - - // Store the data. - if (!$user->save()) - { - $this->setError(Text::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $user->getError())); - - return false; - } - - $app = Factory::getApplication(); - $db = $this->getDbo(); - $query = $db->getQuery(true); - - // Compile the notification mail values. - $data = $user->getProperties(); - $data['fromname'] = $app->get('fromname'); - $data['mailfrom'] = $app->get('mailfrom'); - $data['sitename'] = $app->get('sitename'); - $data['siteurl'] = Uri::root(); - - // Handle account activation/confirmation emails. - if ($useractivation == 2) - { - // Set the link to confirm the user email. - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - $data['activate'] = Route::link( - 'site', - 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], - false, - $linkMode, - true - ); - - $mailtemplate = 'com_users.registration.user.admin_activation'; - } - elseif ($useractivation == 1) - { - // Set the link to activate the user account. - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - $data['activate'] = Route::link( - 'site', - 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], - false, - $linkMode, - true - ); - - $mailtemplate = 'com_users.registration.user.self_activation'; - } - else - { - $mailtemplate = 'com_users.registration.user.registration_mail'; - } - - if ($sendpassword) - { - $mailtemplate .= '_w_pw'; - } - - // Try to send the registration email. - try - { - $mailer = new MailTemplate($mailtemplate, $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($data['email']); - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); - - $return = false; - } - } - - // Send mail to all users with user creating permissions and receiving system emails - if (($params->get('useractivation') < 2) && ($params->get('mail_to_admin') == 1)) - { - // Get all admin users - $query->clear() - ->select($db->quoteName(array('name', 'email', 'sendEmail', 'id'))) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('sendEmail') . ' = 1') - ->where($db->quoteName('block') . ' = 0'); - - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - // Send mail to all superadministrators id - foreach ($rows as $row) - { - $usercreator = Factory::getUser($row->id); - - if (!$usercreator->authorise('core.create', 'com_users') || !$usercreator->authorise('core.manage', 'com_users')) - { - continue; - } - - try - { - $mailer = new MailTemplate('com_users.registration.admin.new_notification', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($row->email); - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); - - return false; - } - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_REGISTRATION_SEND_MAIL_FAILED')); - - // Send a system message to administrators receiving system mails - $db = $this->getDbo(); - $query->clear() - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('block') . ' = 0') - ->where($db->quoteName('sendEmail') . ' = 1'); - $db->setQuery($query); - - try - { - $userids = $db->loadColumn(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - if (count($userids) > 0) - { - $jdate = new Date; - $dateToSql = $jdate->toSql(); - $subject = Text::_('COM_USERS_MAIL_SEND_FAILURE_SUBJECT'); - $message = Text::sprintf('COM_USERS_MAIL_SEND_FAILURE_BODY', $data['username']); - - // Build the query to add the messages - foreach ($userids as $userid) - { - $values = [ - ':user_id_from', - ':user_id_to', - ':date_time', - ':subject', - ':message', - ]; - $query->clear() - ->insert($db->quoteName('#__messages')) - ->columns($db->quoteName(['user_id_from', 'user_id_to', 'date_time', 'subject', 'message'])) - ->values(implode(',', $values)); - $query->bind(':user_id_from', $userid, ParameterType::INTEGER) - ->bind(':user_id_to', $userid, ParameterType::INTEGER) - ->bind(':date_time', $dateToSql) - ->bind(':subject', $subject) - ->bind(':message', $message); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - } - } - - return false; - } - - if ($useractivation == 1) - { - return 'useractivate'; - } - elseif ($useractivation == 2) - { - return 'adminactivate'; - } - else - { - return $user->id; - } - } + /** + * @var object The user registration data. + * @since 1.6 + */ + protected $data; + + /** + * Constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * @param FormFactoryInterface $formFactory The form factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config = array_merge( + array( + 'events_map' => array('validate' => 'user') + ), + $config + ); + + parent::__construct($config, $factory, $formFactory); + } + + /** + * Method to get the user ID from the given token + * + * @param string $token The activation token. + * + * @return mixed False on failure, id of the user on success + * + * @since 3.8.13 + */ + public function getUserIdFromToken($token) + { + $db = $this->getDatabase(); + + // Get the user id based on the token. + $query = $db->getQuery(true); + $query->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('activation') . ' = :activation') + ->where($db->quoteName('block') . ' = 1') + ->where($db->quoteName('lastvisitDate') . ' IS NULL') + ->bind(':activation', $token); + $db->setQuery($query); + + try { + return (int) $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + } + + /** + * Method to activate a user account. + * + * @param string $token The activation token. + * + * @return mixed False on failure, user object on success. + * + * @since 1.6 + */ + public function activate($token) + { + $app = Factory::getApplication(); + $userParams = ComponentHelper::getParams('com_users'); + $userId = $this->getUserIdFromToken($token); + + // Check for a valid user id. + if (!$userId) { + $this->setError(Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND')); + + return false; + } + + // Load the users plugin group. + PluginHelper::importPlugin('user'); + + // Activate the user. + $user = Factory::getUser($userId); + + // Admin activation is on and user is verifying their email + if (($userParams->get('useractivation') == 2) && !$user->getParam('activate', 0)) { + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + // Compile the admin notification mail values. + $data = $user->getProperties(); + $data['activation'] = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $user->set('activation', $data['activation']); + $data['siteurl'] = Uri::base(); + $data['activate'] = Route::link( + 'site', + 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], + false, + $linkMode, + true + ); + + $data['fromname'] = $app->get('fromname'); + $data['mailfrom'] = $app->get('mailfrom'); + $data['sitename'] = $app->get('sitename'); + $user->setParam('activate', 1); + + // Get all admin users + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(array('name', 'email', 'sendEmail', 'id'))) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('sendEmail') . ' = 1') + ->where($db->quoteName('block') . ' = 0'); + + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + // Send mail to all users with users creating permissions and receiving system emails + foreach ($rows as $row) { + $usercreator = Factory::getUser($row->id); + + if ($usercreator->authorise('core.create', 'com_users') && $usercreator->authorise('core.manage', 'com_users')) { + try { + $mailer = new MailTemplate('com_users.registration.admin.verification_request', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($row->email); + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); + + return false; + } + } + } + } elseif (($userParams->get('useractivation') == 2) && $user->getParam('activate', 0)) { + // Admin activation is on and admin is activating the account + $user->set('activation', ''); + $user->set('block', '0'); + + // Compile the user activated notification mail values. + $data = $user->getProperties(); + $user->setParam('activate', 0); + $data['fromname'] = $app->get('fromname'); + $data['mailfrom'] = $app->get('mailfrom'); + $data['sitename'] = $app->get('sitename'); + $data['siteurl'] = Uri::base(); + $mailer = new MailTemplate('com_users.registration.user.admin_activated', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($data['email']); + + try { + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); + + return false; + } + } else { + $user->set('activation', ''); + $user->set('block', '0'); + } + + // Store the user object. + if (!$user->save()) { + $this->setError(Text::sprintf('COM_USERS_REGISTRATION_ACTIVATION_SAVE_FAILED', $user->getError())); + + return false; + } + + return $user; + } + + /** + * Method to get the registration form data. + * + * The base form data is loaded and then an event is fired + * for users plugins to extend the data. + * + * @return mixed Data object on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function getData() + { + if ($this->data === null) { + $this->data = new \stdClass(); + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_users'); + + // Override the base user data with any data in the session. + $temp = (array) $app->getUserState('com_users.registration.data', array()); + + // Don't load the data in this getForm call, or we'll call ourself + $form = $this->getForm(array(), false); + + foreach ($temp as $k => $v) { + // Here we could have a grouped field, let's check it + if (is_array($v)) { + $this->data->$k = new \stdClass(); + + foreach ($v as $key => $val) { + if ($form->getField($key, $k) !== false) { + $this->data->$k->$key = $val; + } + } + } elseif ($form->getField($k) !== false) { + // Only merge the field if it exists in the form. + $this->data->$k = $v; + } + } + + // Get the groups the user should be added to after registration. + $this->data->groups = array(); + + // Get the default new user group, guest or public group if not specified. + $system = $params->get('new_usertype', $params->get('guest_usergroup', 1)); + + $this->data->groups[] = $system; + + // Unset the passwords. + unset($this->data->password1, $this->data->password2); + + // Get the dispatcher and load the users plugins. + PluginHelper::importPlugin('user'); + + // Trigger the data preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.registration', $this->data)); + } + + return $this->data; + } + + /** + * Method to get the registration form. + * + * The base form is loaded from XML and then an event is fired + * for users plugins to extend the form with extra fields. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.registration', 'registration', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // When multilanguage is set, a user's default site language should also be a Content Language + if (Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $data = $this->getData(); + + if (Multilanguage::isEnabled() && empty($data->language)) { + $data->language = Factory::getLanguage()->getTag(); + } + + $this->preprocessData('com_users.registration', $data); + + return $data; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + $userParams = ComponentHelper::getParams('com_users'); + + // Add the choice for site language at registration time + if ($userParams->get('site_language') == 1 && $userParams->get('frontend_userparams') == 1) { + $form->loadFile('sitelang', false); + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $app = Factory::getApplication(); + $params = $app->getParams('com_users'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to save the form data. + * + * @param array $temp The form data. + * + * @return mixed The user id on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function register($temp) + { + $params = ComponentHelper::getParams('com_users'); + + // Initialise the table with Joomla\CMS\User\User. + $user = new User(); + $data = (array) $this->getData(); + + // Merge in the registration data. + foreach ($temp as $k => $v) { + $data[$k] = $v; + } + + // Prepare the data for the user object. + $data['email'] = PunycodeHelper::emailToPunycode($data['email1']); + $data['password'] = $data['password1']; + $useractivation = $params->get('useractivation'); + $sendpassword = $params->get('sendpassword', 1); + + // Check if the user needs to activate their account. + if (($useractivation == 1) || ($useractivation == 2)) { + $data['activation'] = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $data['block'] = 1; + } + + // Bind the data. + if (!$user->bind($data)) { + $this->setError($user->getError()); + + return false; + } + + // Load the users plugin group. + PluginHelper::importPlugin('user'); + + // Store the data. + if (!$user->save()) { + $this->setError(Text::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $user->getError())); + + return false; + } + + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Compile the notification mail values. + $data = $user->getProperties(); + $data['fromname'] = $app->get('fromname'); + $data['mailfrom'] = $app->get('mailfrom'); + $data['sitename'] = $app->get('sitename'); + $data['siteurl'] = Uri::root(); + + // Handle account activation/confirmation emails. + if ($useractivation == 2) { + // Set the link to confirm the user email. + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + $data['activate'] = Route::link( + 'site', + 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], + false, + $linkMode, + true + ); + + $mailtemplate = 'com_users.registration.user.admin_activation'; + } elseif ($useractivation == 1) { + // Set the link to activate the user account. + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + $data['activate'] = Route::link( + 'site', + 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], + false, + $linkMode, + true + ); + + $mailtemplate = 'com_users.registration.user.self_activation'; + } else { + $mailtemplate = 'com_users.registration.user.registration_mail'; + } + + if ($sendpassword) { + $mailtemplate .= '_w_pw'; + } + + // Try to send the registration email. + try { + $mailer = new MailTemplate($mailtemplate, $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($data['email']); + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); + + $return = false; + } + } + + // Send mail to all users with user creating permissions and receiving system emails + if (($params->get('useractivation') < 2) && ($params->get('mail_to_admin') == 1)) { + // Get all admin users + $query->clear() + ->select($db->quoteName(array('name', 'email', 'sendEmail', 'id'))) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('sendEmail') . ' = 1') + ->where($db->quoteName('block') . ' = 0'); + + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + // Send mail to all superadministrators id + foreach ($rows as $row) { + $usercreator = Factory::getUser($row->id); + + if (!$usercreator->authorise('core.create', 'com_users') || !$usercreator->authorise('core.manage', 'com_users')) { + continue; + } + + try { + $mailer = new MailTemplate('com_users.registration.admin.new_notification', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($row->email); + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); + + return false; + } + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_REGISTRATION_SEND_MAIL_FAILED')); + + // Send a system message to administrators receiving system mails + $db = $this->getDatabase(); + $query->clear() + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('block') . ' = 0') + ->where($db->quoteName('sendEmail') . ' = 1'); + $db->setQuery($query); + + try { + $userids = $db->loadColumn(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + if (count($userids) > 0) { + $jdate = new Date(); + $dateToSql = $jdate->toSql(); + $subject = Text::_('COM_USERS_MAIL_SEND_FAILURE_SUBJECT'); + $message = Text::sprintf('COM_USERS_MAIL_SEND_FAILURE_BODY', $data['username']); + + // Build the query to add the messages + foreach ($userids as $userid) { + $values = [ + ':user_id_from', + ':user_id_to', + ':date_time', + ':subject', + ':message', + ]; + $query->clear() + ->insert($db->quoteName('#__messages')) + ->columns($db->quoteName(['user_id_from', 'user_id_to', 'date_time', 'subject', 'message'])) + ->values(implode(',', $values)); + $query->bind(':user_id_from', $userid, ParameterType::INTEGER) + ->bind(':user_id_to', $userid, ParameterType::INTEGER) + ->bind(':date_time', $dateToSql) + ->bind(':subject', $subject) + ->bind(':message', $message); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + } + } + + return false; + } + + if ($useractivation == 1) { + return 'useractivate'; + } elseif ($useractivation == 2) { + return 'adminactivate'; + } else { + return $user->id; + } + } } diff --git a/code/components/com_users/src/Model/RemindModel.php b/code/components/com_users/src/Model/RemindModel.php index c75d9375..395dc246 100644 --- a/code/components/com_users/src/Model/RemindModel.php +++ b/code/components/com_users/src/Model/RemindModel.php @@ -1,4 +1,5 @@ loadForm('com_users.remind', 'remind', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @throws \Exception if there is an error in the form event. - * - * @since 1.6 - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, 'user'); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $app = Factory::getApplication(); - $params = $app->getParams('com_users'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Send the remind username email - * - * @param array $data Array with the data received from the form - * - * @return boolean - * - * @since 1.6 - */ - public function processRemindRequest($data) - { - // Get the form. - $form = $this->getForm(); - $data['email'] = PunycodeHelper::emailToPunycode($data['email']); - - // Check for an error. - if (empty($form)) - { - return false; - } - - // Validate the data. - $data = $this->validate($form, $data); - - // Check for an error. - if ($data instanceof \Exception) - { - return false; - } - - // Check the validation results. - if ($data === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Find the user id for the given email address. - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $data['email']); - - // Get the user id. - $db->setQuery($query); - - try - { - $user = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - // Check for a user. - if (empty($user)) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - // Make sure the user isn't blocked. - if ($user->block) - { - $this->setError(Text::_('COM_USERS_USER_BLOCKED')); - - return false; - } - - $app = Factory::getApplication(); - - // Assemble the login link. - $link = 'index.php?option=com_users&view=login'; - $mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1); - - // Put together the email template data. - $data = ArrayHelper::fromObject($user); - $data['sitename'] = $app->get('sitename'); - $data['link_text'] = Route::_($link, false, $mode); - $data['link_html'] = Route::_($link, true, $mode); - - $mailer = new MailTemplate('com_users.reminder', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($user->email, $user->name); - - // Try to send the password reset request email. - try - { - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_MAIL_FAILED')); - - return false; - } - - Factory::getApplication()->triggerEvent('onUserAfterRemind', array($user)); - - return true; - } + /** + * Method to get the username remind request form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.remind', 'remind', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @throws \Exception if there is an error in the form event. + * + * @since 1.6 + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, 'user'); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $app = Factory::getApplication(); + $params = $app->getParams('com_users'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Send the remind username email + * + * @param array $data Array with the data received from the form + * + * @return boolean + * + * @since 1.6 + */ + public function processRemindRequest($data) + { + // Get the form. + $form = $this->getForm(); + $data['email'] = PunycodeHelper::emailToPunycode($data['email']); + + // Check for an error. + if (empty($form)) { + return false; + } + + // Validate the data. + $data = $this->validate($form, $data); + + // Check for an error. + if ($data instanceof \Exception) { + return false; + } + + // Check the validation results. + if ($data === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Find the user id for the given email address. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $data['email']); + + // Get the user id. + $db->setQuery($query); + + try { + $user = $db->loadObject(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + // Check for a user. + if (empty($user)) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + // Make sure the user isn't blocked. + if ($user->block) { + $this->setError(Text::_('COM_USERS_USER_BLOCKED')); + + return false; + } + + $app = Factory::getApplication(); + + // Assemble the login link. + $link = 'index.php?option=com_users&view=login'; + $mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1); + + // Put together the email template data. + $data = ArrayHelper::fromObject($user); + $data['sitename'] = $app->get('sitename'); + $data['link_text'] = Route::_($link, false, $mode); + $data['link_html'] = Route::_($link, true, $mode); + + $mailer = new MailTemplate('com_users.reminder', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($user->email, $user->name); + + // Try to send the password reset request email. + try { + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_MAIL_FAILED')); + + return false; + } + + Factory::getApplication()->triggerEvent('onUserAfterRemind', array($user)); + + return true; + } } diff --git a/code/components/com_users/src/Model/ResetModel.php b/code/components/com_users/src/Model/ResetModel.php index 979546fa..bf201ac4 100644 --- a/code/components/com_users/src/Model/ResetModel.php +++ b/code/components/com_users/src/Model/ResetModel.php @@ -1,4 +1,5 @@ loadForm('com_users.reset_request', 'reset_request', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the password reset complete form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - */ - public function getResetCompleteForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.reset_complete', 'reset_complete', $options = array('control' => 'jform')); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the password reset confirm form. - * - * @param array $data Data for the form. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - * @throws \Exception - */ - public function getResetConfirmForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.reset_confirm', 'reset_confirm', $options = array('control' => 'jform')); - - if (empty($form)) - { - return false; - } - else - { - $form->setValue('token', '', Factory::getApplication()->input->get('token')); - } - - return $form; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @throws \Exception if there is an error in the form event. - * - * @since 1.6 - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_users'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Save the new password after reset is done - * - * @param array $data The data expected for the form. - * - * @return mixed \Exception | boolean - * - * @since 1.6 - * @throws \Exception - */ - public function processResetComplete($data) - { - // Get the form. - $form = $this->getResetCompleteForm(); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Get the token and user id from the confirmation process. - $app = Factory::getApplication(); - $token = $app->getUserState('com_users.reset.token', null); - $userId = $app->getUserState('com_users.reset.user', null); - - // Check the token and user id. - if (empty($token) || empty($userId)) - { - return new \Exception(Text::_('COM_USERS_RESET_COMPLETE_TOKENS_MISSING'), 403); - } - - // Get the user object. - $user = User::getInstance($userId); - - // Check for a user and that the tokens match. - if (empty($user) || $user->activation !== $token) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - // Make sure the user isn't blocked. - if ($user->block) - { - $this->setError(Text::_('COM_USERS_USER_BLOCKED')); - - return false; - } - - // Check if the user is reusing the current password if required to reset their password - if ($user->requireReset == 1 && UserHelper::verifyPassword($data['password1'], $user->password)) - { - $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_REUSE_PASSWORD')); - - return false; - } - - // Prepare user data. - $data['password'] = $data['password1']; - $data['activation'] = ''; - - // Update the user object. - if (!$user->bind($data)) - { - return new \Exception($user->getError(), 500); - } - - // Save the user to the database. - if (!$user->save(true)) - { - return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500); - } - - // Destroy all active sessions for the user - UserHelper::destroyUserSessions($user->id); - - // Flush the user data from the session. - $app->setUserState('com_users.reset.token', null); - $app->setUserState('com_users.reset.user', null); - - return true; - } - - /** - * Receive the reset password request - * - * @param array $data The data expected for the form. - * - * @return mixed \Exception | boolean - * - * @since 1.6 - * @throws \Exception - */ - public function processResetConfirm($data) - { - // Get the form. - $form = $this->getResetConfirmForm(); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Find the user id for the given token. - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName(['activation', 'id', 'block'])) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('username') . ' = :username') - ->bind(':username', $data['username']); - - // Get the user id. - $db->setQuery($query); - - try - { - $user = $db->loadObject(); - } - catch (\RuntimeException $e) - { - return new \Exception(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage()), 500); - } - - // Check for a user. - if (empty($user)) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - if (!$user->activation) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - // Verify the token - if (!UserHelper::verifyPassword($data['token'], $user->activation)) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - // Make sure the user isn't blocked. - if ($user->block) - { - $this->setError(Text::_('COM_USERS_USER_BLOCKED')); - - return false; - } - - // Push the user data into the session. - $app = Factory::getApplication(); - $app->setUserState('com_users.reset.token', $user->activation); - $app->setUserState('com_users.reset.user', $user->id); - - return true; - } - - /** - * Method to start the password reset process. - * - * @param array $data The data expected for the form. - * - * @return mixed \Exception | boolean - * - * @since 1.6 - * @throws \Exception - */ - public function processResetRequest($data) - { - $app = Factory::getApplication(); - - // Get the form. - $form = $this->getForm(); - - $data['email'] = PunycodeHelper::emailToPunycode($data['email']); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Find the user id for the given email address. - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $data['email']); - - // Get the user object. - $db->setQuery($query); - - try - { - $userId = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - // Check for a user. - if (empty($userId)) - { - $this->setError(Text::_('COM_USERS_INVALID_EMAIL')); - - return false; - } - - // Get the user object. - $user = User::getInstance($userId); - - // Make sure the user isn't blocked. - if ($user->block) - { - $this->setError(Text::_('COM_USERS_USER_BLOCKED')); - - return false; - } - - // Make sure the user isn't a Super Admin. - if ($user->authorise('core.admin')) - { - $this->setError(Text::_('COM_USERS_REMIND_SUPERADMIN_ERROR')); - - return false; - } - - // Make sure the user has not exceeded the reset limit - if (!$this->checkResetLimit($user)) - { - $resetLimit = (int) Factory::getApplication()->getParams()->get('reset_time'); - $this->setError(Text::plural('COM_USERS_REMIND_LIMIT_ERROR_N_HOURS', $resetLimit)); - - return false; - } - - // Set the confirmation token. - $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $hashedToken = UserHelper::hashPassword($token); - - $user->activation = $hashedToken; - - // Save the user to the database. - if (!$user->save(true)) - { - return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500); - } - - // Assemble the password reset confirmation link. - $mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1); - $link = 'index.php?option=com_users&view=reset&layout=confirm&token=' . $token; - - // Put together the email template data. - $data = $user->getProperties(); - $data['sitename'] = $app->get('sitename'); - $data['link_text'] = Route::_($link, false, $mode); - $data['link_html'] = Route::_($link, true, $mode); - $data['token'] = $token; - - $mailer = new MailTemplate('com_users.password_reset', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($user->email, $user->name); - - // Try to send the password reset request email. - try - { - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - return new \Exception(Text::_('COM_USERS_MAIL_FAILED'), 500); - } - else - { - return true; - } - } - - /** - * Method to check if user reset limit has been exceeded within the allowed time period. - * - * @param User $user User doing the password reset - * - * @return boolean true if user can do the reset, false if limit exceeded - * - * @since 2.5 - * @throws \Exception - */ - public function checkResetLimit($user) - { - $params = Factory::getApplication()->getParams(); - $maxCount = (int) $params->get('reset_count'); - $resetHours = (int) $params->get('reset_time'); - $result = true; - - $lastResetTime = strtotime($user->lastResetTime) ?: 0; - $hoursSinceLastReset = (strtotime(Factory::getDate()->toSql()) - $lastResetTime) / 3600; - - if ($hoursSinceLastReset > $resetHours) - { - // If it's been long enough, start a new reset count - $user->lastResetTime = Factory::getDate()->toSql(); - $user->resetCount = 1; - } - elseif ($user->resetCount < $maxCount) - { - // If we are under the max count, just increment the counter - ++$user->resetCount; - } - else - { - // At this point, we know we have exceeded the maximum resets for the time period - $result = false; - } - - return $result; - } + /** + * Method to get the password reset request form. + * + * The base form is loaded from XML and then an event is fired + * for users plugins to extend the form with extra fields. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.reset_request', 'reset_request', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the password reset complete form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getResetCompleteForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.reset_complete', 'reset_complete', $options = array('control' => 'jform')); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the password reset confirm form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + * @throws \Exception + */ + public function getResetConfirmForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.reset_confirm', 'reset_confirm', $options = array('control' => 'jform')); + + if (empty($form)) { + return false; + } else { + $form->setValue('token', '', Factory::getApplication()->input->get('token')); + } + + return $form; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @throws \Exception if there is an error in the form event. + * + * @since 1.6 + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_users'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Save the new password after reset is done + * + * @param array $data The data expected for the form. + * + * @return mixed \Exception | boolean + * + * @since 1.6 + * @throws \Exception + */ + public function processResetComplete($data) + { + // Get the form. + $form = $this->getResetCompleteForm(); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Get the token and user id from the confirmation process. + $app = Factory::getApplication(); + $token = $app->getUserState('com_users.reset.token', null); + $userId = $app->getUserState('com_users.reset.user', null); + + // Check the token and user id. + if (empty($token) || empty($userId)) { + return new \Exception(Text::_('COM_USERS_RESET_COMPLETE_TOKENS_MISSING'), 403); + } + + // Get the user object. + $user = User::getInstance($userId); + + // Check for a user and that the tokens match. + if (empty($user) || $user->activation !== $token) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + // Make sure the user isn't blocked. + if ($user->block) { + $this->setError(Text::_('COM_USERS_USER_BLOCKED')); + + return false; + } + + // Check if the user is reusing the current password if required to reset their password + if ($user->requireReset == 1 && UserHelper::verifyPassword($data['password1'], $user->password)) { + $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_REUSE_PASSWORD')); + + return false; + } + + // Prepare user data. + $data['password'] = $data['password1']; + $data['activation'] = ''; + + // Update the user object. + if (!$user->bind($data)) { + return new \Exception($user->getError(), 500); + } + + // Save the user to the database. + if (!$user->save(true)) { + return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500); + } + + // Destroy all active sessions for the user + UserHelper::destroyUserSessions($user->id); + + // Flush the user data from the session. + $app->setUserState('com_users.reset.token', null); + $app->setUserState('com_users.reset.user', null); + + return true; + } + + /** + * Receive the reset password request + * + * @param array $data The data expected for the form. + * + * @return mixed \Exception | boolean + * + * @since 1.6 + * @throws \Exception + */ + public function processResetConfirm($data) + { + // Get the form. + $form = $this->getResetConfirmForm(); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Find the user id for the given token. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['activation', 'id', 'block'])) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('username') . ' = :username') + ->bind(':username', $data['username']); + + // Get the user id. + $db->setQuery($query); + + try { + $user = $db->loadObject(); + } catch (\RuntimeException $e) { + return new \Exception(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage()), 500); + } + + // Check for a user. + if (empty($user)) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + if (!$user->activation) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + // Verify the token + if (!UserHelper::verifyPassword($data['token'], $user->activation)) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + // Make sure the user isn't blocked. + if ($user->block) { + $this->setError(Text::_('COM_USERS_USER_BLOCKED')); + + return false; + } + + // Push the user data into the session. + $app = Factory::getApplication(); + $app->setUserState('com_users.reset.token', $user->activation); + $app->setUserState('com_users.reset.user', $user->id); + + return true; + } + + /** + * Method to start the password reset process. + * + * @param array $data The data expected for the form. + * + * @return mixed \Exception | boolean + * + * @since 1.6 + * @throws \Exception + */ + public function processResetRequest($data) + { + $app = Factory::getApplication(); + + // Get the form. + $form = $this->getForm(); + + $data['email'] = PunycodeHelper::emailToPunycode($data['email']); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Find the user id for the given email address. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $data['email']); + + // Get the user object. + $db->setQuery($query); + + try { + $userId = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + // Check for a user. + if (empty($userId)) { + $this->setError(Text::_('COM_USERS_INVALID_EMAIL')); + + return false; + } + + // Get the user object. + $user = User::getInstance($userId); + + // Make sure the user isn't blocked. + if ($user->block) { + $this->setError(Text::_('COM_USERS_USER_BLOCKED')); + + return false; + } + + // Make sure the user isn't a Super Admin. + if ($user->authorise('core.admin')) { + $this->setError(Text::_('COM_USERS_REMIND_SUPERADMIN_ERROR')); + + return false; + } + + // Make sure the user has not exceeded the reset limit + if (!$this->checkResetLimit($user)) { + $resetLimit = (int) Factory::getApplication()->getParams()->get('reset_time'); + $this->setError(Text::plural('COM_USERS_REMIND_LIMIT_ERROR_N_HOURS', $resetLimit)); + + return false; + } + + // Set the confirmation token. + $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $hashedToken = UserHelper::hashPassword($token); + + $user->activation = $hashedToken; + + // Save the user to the database. + if (!$user->save(true)) { + return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500); + } + + // Assemble the password reset confirmation link. + $mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1); + $link = 'index.php?option=com_users&view=reset&layout=confirm&token=' . $token; + + // Put together the email template data. + $data = $user->getProperties(); + $data['sitename'] = $app->get('sitename'); + $data['link_text'] = Route::_($link, false, $mode); + $data['link_html'] = Route::_($link, true, $mode); + $data['token'] = $token; + + $mailer = new MailTemplate('com_users.password_reset', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($user->email, $user->name); + + // Try to send the password reset request email. + try { + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + return new \Exception(Text::_('COM_USERS_MAIL_FAILED'), 500); + } else { + return true; + } + } + + /** + * Method to check if user reset limit has been exceeded within the allowed time period. + * + * @param User $user User doing the password reset + * + * @return boolean true if user can do the reset, false if limit exceeded + * + * @since 2.5 + * @throws \Exception + */ + public function checkResetLimit($user) + { + $params = Factory::getApplication()->getParams(); + $maxCount = (int) $params->get('reset_count'); + $resetHours = (int) $params->get('reset_time'); + $result = true; + + $lastResetTime = strtotime($user->lastResetTime) ?: 0; + $hoursSinceLastReset = (strtotime(Factory::getDate()->toSql()) - $lastResetTime) / 3600; + + if ($hoursSinceLastReset > $resetHours) { + // If it's been long enough, start a new reset count + $user->lastResetTime = Factory::getDate()->toSql(); + $user->resetCount = 1; + } elseif ($user->resetCount < $maxCount) { + // If we are under the max count, just increment the counter + ++$user->resetCount; + } else { + // At this point, we know we have exceeded the maximum resets for the time period + $result = false; + } + + return $result; + } } diff --git a/code/components/com_users/src/Rule/LoginUniqueFieldRule.php b/code/components/com_users/src/Rule/LoginUniqueFieldRule.php index 04310e7d..2a29e749 100644 --- a/code/components/com_users/src/Rule/LoginUniqueFieldRule.php +++ b/code/components/com_users/src/Rule/LoginUniqueFieldRule.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - * - * @since 3.6 - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $loginRedirectUrl = $input['params']->login_redirect_url; - $loginRedirectMenuitem = $input['params']->login_redirect_menuitem; + /** + * Method to test if two fields have a value in order to use only one field. + * To use this rule, the form + * XML needs a validate attribute of loginuniquefield and a field attribute + * that is equal to the field to test against. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + * + * @since 3.6 + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $loginRedirectUrl = $input['params']->login_redirect_url; + $loginRedirectMenuitem = $input['params']->login_redirect_menuitem; - if ($form === null) - { - throw new \InvalidArgumentException(sprintf('The value for $form must not be null in %s', get_class($this))); - } + if ($form === null) { + throw new \InvalidArgumentException(sprintf('The value for $form must not be null in %s', get_class($this))); + } - if ($input === null) - { - throw new \InvalidArgumentException(sprintf('The value for $input must not be null in %s', get_class($this))); - } + if ($input === null) { + throw new \InvalidArgumentException(sprintf('The value for $input must not be null in %s', get_class($this))); + } - // Test the input values for login. - if ($loginRedirectUrl != '' && $loginRedirectMenuitem != '') - { - return false; - } + // Test the input values for login. + if ($loginRedirectUrl != '' && $loginRedirectMenuitem != '') { + return false; + } - return true; - } + return true; + } } diff --git a/code/components/com_users/src/Rule/LogoutUniqueFieldRule.php b/code/components/com_users/src/Rule/LogoutUniqueFieldRule.php index 89dd4eaf..e028c59e 100644 --- a/code/components/com_users/src/Rule/LogoutUniqueFieldRule.php +++ b/code/components/com_users/src/Rule/LogoutUniqueFieldRule.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - * - * @since 3.6 - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $logoutRedirectUrl = $input['params']->logout_redirect_url; - $logoutRedirectMenuitem = $input['params']->logout_redirect_menuitem; + /** + * Method to test if two fields have a value in order to use only one field. + * To use this rule, the form + * XML needs a validate attribute of logoutuniquefield and a field attribute + * that is equal to the field to test against. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + * + * @since 3.6 + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $logoutRedirectUrl = $input['params']->logout_redirect_url; + $logoutRedirectMenuitem = $input['params']->logout_redirect_menuitem; - if ($form === null) - { - throw new \InvalidArgumentException(sprintf('The value for $form must not be null in %s', get_class($this))); - } + if ($form === null) { + throw new \InvalidArgumentException(sprintf('The value for $form must not be null in %s', get_class($this))); + } - if ($input === null) - { - throw new \InvalidArgumentException(sprintf('The value for $input must not be null in %s', get_class($this))); - } + if ($input === null) { + throw new \InvalidArgumentException(sprintf('The value for $input must not be null in %s', get_class($this))); + } - // Test the input values for logout. - if ($logoutRedirectUrl != '' && $logoutRedirectMenuitem != '') - { - return false; - } + // Test the input values for logout. + if ($logoutRedirectUrl != '' && $logoutRedirectMenuitem != '') { + return false; + } - return true; - } + return true; + } } diff --git a/code/components/com_users/src/Service/Router.php b/code/components/com_users/src/Service/Router.php index 632ab267..db939851 100644 --- a/code/components/com_users/src/Service/Router.php +++ b/code/components/com_users/src/Service/Router.php @@ -1,4 +1,5 @@ registerView(new RouterViewConfiguration('login')); - $profile = new RouterViewConfiguration('profile'); - $profile->addLayout('edit'); - $this->registerView($profile); - $this->registerView(new RouterViewConfiguration('registration')); - $this->registerView(new RouterViewConfiguration('remind')); - $this->registerView(new RouterViewConfiguration('reset')); - - parent::__construct($app, $menu); - - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } + /** + * Users Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + */ + public function __construct(SiteApplication $app, AbstractMenu $menu) + { + $this->registerView(new RouterViewConfiguration('login')); + $profile = new RouterViewConfiguration('profile'); + $profile->addLayout('edit'); + $this->registerView($profile); + $this->registerView(new RouterViewConfiguration('registration')); + $this->registerView(new RouterViewConfiguration('remind')); + $this->registerView(new RouterViewConfiguration('reset')); + $this->registerView(new RouterViewConfiguration('callback')); + $this->registerView(new RouterViewConfiguration('captive')); + $this->registerView(new RouterViewConfiguration('methods')); + + $method = new RouterViewConfiguration('method'); + $method->setKey('id'); + $this->registerView($method); + + parent::__construct($app, $menu); + + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } + + /** + * Get the method ID from a URL segment + * + * @param string $segment The URL segment + * @param array $query The URL query parameters + * + * @return integer + * @since 4.2.0 + */ + public function getMethodId($segment, $query) + { + return (int) $segment; + } + + /** + * Get a segment from a method ID + * + * @param integer $id The method ID + * @param array $query The URL query parameters + * + * @return int[] + * @since 4.2.0 + */ + public function getMethodSegment($id, $query) + { + return [$id => (int) $id]; + } } diff --git a/code/components/com_users/src/View/Captive/HtmlView.php b/code/components/com_users/src/View/Captive/HtmlView.php new file mode 100644 index 00000000..3039e51d --- /dev/null +++ b/code/components/com_users/src/View/Captive/HtmlView.php @@ -0,0 +1,24 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Site\View\Captive; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * View for Multi-factor Authentication captive page + * + * @since 4.2.0 + */ +class HtmlView extends \Joomla\Component\Users\Administrator\View\Captive\HtmlView +{ +} diff --git a/code/components/com_users/src/View/Login/HtmlView.php b/code/components/com_users/src/View/Login/HtmlView.php index 8288eced..fb2feaa7 100644 --- a/code/components/com_users/src/View/Login/HtmlView.php +++ b/code/components/com_users/src/View/Login/HtmlView.php @@ -1,4 +1,5 @@ user = Factory::getUser(); - $this->form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->get('params'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check for layout override - $active = Factory::getApplication()->getMenu()->getActive(); - - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - - $tfa = AuthenticationHelper::getTwoFactorMethods(); - $this->tfa = is_array($tfa) && count($tfa) > 1; - - $this->extraButtons = AuthenticationHelper::getLoginButtons('com-users-login__form'); - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - $login = Factory::getUser()->get('guest') ? true : false; - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', $login ? Text::_('JLOGIN') : Text::_('JLOGOUT')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The logged in user + * + * @var User + */ + protected $user; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * No longer used + * + * @var boolean + * @since 4.0.0 + * @deprecated 4.2.0 Will be removed in 5.0. + */ + protected $tfa = false; + + /** + * Additional buttons to show on the login page + * + * @var array + * @since 4.0.0 + */ + protected $extraButtons = []; + + /** + * Method to display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + * @throws \Exception + */ + public function display($tpl = null) + { + // Get the view data. + $this->user = $this->getCurrentUser(); + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->get('params'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check for layout override + $active = Factory::getApplication()->getMenu()->getActive(); + + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + + $this->extraButtons = AuthenticationHelper::getLoginButtons('com-users-login__form'); + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + $login = $this->getCurrentUser()->get('guest') ? true : false; + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', $login ? Text::_('JLOGIN') : Text::_('JLOGOUT')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/code/components/com_users/src/View/Method/HtmlView.php b/code/components/com_users/src/View/Method/HtmlView.php new file mode 100644 index 00000000..463f759e --- /dev/null +++ b/code/components/com_users/src/View/Method/HtmlView.php @@ -0,0 +1,24 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Site\View\Method; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * View for Multi-factor Authentication method add/edit page + * + * @since 4.2.0 + */ +class HtmlView extends \Joomla\Component\Users\Administrator\View\Method\HtmlView +{ +} diff --git a/code/components/com_users/src/View/Methods/HtmlView.php b/code/components/com_users/src/View/Methods/HtmlView.php new file mode 100644 index 00000000..7363d507 --- /dev/null +++ b/code/components/com_users/src/View/Methods/HtmlView.php @@ -0,0 +1,24 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Users\Site\View\Methods; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * View for Multi-factor Authentication methods list page + * + * @since 4.2.0 + */ +class HtmlView extends \Joomla\Component\Users\Administrator\View\Methods\HtmlView +{ +} diff --git a/code/components/com_users/src/View/Profile/HtmlView.php b/code/components/com_users/src/View/Profile/HtmlView.php index e9fb8709..a9f87e8d 100644 --- a/code/components/com_users/src/View/Profile/HtmlView.php +++ b/code/components/com_users/src/View/Profile/HtmlView.php @@ -1,4 +1,5 @@ data = $this->get('Data'); - $this->form = $this->getModel()->getForm(new CMSObject(array('id' => $user->id))); - $this->state = $this->get('State'); - $this->params = $this->state->get('params'); - $this->twofactorform = $this->get('Twofactorform'); - $this->twofactormethods = UsersHelper::getTwoFactorMethods(); - $this->otpConfig = $this->get('OtpConfig'); - $this->db = Factory::getDbo(); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // View also takes responsibility for checking if the user logged in with remember me. - $cookieLogin = $user->get('cookieLogin'); - - if (!empty($cookieLogin)) - { - // If so, the user must login to edit the password and other data. - // What should happen here? Should we force a logout which destroys the cookies? - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('JGLOBAL_REMEMBER_MUST_LOGIN'), 'message'); - $app->redirect(Route::_('index.php?option=com_users&view=login', false)); - - return false; - } - - // Check if a user was found. - if (!$this->data->id) - { - throw new \Exception(Text::_('JERROR_USERS_PROFILE_NOT_FOUND'), 404); - } - - PluginHelper::importPlugin('content'); - $this->data->text = ''; - Factory::getApplication()->triggerEvent('onContentPrepare', array ('com_users.user', &$this->data, &$this->data->params, 0)); - unset($this->data->text); - - // Check for layout from menu item. - $query = Factory::getApplication()->getMenu()->getActive()->query; - - if (isset($query['layout']) && isset($query['option']) && $query['option'] === 'com_users' - && isset($query['view']) && $query['view'] === 'profile') - { - $this->setLayout($query['layout']); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', Factory::getUser()->name)); - } - else - { - $this->params->def('page_heading', Text::_('COM_USERS_PROFILE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * Profile form data for the user + * + * @var User + */ + protected $data; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * An instance of DatabaseDriver. + * + * @var DatabaseDriver + * @since 3.6.3 + * + * @deprecated 5.0 Will be removed without replacement + */ + protected $db; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The Multi-factor Authentication configuration interface for the user. + * + * @var string|null + * @since 4.2.0 + */ + protected $mfaConfigurationUI; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + * + * @since 1.6 + * @throws \Exception + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + + // Get the view data. + $this->data = $this->get('Data'); + $this->form = $this->getModel()->getForm(new CMSObject(['id' => $user->id])); + $this->state = $this->get('State'); + $this->params = $this->state->get('params'); + $this->mfaConfigurationUI = Mfa::getConfigurationInterface($user); + $this->db = Factory::getDbo(); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // View also takes responsibility for checking if the user logged in with remember me. + $cookieLogin = $user->get('cookieLogin'); + + if (!empty($cookieLogin)) { + // If so, the user must login to edit the password and other data. + // What should happen here? Should we force a logout which destroys the cookies? + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('JGLOBAL_REMEMBER_MUST_LOGIN'), 'message'); + $app->redirect(Route::_('index.php?option=com_users&view=login', false)); + + return false; + } + + // Check if a user was found. + if (!$this->data->id) { + throw new \Exception(Text::_('JERROR_USERS_PROFILE_NOT_FOUND'), 404); + } + + PluginHelper::importPlugin('content'); + $this->data->text = ''; + Factory::getApplication()->triggerEvent('onContentPrepare', array ('com_users.user', &$this->data, &$this->data->params, 0)); + unset($this->data->text); + + // Check for layout from menu item. + $query = Factory::getApplication()->getMenu()->getActive()->query; + + if ( + isset($query['layout']) && isset($query['option']) && $query['option'] === 'com_users' + && isset($query['view']) && $query['view'] === 'profile' + ) { + $this->setLayout($query['layout']); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $this->getCurrentUser()->name)); + } else { + $this->params->def('page_heading', Text::_('COM_USERS_PROFILE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/code/components/com_users/src/View/Registration/HtmlView.php b/code/components/com_users/src/View/Registration/HtmlView.php index 98741cff..b0f09878 100644 --- a/code/components/com_users/src/View/Registration/HtmlView.php +++ b/code/components/com_users/src/View/Registration/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->data = $this->get('Data'); - $this->state = $this->get('State'); - $this->params = $this->state->get('params'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check for layout override - $active = Factory::getApplication()->getMenu()->getActive(); - - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_USERS_REGISTRATION')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * Registration form data + * + * @var \stdClass|false + */ + protected $data; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The HtmlDocument instance + * + * @var HtmlDocument + */ + public $document; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Method to display the view. + * + * @param string $tpl The template file to include + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + public function display($tpl = null) + { + // Get the view data. + $this->form = $this->get('Form'); + $this->data = $this->get('Data'); + $this->state = $this->get('State'); + $this->params = $this->state->get('params'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check for layout override + $active = Factory::getApplication()->getMenu()->getActive(); + + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_USERS_REGISTRATION')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/code/components/com_users/src/View/Remind/HtmlView.php b/code/components/com_users/src/View/Remind/HtmlView.php index 8855d9d1..6455ab73 100644 --- a/code/components/com_users/src/View/Remind/HtmlView.php +++ b/code/components/com_users/src/View/Remind/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->params; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check for layout override - $active = Factory::getApplication()->getMenu()->getActive(); - - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_USERS_REMIND')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Method to display the view. + * + * @param string $tpl The template file to include + * + * @return mixed + * + * @since 1.5 + * @throws \Exception + */ + public function display($tpl = null) + { + // Get the view data. + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->params; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check for layout override + $active = Factory::getApplication()->getMenu()->getActive(); + + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_USERS_REMIND')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/code/components/com_users/src/View/Reset/HtmlView.php b/code/components/com_users/src/View/Reset/HtmlView.php index c5266911..e205532b 100644 --- a/code/components/com_users/src/View/Reset/HtmlView.php +++ b/code/components/com_users/src/View/Reset/HtmlView.php @@ -1,4 +1,5 @@ getLayout(); - - // Check that the name is valid - has an associated model. - if (!in_array($name, array('confirm', 'complete'))) - { - $name = 'default'; - } - - if ('default' === $name) - { - $formname = 'Form'; - } - else - { - $formname = ucfirst($this->_name) . ucfirst($name) . 'Form'; - } - - // Get the view data. - $this->form = $this->get($formname); - $this->state = $this->get('State'); - $this->params = $this->state->params; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_USERS_RESET')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Method to display the view. + * + * @param string $tpl The template file to include + * + * @return mixed + * + * @since 1.5 + */ + public function display($tpl = null) + { + // This name will be used to get the model + $name = $this->getLayout(); + + // Check that the name is valid - has an associated model. + if (!in_array($name, array('confirm', 'complete'))) { + $name = 'default'; + } + + if ('default' === $name) { + $formname = 'Form'; + } else { + $formname = ucfirst($this->_name) . ucfirst($name) . 'Form'; + } + + // Get the view data. + $this->form = $this->get($formname); + $this->state = $this->get('State'); + $this->params = $this->state->params; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_USERS_RESET')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/code/components/com_users/tmpl/captive/default.php b/code/components/com_users/tmpl/captive/default.php new file mode 100644 index 00000000..f656fbaa --- /dev/null +++ b/code/components/com_users/tmpl/captive/default.php @@ -0,0 +1,136 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +use Joomla\CMS\Factory; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Site\Model\CaptiveModel; +use Joomla\Component\Users\Site\View\Captive\HtmlView; +use Joomla\Utilities\ArrayHelper; + +/** + * @var HtmlView $this View object + * @var CaptiveModel $model The model + */ +$model = $this->getModel(); + +if ($this->renderOptions['field_type'] !== 'custom') { + $this->document->getWebAssetManager() + ->useScript('com_users.two-factor-focus'); +} + +?> +
    +

    + renderOptions['help_url'])) : ?> + + + + + + + + title)) : ?> + title ?> – + + allowEntryBatching) : ?> + escape($this->record->title) ?> + + escape($this->getModel()->translateMethodName($this->record->method)) ?> + + title)) : ?> + + +

    + + renderOptions['pre_message']) : ?> +
    + renderOptions['pre_message'] ?> +
    + + +
    + + +
    + renderOptions['field_type'] == 'custom') : ?> + renderOptions['html']; ?> + +
    + renderOptions['label']) : ?> + + +
    + $this->renderOptions['input_type'], + 'name' => 'code', + 'value' => '', + 'placeholder' => $this->renderOptions['placeholder'] ?? null, + 'id' => 'users-mfa-code', + 'class' => 'form-control' + ], + $this->renderOptions['input_attributes'] + ); + + if (strpos($attributes['class'], 'form-control') === false) { + $attributes['class'] .= ' form-control'; + } + ?> + > +
    +
    +
    + +
    +
    + + + + + + + + records) > 1) : ?> +
    + + + +
    + +
    +
    +
    + + renderOptions['post_message']) : ?> +
    + renderOptions['post_message'] ?> +
    + + +
    diff --git a/code/components/com_users/tmpl/captive/select.php b/code/components/com_users/tmpl/captive/select.php new file mode 100644 index 00000000..c09272c8 --- /dev/null +++ b/code/components/com_users/tmpl/captive/select.php @@ -0,0 +1,78 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Prevent direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Uri\Uri; +use Joomla\Component\Users\Site\View\Captive\HtmlView; + +/** @var HtmlView $this */ + +$shownMethods = []; + +?> +
    +

    + +

    +
    +

    + +

    +
    + +
    + records as $record) : + if (!array_key_exists($record->method, $this->mfaMethods) && ($record->method != 'backupcodes')) { + continue; + } + + $allowEntryBatching = isset($this->mfaMethods[$record->method]) ? $this->mfaMethods[$record->method]['allowEntryBatching'] : false; + + if ($this->allowEntryBatching) { + if ($allowEntryBatching && in_array($record->method, $shownMethods)) { + continue; + } + $shownMethods[] = $record->method; + } + + $methodName = $this->getModel()->translateMethodName($record->method); + ?> + + <?php echo $this->escape(strip_tags($record->title)) ?> + allowEntryBatching || !$allowEntryBatching) : ?> + + method === 'backupcodes') : ?> + title ?> + + escape($record->title) ?> + + + + + + + + + + + + + + + +
    +
    diff --git a/code/components/com_users/tmpl/login/default.php b/code/components/com_users/tmpl/login/default.php index 3766d377..0fbcc008 100644 --- a/code/components/com_users/tmpl/login/default.php +++ b/code/components/com_users/tmpl/login/default.php @@ -1,4 +1,5 @@ user->get('cookieLogin'); -if (!empty($cookieLogin) || $this->user->get('guest')) -{ - // The user is not logged in or needs to provide a password. - echo $this->loadTemplate('login'); -} -else -{ - // The user is already logged in. - echo $this->loadTemplate('logout'); +if (!empty($cookieLogin) || $this->user->get('guest')) { + // The user is not logged in or needs to provide a password. + echo $this->loadTemplate('login'); +} else { + // The user is already logged in. + echo $this->loadTemplate('logout'); } diff --git a/code/components/com_users/tmpl/login/default_login.php b/code/components/com_users/tmpl/login/default_login.php index 4b082f62..12c24a17 100644 --- a/code/components/com_users/tmpl/login/default_login.php +++ b/code/components/com_users/tmpl/login/default_login.php @@ -1,4 +1,5 @@ diff --git a/code/components/com_users/tmpl/login/default_logout.php b/code/components/com_users/tmpl/login/default_logout.php index 408c0240..099ab56f 100644 --- a/code/components/com_users/tmpl/login/default_logout.php +++ b/code/components/com_users/tmpl/login/default_logout.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - - - params->get('logoutdescription_show') == 1 && str_replace(' ', '', $this->params->get('logout_description')) != '')|| $this->params->get('logout_image') != '') : ?> -
    - - - params->get('logoutdescription_show') == 1) : ?> - params->get('logout_description'); ?> - - - params->get('logout_image') != '') : ?> - params->get('logout_image'), empty($this->params->get('logout_image_alt')) && empty($this->params->get('logout_image_alt_empty')) ? false : $this->params->get('logout_image_alt'), ['class' => 'com-users-logout__image thumbnail float-end logout-image']); ?> - - - params->get('logoutdescription_show') == 1 && str_replace(' ', '', $this->params->get('logout_description')) != '')|| $this->params->get('logout_image') != '') : ?> -
    - - -
    -
    -
    - -
    -
    - params->get('logout_redirect_url')) : ?> - - - - - -
    + params->get('show_page_heading')) : ?> + + + + params->get('logoutdescription_show') == 1 && str_replace(' ', '', $this->params->get('logout_description')) != '') || $this->params->get('logout_image') != '') : ?> +
    + + + params->get('logoutdescription_show') == 1) : ?> + params->get('logout_description'); ?> + + + params->get('logout_image') != '') : ?> + params->get('logout_image'), empty($this->params->get('logout_image_alt')) && empty($this->params->get('logout_image_alt_empty')) ? false : $this->params->get('logout_image_alt'), ['class' => 'com-users-logout__image thumbnail float-end logout-image']); ?> + + + params->get('logoutdescription_show') == 1 && str_replace(' ', '', $this->params->get('logout_description')) != '') || $this->params->get('logout_image') != '') : ?> +
    + + +
    +
    +
    + +
    +
    + params->get('logout_redirect_url')) : ?> + + + + + +
    diff --git a/code/components/com_users/tmpl/method/backupcodes.php b/code/components/com_users/tmpl/method/backupcodes.php new file mode 100644 index 00000000..ba5224a6 --- /dev/null +++ b/code/components/com_users/tmpl/method/backupcodes.php @@ -0,0 +1,77 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Prevent direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Site\View\Method\HtmlView; + +/** @var HtmlView $this */ + +HTMLHelper::_('bootstrap.tooltip', '.hasTooltip'); + +$cancelURL = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id); + +if (!empty($this->returnURL)) { + $cancelURL = $this->escape(base64_decode($this->returnURL)); +} + +if ($this->record->method != 'backupcodes') { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); +} + +?> +

    + +

    + +
    + +
    + + + backupCodes) / 2); $i++) : ?> + + + + + +
    + backupCodes[2 * $i])) : ?> + + + backupCodes[2 * $i] ?> + + + backupCodes[1 + 2 * $i])) : ?> + + + backupCodes[1 + 2 * $i] ?> + +
    + +

    + +

    + +user->id, Factory::getApplication()->getFormToken(), empty($this->returnURL) ? '' : '&returnurl=' . $this->returnURL)) ?>"> + + + + + + + + diff --git a/code/components/com_users/tmpl/method/edit.php b/code/components/com_users/tmpl/method/edit.php new file mode 100644 index 00000000..c19017e9 --- /dev/null +++ b/code/components/com_users/tmpl/method/edit.php @@ -0,0 +1,181 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Prevent direct access +defined('_JEXEC') or die; + +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Site\View\Method\HtmlView; +use Joomla\Utilities\ArrayHelper; + +/** @var HtmlView $this */ + +$cancelURL = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id); + +if (!empty($this->returnURL)) { + $cancelURL = $this->escape(base64_decode($this->returnURL)); +} + +$recordId = (int) $this->record->id ?? 0; +$method = $this->record->method ?? $this->getModel()->getState('method'); +$userId = (int) $this->user->id ?? 0; +$headingLevel = 2; +$hideSubmit = !$this->renderOptions['show_submit'] && !$this->isEditExisting +?> +
    +
    " + class="form form-horizontal" id="com-users-method-edit" method="post"> + + returnURL)) : ?> + + + + renderOptions['hidden_data'])) : ?> + renderOptions['hidden_data'] as $key => $value) : ?> + + + + + title)) : ?> + renderOptions['help_url'])) : ?> + + + + + + + + id="com-users-method-edit-head"> + title) ?> + > + + + +
    + +
    + +

    + escape(Text::_('COM_USERS_MFA_EDIT_FIELD_TITLE_DESC')) ?> +

    +
    +
    + +
    +
    +
    + record->default ? 'checked="checked"' : ''; ?> name="default"> + +
    +
    +
    + + renderOptions['pre_message'])) : ?> +
    + renderOptions['pre_message'] ?> +
    + + + renderOptions['tabular_data'])) : ?> +
    + renderOptions['table_heading'])) : ?> + class="h3 border-bottom mb-3"> + renderOptions['table_heading'] ?> + > + + + + renderOptions['tabular_data'] as $cell1 => $cell2) : ?> + + + + + + +
    + + + +
    +
    + + + renderOptions['field_type'] == 'custom') : ?> + renderOptions['html']; ?> + +
    + renderOptions['label']) : ?> + + +
    renderOptions['label'] ? '' : 'offset-sm-3' ?>> + $this->renderOptions['input_type'], + 'name' => 'code', + 'value' => $this->escape($this->renderOptions['input_value']), + 'id' => 'com-users-method-code', + 'class' => 'form-control', + 'aria-describedby' => 'com-users-method-code-help', + ], + $this->renderOptions['input_attributes'] + ); + + if (strpos($attributes['class'], 'form-control') === false) { + $attributes['class'] .= ' form-control'; + } + ?> + > + +

    + escape($this->renderOptions['placeholder']) ?> +

    +
    +
    + +
    +
    + + + + + + +
    +
    + + renderOptions['post_message'])) : ?> +
    + renderOptions['post_message'] ?> +
    + +
    +
    diff --git a/code/components/com_users/tmpl/methods/default.php b/code/components/com_users/tmpl/methods/default.php new file mode 100644 index 00000000..ceb3a506 --- /dev/null +++ b/code/components/com_users/tmpl/methods/default.php @@ -0,0 +1,60 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Prevent direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Site\View\Methods\HtmlView; + +/** @var HtmlView $this */ +?> +
    + get('forHMVC', false)) : ?> +

    + +

    + + +
    +
    + mfaActive ? 'ON' : 'OFF')) ?> +
    + mfaActive) : ?> +
    + + + +
    + +
    + + methods)) : ?> +
    + + +
    + isMandatoryMFASetup) : ?> +
    +

    + +

    +

    + +

    +
    + + + setLayout('list'); + echo $this->loadTemplate(); ?> +
    diff --git a/code/components/com_users/tmpl/methods/firsttime.php b/code/components/com_users/tmpl/methods/firsttime.php new file mode 100644 index 00000000..73f5f17e --- /dev/null +++ b/code/components/com_users/tmpl/methods/firsttime.php @@ -0,0 +1,50 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Prevent direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\Component\Users\Site\View\Methods\HtmlView; + +/** @var HtmlView $this */ + +$headingLevel = 2; +?> +
    + isAdmin) : ?> + id="com-users-methods-list-head"> + + > + +
    + class="alert-heading"> + + + > +

    + +

    + + + +
    + + setLayout('list'); + echo $this->loadTemplate(); ?> +
    diff --git a/code/components/com_users/tmpl/methods/list.php b/code/components/com_users/tmpl/methods/list.php new file mode 100644 index 00000000..ebcdfb89 --- /dev/null +++ b/code/components/com_users/tmpl/methods/list.php @@ -0,0 +1,144 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Prevent direct access +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Uri\Uri; +use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; +use Joomla\Component\Users\Site\Model\MethodsModel; +use Joomla\Component\Users\Site\View\Methods\HtmlView; + +/** @var HtmlView $this */ + +/** @var MethodsModel $model */ +$model = $this->getModel(); + +$this->document->getWebAssetManager()->useScript('com_users.two-factor-list'); + +HTMLHelper::_('bootstrap.tooltip', '.hasTooltip'); + +$canAddEdit = MfaHelper::canAddEditMethod($this->user); +$canDelete = MfaHelper::canDeleteMethod($this->user); +?> +
    + methods as $methodName => $method) : + $methodClass = 'com-users-methods-list-method-name-' . htmlentities($method['name']) + . ($this->defaultMethod == $methodName ? ' com-users-methods-list-method-default' : ''); + ?> +
    +
    +
    + <?php echo $this->escape($method['display']) ?> +
    +
    +

    + + + + defaultMethod == $methodName) : ?> + + + + +

    +
    +
    + +
    +
    + +
    + + +
    + +
    +
    + + +
    + id . ($this->returnURL ? '&returnurl=' . $this->escape(urlencode($this->returnURL)) : '') . '&user_id=' . $this->user->id)) ?> +
    + + +

    + default) : ?> + + + escape(Text::_('COM_USERS_MFA_LIST_DEFAULTTAG')) ?> + + + + escape($record->title); ?> + +

    + + +
    + + formatRelative($record->created_on)) ?> + + + formatRelative($record->last_used)) ?> + +
    + +
    + + + + +
    + +
    + + + + + +
    +
    + +
    diff --git a/code/components/com_users/tmpl/profile/default.php b/code/components/com_users/tmpl/profile/default.php index e6b2af7c..a534e5b4 100644 --- a/code/components/com_users/tmpl/profile/default.php +++ b/code/components/com_users/tmpl/profile/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - + params->get('show_page_heading')) : ?> + + - id == $this->data->id) : ?> - - + id == $this->data->id) : ?> + + - loadTemplate('core'); ?> - loadTemplate('params'); ?> - loadTemplate('custom'); ?> + loadTemplate('core'); ?> + loadTemplate('params'); ?> + loadTemplate('custom'); ?>
    diff --git a/code/components/com_users/tmpl/profile/default_core.php b/code/components/com_users/tmpl/profile/default_core.php index 01d085e2..066094aa 100644 --- a/code/components/com_users/tmpl/profile/default_core.php +++ b/code/components/com_users/tmpl/profile/default_core.php @@ -1,4 +1,5 @@
    - - - -
    -
    - -
    -
    - escape($this->data->name); ?> -
    -
    - -
    -
    - escape($this->data->username); ?> -
    -
    - -
    -
    - data->registerDate, Text::_('DATE_FORMAT_LC1')); ?> -
    -
    - -
    - data->lastvisitDate !== null) : ?> -
    - data->lastvisitDate, Text::_('DATE_FORMAT_LC1')); ?> -
    - -
    - -
    - -
    + + + +
    +
    + +
    +
    + escape($this->data->name); ?> +
    +
    + +
    +
    + escape($this->data->username); ?> +
    +
    + +
    +
    + data->registerDate, Text::_('DATE_FORMAT_LC1')); ?> +
    +
    + +
    + data->lastvisitDate !== null) : ?> +
    + data->lastvisitDate, Text::_('DATE_FORMAT_LC1')); ?> +
    + +
    + +
    + +
    diff --git a/code/components/com_users/tmpl/profile/default_custom.php b/code/components/com_users/tmpl/profile/default_custom.php index 5106b09f..6bf103ec 100644 --- a/code/components/com_users/tmpl/profile/default_custom.php +++ b/code/components/com_users/tmpl/profile/default_custom.php @@ -1,4 +1,5 @@ form->getFieldsets(); -if (isset($fieldsets['core'])) -{ - unset($fieldsets['core']); +if (isset($fieldsets['core'])) { + unset($fieldsets['core']); } -if (isset($fieldsets['params'])) -{ - unset($fieldsets['params']); +if (isset($fieldsets['params'])) { + unset($fieldsets['params']); } $tmp = $this->data->jcfields ?? array(); $customFields = array(); -foreach ($tmp as $customField) -{ - $customFields[$customField->name] = $customField; +foreach ($tmp as $customField) { + $customFields[$customField->name] = $customField; } ?> $fieldset) : ?> - form->getFieldset($group); ?> - -
    - label) && ($legend = trim(Text::_($fieldset->label))) !== '') : ?> - - - description) && trim($fieldset->description)) : ?> -

    escape(Text::_($fieldset->description)); ?>

    - -
    - - hidden && $field->type !== 'Spacer') : ?> -
    - title; ?> -
    -
    - fieldname, $customFields)) : ?> - fieldname]->value) ? $customFields[$field->fieldname]->value : Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND'); ?> - id)) : ?> - id, $field->value); ?> - fieldname)) : ?> - fieldname, $field->value); ?> - type)) : ?> - type, $field->value); ?> - - value); ?> - -
    - - -
    -
    - + form->getFieldset($group); ?> + +
    + label) && ($legend = trim(Text::_($fieldset->label))) !== '') : ?> + + + description) && trim($fieldset->description)) : ?> +

    escape(Text::_($fieldset->description)); ?>

    + +
    + + hidden && $field->type !== 'Spacer') : ?> +
    + title; ?> +
    +
    + fieldname, $customFields)) : ?> + fieldname]->value) ? $customFields[$field->fieldname]->value : Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND'); ?> + id)) : ?> + id, $field->value); ?> + fieldname)) : ?> + fieldname, $field->value); ?> + type)) : ?> + type, $field->value); ?> + + value); ?> + +
    + + +
    +
    + diff --git a/code/components/com_users/tmpl/profile/default_params.php b/code/components/com_users/tmpl/profile/default_params.php index 80aa06bf..f7906946 100644 --- a/code/components/com_users/tmpl/profile/default_params.php +++ b/code/components/com_users/tmpl/profile/default_params.php @@ -1,4 +1,5 @@ form->getFieldset('params'); ?> -
    - -
    - - hidden) : ?> -
    - title; ?> -
    -
    - id)) : ?> - id, $field->value); ?> - fieldname)) : ?> - fieldname, $field->value); ?> - type)) : ?> - type, $field->value); ?> - - value); ?> - -
    - - -
    -
    +
    + +
    + + hidden) : ?> +
    + title; ?> +
    +
    + id)) : ?> + id, $field->value); ?> + fieldname)) : ?> + fieldname, $field->value); ?> + type)) : ?> + type, $field->value); ?> + + value); ?> + +
    + + +
    +
    diff --git a/code/components/com_users/tmpl/profile/edit.php b/code/components/com_users/tmpl/profile/edit.php index cbe0c850..357a4f52 100644 --- a/code/components/com_users/tmpl/profile/edit.php +++ b/code/components/com_users/tmpl/profile/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_users.two-factor-switcher'); + ->useScript('form.validate'); ?>
    - params->get('show_page_heading')) : ?> - - - -
    - - form->getFieldsets() as $group => $fieldset) : ?> - form->getFieldset($group); ?> - -
    - - label)) : ?> - - label); ?> - - - description) && trim($fieldset->description)) : ?> -

    - escape(Text::_($fieldset->description)); ?> -

    - - - - renderField(); ?> - -
    - - - - twofactormethods) > 1 && !empty($this->twofactorform)) : ?> -
    - + params->get('show_page_heading')) : ?> + + -
    -
    - -
    -
    - twofactormethods, 'jform[twofactor][method]', array('onchange' => 'Joomla.twoFactorMethodChange();', 'class' => 'form-select'), 'value', 'text', $this->otpConfig->method, 'jform_twofactor_method', false); ?> -
    -
    -
    - twofactorform as $form) : ?> - otpConfig->method ? '' : ' class="hidden"'; ?> -
    > - -
    - -
    -
    + + + form->getFieldsets() as $group => $fieldset) : ?> + form->getFieldset($group); ?> + +
    + + label)) : ?> + + label); ?> + + + description) && trim($fieldset->description)) : ?> +

    + escape(Text::_($fieldset->description)); ?> +

    + + + + renderField(); ?> + +
    + + -
    - - - -
    - - -
    - otpConfig->otep)) : ?> -
    - - -
    - - otpConfig->otep as $otep) : ?> -
    - -
    - -
    - + mfaConfigurationUI) : ?> +
    + + mfaConfigurationUI ?> +
    + -
    -
    - - - -
    -
    - -
    +
    +
    + + + +
    +
    + +
    diff --git a/code/components/com_users/tmpl/registration/complete.php b/code/components/com_users/tmpl/registration/complete.php index 36ab2698..15c5efc4 100644 --- a/code/components/com_users/tmpl/registration/complete.php +++ b/code/components/com_users/tmpl/registration/complete.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    +
    diff --git a/code/components/com_users/tmpl/registration/default.php b/code/components/com_users/tmpl/registration/default.php index e9b3093b..79fcbd67 100644 --- a/code/components/com_users/tmpl/registration/default.php +++ b/code/components/com_users/tmpl/registration/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - + params->get('show_page_heading')) : ?> + + -
    - - form->getFieldsets() as $fieldset) : ?> - form->getFieldset($fieldset->name); ?> - -
    - - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - - -
    -
    - - - -
    -
    - -
    +
    + + form->getFieldsets() as $fieldset) : ?> + form->getFieldset($fieldset->name); ?> + +
    + + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + + +
    +
    + + + +
    +
    + +
    diff --git a/code/components/com_users/tmpl/remind/default.php b/code/components/com_users/tmpl/remind/default.php index cff4f4dc..e1b0ecf1 100644 --- a/code/components/com_users/tmpl/remind/default.php +++ b/code/components/com_users/tmpl/remind/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/code/components/com_users/tmpl/reset/complete.php b/code/components/com_users/tmpl/reset/complete.php index 463c6b06..9ca759f0 100644 --- a/code/components/com_users/tmpl/reset/complete.php +++ b/code/components/com_users/tmpl/reset/complete.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/code/components/com_users/tmpl/reset/confirm.php b/code/components/com_users/tmpl/reset/confirm.php index b77b17bb..c1bf30e6 100644 --- a/code/components/com_users/tmpl/reset/confirm.php +++ b/code/components/com_users/tmpl/reset/confirm.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/code/components/com_users/tmpl/reset/default.php b/code/components/com_users/tmpl/reset/default.php index d56e2181..4c8687ec 100644 --- a/code/components/com_users/tmpl/reset/default.php +++ b/code/components/com_users/tmpl/reset/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/code/components/com_wrapper/src/Controller/DisplayController.php b/code/components/com_wrapper/src/Controller/DisplayController.php index 2062abda..8b47bd80 100644 --- a/code/components/com_wrapper/src/Controller/DisplayController.php +++ b/code/components/com_wrapper/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'wrapper'); - $this->input->set('view', $vName); + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'wrapper'); + $this->input->set('view', $vName); - return parent::display($cachable, array('Itemid' => 'INT')); - } + return parent::display($cachable, array('Itemid' => 'INT')); + } } diff --git a/code/components/com_wrapper/src/Service/Router.php b/code/components/com_wrapper/src/Service/Router.php index 32b3c111..d2c98969 100644 --- a/code/components/com_wrapper/src/Service/Router.php +++ b/code/components/com_wrapper/src/Service/Router.php @@ -1,4 +1,5 @@ 'wrapper'); - } + /** + * Parse the segments of a URL. + * + * @param array $segments The segments of the URL to parse. + * + * @return array The URL attributes to be used by the application. + * + * @since 3.3 + */ + public function parse(&$segments) + { + return array('view' => 'wrapper'); + } } diff --git a/code/components/com_wrapper/src/View/Wrapper/HtmlView.php b/code/components/com_wrapper/src/View/Wrapper/HtmlView.php index 6e5b41fe..ec408cb2 100644 --- a/code/components/com_wrapper/src/View/Wrapper/HtmlView.php +++ b/code/components/com_wrapper/src/View/Wrapper/HtmlView.php @@ -1,4 +1,5 @@ getParams(); - - // Because the application sets a default page title, we need to get it - // right from the menu item itself - - $this->setDocumentTitle($params->get('page_title', '')); - - if ($params->get('menu-meta_description')) - { - $this->document->setDescription($params->get('menu-meta_description')); - } - - if ($params->get('robots')) - { - $this->document->setMetaData('robots', $params->get('robots')); - } - - $wrapper = new \stdClass; - - // Auto height control - if ($params->def('height_auto')) - { - $wrapper->load = 'onload="iFrameHeight(this)"'; - } - else - { - $wrapper->load = ''; - } - - $url = $params->def('url', ''); - - if ($params->def('add_scheme', 1)) - { - // Adds 'http://' or 'https://' if none is set - if (strpos($url, '//') === 0) - { - // URL without scheme in component. Prepend current scheme. - $wrapper->url = Uri::getInstance()->toString(array('scheme')) . substr($url, 2); - } - elseif (strpos($url, '/') === 0) - { - // Relative URL in component. Use scheme + host + port. - $wrapper->url = Uri::getInstance()->toString(array('scheme', 'host', 'port')) . $url; - } - elseif (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) - { - // URL doesn't start with either 'http://' or 'https://'. Add current scheme. - $wrapper->url = Uri::getInstance()->toString(array('scheme')) . $url; - } - else - { - // URL starts with either 'http://' or 'https://'. Do not change it. - $wrapper->url = $url; - } - } - else - { - $wrapper->url = $url; - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - $this->params = &$params; - $this->wrapper = &$wrapper; - - parent::display($tpl); - } + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * @since 4.0.0 + */ + protected $params = null; + + /** + * The page parameters + * + * @var \stdClass + * @since 4.0.0 + */ + protected $wrapper = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + */ + public function display($tpl = null) + { + $params = Factory::getApplication()->getParams(); + + // Because the application sets a default page title, we need to get it + // right from the menu item itself + + $this->setDocumentTitle($params->get('page_title', '')); + + if ($params->get('menu-meta_description')) { + $this->document->setDescription($params->get('menu-meta_description')); + } + + if ($params->get('robots')) { + $this->document->setMetaData('robots', $params->get('robots')); + } + + $wrapper = new \stdClass(); + + // Auto height control + if ($params->def('height_auto')) { + $wrapper->load = 'onload="iFrameHeight(this)"'; + } else { + $wrapper->load = ''; + } + + $url = $params->def('url', ''); + + if ($params->def('add_scheme', 1)) { + // Adds 'http://' or 'https://' if none is set + if (strpos($url, '//') === 0) { + // URL without scheme in component. Prepend current scheme. + $wrapper->url = Uri::getInstance()->toString(array('scheme')) . substr($url, 2); + } elseif (strpos($url, '/') === 0) { + // Relative URL in component. Use scheme + host + port. + $wrapper->url = Uri::getInstance()->toString(array('scheme', 'host', 'port')) . $url; + } elseif (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) { + // URL doesn't start with either 'http://' or 'https://'. Add current scheme. + $wrapper->url = Uri::getInstance()->toString(array('scheme')) . $url; + } else { + // URL starts with either 'http://' or 'https://'. Do not change it. + $wrapper->url = $url; + } + } else { + $wrapper->url = $url; + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + $this->params = &$params; + $this->wrapper = &$wrapper; + + parent::display($tpl); + } } diff --git a/code/components/com_wrapper/tmpl/wrapper/default.php b/code/components/com_wrapper/tmpl/wrapper/default.php index 73fdd91b..5e69c5bf 100644 --- a/code/components/com_wrapper/tmpl/wrapper/default.php +++ b/code/components/com_wrapper/tmpl/wrapper/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager() - ->registerAndUseScript('com_wrapper.iframe', 'com_wrapper/iframe-height.min.js', [], ['defer' => true]); + ->registerAndUseScript('com_wrapper.iframe', 'com_wrapper/iframe-height.min.js', [], ['defer' => true]); ?>
    - params->get('show_page_heading')) : ?> - - - + params->get('show_page_heading')) : ?> + + +
    diff --git a/code/includes/app.php b/code/includes/app.php index 15fdbddf..b5000504 100644 --- a/code/includes/app.php +++ b/code/includes/app.php @@ -1,4 +1,5 @@ alias('session.web', 'session.web.site') - ->alias('session', 'session.web.site') - ->alias('JSession', 'session.web.site') - ->alias(\Joomla\CMS\Session\Session::class, 'session.web.site') - ->alias(\Joomla\Session\Session::class, 'session.web.site') - ->alias(\Joomla\Session\SessionInterface::class, 'session.web.site'); + ->alias('session', 'session.web.site') + ->alias('JSession', 'session.web.site') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.site') + ->alias(\Joomla\Session\Session::class, 'session.web.site') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.site'); // Instantiate the application. $app = $container->get(\Joomla\CMS\Application\SiteApplication::class); diff --git a/code/includes/defines.php b/code/includes/defines.php index 1b89ecbd..a0d6d924 100644 --- a/code/includes/defines.php +++ b/code/includes/defines.php @@ -1,4 +1,5 @@ isInDevelopmentState()))) -{ - if (file_exists(JPATH_INSTALLATION . '/index.php')) - { - header('Location: ' . substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], 'index.php')) . 'installation/index.php'); - - exit; - } - else - { - echo 'No configuration file found and no installation code available. Exiting...'; - - exit; - } +if ( + !file_exists(JPATH_CONFIGURATION . '/configuration.php') + || (filesize(JPATH_CONFIGURATION . '/configuration.php') < 10) + || (file_exists(JPATH_INSTALLATION . '/index.php') && (false === (new Version())->isInDevelopmentState())) +) { + if (file_exists(JPATH_INSTALLATION . '/index.php')) { + header('Location: ' . substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], 'index.php')) . 'installation/index.php'); + + exit; + } else { + echo 'No configuration file found and no installation code available. Exiting...'; + + exit; + } } // Pre-Load configuration. Don't remove the Output Buffering due to BOM issues, see JCode 26026 @@ -39,68 +38,61 @@ ob_end_clean(); // System configuration. -$config = new JConfig; +$config = new JConfig(); // Set the error_reporting, and adjust a global Error Handler -switch ($config->error_reporting) -{ - case 'default': - case '-1': - - break; +switch ($config->error_reporting) { + case 'default': + case '-1': + break; - case 'none': - case '0': - error_reporting(0); + case 'none': + case '0': + error_reporting(0); - break; + break; - case 'simple': - error_reporting(E_ERROR | E_WARNING | E_PARSE); - ini_set('display_errors', 1); + case 'simple': + error_reporting(E_ERROR | E_WARNING | E_PARSE); + ini_set('display_errors', 1); - break; + break; - case 'maximum': - case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 - error_reporting(E_ALL); - ini_set('display_errors', 1); + case 'maximum': + case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 + error_reporting(E_ALL); + ini_set('display_errors', 1); - break; + break; - default: - error_reporting($config->error_reporting); - ini_set('display_errors', 1); + default: + error_reporting($config->error_reporting); + ini_set('display_errors', 1); - break; + break; } -if (!defined('JDEBUG')) -{ - define('JDEBUG', $config->debug); +if (!defined('JDEBUG')) { + define('JDEBUG', $config->debug); } // Check deprecation logging -if (empty($config->log_deprecated)) -{ - // Reset handler for E_USER_DEPRECATED - set_error_handler(null, E_USER_DEPRECATED); -} -else -{ - // Make sure handler for E_USER_DEPRECATED is registered - set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); +if (empty($config->log_deprecated)) { + // Reset handler for E_USER_DEPRECATED + set_error_handler(null, E_USER_DEPRECATED); +} else { + // Make sure handler for E_USER_DEPRECATED is registered + set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); } -if (JDEBUG || $config->error_reporting === 'maximum') -{ - // Set new Exception handler with debug enabled - $errorHandler->setExceptionHandler( - [ - new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), - 'renderException' - ] - ); +if (JDEBUG || $config->error_reporting === 'maximum') { + // Set new Exception handler with debug enabled + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException' + ] + ); } /** @@ -109,15 +101,12 @@ * We need to do this as high up the stack as we can, as the default in \Joomla\Utilities\IpHelper is to * $allowIpOverride = true which is the wrong default for a generic site NOT behind a trusted proxy/load balancer. */ -if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) -{ - // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR - IpHelper::setAllowIpOverrides(true); -} -else -{ - // We disable the allowing of IP overriding using headers by default. - IpHelper::setAllowIpOverrides(false); +if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) { + // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR + IpHelper::setAllowIpOverrides(true); +} else { + // We disable the allowing of IP overriding using headers by default. + IpHelper::setAllowIpOverrides(false); } unset($config); diff --git a/code/index.php b/code/index.php index c2b6893a..59acdd25 100644 --- a/code/index.php +++ b/code/index.php @@ -1,4 +1,5 @@ ' or '' element." COM_MEDIA_BREADCRUMB_LABEL="Breadcrumb" diff --git a/code/language/en-GB/com_users.ini b/code/language/en-GB/com_users.ini index 281002a1..2af0e3ee 100644 --- a/code/language/en-GB/com_users.ini +++ b/code/language/en-GB/com_users.ini @@ -8,10 +8,10 @@ COM_USERS_CAPTCHA_LABEL="Captcha" COM_USERS_DATABASE_ERROR="Error getting the user from the database: %s" COM_USERS_EDIT_PROFILE="Edit Profile" COM_USERS_EMAIL_ACCOUNT_DETAILS="Account Details for {NAME} at {SITENAME}" -COM_USERS_EMAIL_ACTIVATE_WITH_ADMIN_ACTIVATION_BODY="Hello administrator,\n\nA new user has registered at {SITENAME}.\nThe user has verified their email address and requests that you approve their account.\nThis email has their details:\n\n Name : {NAME} \n email: {EMAIL} \n Username: {USERNAME} \n\nYou can activate the user by selecting the link below:\n{ACTIVATE}" -COM_USERS_EMAIL_ACTIVATE_WITH_ADMIN_ACTIVATION_SUBJECT="Registration approval required for account of {NAME} at {SITENAME}" COM_USERS_EMAIL_ACTIVATED_BY_ADMIN_ACTIVATION_BODY="Hello {NAME},\n\nYour account has been activated by an administrator. You can now login at {SITEURL} using the username {USERNAME} and the password you chose while registering." COM_USERS_EMAIL_ACTIVATED_BY_ADMIN_ACTIVATION_SUBJECT="Account activated for {NAME} at {SITENAME}" +COM_USERS_EMAIL_ACTIVATE_WITH_ADMIN_ACTIVATION_BODY="Hello administrator,\n\nA new user has registered at {SITENAME}.\nThe user has verified their email address and requests that you approve their account.\nThis email has their details:\n\n Name : {NAME} \n email: {EMAIL} \n Username: {USERNAME} \n\nYou can activate the user by selecting the link below:\n{ACTIVATE}" +COM_USERS_EMAIL_ACTIVATE_WITH_ADMIN_ACTIVATION_SUBJECT="Registration approval required for account of {NAME} at {SITENAME}" COM_USERS_EMAIL_PASSWORD_RESET_BODY="Hello,\n\nA request has been made to reset your {SITENAME} account password. To reset your password, you will need to submit this verification code to verify that the request was legitimate.\n\nThe verification code is {TOKEN}\n\nSelect the URL below and proceed with resetting your password.\n\n{LINK_TEXT} \n\nThank you." COM_USERS_EMAIL_PASSWORD_RESET_SUBJECT="Your {SITENAME} password reset request" COM_USERS_EMAIL_REGISTERED_BODY="Hello {NAME},\n\nThank you for registering at {SITENAME}.\n\nYou may now log in to {SITEURL} using the following username and password:\n\nUsername: {USERNAME}\nPassword: {PASSWORD_CLEAR}" @@ -23,7 +23,6 @@ COM_USERS_EMAIL_REGISTERED_WITH_ADMIN_ACTIVATION_BODY="Hello {NAME},\n\nThank yo COM_USERS_EMAIL_REGISTERED_WITH_ADMIN_ACTIVATION_BODY_NOPW="Hello {NAME},\n\nThank you for registering at {SITENAME}. Your account is created and must be verified before you can use it.\n\nTo verify the account select the following link or copy-paste it in your browser:\n{ACTIVATE} \n\nAfter verification an administrator will be notified to activate your account. You'll receive a confirmation when it's done.\n\nOnce that account has been activated you may login to {SITEURL} using the following username and the password you entered during registration:\n\nUsername: {USERNAME}" COM_USERS_EMAIL_USERNAME_REMINDER_BODY="Hello,\n\nA username reminder has been requested for your {SITENAME} account.\n\nYour username is {USERNAME}.\n\nTo login to your account, select the link below.\n\n{LINK_TEXT} \n\nThank you." COM_USERS_EMAIL_USERNAME_REMINDER_SUBJECT="Your {SITENAME} username" -COM_USERS_ERROR_SECRET_CODE_WITHOUT_TFA="You have entered a Secret Code but two factor authentication is not enabled in your user account. If you want to use a secret code to secure your login please edit your user profile and enable two factor authentication." COM_USERS_FIELD_PASSWORD_RESET_LABEL="Email Address" COM_USERS_FIELD_REMIND_EMAIL_LABEL="Email Address" COM_USERS_FIELD_RESET_CONFIRM_TOKEN_LABEL="Verification Code" @@ -32,6 +31,7 @@ COM_USERS_FIELD_RESET_PASSWORD1_LABEL="Password" COM_USERS_FIELD_RESET_PASSWORD1_MESSAGE="The passwords you entered do not match. Please enter your desired password in the password field and confirm your entry by entering it in the confirm password field." COM_USERS_FIELD_RESET_PASSWORD2_LABEL="Confirm Password" COM_USERS_INVALID_EMAIL="Invalid email address" +COM_USERS_LBL_SELECT_INSTRUCTIONS="Please select how you would like to verify your login to this site." COM_USERS_LOGIN_DEFAULT_LABEL="User Login" COM_USERS_LOGIN_REGISTER="Don't have an account?" COM_USERS_LOGIN_REMEMBER_ME="Remember me" @@ -41,27 +41,58 @@ COM_USERS_LOGIN_USERNAME_LABEL="Username" COM_USERS_MAIL_FAILED="Failed sending email." COM_USERS_MAIL_SEND_FAILURE_BODY="An error was encountered when sending the user registration email. The user who tried to register is: %s" COM_USERS_MAIL_SEND_FAILURE_SUBJECT="Error sending email" +COM_USERS_MFA_ADD_AUTHENTICATOR_OF_TYPE="Add a new %s" +COM_USERS_MFA_ADD_PAGE_HEAD="Add a Multi-factor Authentication Method" +COM_USERS_MFA_BACKUPCODES_PRINT_PROMPT="Backup Codes let you log into the site if your regular Multi-factor Authentication method does not work or you no longer have access to it. Each code can be used only once." +COM_USERS_MFA_BACKUPCODES_PRINT_PROMPT_HEAD="Print these codes and keep them in your wallet." +COM_USERS_MFA_BACKUPCODES_RESET="Regenerate Backup Codes" +COM_USERS_MFA_BACKUPCODES_RESET_INFO="Use the button below to generate a new set of Backup Codes. We recommend that you do this if you think your Backup Codes are compromised, e.g. someone got hold of a printout with them, or if you are running low on available Backup Codes." +COM_USERS_MFA_EDIT_FIELD_DEFAULT="Make this the default Multi-factor Authentication method" +COM_USERS_MFA_EDIT_FIELD_TITLE="Title" +COM_USERS_MFA_EDIT_FIELD_TITLE_DESC="You and the site administrators will see this name in the list of available Multi-factor Authentication methods for your user account. Please do not include any sensitive or personally identifiable information." +COM_USERS_MFA_EDIT_PAGE_HEAD="Modify a Multi-factor Authentication method" +COM_USERS_MFA_FIRSTTIME_INSTRUCTIONS_HEAD="Use Multi-factor Authentication for added security" +COM_USERS_MFA_FIRSTTIME_INSTRUCTIONS_WHATITDOES="Here's how it works. Add a Multi-factor Authentication method below. From now on, every time you log into the site you will be asked to use this method to complete the login. Even if someone steals your username and password they won't have access to your account on this site." +COM_USERS_MFA_FIRSTTIME_NOTINTERESTED="Don't show this again" +COM_USERS_MFA_FIRSTTIME_PAGE_HEAD="Set up your Multi-factor Authentication" +COM_USERS_MFA_INVALID_CODE="Multi-factor Authentication failed. Please try again." +COM_USERS_MFA_INVALID_METHOD="Invalid Multi-factor Authentication method." +COM_USERS_MFA_LBL_CREATEDON="Added: %s" +COM_USERS_MFA_LBL_DATE_FORMAT_PAST="F d, Y" +COM_USERS_MFA_LBL_DATE_FORMAT_TODAY="H:i" +COM_USERS_MFA_LBL_DATE_FORMAT_YESTERDAY="H:i" +COM_USERS_MFA_LBL_LASTUSED="Last used: %s" +COM_USERS_MFA_LBL_PAST="%s" +COM_USERS_MFA_LBL_TODAY="Today, %s" +COM_USERS_MFA_LBL_YESTERDAY="Yesterday, %s" +COM_USERS_MFA_LIST_DEFAULTTAG="Default" +COM_USERS_MFA_LIST_INSTRUCTIONS="Add at least one Multi-factor Authentication method. Every time you log into the site you will be asked to provide it." +COM_USERS_MFA_LIST_PAGE_HEAD="Your Multi-factor Authentication options" +COM_USERS_MFA_LIST_REMOVEALL="Turn Off" +COM_USERS_MFA_LIST_STATUS_OFF="Multi-factor Authentication is not enabled." +COM_USERS_MFA_LIST_STATUS_ON="Multi-factor Authentication is enabled." +COM_USERS_MFA_LOGOUT="Log Out" +COM_USERS_MFA_MANDATORY_NOTICE_BODY="Please enable a Multi-factor Authentication method for your user account. You will not be able to proceed using the site until you do so." +COM_USERS_MFA_MANDATORY_NOTICE_HEAD="Multi-factor Authentication is mandatory for your user account" +COM_USERS_MFA_SELECT_PAGE_HEAD="Select a Multi-factor Authentication method" +COM_USERS_MFA_USE_DIFFERENT_METHOD="Select a different method" +COM_USERS_MFA_VALIDATE="Validate" COM_USERS_OR="or" COM_USERS_PROFILE="User Profile" COM_USERS_PROFILE_CORE_LEGEND="Profile" COM_USERS_PROFILE_DEFAULT_LABEL="Edit Your Profile" COM_USERS_PROFILE_EMAIL1_LABEL="Email Address" COM_USERS_PROFILE_LAST_VISITED_DATE_LABEL="Last Visited Date" +COM_USERS_PROFILE_MULTIFACTOR_AUTH="Multi-factor Authentication" COM_USERS_PROFILE_NAME_LABEL="Name" COM_USERS_PROFILE_NEVER_VISITED="This is the first time you have visited this site." COM_USERS_PROFILE_NOCHANGE_USERNAME_DESC="If you want to change your username, please contact a site administrator." -COM_USERS_PROFILE_OTEPS="One time emergency passwords" -COM_USERS_PROFILE_OTEPS_DESC="If you do not have access to your two factor authentication device you can use any of the following passwords instead of a regular security code. Each one of these emergency passwords is immediately destroyed upon use. We recommend printing these passwords out and keeping the printout in a safe and accessible location, eg your wallet or a safety deposit box." -COM_USERS_PROFILE_OTEPS_WAIT_DESC="There are no emergency one time passwords generated in your account. The passwords will be generated automatically and displayed here as soon as you activate two factor authentication." COM_USERS_PROFILE_PASSWORD1_LABEL="Password" COM_USERS_PROFILE_PASSWORD1_MESSAGE="The passwords you entered do not match. Please enter your desired password in the password field and confirm your entry by entering it in the confirm password field." COM_USERS_PROFILE_PASSWORD2_LABEL="Confirm Password" COM_USERS_PROFILE_REGISTERED_DATE_LABEL="Registered Date" COM_USERS_PROFILE_SAVE_FAILED="Profile could not be saved: %s" COM_USERS_PROFILE_SAVE_SUCCESS="Profile saved." -COM_USERS_PROFILE_TWO_FACTOR_AUTH="Two Factor Authentication" -COM_USERS_PROFILE_TWOFACTOR_DESC="Select the two factor authentication method you want to use." -COM_USERS_PROFILE_TWOFACTOR_LABEL="Authentication Method" COM_USERS_PROFILE_USERNAME_LABEL="Username" COM_USERS_PROFILE_USERNAME_MESSAGE="The username you entered is not available. Please pick another username." COM_USERS_PROFILE_VALUE_NOT_FOUND="Website default" @@ -107,11 +138,25 @@ COM_USERS_RESET_REQUEST_ERROR="Error requesting password reset." COM_USERS_RESET_REQUEST_FAILED="Reset password failed: %s" COM_USERS_RESET_REQUEST_LABEL="Please enter the email address for your account. A verification code will be sent to you. Once you have received the verification code, you will be able to choose a new password for your account." COM_USERS_SETTINGS_FIELDSET_LABEL="Basic Settings" +COM_USERS_USER_BACKUPCODE="Backup Code" +COM_USERS_USER_BACKUPCODES="Backup Codes" +COM_USERS_USER_BACKUPCODES_CAPTIVE_PROMPT="If you do not have access to your usual Multi-factor Authentication method use any of your Backup Codes in the field below. Please remember that this emergency backup code cannot be reused." +COM_USERS_USER_BACKUPCODES_DESC="If you do not have access to your Multi-factor Authentication device you can use any of the following passwords instead of a regular security code. Each one of these emergency codes is immediately destroyed upon use. We recommend printing these codes out and keeping the printout in a safe and accessible location, eg your wallet or a safety deposit box." COM_USERS_USER_BLOCKED="This user is blocked. If this is an error, please contact an administrator." COM_USERS_USER_FIELD_BACKEND_LANGUAGE_LABEL="Backend Language" COM_USERS_USER_FIELD_BACKEND_TEMPLATE_LABEL="Backend Template Style" COM_USERS_USER_FIELD_EDITOR_LABEL="Editor" COM_USERS_USER_FIELD_FRONTEND_LANGUAGE_LABEL="Frontend Language" COM_USERS_USER_FIELD_TIMEZONE_LABEL="Time Zone" +COM_USERS_USER_MULTIFACTOR_AUTH="Multi-factor Authentication" COM_USERS_USER_NOT_FOUND="User not found." COM_USERS_USER_SAVE_FAILED="Failed to save user: %s" + +; Obsolete language strings since 4.2.0 -- Remove them in Joomla 5.0 +COM_USERS_ERROR_SECRET_CODE_WITHOUT_TFA="You have entered a Secret Code but two factor authentication is not enabled in your user account. If you want to use a secret code to secure your login please edit your user profile and enable two factor authentication." +COM_USERS_PROFILE_OTEPS="One time emergency passwords" +COM_USERS_PROFILE_OTEPS_DESC="If you do not have access to your two factor authentication device you can use any of the following passwords instead of a regular security code. Each one of these emergency passwords is immediately destroyed upon use. We recommend printing these passwords out and keeping the printout in a safe and accessible location, eg your wallet or a safety deposit box." +COM_USERS_PROFILE_OTEPS_WAIT_DESC="There are no emergency one time passwords generated in your account. The passwords will be generated automatically and displayed here as soon as you activate two factor authentication." +COM_USERS_PROFILE_TWOFACTOR_DESC="Select the two factor authentication method you want to use." +COM_USERS_PROFILE_TWOFACTOR_LABEL="Authentication Method" +COM_USERS_PROFILE_TWO_FACTOR_AUTH="Two Factor Authentication" diff --git a/code/language/en-GB/finder_cli.ini b/code/language/en-GB/finder_cli.ini index 966ad1d0..bcc3edba 100644 --- a/code/language/en-GB/finder_cli.ini +++ b/code/language/en-GB/finder_cli.ini @@ -5,8 +5,8 @@ FINDER_CLI="Smart Search INDEXER" FINDER_CLI_BATCH_COMPLETE=" * Processed batch %s in %s seconds." -FINDER_CLI_BATCH_CONTINUING=" * Continuing processing of batch ..." -FINDER_CLI_BATCH_PAUSING=" * Pausing processing for %s seconds ..." +FINDER_CLI_BATCH_CONTINUING=" * Continuing processing of batch …" +FINDER_CLI_BATCH_PAUSING=" * Pausing processing for %s seconds …" FINDER_CLI_FILTER_RESTORE_WARNING="Warning: Did not find taxonomy %s/%s in filter %s" FINDER_CLI_INDEX_PURGE="Clear index" FINDER_CLI_INDEX_PURGE_FAILED="- index clear failed." diff --git a/code/language/en-GB/install.xml b/code/language/en-GB/install.xml index 706d4782..a24f87f7 100644 --- a/code/language/en-GB/install.xml +++ b/code/language/en-GB/install.xml @@ -2,8 +2,8 @@ English (en-GB) en-GB - 4.1.5 - June 2022 + 4.2.3 + 2022-09 Joomla! Project admin@joomla.org www.joomla.org diff --git a/code/language/en-GB/joomla.ini b/code/language/en-GB/joomla.ini index f0a54661..64186fd9 100644 --- a/code/language/en-GB/joomla.ini +++ b/code/language/en-GB/joomla.ini @@ -88,6 +88,7 @@ JINVALID_TOKEN_NOTICE="The security token did not match. The request was aborted JLOGIN="Log in" JLOGOUT="Log out" JMONTH="Month" +JNEVER="Never" JNEW="New" JNEXT="Next" JNEXT_TITLE="Next article: %s" @@ -110,6 +111,7 @@ JREGISTER="Register" JREQUIRED="Required" JRESET="Reset" JSAVE="Save" +JSAVEANDCLOSE="Save & Close" JSAVEASCOPY="Save As Copy" JSELECT="Select" JSHOW="Show" @@ -169,7 +171,7 @@ JERROR_NOLOGIN_BLOCKED="Login denied! Your account has either been blocked or yo JERROR_PAGE_NOT_FOUND="Page not found" JERROR_SENDING_EMAIL="Email could not be sent." JERROR_SESSION_STARTUP="Error starting the session." -JERROR_TABLE_BIND_FAILED="hmm %s ..." +JERROR_TABLE_BIND_FAILED="hmm %s …" JERROR_USERS_PROFILE_NOT_FOUND="User profile not found" JFIELD_ACCESS_DESC="Access level for this content." @@ -245,7 +247,7 @@ JGLOBAL_AUTH_FAIL="Authentication failed" JGLOBAL_AUTH_FAILED="Failed to authenticate: %s" JGLOBAL_AUTH_INCORRECT="Incorrect username/password" JGLOBAL_AUTH_INVALID_PASS="Username and password do not match or you do not have an account yet." -JGLOBAL_AUTH_INVALID_SECRETKEY="The two factor authentication Secret Key is invalid." +JGLOBAL_AUTH_INVALID_SECRETKEY="The Multi-factor Authentication Secret Key is invalid." JGLOBAL_AUTH_NO_REDIRECT="Could not redirect to server: %s" JGLOBAL_AUTH_NO_USER="Username and password do not match or you do not have an account yet." JGLOBAL_AUTH_NOT_CONNECT="Unable to connect to authentication service." @@ -318,7 +320,7 @@ JGLOBAL_ICON_SEP="|" JGLOBAL_INHERIT="Inherit" JGLOBAL_INTRO_TEXT="Intro Text" JGLOBAL_JOOA11Y="Accessibility Check" -JGLOBAL_KEEP_TYPING="Keep typing ..." +JGLOBAL_KEEP_TYPING="Keep typing …" JGLOBAL_LEARN_MORE="Learn More" JGLOBAL_LEFT="Left" JGLOBAL_LIST_ALIAS="(Alias: %s)" @@ -331,7 +333,7 @@ JGLOBAL_NAME_DESC="Name descending" JGLOBAL_NEWITEMSLAST_DESC="New items default to the last position. Ordering can be changed after this item has been saved." JGLOBAL_NO_MATCHING_RESULTS="No Matching Results" JGLOBAL_NUM="#" -JGLOBAL_OTPMETHOD_NONE="Disable Two Factor Authentication" +JGLOBAL_OTPMETHOD_NONE="Disable Multi-factor Authentication" JGLOBAL_PASSWORD="Password" JGLOBAL_PASSWORD_RESET_REQUIRED="You are required to reset your password before proceeding." JGLOBAL_PREVIEW_POSITION="Position: %s" @@ -347,7 +349,7 @@ JGLOBAL_RESOURCE_NOT_FOUND="Resource not found" JGLOBAL_RIGHT="Right" JGLOBAL_ROOT="Root" JGLOBAL_SECRETKEY="Secret Key" -JGLOBAL_SECRETKEY_HELP="If you have enabled two factor authentication in your user account please enter your secret key. If you do not know what this means, you can leave this field blank." +JGLOBAL_SECRETKEY_HELP="If you have enabled Multi-factor Authentication in your user account please enter your secret key. If you do not know what this means, you can leave this field blank." JGLOBAL_SELECT_AN_OPTION="Select an option" JGLOBAL_SELECT_NO_RESULTS_MATCH="No results match" JGLOBAL_SELECT_PRESS_TO_SELECT="Press to select" @@ -505,9 +507,9 @@ JTABLE_OPTIONS="Table Options" JTABLE_OPTIONS_ORDERING="Order by:" ; Read more layout -JGLOBAL_READ_MORE="Read more ..." +JGLOBAL_READ_MORE="Read more …" JGLOBAL_READ_MORE_TITLE="Read more: %s" -JGLOBAL_REGISTER_TO_READ_MORE="Register to read more ..." +JGLOBAL_REGISTER_TO_READ_MORE="Register to read more …" ; States assets translations ARCHIVE="Archive" diff --git a/code/language/en-GB/langmetadata.xml b/code/language/en-GB/langmetadata.xml index e3f6685e..e8766411 100644 --- a/code/language/en-GB/langmetadata.xml +++ b/code/language/en-GB/langmetadata.xml @@ -1,8 +1,8 @@ English (en-GB) - 4.1.5 - June 2022 + 4.2.3 + 2022-09 Joomla! Project admin@joomla.org www.joomla.org diff --git a/code/language/en-GB/lib_joomla.ini b/code/language/en-GB/lib_joomla.ini index ffde173a..e0208840 100644 --- a/code/language/en-GB/lib_joomla.ini +++ b/code/language/en-GB/lib_joomla.ini @@ -346,7 +346,7 @@ JLIB_FORM_VALUE_CACHE_APCU="APC User Cache" JLIB_FORM_VALUE_CACHE_FILE="File" JLIB_FORM_VALUE_CACHE_MEMCACHED="Memcached (Experimental)" JLIB_FORM_VALUE_CACHE_REDIS="Redis" -JLIB_FORM_VALUE_CACHE_WINCACHE="Windows Cache" +JLIB_FORM_VALUE_CACHE_WINCACHE="Windows Cache (deprecated)" JLIB_FORM_VALUE_FROM_TEMPLATE="From Template" JLIB_FORM_VALUE_INHERITED="Inherited" JLIB_FORM_VALUE_SESSION_APCU="APC User Cache" @@ -354,7 +354,7 @@ JLIB_FORM_VALUE_SESSION_DATABASE="Database" JLIB_FORM_VALUE_SESSION_FILESYSTEM="Filesystem" JLIB_FORM_VALUE_SESSION_MEMCACHED="Memcached (Experimental)" JLIB_FORM_VALUE_SESSION_REDIS="Redis" -JLIB_FORM_VALUE_SESSION_WINCACHE="Windows Cache" +JLIB_FORM_VALUE_SESSION_WINCACHE="Windows Cache (deprecated)" JLIB_FORM_VALUE_TIMEZONE_UTC="Universal Time, Coordinated (UTC)" JLIB_HTML_ACCESS_MODIFY_DESC_CAPTION_ACL="ACL" JLIB_HTML_ACCESS_MODIFY_DESC_CAPTION_TABLE="Table" diff --git a/code/language/en-GB/localise.php b/code/language/en-GB/localise.php index 8b46cfdc..af8165be 100644 --- a/code/language/en-GB/localise.php +++ b/code/language/en-GB/localise.php @@ -1,12 +1,19 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * en-GB localise class. @@ -15,28 +22,23 @@ */ abstract class En_GBLocalise { - /** - * Returns the potential suffixes for a specific number of items - * - * @param integer $count The number of items. - * - * @return array An array of potential suffixes. - * - * @since 1.6 - */ - public static function getPluralSuffixes($count) - { - if ($count == 0) - { - return array('0'); - } - elseif ($count == 1) - { - return array('ONE', '1'); - } - else - { - return array('OTHER', 'MORE'); - } - } + /** + * Returns the potential suffixes for a specific number of items + * + * @param integer $count The number of items. + * + * @return array An array of potential suffixes. + * + * @since 1.6 + */ + public static function getPluralSuffixes($count) + { + if ($count == 0) { + return array('0'); + } elseif ($count == 1) { + return array('ONE', '1'); + } else { + return array('OTHER', 'MORE'); + } + } } diff --git a/code/language/en-GB/mod_articles_category.ini b/code/language/en-GB/mod_articles_category.ini index 8f420b15..a734bfe1 100644 --- a/code/language/en-GB/mod_articles_category.ini +++ b/code/language/en-GB/mod_articles_category.ini @@ -69,7 +69,7 @@ MOD_ARTICLES_CATEGORY_OPTION_STARTPUBLISHING_VALUE="Start Publishing Date" MOD_ARTICLES_CATEGORY_OPTION_VOTE_VALUE="Vote" MOD_ARTICLES_CATEGORY_OPTION_YEAR_VALUE="Year" MOD_ARTICLES_CATEGORY_READ_MORE="Read more: " -MOD_ARTICLES_CATEGORY_READ_MORE_TITLE="Read More ..." +MOD_ARTICLES_CATEGORY_READ_MORE_TITLE="Read More …" MOD_ARTICLES_CATEGORY_REGISTER_TO_READ_MORE="Register to read more" MOD_ARTICLES_CATEGORY_UNTAGGED="Untagged" MOD_ARTICLES_CATEGORY_XML_DESCRIPTION="This module displays a list of articles from one or more categories." diff --git a/code/language/en-GB/mod_articles_news.ini b/code/language/en-GB/mod_articles_news.ini index 581dcd3f..bb442222 100644 --- a/code/language/en-GB/mod_articles_news.ini +++ b/code/language/en-GB/mod_articles_news.ini @@ -17,14 +17,14 @@ MOD_ARTICLES_NEWS_FIELD_ORDERING_MODIFIED_DATE="Modified Date" MOD_ARTICLES_NEWS_FIELD_ORDERING_ORDERING="Ordering" MOD_ARTICLES_NEWS_FIELD_ORDERING_PUBLISHED_DATE="Published Date" MOD_ARTICLES_NEWS_FIELD_ORDERING_RANDOM="Random" -MOD_ARTICLES_NEWS_FIELD_READMORE_LABEL="'Read more ...' Link" +MOD_ARTICLES_NEWS_FIELD_READMORE_LABEL="'Read more …' Link" MOD_ARTICLES_NEWS_FIELD_SEPARATOR_LABEL="Last Separator" MOD_ARTICLES_NEWS_FIELD_SHOWINTROTEXT_LABEL="Intro Text" MOD_ARTICLES_NEWS_FIELD_TITLE_LABEL="Article Title" MOD_ARTICLES_NEWS_FIELD_TRIGGEREVENTS_LABEL="Trigger Plugin Events" MOD_ARTICLES_NEWS_OPTION_FULLIMAGE="Full Image" MOD_ARTICLES_NEWS_OPTION_INTROIMAGE="Intro Image" -MOD_ARTICLES_NEWS_READMORE="Read more ..." +MOD_ARTICLES_NEWS_READMORE="Read more …" MOD_ARTICLES_NEWS_READMORE_REGISTER="Register to Read More" MOD_ARTICLES_NEWS_TITLE_HEADING="Header Level" MOD_ARTICLES_NEWS_VALUE_ONLY_SHOW_FEATURED="Only show Featured Articles" diff --git a/code/language/en-GB/mod_finder.ini b/code/language/en-GB/mod_finder.ini index 41b22632..3c682c52 100644 --- a/code/language/en-GB/mod_finder.ini +++ b/code/language/en-GB/mod_finder.ini @@ -24,7 +24,7 @@ MOD_FINDER_FIELDSET_BASIC_SEARCHFILTER_LABEL="Search Filter" MOD_FINDER_FIELDSET_BASIC_SHOW_ADVANCED_LABEL="Advanced Search" MOD_FINDER_FIELDSET_BASIC_SHOW_ADVANCED_OPTION_LINK="Link to Component" MOD_FINDER_OPENSEARCH_NAME="OpenSearch" -MOD_FINDER_SEARCH_VALUE="Search ..." +MOD_FINDER_SEARCH_VALUE="Search …" MOD_FINDER_SEARCHBUTTON_TEXT="Search" MOD_FINDER_SELECT_MENU_ITEMID="Select a menu item" MOD_FINDER_XML_DESCRIPTION="This is a Smart Search module." diff --git a/code/layouts/chromes/html5.php b/code/layouts/chromes/html5.php index 8979e47b..092d469f 100644 --- a/code/layouts/chromes/html5.php +++ b/code/layouts/chromes/html5.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } $moduleTag = htmlspecialchars($params->get('module_tag', 'div'), ENT_QUOTES, 'UTF-8'); @@ -31,27 +31,25 @@ $headerAttribs = []; // Only output a header class if one is set -if ($headerClass !== '') -{ - $headerAttribs['class'] = $headerClass; +if ($headerClass !== '') { + $headerAttribs['class'] = $headerClass; } // Only add aria if the moduleTag is not a div -if ($moduleTag !== 'div') -{ - if ($module->showtitle) : - $moduleAttribs['aria-labelledby'] = 'mod-' . $module->id; - $headerAttribs['id'] = 'mod-' . $module->id; - else: - $moduleAttribs['aria-label'] = $module->title; - endif; +if ($moduleTag !== 'div') { + if ($module->showtitle) : + $moduleAttribs['aria-labelledby'] = 'mod-' . $module->id; + $headerAttribs['id'] = 'mod-' . $module->id; + else : + $moduleAttribs['aria-label'] = $module->title; + endif; } $header = '<' . $headerTag . ' ' . ArrayHelper::toString($headerAttribs) . '>' . $module->title . ''; ?> < > - showtitle) : ?> - - - content; ?> + showtitle) : ?> + + + content; ?> > diff --git a/code/layouts/chromes/none.php b/code/layouts/chromes/none.php index aeab58ca..8c8c4077 100644 --- a/code/layouts/chromes/none.php +++ b/code/layouts/chromes/none.php @@ -1,4 +1,5 @@ getDocument() - ->getWebAssetManager() - ->registerAndUseStyle('layouts.chromes.outline', 'layouts/chromes/outline.css'); + ->getWebAssetManager() + ->registerAndUseStyle('layouts.chromes.outline', 'layouts/chromes/outline.css'); $module = $displayData['module']; ?>
    -
    -
    - position); ?> -
    -
    - style); ?> -
    -
    -
    - content; ?> -
    +
    +
    + position); ?> +
    +
    + style); ?> +
    +
    +
    + content; ?> +
    diff --git a/code/layouts/chromes/table.php b/code/layouts/chromes/table.php index 7d0fc8fe..b19b8c1c 100644 --- a/code/layouts/chromes/table.php +++ b/code/layouts/chromes/table.php @@ -1,4 +1,5 @@ - showtitle) : ?> - - - - - - - + class="moduletable get('moduleclass_sfx'), ENT_COMPAT, 'UTF-8'); ?>"> + showtitle) : ?> + + + + + + +
    - title; ?> -
    - content; ?> -
    + title; ?> +
    + content; ?> +
    diff --git a/code/layouts/joomla/button/action-button.php b/code/layouts/joomla/button/action-button.php index e3c630e7..089812c0 100644 --- a/code/layouts/joomla/button/action-button.php +++ b/code/layouts/joomla/button/action-button.php @@ -1,4 +1,5 @@ diff --git a/code/layouts/joomla/button/iconclass.php b/code/layouts/joomla/button/iconclass.php index 0697d8da..ca0e74e7 100644 --- a/code/layouts/joomla/button/iconclass.php +++ b/code/layouts/joomla/button/iconclass.php @@ -1,4 +1,5 @@ -
    - - - - escape($options['title'])), - HTMLHelper::_('select.option', '-1', '--------', ['disable' => true]) - ]; +
    + + + + escape($options['title'])), + HTMLHelper::_('select.option', '-1', '--------', ['disable' => true]) + ]; - $transitions = array_merge($default, $options['transitions']); + $transitions = array_merge($default, $options['transitions']); - $attribs = [ - 'id' => 'transition-select_' . (int) $row ?? '', - 'list.attr' => [ - 'class' => 'form-select form-select-sm w-auto', - 'onchange' => "this.form.transition_id.value=this.value;Joomla.listItemTask('" . $checkboxName . $this->escape($row ?? '') . "', '" . $task . "')"] - ]; + $attribs = [ + 'id' => 'transition-select_' . (int) $row ?? '', + 'list.attr' => [ + 'class' => 'form-select form-select-sm w-auto', + 'onchange' => "this.form.transition_id.value=this.value;Joomla.listItemTask('" . $checkboxName . $this->escape($row ?? '') . "', '" . $task . "')"] + ]; - echo HTMLHelper::_('select.genericlist', $transitions, '', $attribs); - ?> -
    + echo HTMLHelper::_('select.genericlist', $transitions, '', $attribs); + ?> +
    diff --git a/code/layouts/joomla/content/associations.php b/code/layouts/joomla/content/associations.php index 57ebb493..9104f74e 100644 --- a/code/layouts/joomla/content/associations.php +++ b/code/layouts/joomla/content/associations.php @@ -1,4 +1,5 @@ -
      - $item) : ?> - -
    • - -
    • - link)) : ?> -
    • - link; ?> -
    • - - -
    +
      + $item) : ?> + +
    • + +
    • + link)) : ?> +
    • + link; ?> +
    • + + +
    diff --git a/code/layouts/joomla/content/blog_style_default_item_title.php b/code/layouts/joomla/content/blog_style_default_item_title.php index b2a5ec48..7a3128e5 100644 --- a/code/layouts/joomla/content/blog_style_default_item_title.php +++ b/code/layouts/joomla/content/blog_style_default_item_title.php @@ -1,4 +1,5 @@ params->get('access-edit'); $currentDate = Factory::getDate()->format('Y-m-d H:i:s'); +$link = RouteHelper::getArticleRoute($displayData->slug, $displayData->catid, $displayData->language); ?> -state == 0 || $params->get('show_title') || ($params->get('show_author') && !empty($displayData->author ))) : ?> - +state == 0 || $params->get('show_title') || ($params->get('show_author') && !empty($displayData->author))) : ?> + diff --git a/code/layouts/joomla/content/categories_default.php b/code/layouts/joomla/content/categories_default.php index baab179c..6d1b30e8 100644 --- a/code/layouts/joomla/content/categories_default.php +++ b/code/layouts/joomla/content/categories_default.php @@ -1,4 +1,5 @@ params->get('show_page_heading')) : ?>

    - escape($displayData->params->get('page_heading')); ?> + escape($displayData->params->get('page_heading')); ?>

    params->get('show_base_description')) : ?> - - params->get('categories_description')) : ?> -
    - params->get('categories_description'), '', $displayData->get('extension') . '.categories'); ?> -
    - - - parent->description) : ?> -
    - parent->description, '', $displayData->parent->extension . '.categories'); ?> -
    - - + + params->get('categories_description')) : ?> +
    + params->get('categories_description'), '', $displayData->get('extension') . '.categories'); ?> +
    + + + parent->description) : ?> +
    + parent->description, '', $displayData->parent->extension . '.categories'); ?> +
    + + diff --git a/code/layouts/joomla/content/categories_default_items.php b/code/layouts/joomla/content/categories_default_items.php index 49e820c9..8cf8681b 100644 --- a/code/layouts/joomla/content/categories_default_items.php +++ b/code/layouts/joomla/content/categories_default_items.php @@ -1,4 +1,5 @@ extension; $canEdit = $params->get('access-edit'); $className = substr($extension, 4); -$htag = $params->get('show_page_heading') ? 'h2' : 'h1'; +$htag = $params->get('show_page_heading') ? 'h2' : 'h1'; $app = Factory::getApplication(); @@ -44,59 +45,58 @@ * This will work for the core components but not necessarily for other components * that may have different pluralisation rules. */ -if (substr($className, -1) === 's') -{ - $className = rtrim($className, 's'); +if (substr($className, -1) === 's') { + $className = rtrim($className, 's'); } $tagsData = $category->tags->itemTags; ?>
    - get('show_page_heading')) : ?> -

    - escape($params->get('page_heading')); ?> -

    - + get('show_page_heading')) : ?> +

    + escape($params->get('page_heading')); ?> +

    + - get('show_category_title', 1)) : ?> - <> - title, '', $extension . '.category.title'); ?> - > - - + get('show_category_title', 1)) : ?> + <> + title, '', $extension . '.category.title'); ?> + > + + - get('show_cat_tags', 1)) : ?> - - + get('show_cat_tags', 1)) : ?> + + - get('show_description', 1) || $params->def('show_description_image', 1)) : ?> -
    - get('show_description_image') && $category->getParams()->get('image')) : ?> - $category->getParams()->get('image'), - 'alt' => empty($category->getParams()->get('image_alt')) && empty($category->getParams()->get('image_alt_empty')) ? false : $category->getParams()->get('image_alt'), - ] - ); ?> - - - get('show_description') && $category->description) : ?> - description, '', $extension . '.category.description'); ?> - - -
    - - loadTemplate($displayData->subtemplatename); ?> + get('show_description', 1) || $params->def('show_description_image', 1)) : ?> +
    + get('show_description_image') && $category->getParams()->get('image')) : ?> + $category->getParams()->get('image'), + 'alt' => empty($category->getParams()->get('image_alt')) && empty($category->getParams()->get('image_alt_empty')) ? false : $category->getParams()->get('image_alt'), + ] + ); ?> + + + get('show_description') && $category->description) : ?> + description, '', $extension . '.category.description'); ?> + + +
    + + loadTemplate($displayData->subtemplatename); ?> - maxLevel != 0 && $displayData->get('children')) : ?> -
    - get('show_category_heading_title_text', 1) == 1) : ?> -

    - -

    - - loadTemplate('children'); ?> -
    - + maxLevel != 0 && $displayData->get('children')) : ?> +
    + get('show_category_heading_title_text', 1) == 1) : ?> +

    + +

    + + loadTemplate('children'); ?> +
    +
    diff --git a/code/layouts/joomla/content/emptystate.php b/code/layouts/joomla/content/emptystate.php index d513135a..e78d3a27 100644 --- a/code/layouts/joomla/content/emptystate.php +++ b/code/layouts/joomla/content/emptystate.php @@ -1,4 +1,5 @@ input->get('option')); +if (!$textPrefix) { + $textPrefix = strtoupper(Factory::getApplication()->input->get('option')); } $formURL = $displayData['formURL'] ?? ''; @@ -33,32 +33,32 @@
    -
    - -

    -
    -

    - -

    -
    - input->get('tmpl') !== 'component') : ?> - - - - - -
    -
    -
    +
    + +

    +
    +

    + +

    +
    + input->get('tmpl') !== 'component') : ?> + + + + + +
    +
    +
    - + - - - + + +
    diff --git a/code/layouts/joomla/content/emptystate_module.php b/code/layouts/joomla/content/emptystate_module.php index c89d4703..46be472e 100644 --- a/code/layouts/joomla/content/emptystate_module.php +++ b/code/layouts/joomla/content/emptystate_module.php @@ -1,4 +1,5 @@ getLanguage()->hasKey($moduleLangString) ? $moduleLangString : $componentLangString; +if (!$title) { + // Can we find a *_EMPTYSTATE_MODULE_TITLE translation, Else use the components *_EMPTYSTATE_TITLE string + $title = Factory::getApplication()->getLanguage()->hasKey($moduleLangString) ? $moduleLangString : $componentLangString; } ?>
    -

    - -

    +

    + +

    diff --git a/code/layouts/joomla/content/full_image.php b/code/layouts/joomla/content/full_image.php index 44b43401..83fcc57b 100644 --- a/code/layouts/joomla/content/full_image.php +++ b/code/layouts/joomla/content/full_image.php @@ -1,4 +1,5 @@ params; $images = json_decode($displayData->images); -if (empty($images->image_fulltext)) -{ - return; +if (empty($images->image_fulltext)) { + return; } $imgclass = empty($images->float_fulltext) ? $params->get('float_fulltext') : $images->float_fulltext; $layoutAttr = [ - 'src' => $images->image_fulltext, - 'itemprop' => 'image', - 'alt' => empty($images->image_fulltext_alt) && empty($images->image_fulltext_alt_empty) ? false : $images->image_fulltext_alt, + 'src' => $images->image_fulltext, + 'itemprop' => 'image', + 'alt' => empty($images->image_fulltext_alt) && empty($images->image_fulltext_alt_empty) ? false : $images->image_fulltext_alt, ]; ?>
    - - image_fulltext_caption) && $images->image_fulltext_caption !== '') : ?> -
    escape($images->image_fulltext_caption); ?>
    - + + image_fulltext_caption) && $images->image_fulltext_caption !== '') : ?> +
    escape($images->image_fulltext_caption); ?>
    +
    diff --git a/code/layouts/joomla/content/icons.php b/code/layouts/joomla/content/icons.php index ba1d2713..3369c654 100644 --- a/code/layouts/joomla/content/icons.php +++ b/code/layouts/joomla/content/icons.php @@ -1,4 +1,5 @@ -
    -
    -
    - -
    -
    -
    +
    +
    +
    + +
    +
    +
    diff --git a/code/layouts/joomla/content/icons/create.php b/code/layouts/joomla/content/icons/create.php index 74183d7b..2a78ae4c 100644 --- a/code/layouts/joomla/content/icons/create.php +++ b/code/layouts/joomla/content/icons/create.php @@ -1,4 +1,5 @@ get('show_icons')) : ?> - - + + - + diff --git a/code/layouts/joomla/content/icons/edit.php b/code/layouts/joomla/content/icons/edit.php index fc6702db..117b2322 100644 --- a/code/layouts/joomla/content/icons/edit.php +++ b/code/layouts/joomla/content/icons/edit.php @@ -1,4 +1,5 @@ state ? 'edit' : 'eye-slash'; $currentDate = Factory::getDate()->format('Y-m-d H:i:s'); $isUnpublished = ($article->publish_up > $currentDate) - || !is_null($article->publish_down) && ($article->publish_down < $currentDate); + || !is_null($article->publish_down) && ($article->publish_down < $currentDate); -if ($isUnpublished) -{ - $icon = 'eye-slash'; +if ($isUnpublished) { + $icon = 'eye-slash'; } $aria_described = 'editarticle-' . (int) $article->id; ?> - + diff --git a/code/layouts/joomla/content/icons/edit_lock.php b/code/layouts/joomla/content/icons/edit_lock.php index 3b8def51..4ebd721f 100644 --- a/code/layouts/joomla/content/icons/edit_lock.php +++ b/code/layouts/joomla/content/icons/edit_lock.php @@ -1,4 +1,5 @@ id; -} -elseif (isset($displayData['contact'])) -{ - $contact = $displayData['contact']; - $aria_described = 'editcontact-' . (int) $contact->id; +if (isset($displayData['ariaDescribed'])) { + $aria_described = $displayData['ariaDescribed']; +} elseif (isset($displayData['article'])) { + $article = $displayData['article']; + $aria_described = 'editarticle-' . (int) $article->id; +} elseif (isset($displayData['contact'])) { + $contact = $displayData['contact']; + $aria_described = 'editcontact-' . (int) $contact->id; } $tooltip = $displayData['tooltip']; ?> - + diff --git a/code/layouts/joomla/content/info_block.php b/code/layouts/joomla/content/info_block.php index 402e1179..6b9a1890 100644 --- a/code/layouts/joomla/content/info_block.php +++ b/code/layouts/joomla/content/info_block.php @@ -1,4 +1,5 @@ diff --git a/code/layouts/joomla/content/info_block/associations.php b/code/layouts/joomla/content/info_block/associations.php index 9732c557..cfba6637 100644 --- a/code/layouts/joomla/content/info_block/associations.php +++ b/code/layouts/joomla/content/info_block/associations.php @@ -1,4 +1,5 @@ associations)) : ?> -associations; ?> + associations; ?>
    - - - - params->get('flags', 1) && $association['language']->image) : ?> - image . '.gif', $association['language']->title_native, array('title' => $association['language']->title_native), true); ?> - - - lang_code); ?> - lang_code; ?> - title_native; ?> - - - + + + + params->get('flags', 1) && $association['language']->image) : ?> + image . '.gif', $association['language']->title_native, array('title' => $association['language']->title_native), true); ?> + + + lang_code); ?> + lang_code; ?> + title_native; ?> + + +
    diff --git a/code/layouts/joomla/content/info_block/author.php b/code/layouts/joomla/content/info_block/author.php index 5d744172..dcfaf6f8 100644 --- a/code/layouts/joomla/content/info_block/author.php +++ b/code/layouts/joomla/content/info_block/author.php @@ -1,4 +1,5 @@ diff --git a/code/layouts/joomla/content/info_block/category.php b/code/layouts/joomla/content/info_block/category.php index 2803fe3c..4006b0f3 100644 --- a/code/layouts/joomla/content/info_block/category.php +++ b/code/layouts/joomla/content/info_block/category.php @@ -1,4 +1,5 @@
    - 'icon-folder-open icon-fw']); ?> - escape($displayData['item']->category_title); ?> - get('link_category') && !empty($displayData['item']->catid)) : ?> - catid, $displayData['item']->category_language) - ) - . '" itemprop="genre">' . $title . ''; ?> - - - ' . $title . ''); ?> - + 'icon-folder-open icon-fw']); ?> + escape($displayData['item']->category_title); ?> + get('link_category') && !empty($displayData['item']->catid)) : ?> + catid, $displayData['item']->category_language) + ) + . '" itemprop="genre">' . $title . ''; ?> + + + ' . $title . ''); ?> +
    diff --git a/code/layouts/joomla/content/info_block/create_date.php b/code/layouts/joomla/content/info_block/create_date.php index 75cdc81c..0fed2373 100644 --- a/code/layouts/joomla/content/info_block/create_date.php +++ b/code/layouts/joomla/content/info_block/create_date.php @@ -1,4 +1,5 @@
    - - + +
    diff --git a/code/layouts/joomla/content/info_block/hits.php b/code/layouts/joomla/content/info_block/hits.php index 8d454f34..1ff76321 100644 --- a/code/layouts/joomla/content/info_block/hits.php +++ b/code/layouts/joomla/content/info_block/hits.php @@ -1,4 +1,5 @@
    - - - hits); ?> + + + hits); ?>
    diff --git a/code/layouts/joomla/content/info_block/modify_date.php b/code/layouts/joomla/content/info_block/modify_date.php index 568b72b0..5c2aaf90 100644 --- a/code/layouts/joomla/content/info_block/modify_date.php +++ b/code/layouts/joomla/content/info_block/modify_date.php @@ -1,4 +1,5 @@
    - - + +
    diff --git a/code/layouts/joomla/content/info_block/parent_category.php b/code/layouts/joomla/content/info_block/parent_category.php index 663a9467..39a31106 100644 --- a/code/layouts/joomla/content/info_block/parent_category.php +++ b/code/layouts/joomla/content/info_block/parent_category.php @@ -1,4 +1,5 @@
    - 'icon-folder icon-fw']); ?> - escape($displayData['item']->parent_title); ?> - get('link_parent_category') && !empty($displayData['item']->parent_id)) : ?> - parent_id, $displayData['item']->parent_language) - ) - . '" itemprop="genre">' . $title . ''; ?> - - - ' . $title . ''); ?> - + 'icon-folder icon-fw']); ?> + escape($displayData['item']->parent_title); ?> + get('link_parent_category') && !empty($displayData['item']->parent_id)) : ?> + parent_id, $displayData['item']->parent_language) + ) + . '" itemprop="genre">' . $title . ''; ?> + + + ' . $title . ''); ?> +
    diff --git a/code/layouts/joomla/content/info_block/publish_date.php b/code/layouts/joomla/content/info_block/publish_date.php index 8a528abb..04fbb449 100644 --- a/code/layouts/joomla/content/info_block/publish_date.php +++ b/code/layouts/joomla/content/info_block/publish_date.php @@ -1,4 +1,5 @@
    - - + +
    diff --git a/code/layouts/joomla/content/intro_image.php b/code/layouts/joomla/content/intro_image.php index 9c718867..f7db3b42 100644 --- a/code/layouts/joomla/content/intro_image.php +++ b/code/layouts/joomla/content/intro_image.php @@ -1,4 +1,5 @@ params; $images = json_decode($displayData->images); -if (empty($images->image_intro)) -{ - return; +if (empty($images->image_intro)) { + return; } $imgclass = empty($images->float_intro) ? $params->get('float_intro') : $images->float_intro; $layoutAttr = [ - 'src' => $images->image_intro, - 'alt' => empty($images->image_intro_alt) && empty($images->image_intro_alt_empty) ? false : $images->image_intro_alt, + 'src' => $images->image_intro, + 'alt' => empty($images->image_intro_alt) && empty($images->image_intro_alt_empty) ? false : $images->image_intro_alt, ]; ?>
    - get('link_intro_image') && ($params->get('access-view') || $params->get('show_noauth', '0') == '1')) : ?> - - - 'thumbnail'])); ?> - - image_intro_caption) && $images->image_intro_caption !== '') : ?> -
    escape($images->image_intro_caption); ?>
    - + get('link_intro_image') && ($params->get('access-view') || $params->get('show_noauth', '0') == '1')) : ?> + + + 'thumbnail'])); ?> + + image_intro_caption) && $images->image_intro_caption !== '') : ?> +
    escape($images->image_intro_caption); ?>
    +
    diff --git a/code/layouts/joomla/content/language.php b/code/layouts/joomla/content/language.php index b77ac4fd..90daa395 100644 --- a/code/layouts/joomla/content/language.php +++ b/code/layouts/joomla/content/language.php @@ -1,4 +1,5 @@ language === '*') -{ - echo Text::alt('JALL', 'language'); -} -elseif ($item->language_image) -{ - echo HTMLHelper::_('image', 'mod_languages/' . $item->language_image . '.gif', '', array('class' => 'me-1'), true) . htmlspecialchars($item->language_title, ENT_COMPAT, 'UTF-8'); -} -elseif ($item->language_title) -{ - echo htmlspecialchars($item->language_title, ENT_COMPAT, 'UTF-8'); -} -else -{ - echo Text::_('JUNDEFINED'); +if ($item->language === '*') { + echo Text::alt('JALL', 'language'); +} elseif ($item->language_image) { + echo HTMLHelper::_('image', 'mod_languages/' . $item->language_image . '.gif', '', array('class' => 'me-1'), true) . htmlspecialchars($item->language_title, ENT_COMPAT, 'UTF-8'); +} elseif ($item->language_title) { + echo htmlspecialchars($item->language_title, ENT_COMPAT, 'UTF-8'); +} else { + echo Text::_('JUNDEFINED'); } diff --git a/code/layouts/joomla/content/options_default.php b/code/layouts/joomla/content/options_default.php index 77f8f0a4..9d0981f4 100644 --- a/code/layouts/joomla/content/options_default.php +++ b/code/layouts/joomla/content/options_default.php @@ -1,4 +1,5 @@
    - name; ?> - description)) : ?> -

    description; ?>

    - - fieldsname); ?> -
    - - form->getFieldset($fieldname) as $field) : ?> - - type === 'Spacer' ? ' field-spacer' : ''; ?> - showon) : ?> - useScript('showon'); ?> - showon, $field->formControl, $field->group)) . '\''; ?> - + name; ?> + description)) : ?> +

    description; ?>

    + + fieldsname); ?> +
    + + form->getFieldset($fieldname) as $field) : ?> + + type === 'Spacer' ? ' field-spacer' : ''; ?> + showon) : ?> + useScript('showon'); ?> + showon, $field->formControl, $field->group)) . '\''; ?> + - showlabel)) : ?> -
    > -
    input; ?>
    -
    - - renderField(); ?> - - - -
    + showlabel)) : ?> +
    > +
    input; ?>
    +
    + + renderField(); ?> + + + +
    diff --git a/code/layouts/joomla/content/readmore.php b/code/layouts/joomla/content/readmore.php index 7b4f89e0..53127a96 100644 --- a/code/layouts/joomla/content/readmore.php +++ b/code/layouts/joomla/content/readmore.php @@ -1,4 +1,5 @@

    - get('access-view')) : ?> - - '; ?> - - - alternative_readmore) : ?> - - '; ?> - - get('show_readmore_title', 0) != 0) : ?> - title, $params->get('readmore_limit')); ?> - - - get('show_readmore_title', 0) == 0) : ?> - - '; ?> - - - - - '; ?> - title, $params->get('readmore_limit'))); ?> - - + get('access-view')) : ?> + + '; ?> + + + alternative_readmore) : ?> + + '; ?> + + get('show_readmore_title', 0) != 0) : ?> + title, $params->get('readmore_limit')); ?> + + + get('show_readmore_title', 0) == 0) : ?> + + '; ?> + + + + + '; ?> + title, $params->get('readmore_limit'))); ?> + +

    diff --git a/code/layouts/joomla/content/tags.php b/code/layouts/joomla/content/tags.php index 10ce20ca..1bef066e 100644 --- a/code/layouts/joomla/content/tags.php +++ b/code/layouts/joomla/content/tags.php @@ -1,4 +1,5 @@ - + diff --git a/code/layouts/joomla/content/text_filters.php b/code/layouts/joomla/content/text_filters.php index 215b081f..3fc04eaa 100644 --- a/code/layouts/joomla/content/text_filters.php +++ b/code/layouts/joomla/content/text_filters.php @@ -1,4 +1,5 @@
    - name; ?> -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    - fieldsname); ?> - - form->getFieldset($fieldname) as $field) : ?> -
    input; ?>
    - - + name; ?> +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + fieldsname); ?> + + form->getFieldset($fieldname) as $field) : ?> +
    input; ?>
    + +
    diff --git a/code/layouts/joomla/edit/admin_modules.php b/code/layouts/joomla/edit/admin_modules.php index f7caf445..f5d84253 100644 --- a/code/layouts/joomla/edit/admin_modules.php +++ b/code/layouts/joomla/edit/admin_modules.php @@ -1,4 +1,5 @@ input; $fields = $displayData->get('fields') ?: array( - array('parent', 'parent_id'), - array('published', 'state', 'enabled'), - array('category', 'catid'), - 'featured', - 'sticky', - 'access', - 'language', - 'tags', - 'note', - 'version_note', + array('parent', 'parent_id'), + array('published', 'state', 'enabled'), + array('category', 'catid'), + 'featured', + 'sticky', + 'access', + 'language', + 'tags', + 'note', + 'version_note', ); $hiddenFields = $displayData->get('hidden_fields') ?: array(); -if (!ModuleHelper::isAdminMultilang()) -{ - $hiddenFields[] = 'language'; - $form->setFieldAttribute('language', 'default', '*'); +if (!ModuleHelper::isAdminMultilang()) { + $hiddenFields[] = 'language'; + $form->setFieldAttribute('language', 'default', '*'); } $html = array(); $html[] = '
    '; -foreach ($fields as $field) -{ - foreach ((array) $field as $f) - { - if ($form->getField($f)) - { - if (in_array($f, $hiddenFields)) - { - $form->setFieldAttribute($f, 'type', 'hidden'); - } - - $html[] = $form->renderField($f); - break; - } - } +foreach ($fields as $field) { + foreach ((array) $field as $f) { + if ($form->getField($f)) { + if (in_array($f, $hiddenFields)) { + $form->setFieldAttribute($f, 'type', 'hidden'); + } + + $html[] = $form->renderField($f); + break; + } + } } $html[] = '
    '; diff --git a/code/layouts/joomla/edit/associations.php b/code/layouts/joomla/edit/associations.php index 96e59f01..423cc21b 100644 --- a/code/layouts/joomla/edit/associations.php +++ b/code/layouts/joomla/edit/associations.php @@ -1,4 +1,5 @@ getForm(); $options = array( - 'formControl' => $form->getFormControl(), - 'hidden' => (int) ($form->getValue('language', null, '*') === '*'), + 'formControl' => $form->getFormControl(), + 'hidden' => (int) ($form->getValue('language', null, '*') === '*'), ); // Load JavaScript message titles diff --git a/code/layouts/joomla/edit/fieldset.php b/code/layouts/joomla/edit/fieldset.php index ab45d780..0a20a3be 100644 --- a/code/layouts/joomla/edit/fieldset.php +++ b/code/layouts/joomla/edit/fieldset.php @@ -1,4 +1,5 @@ get('fieldset'); $fieldSet = $form->getFieldset($name); -if (empty($fieldSet)) -{ - return; +if (empty($fieldSet)) { + return; } $ignoreFields = $displayData->get('ignore_fields') ? : array(); $extraFields = $displayData->get('extra_fields') ? : array(); -if (!empty($displayData->showOptions) || $displayData->get('show_options', 1)) -{ - if (isset($extraFields[$name])) - { - foreach ($extraFields[$name] as $f) - { - if (in_array($f, $ignoreFields)) - { - continue; - } - if ($form->getField($f)) - { - $fieldSet[] = $form->getField($f); - } - } - } - - $html = array(); - - foreach ($fieldSet as $field) - { - $html[] = $field->renderField(); - } - - echo implode('', $html); -} -else -{ - $html = array(); - $html[] = ''; - - echo implode('', $html); +if (!empty($displayData->showOptions) || $displayData->get('show_options', 1)) { + if (isset($extraFields[$name])) { + foreach ($extraFields[$name] as $f) { + if (in_array($f, $ignoreFields)) { + continue; + } + if ($form->getField($f)) { + $fieldSet[] = $form->getField($f); + } + } + } + + $html = array(); + + foreach ($fieldSet as $field) { + $html[] = $field->renderField(); + } + + echo implode('', $html); +} else { + $html = array(); + $html[] = ''; + + echo implode('', $html); } diff --git a/code/layouts/joomla/edit/frontediting_modules.php b/code/layouts/joomla/edit/frontediting_modules.php index 14a95464..373a2cde 100644 --- a/code/layouts/joomla/edit/frontediting_modules.php +++ b/code/layouts/joomla/edit/frontediting_modules.php @@ -1,4 +1,5 @@ id; // If Module editing site -if ($parameters->get('redirect_edit', 'site') === 'site') -{ - $editUrl = Uri::base() . 'index.php?option=com_config&view=modules&id=' . (int) $mod->id . '&Itemid=' . $itemid . $redirectUri; - $target = '_self'; +if ($parameters->get('redirect_edit', 'site') === 'site') { + $editUrl = Uri::base() . 'index.php?option=com_config&view=modules&id=' . (int) $mod->id . '&Itemid=' . $itemid . $redirectUri; + $target = '_self'; } // Add link for editing the module $count = 0; $moduleHtml = preg_replace( - // Find first tag of module - '/^(\s*<(?:div|span|nav|ul|ol|h\d|section|aside|address|article|form) [^>]*>)/', - // Create and add the edit link and tooltip - '\\1 + // Find first tag of module + '/^(\s*<(?:div|span|nav|ul|ol|h\d|section|aside|address|article|form) [^>]*>)/', + // Create and add the edit link and tooltip + '\\1 ' . Text::_('JGLOBAL_EDIT') . ' ', - $moduleHtml, - 1, - $count + $moduleHtml, + 1, + $count ); // If menu editing is enabled and allowed and it's a menu module add link for editing -if ($menusEditing && $mod->module === 'mod_menu') -{ - // find the menu item id - $regex = '/\bitem-(\d+)\b/'; +if ($menusEditing && $mod->module === 'mod_menu') { + // find the menu item id + $regex = '/\bitem-(\d+)\b/'; - preg_match_all($regex, $moduleHtml, $menuItemids); - if ($menuItemids) - { - foreach ($menuItemids[1] as $menuItemid) - { - $menuitemEditUrl = Uri::base() . 'administrator/index.php?option=com_menus&view=item&client_id=0&layout=edit&id=' . (int) $menuItemid; - $moduleHtml = preg_replace( - // Find the link - '/()/', - // Create and add the edit link - '\\1 + preg_match_all($regex, $moduleHtml, $menuItemids); + if ($menuItemids) { + foreach ($menuItemids[1] as $menuItemid) { + $menuitemEditUrl = Uri::base() . 'administrator/index.php?option=com_menus&view=item&client_id=0&layout=edit&id=' . (int) $menuItemid; + $moduleHtml = preg_replace( + // Find the link + '/()/', + // Create and add the edit link + '\\1 ', - $moduleHtml - ); - } - } + $moduleHtml + ); + } + } } diff --git a/code/layouts/joomla/edit/global.php b/code/layouts/joomla/edit/global.php index 4cee42c8..35f72e68 100644 --- a/code/layouts/joomla/edit/global.php +++ b/code/layouts/joomla/edit/global.php @@ -1,4 +1,5 @@ input; $component = $input->getCmd('option', 'com_content'); -if ($component === 'com_categories') -{ - $extension = $input->getCmd('extension', 'com_content'); - $parts = explode('.', $extension); - $component = $parts[0]; +if ($component === 'com_categories') { + $extension = $input->getCmd('extension', 'com_content'); + $parts = explode('.', $extension); + $component = $parts[0]; } $saveHistory = ComponentHelper::getParams($component)->get('save_history', 0); $fields = $displayData->get('fields') ?: array( - 'transition', - array('parent', 'parent_id'), - array('published', 'state', 'enabled'), - array('category', 'catid'), - 'featured', - 'sticky', - 'access', - 'language', - 'tags', - 'note', - 'version_note', + 'transition', + array('parent', 'parent_id'), + array('published', 'state', 'enabled'), + array('category', 'catid'), + 'featured', + 'sticky', + 'access', + 'language', + 'tags', + 'note', + 'version_note', ); $hiddenFields = $displayData->get('hidden_fields') ?: array(); -if (!$saveHistory) -{ - $hiddenFields[] = 'version_note'; +if (!$saveHistory) { + $hiddenFields[] = 'version_note'; } -if (!Multilanguage::isEnabled()) -{ - $hiddenFields[] = 'language'; - $form->setFieldAttribute('language', 'default', '*'); +if (!Multilanguage::isEnabled()) { + $hiddenFields[] = 'language'; + $form->setFieldAttribute('language', 'default', '*'); } $html = array(); $html[] = '
    '; $html[] = '' . Text::_('JGLOBAL_FIELDSET_GLOBAL') . ''; -foreach ($fields as $field) -{ - foreach ((array) $field as $f) - { - if ($form->getField($f)) - { - if (in_array($f, $hiddenFields)) - { - $form->setFieldAttribute($f, 'type', 'hidden'); - } +foreach ($fields as $field) { + foreach ((array) $field as $f) { + if ($form->getField($f)) { + if (in_array($f, $hiddenFields)) { + $form->setFieldAttribute($f, 'type', 'hidden'); + } - $html[] = $form->renderField($f); - break; - } - } + $html[] = $form->renderField($f); + break; + } + } } $html[] = '
    '; diff --git a/code/layouts/joomla/edit/metadata.php b/code/layouts/joomla/edit/metadata.php index ee5be2a7..c7860350 100644 --- a/code/layouts/joomla/edit/metadata.php +++ b/code/layouts/joomla/edit/metadata.php @@ -1,4 +1,5 @@ $fieldSet) : ?> - description) && trim($fieldSet->description)) : ?> -
    - - escape(Text::_($fieldSet->description)); ?> -
    - - - renderField('metadesc'); - echo $form->renderField('metakey'); - } - - foreach ($form->getFieldset($name) as $field) - { - if ($field->name !== 'jform[metadata][tags][]') - { - echo $field->renderField(); - } - } ?> + description) && trim($fieldSet->description)) : ?> +
    + + escape(Text::_($fieldSet->description)); ?> +
    + + + renderField('metadesc'); + echo $form->renderField('metakey'); + } + + foreach ($form->getFieldset($name) as $field) { + if ($field->name !== 'jform[metadata][tags][]') { + echo $field->renderField(); + } + } ?> diff --git a/code/layouts/joomla/edit/params.php b/code/layouts/joomla/edit/params.php index ae9cd063..dffef9fa 100644 --- a/code/layouts/joomla/edit/params.php +++ b/code/layouts/joomla/edit/params.php @@ -1,4 +1,5 @@ getFieldsets(); $helper = $displayData->get('useCoreUI', false) ? 'uitab' : 'bootstrap'; -if (empty($fieldSets)) -{ - return; +if (empty($fieldSets)) { + return; } $ignoreFieldsets = $displayData->get('ignore_fieldsets') ?: array(); @@ -38,44 +38,38 @@ $configFieldsets = $displayData->get('configFieldsets') ?: array(); // Handle the hidden fieldsets when show_options is set false -if (!$displayData->get('show_options', 1)) -{ - // The HTML buffer - $html = array(); - - // Loop over the fieldsets - foreach ($fieldSets as $name => $fieldSet) - { - // Check if the fieldset should be ignored - if (in_array($name, $ignoreFieldsets, true)) - { - continue; - } - - // If it is a hidden fieldset, render the inputs - if (in_array($name, $hiddenFieldsets)) - { - // Loop over the fields - foreach ($form->getFieldset($name) as $field) - { - // Add only the input on the buffer - $html[] = $field->input; - } - - // Make sure the fieldset is not rendered twice - $ignoreFieldsets[] = $name; - } - - // Check if it is the correct fieldset to ignore - if (strpos($name, 'basic') === 0) - { - // Ignore only the fieldsets which are defined by the options not the custom fields ones - $ignoreFieldsets[] = $name; - } - } - - // Echo the hidden fieldsets - echo implode('', $html); +if (!$displayData->get('show_options', 1)) { + // The HTML buffer + $html = array(); + + // Loop over the fieldsets + foreach ($fieldSets as $name => $fieldSet) { + // Check if the fieldset should be ignored + if (in_array($name, $ignoreFieldsets, true)) { + continue; + } + + // If it is a hidden fieldset, render the inputs + if (in_array($name, $hiddenFieldsets)) { + // Loop over the fields + foreach ($form->getFieldset($name) as $field) { + // Add only the input on the buffer + $html[] = $field->input; + } + + // Make sure the fieldset is not rendered twice + $ignoreFieldsets[] = $name; + } + + // Check if it is the correct fieldset to ignore + if (strpos($name, 'basic') === 0) { + // Ignore only the fieldsets which are defined by the options not the custom fields ones + $ignoreFieldsets[] = $name; + } + } + + // Echo the hidden fieldsets + echo implode('', $html); } $opentab = false; @@ -83,131 +77,112 @@ $xml = $form->getXml(); // Loop again over the fieldsets -foreach ($fieldSets as $name => $fieldSet) -{ - // Ensure any fieldsets we don't want to show are skipped (including repeating formfield fieldsets) - if ((isset($fieldSet->repeat) && $fieldSet->repeat === true) - || in_array($name, $ignoreFieldsets) - || (!empty($configFieldsets) && in_array($name, $configFieldsets, true)) - || (!empty($hiddenFieldsets) && in_array($name, $hiddenFieldsets, true)) - ) - { - continue; - } - - // Determine the label - if (!empty($fieldSet->label)) - { - $label = Text::_($fieldSet->label); - } - else - { - $label = strtoupper('JGLOBAL_FIELDSET_' . $name); - if (Text::_($label) === $label) - { - $label = strtoupper($app->input->get('option') . '_' . $name . '_FIELDSET_LABEL'); - } - $label = Text::_($label); - } - - $hasChildren = $xml->xpath('//fieldset[@name="' . $name . '"]//fieldset[not(ancestor::field/form/*)]'); - $hasParent = $xml->xpath('//fieldset//fieldset[@name="' . $name . '"]'); - $isGrandchild = $xml->xpath('//fieldset//fieldset//fieldset[@name="' . $name . '"]'); - - if (!$isGrandchild && $hasParent) - { - echo '
    '; - echo '' . $label . ''; - - // Include the description when available - if (!empty($fieldSet->description)) - { - echo '
    '; - echo '' . Text::_('INFO') . ' '; - echo Text::_($fieldSet->description); - echo '
    '; - } - - echo '
    '; - } - // Tabs - elseif (!$hasParent) - { - if ($opentab) - { - if ($opentab > 1) - { - echo '
    '; - echo '
    '; - } - - // End previous tab - echo HTMLHelper::_($helper . '.endTab'); - } - - // Start the tab - echo HTMLHelper::_($helper . '.addTab', $tabName, 'attrib-' . $name, $label); - - $opentab = 1; - - // Directly add a fieldset if we have no children - if (!$hasChildren) - { - echo '
    '; - echo '' . $label . ''; - - // Include the description when available - if (!empty($fieldSet->description)) - { - echo '
    '; - echo '' . Text::_('INFO') . ' '; - echo Text::_($fieldSet->description); - echo '
    '; - } - - echo '
    '; - - $opentab = 2; - } - // Include the description when available - elseif (!empty($fieldSet->description)) - { - echo '
    '; - echo '' . Text::_('INFO') . ' '; - echo Text::_($fieldSet->description); - echo '
    '; - } - } - - // We're on the deepest level => output fields - if (!$hasChildren) - { - // The name of the fieldset to render - $displayData->fieldset = $name; - - // Force to show the options - $displayData->showOptions = true; - - // Render the fieldset - echo LayoutHelper::render('joomla.edit.fieldset', $displayData); - } - - // Close open fieldset - if (!$isGrandchild && $hasParent) - { - echo '
    '; - echo '
    '; - } +foreach ($fieldSets as $name => $fieldSet) { + // Ensure any fieldsets we don't want to show are skipped (including repeating formfield fieldsets) + if ( + (isset($fieldSet->repeat) && $fieldSet->repeat === true) + || in_array($name, $ignoreFieldsets) + || (!empty($configFieldsets) && in_array($name, $configFieldsets, true)) + || (!empty($hiddenFieldsets) && in_array($name, $hiddenFieldsets, true)) + ) { + continue; + } + + // Determine the label + if (!empty($fieldSet->label)) { + $label = Text::_($fieldSet->label); + } else { + $label = strtoupper('JGLOBAL_FIELDSET_' . $name); + if (Text::_($label) === $label) { + $label = strtoupper($app->input->get('option') . '_' . $name . '_FIELDSET_LABEL'); + } + $label = Text::_($label); + } + + $hasChildren = $xml->xpath('//fieldset[@name="' . $name . '"]//fieldset[not(ancestor::field/form/*)]'); + $hasParent = $xml->xpath('//fieldset//fieldset[@name="' . $name . '"]'); + $isGrandchild = $xml->xpath('//fieldset//fieldset//fieldset[@name="' . $name . '"]'); + + if (!$isGrandchild && $hasParent) { + echo '
    '; + echo '' . $label . ''; + + // Include the description when available + if (!empty($fieldSet->description)) { + echo '
    '; + echo '' . Text::_('INFO') . ' '; + echo Text::_($fieldSet->description); + echo '
    '; + } + + echo '
    '; + } elseif (!$hasParent) { + // Tabs + if ($opentab) { + if ($opentab > 1) { + echo '
    '; + echo '
    '; + } + + // End previous tab + echo HTMLHelper::_($helper . '.endTab'); + } + + // Start the tab + echo HTMLHelper::_($helper . '.addTab', $tabName, 'attrib-' . $name, $label); + + $opentab = 1; + + // Directly add a fieldset if we have no children + if (!$hasChildren) { + echo '
    '; + echo '' . $label . ''; + + // Include the description when available + if (!empty($fieldSet->description)) { + echo '
    '; + echo '' . Text::_('INFO') . ' '; + echo Text::_($fieldSet->description); + echo '
    '; + } + + echo '
    '; + + $opentab = 2; + } elseif (!empty($fieldSet->description)) { + // Include the description when available + echo '
    '; + echo '' . Text::_('INFO') . ' '; + echo Text::_($fieldSet->description); + echo '
    '; + } + } + + // We're on the deepest level => output fields + if (!$hasChildren) { + // The name of the fieldset to render + $displayData->fieldset = $name; + + // Force to show the options + $displayData->showOptions = true; + + // Render the fieldset + echo LayoutHelper::render('joomla.edit.fieldset', $displayData); + } + + // Close open fieldset + if (!$isGrandchild && $hasParent) { + echo '
    '; + echo '
    '; + } } -if ($opentab) -{ - if ($opentab > 1) - { - echo ''; - echo '
    '; - } +if ($opentab) { + if ($opentab > 1) { + echo ''; + echo ''; + } - // End previous tab - echo HTMLHelper::_($helper . '.endTab'); + // End previous tab + echo HTMLHelper::_($helper . '.endTab'); } diff --git a/code/layouts/joomla/edit/publishingdata.php b/code/layouts/joomla/edit/publishingdata.php index 452f393c..9bbbce0b 100644 --- a/code/layouts/joomla/edit/publishingdata.php +++ b/code/layouts/joomla/edit/publishingdata.php @@ -1,4 +1,5 @@ getForm(); $fields = $displayData->get('fields') ?: array( - 'publish_up', - 'publish_down', - 'featured_up', - 'featured_down', - array('created', 'created_time'), - array('created_by', 'created_user_id'), - 'created_by_alias', - array('modified', 'modified_time'), - array('modified_by', 'modified_user_id'), - 'version', - 'hits', - 'id' + 'publish_up', + 'publish_down', + 'featured_up', + 'featured_down', + array('created', 'created_time'), + array('created_by', 'created_user_id'), + 'created_by_alias', + array('modified', 'modified_time'), + array('modified_by', 'modified_user_id'), + 'version', + 'hits', + 'id' ); $hiddenFields = $displayData->get('hidden_fields') ?: array(); -foreach ($fields as $field) -{ - foreach ((array) $field as $f) - { - if ($form->getField($f)) - { - if (in_array($f, $hiddenFields)) - { - $form->setFieldAttribute($f, 'type', 'hidden'); - } +foreach ($fields as $field) { + foreach ((array) $field as $f) { + if ($form->getField($f)) { + if (in_array($f, $hiddenFields)) { + $form->setFieldAttribute($f, 'type', 'hidden'); + } - echo $form->renderField($f); - break; - } - } + echo $form->renderField($f); + break; + } + } } diff --git a/code/layouts/joomla/edit/title_alias.php b/code/layouts/joomla/edit/title_alias.php index a241cc4a..1bcb121b 100644 --- a/code/layouts/joomla/edit/title_alias.php +++ b/code/layouts/joomla/edit/title_alias.php @@ -1,4 +1,5 @@
    -
    - renderField($title) : ''; ?> -
    -
    - renderField('alias'); ?> -
    +
    + renderField($title) : ''; ?> +
    +
    + renderField('alias'); ?> +
    diff --git a/code/layouts/joomla/editors/buttons.php b/code/layouts/joomla/editors/buttons.php index 08db5c0d..3176e1dc 100644 --- a/code/layouts/joomla/editors/buttons.php +++ b/code/layouts/joomla/editors/buttons.php @@ -1,4 +1,5 @@ diff --git a/code/layouts/joomla/editors/buttons/button.php b/code/layouts/joomla/editors/buttons/button.php index 49993ee4..c7a0712f 100644 --- a/code/layouts/joomla/editors/buttons/button.php +++ b/code/layouts/joomla/editors/buttons/button.php @@ -1,4 +1,5 @@ get('name')) : - $class = 'btn btn-secondary'; - $class .= ($button->get('class')) ? ' ' . $button->get('class') : null; - $class .= ($button->get('modal')) ? ' modal-button' : null; - $href = '#' . strtolower($button->get('name')) . '_modal'; - $link = ($button->get('link')) ? Uri::base() . $button->get('link') : null; - $onclick = ($button->get('onclick')) ? ' onclick="' . $button->get('onclick') . '"' : ''; - $title = ($button->get('title')) ? $button->get('title') : $button->get('text'); - $icon = ($button->get('icon')) ? $button->get('icon') : $button->get('name'); -?> + $class = 'btn btn-secondary'; + $class .= ($button->get('class')) ? ' ' . $button->get('class') : null; + $class .= ($button->get('modal')) ? ' modal-button' : null; + $href = '#' . strtolower($button->get('name')) . '_modal'; + $link = ($button->get('link')) ? Uri::base() . $button->get('link') : null; + $onclick = ($button->get('onclick')) ? ' onclick="' . $button->get('onclick') . '"' : ''; + $title = ($button->get('title')) ? $button->get('title') : $button->get('text'); + $icon = ($button->get('icon')) ? $button->get('icon') : $button->get('name'); + ?> diff --git a/code/layouts/joomla/editors/buttons/modal.php b/code/layouts/joomla/editors/buttons/modal.php index 2ae1074d..771de064 100644 --- a/code/layouts/joomla/editors/buttons/modal.php +++ b/code/layouts/joomla/editors/buttons/modal.php @@ -1,4 +1,5 @@ get('modal')) -{ - return; +if (!$button->get('modal')) { + return; } $class = ($button->get('class')) ? $button->get('class') : null; @@ -30,34 +30,30 @@ $confirm = ''; -if (is_array($button->get('options')) && isset($options['confirmText']) && isset($options['confirmCallback'])) -{ - $confirm = ''; +if (is_array($button->get('options')) && isset($options['confirmText']) && isset($options['confirmCallback'])) { + $confirm = ''; } -if (null !== $button->get('id')) -{ - $id = str_replace(' ', '', $button->get('id')); -} -else -{ - $id = strtolower($button->get('name')) . '_modal'; +if (null !== $button->get('id')) { + $id = str_replace(' ', '', $button->get('id')); +} else { + $id = strtolower($button->get('name')) . '_modal'; } // @todo: J4: Move Make buttons fullscreen on smaller devices per https://github.com/joomla/joomla-cms/pull/23091 // Create the modal echo HTMLHelper::_( - 'bootstrap.renderModal', - $id, - array( - 'url' => $link, - 'title' => $title, - 'height' => array_key_exists('height', $options) ? $options['height'] : '400px', - 'width' => array_key_exists('width', $options) ? $options['width'] : '800px', - 'bodyHeight' => array_key_exists('bodyHeight', $options) ? $options['bodyHeight'] : '70', - 'modalWidth' => array_key_exists('modalWidth', $options) ? $options['modalWidth'] : '80', - 'footer' => $confirm . '' - ) + 'bootstrap.renderModal', + $id, + array( + 'url' => $link, + 'title' => $title, + 'height' => array_key_exists('height', $options) ? $options['height'] : '400px', + 'width' => array_key_exists('width', $options) ? $options['width'] : '800px', + 'bodyHeight' => array_key_exists('bodyHeight', $options) ? $options['bodyHeight'] : '70', + 'modalWidth' => array_key_exists('modalWidth', $options) ? $options['modalWidth'] : '80', + 'footer' => $confirm . '' + ) ); diff --git a/code/layouts/joomla/error/backtrace.php b/code/layouts/joomla/error/backtrace.php index 415328df..2742f143 100644 --- a/code/layouts/joomla/error/backtrace.php +++ b/code/layouts/joomla/error/backtrace.php @@ -1,4 +1,5 @@ - - - + + + - - - - - + + + + + - $backtrace): ?> - - + $backtrace) : ?> + + - - - - - + + + + + - - - - - - - + + + + + + +
    - Call stack -
    + Call stack +
    - # - - Function - - Location -
    + # + + Function + + Location +
    - -
    + + - - - - + + + + - - -   -
    + + +   +
    diff --git a/code/layouts/joomla/form/field/calendar.php b/code/layouts/joomla/form/field/calendar.php index a977ef17..f81563ab 100644 --- a/code/layouts/joomla/form/field/calendar.php +++ b/code/layouts/joomla/form/field/calendar.php @@ -1,4 +1,5 @@ format('Y-m-d H:i:s'); +if (strtoupper($value) === 'NOW') { + $value = Factory::getDate()->format('Y-m-d H:i:s'); } $readonly = isset($attributes['readonly']) && $attributes['readonly'] === 'readonly'; $disabled = isset($attributes['disabled']) && $attributes['disabled'] === 'disabled'; -if (is_array($attributes)) -{ - $attributes = ArrayHelper::toString($attributes); +if (is_array($attributes)) { + $attributes = ArrayHelper::toString($attributes); } $calendarAttrs = [ - 'data-inputfield' => $id, - 'data-button' => $id . '_btn', - 'data-date-format' => $format, - 'data-firstday' => empty($firstday) ? '' : $firstday, - 'data-weekend' => empty($weekend) ? '' : implode(',', $weekend), - 'data-today-btn' => $todaybutton, - 'data-week-numbers' => $weeknumbers, - 'data-show-time' => $showtime, - 'data-show-others' => $filltable, - 'data-time24' => $timeformat, - 'data-only-months-nav' => $singleheader, - 'data-min-year' => $minYear, - 'data-max-year' => $maxYear, - 'data-date-type' => strtolower($calendar), + 'data-inputfield' => $id, + 'data-button' => $id . '_btn', + 'data-date-format' => $format, + 'data-firstday' => empty($firstday) ? '' : $firstday, + 'data-weekend' => empty($weekend) ? '' : implode(',', $weekend), + 'data-today-btn' => $todaybutton, + 'data-week-numbers' => $weeknumbers, + 'data-show-time' => $showtime, + 'data-show-others' => $filltable, + 'data-time24' => $timeformat, + 'data-only-months-nav' => $singleheader, + 'data-min-year' => $minYear, + 'data-max-year' => $maxYear, + 'data-date-type' => strtolower($calendar), ]; $calendarAttrsStr = ArrayHelper::toString($calendarAttrs); // Add language strings $strings = [ - // Days - 'SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', - // Short days - 'SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', - // Months - 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', - // Short months - 'JANUARY_SHORT', 'FEBRUARY_SHORT', 'MARCH_SHORT', 'APRIL_SHORT', 'MAY_SHORT', 'JUNE_SHORT', - 'JULY_SHORT', 'AUGUST_SHORT', 'SEPTEMBER_SHORT', 'OCTOBER_SHORT', 'NOVEMBER_SHORT', 'DECEMBER_SHORT', - // Buttons - 'JCLOSE', 'JCLEAR', 'JLIB_HTML_BEHAVIOR_TODAY', - // Miscellaneous - 'JLIB_HTML_BEHAVIOR_WK', + // Days + 'SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', + // Short days + 'SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', + // Months + 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', + // Short months + 'JANUARY_SHORT', 'FEBRUARY_SHORT', 'MARCH_SHORT', 'APRIL_SHORT', 'MAY_SHORT', 'JUNE_SHORT', + 'JULY_SHORT', 'AUGUST_SHORT', 'SEPTEMBER_SHORT', 'OCTOBER_SHORT', 'NOVEMBER_SHORT', 'DECEMBER_SHORT', + // Buttons + 'JCLOSE', 'JCLEAR', 'JLIB_HTML_BEHAVIOR_TODAY', + // Miscellaneous + 'JLIB_HTML_BEHAVIOR_WK', ]; -foreach ($strings as $c) -{ - Text::script($c); +foreach ($strings as $c) { + Text::script($c); } // These are new strings. Make sure they exist. Can be generalised at later time: eg in 4.1 version. -if ($lang->hasKey('JLIB_HTML_BEHAVIOR_AM')) -{ - Text::script('JLIB_HTML_BEHAVIOR_AM'); +if ($lang->hasKey('JLIB_HTML_BEHAVIOR_AM')) { + Text::script('JLIB_HTML_BEHAVIOR_AM'); } -if ($lang->hasKey('JLIB_HTML_BEHAVIOR_PM')) -{ - Text::script('JLIB_HTML_BEHAVIOR_PM'); +if ($lang->hasKey('JLIB_HTML_BEHAVIOR_PM')) { + Text::script('JLIB_HTML_BEHAVIOR_PM'); } // Redefine locale/helper assets to use correct path, and load calendar assets $document->getWebAssetManager() - ->registerAndUseScript('field.calendar.helper', $helperPath, [], ['defer' => true]) - ->useStyle('field.calendar' . ($direction === 'rtl' ? '-rtl' : '')) - ->useScript('field.calendar'); + ->registerAndUseScript('field.calendar.helper', $helperPath, [], ['defer' => true]) + ->useStyle('field.calendar' . ($direction === 'rtl' ? '-rtl' : '')) + ->useScript('field.calendar'); ?>
    - -
    - - - - - - data-alt-value="" autocomplete="off"> - - -
    - + +
    + + + + + + data-alt-value="" autocomplete="off"> + + +
    +
    diff --git a/code/layouts/joomla/form/field/checkbox.php b/code/layouts/joomla/form/field/checkbox.php index 4134cdb9..f4049fe6 100644 --- a/code/layouts/joomla/form/field/checkbox.php +++ b/code/layouts/joomla/form/field/checkbox.php @@ -1,4 +1,5 @@
    - - > + + >
    diff --git a/code/layouts/joomla/form/field/checkboxes.php b/code/layouts/joomla/form/field/checkboxes.php index 58dd3878..905b5238 100644 --- a/code/layouts/joomla/form/field/checkboxes.php +++ b/code/layouts/joomla/form/field/checkboxes.php @@ -1,4 +1,5 @@
    - - > - + + + > + - $option) : ?> - value, $checkedOptions, true) ? 'checked' : ''; + $option) : ?> + value, $checkedOptions, true) ? 'checked' : ''; - // In case there is no stored value, use the option's default state. - $checked = (!$hasValue && $option->checked) ? 'checked' : $checked; - $optionClass = !empty($option->class) ? 'class="form-check-input ' . $option->class . '"' : ' class="form-check-input"'; - $optionDisabled = !empty($option->disable) || $disabled ? 'disabled' : ''; + // In case there is no stored value, use the option's default state. + $checked = (!$hasValue && $option->checked) ? 'checked' : $checked; + $optionClass = !empty($option->class) ? 'class="form-check-input ' . $option->class . '"' : ' class="form-check-input"'; + $optionDisabled = !empty($option->disable) || $disabled ? 'disabled' : ''; - // Initialize some JavaScript option attributes. - $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; - $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; + // Initialize some JavaScript option attributes. + $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; + $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; - $oid = $id . $i; - $value = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); - $attributes = array_filter(array($checked, $optionClass, $optionDisabled, $onchange, $onclick)); - ?> -
    - - -
    - + $oid = $id . $i; + $value = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); + $attributes = array_filter(array($checked, $optionClass, $optionDisabled, $onchange, $onclick)); + ?> +
    + + +
    +
    diff --git a/code/layouts/joomla/form/field/color/advanced.php b/code/layouts/joomla/form/field/color/advanced.php index 46836867..db1a6425 100644 --- a/code/layouts/joomla/form/field/color/advanced.php +++ b/code/layouts/joomla/form/field/color/advanced.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager(); $wa->usePreset('minicolors') - ->useScript('field.color-adv'); + ->useScript('field.color-adv'); ?> /> diff --git a/code/layouts/joomla/form/field/color/simple.php b/code/layouts/joomla/form/field/color/simple.php index 0e043aff..cfa19235 100644 --- a/code/layouts/joomla/form/field/color/simple.php +++ b/code/layouts/joomla/form/field/color/simple.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useStyle('webcomponent.field-simple-color') - ->useScript('webcomponent.field-simple-color'); + ->useStyle('webcomponent.field-simple-color') + ->useScript('webcomponent.field-simple-color'); ?> - + diff --git a/code/layouts/joomla/form/field/color/slider.php b/code/layouts/joomla/form/field/color/slider.php index ec0c089b..c3f0b8a6 100644 --- a/code/layouts/joomla/form/field/color/slider.php +++ b/code/layouts/joomla/form/field/color/slider.php @@ -1,4 +1,5 @@
    + > - - - > - - - - > - + + + > + + + + > + - - - - > - - - - - > - - - - - > - - - - - > - + + + + > + + + + + > + + + + + > + + + + + > +
    diff --git a/code/layouts/joomla/form/field/combo.php b/code/layouts/joomla/form/field/combo.php index 04dbe8ba..86f52cee 100644 --- a/code/layouts/joomla/form/field/combo.php +++ b/code/layouts/joomla/form/field/combo.php @@ -1,4 +1,5 @@ text; +foreach ($options as $option) { + $val[] = $option->text; } ?> - data-list="" - + type="text" + name="" + id="" + value="" + + data-list="" + /> diff --git a/code/layouts/joomla/form/field/contenthistory.php b/code/layouts/joomla/form/field/contenthistory.php index 89b43ac2..1091c6fb 100644 --- a/code/layouts/joomla/form/field/contenthistory.php +++ b/code/layouts/joomla/form/field/contenthistory.php @@ -1,4 +1,5 @@ Route::_($link), - 'title' => $label, - 'height' => '100%', - 'width' => '100%', - 'modalWidth' => '80', - 'bodyHeight' => '60', - 'footer' => '' - ) + 'bootstrap.renderModal', + 'versionsModal', + array( + 'url' => Route::_($link), + 'title' => $label, + 'height' => '100%', + 'width' => '100%', + 'modalWidth' => '80', + 'bodyHeight' => '60', + 'footer' => '' + ) ); ?> diff --git a/code/layouts/joomla/form/field/email.php b/code/layouts/joomla/form/field/email.php index 25448939..2f9e75e0 100644 --- a/code/layouts/joomla/form/field/email.php +++ b/code/layouts/joomla/form/field/email.php @@ -1,4 +1,5 @@ '; diff --git a/code/layouts/joomla/form/field/file.php b/code/layouts/joomla/form/field/file.php index d4f93a01..01209732 100644 --- a/code/layouts/joomla/form/field/file.php +++ b/code/layouts/joomla/form/field/file.php @@ -1,4 +1,5 @@ - - - - - - - - >
    - + name="" + id="" + + + + + + + + + >
    + diff --git a/code/layouts/joomla/form/field/groupedlist-fancy-select.php b/code/layouts/joomla/form/field/groupedlist-fancy-select.php index 9ce2f3b9..633ce7e0 100644 --- a/code/layouts/joomla/form/field/groupedlist-fancy-select.php +++ b/code/layouts/joomla/form/field/groupedlist-fancy-select.php @@ -1,4 +1,5 @@ escape($hint ?: Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_OPTIONS')) . '" '; -if ($required) -{ - $attr .= ' required class="required"'; - $attr2 .= ' required'; +if ($required) { + $attr .= ' required class="required"'; + $attr2 .= ' required'; } // Create a read-only list (no name) with a hidden input to store the value. -if ($readonly) -{ - $html[] = HTMLHelper::_( - 'select.groupedlist', $groups, null, - array( - 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, - 'option.text.toHtml' => false, - ) - ); - - // E.g. form field type tag sends $this->value as array - if ($multiple && \is_array($value)) - { - if (!\count($value)) - { - $value[] = ''; - } - - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} - -// Create a regular list. -else -{ - $html[] = HTMLHelper::_( - 'select.groupedlist', $groups, $name, - array( - 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, - 'option.text.toHtml' => false, - ) - ); +if ($readonly) { + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + null, + array( + 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, + 'option.text.toHtml' => false, + ) + ); + + // E.g. form field type tag sends $this->value as array + if ($multiple && \is_array($value)) { + if (!\count($value)) { + $value[] = ''; + } + + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else { + // Create a regular list. + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + $name, + array( + 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, + 'option.text.toHtml' => false, + ) + ); } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getApplication()->getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/code/layouts/joomla/form/field/groupedlist.php b/code/layouts/joomla/form/field/groupedlist.php index f0f0eda4..e781dde4 100644 --- a/code/layouts/joomla/form/field/groupedlist.php +++ b/code/layouts/joomla/form/field/groupedlist.php @@ -1,4 +1,5 @@ $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, - 'option.text.toHtml' => false, - ) - ); - - // E.g. form field type tag sends $this->value as array - if ($multiple && \is_array($value)) - { - if (!\count($value)) - { - $value[] = ''; - } +if ($readonly) { + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + null, + array( + 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, + 'option.text.toHtml' => false, + ) + ); - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} + // E.g. form field type tag sends $this->value as array + if ($multiple && \is_array($value)) { + if (!\count($value)) { + $value[] = ''; + } -// Create a regular list. -else -{ - $html[] = HTMLHelper::_( - 'select.groupedlist', $groups, $name, - array( - 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, - 'option.text.toHtml' => false, - ) - ); + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else { + // Create a regular list. + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + $name, + array( + 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, + 'option.text.toHtml' => false, + ) + ); } echo implode($html); diff --git a/code/layouts/joomla/form/field/hidden.php b/code/layouts/joomla/form/field/hidden.php index 539e99ae..88ba0160 100644 --- a/code/layouts/joomla/form/field/hidden.php +++ b/code/layouts/joomla/form/field/hidden.php @@ -1,4 +1,5 @@ > + type="hidden" + name="" + id="" + value="" + > diff --git a/code/layouts/joomla/form/field/list-fancy-select.php b/code/layouts/joomla/form/field/list-fancy-select.php index 8820fe0f..89fba452 100644 --- a/code/layouts/joomla/form/field/list-fancy-select.php +++ b/code/layouts/joomla/form/field/list-fancy-select.php @@ -1,4 +1,5 @@ escape($hint ?: Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_OPTIONS')) . '" '; -if ($required) -{ - $attr .= ' required class="required"'; - $attr2 .= ' required'; +if ($required) { + $attr .= ' required class="required"'; + $attr2 .= ' required'; } // Create a read-only list (no name) with hidden input(s) to store the value(s). -if ($readonly) -{ - $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); - - // E.g. form field type tag sends $this->value as array - if ($multiple && is_array($value)) - { - if (!count($value)) - { - $value[] = ''; - } - - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} -else -// Create a regular list. +if ($readonly) { + $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); + + // E.g. form field type tag sends $this->value as array + if ($multiple && is_array($value)) { + if (!count($value)) { + $value[] = ''; + } + + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else // Create a regular list. { - $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); + $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getApplication()->getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/code/layouts/joomla/form/field/list.php b/code/layouts/joomla/form/field/list.php index c827e314..99e62922 100644 --- a/code/layouts/joomla/form/field/list.php +++ b/code/layouts/joomla/form/field/list.php @@ -1,4 +1,5 @@ value as array - if ($multiple && is_array($value)) - { - if (!count($value)) - { - $value[] = ''; - } + // E.g. form field type tag sends $this->value as array + if ($multiple && is_array($value)) { + if (!count($value)) { + $value[] = ''; + } - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} -else -// Create a regular list passing the arguments in an array. + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else // Create a regular list passing the arguments in an array. { - $listoptions = array(); - $listoptions['option.key'] = 'value'; - $listoptions['option.text'] = 'text'; - $listoptions['list.select'] = $value; - $listoptions['id'] = $id; - $listoptions['list.translate'] = false; - $listoptions['option.attr'] = 'optionattr'; - $listoptions['list.attr'] = trim($attr); - $html[] = HTMLHelper::_('select.genericlist', $options, $name, $listoptions); + $listoptions = array(); + $listoptions['option.key'] = 'value'; + $listoptions['option.text'] = 'text'; + $listoptions['list.select'] = $value; + $listoptions['id'] = $id; + $listoptions['list.translate'] = false; + $listoptions['option.attr'] = 'optionattr'; + $listoptions['list.attr'] = trim($attr); + $html[] = HTMLHelper::_('select.genericlist', $options, $name, $listoptions); } echo implode($html); diff --git a/code/layouts/joomla/form/field/media.php b/code/layouts/joomla/form/field/media.php index 13ce9086..c62233c5 100644 --- a/code/layouts/joomla/form/field/media.php +++ b/code/layouts/joomla/form/field/media.php @@ -1,4 +1,5 @@ 0) ? 'max-width:' . $width . 'px;' : ''; - $style .= ($height > 0) ? 'max-height:' . $height . 'px;' : ''; - - $imgattr = array( - 'id' => $id . '_preview', - 'class' => 'media-preview', - 'style' => $style, - ); - - $img = HTMLHelper::_('image', $src, Text::_('JLIB_FORM_MEDIA_PREVIEW_ALT'), $imgattr); - - $previewImg = '
    ' . $img . '
    '; - $previewImgEmpty = ''; - - $showPreview = 'static'; +if ($showPreview) { + $cleanValue = MediaHelper::getCleanMediaFieldValue($value); + + if ($cleanValue && file_exists(JPATH_ROOT . '/' . $cleanValue)) { + $src = Uri::root() . $value; + } else { + $src = ''; + } + + $width = $previewWidth; + $height = $previewHeight; + $style = ''; + $style .= ($width > 0) ? 'max-width:' . $width . 'px;' : ''; + $style .= ($height > 0) ? 'max-height:' . $height . 'px;' : ''; + + $imgattr = array( + 'id' => $id . '_preview', + 'class' => 'media-preview', + 'style' => $style, + ); + + $img = HTMLHelper::_('image', $src, Text::_('JLIB_FORM_MEDIA_PREVIEW_ALT'), $imgattr); + + $previewImg = '
    ' . $img . '
    '; + $previewImgEmpty = ''; + + $showPreview = 'static'; } // The url for the modal $url = ($readonly ? '' - : ($link ?: 'index.php?option=com_media&view=media&tmpl=component&mediatypes=' . $mediaTypes - . '&asset=' . $asset . '&author=' . $authorId) - . '&fieldid={field-media-id}&path=' . $folder); + : ($link ?: 'index.php?option=com_media&view=media&tmpl=component&mediatypes=' . $mediaTypes + . '&asset=' . $asset . '&author=' . $authorId) + . '&fieldid={field-media-id}&path=' . $folder); // Correctly route the url to ensure it's correctly using sef modes and subfolders $url = Route::_($url); @@ -143,63 +140,63 @@ Text::script('JLIB_FORM_MEDIA_PREVIEW_EMPTY', true); $modalHTML = HTMLHelper::_( - 'bootstrap.renderModal', - 'imageModal_' . $id, - [ - 'url' => $url, - 'title' => Text::_('JLIB_FORM_CHANGE_IMAGE'), - 'closeButton' => true, - 'height' => '100%', - 'width' => '100%', - 'modalWidth' => '80', - 'bodyHeight' => '60', - 'footer' => '' - . '', - ] + 'bootstrap.renderModal', + 'imageModal_' . $id, + [ + 'url' => $url, + 'title' => Text::_('JLIB_FORM_CHANGE_IMAGE'), + 'closeButton' => true, + 'height' => '100%', + 'width' => '100%', + 'modalWidth' => '80', + 'bodyHeight' => '60', + 'footer' => '' + . '', + ] ); $wam->useStyle('webcomponent.field-media') - ->useScript('webcomponent.field-media'); + ->useScript('webcomponent.field-media'); if (count($doc->getScriptOptions('media-picker')) === 0) { - $doc->addScriptOptions('media-picker', [ - 'images' => $imagesExt, - 'audios' => $audiosExt, - 'videos' => $videosExt, - 'documents' => $documentsExt, - ]); + $doc->addScriptOptions('media-picker', [ + 'images' => $imagesExt, + 'audios' => $audiosExt, + 'videos' => $videosExt, + 'documents' => $documentsExt, + ]); } ?> - base-path="" - root-folder="get('file_path', 'images'); ?>" - url="" - modal-container=".modal" - modal-width="100%" - modal-height="400px" - input=".field-media-input" - button-select=".button-select" - button-clear=".button-clear" - button-save-selected=".button-save-selected" - preview="static" - preview-container=".field-media-preview" - preview-width="" - preview-height="" - supported-extensions=" $imagesAllowedExt, 'audios' => $audiosAllowedExt, 'videos' => $videosAllowedExt, 'documents' => $documentsAllowedExt])); ?> + base-path="" + root-folder="get('file_path', 'images'); ?>" + url="" + modal-container=".modal" + modal-width="100%" + modal-height="400px" + input=".field-media-input" + button-select=".button-select" + button-clear=".button-clear" + button-save-selected=".button-save-selected" + preview="static" + preview-container=".field-media-preview" + preview-width="" + preview-height="" + supported-extensions=" $imagesAllowedExt, 'audios' => $audiosAllowedExt, 'videos' => $videosAllowedExt, 'documents' => $documentsAllowedExt])); ?> "> - - -
    - - -
    - -
    - > - - - - -
    + + +
    + + +
    + +
    + > + + + + +
    diff --git a/code/layouts/joomla/form/field/meter.php b/code/layouts/joomla/form/field/meter.php index 3e2a271b..f6c112d8 100644 --- a/code/layouts/joomla/form/field/meter.php +++ b/code/layouts/joomla/form/field/meter.php @@ -1,4 +1,5 @@
    -
    - style="width:%;">
    +
    + style="width:%;">
    diff --git a/code/layouts/joomla/form/field/moduleorder.php b/code/layouts/joomla/form/field/moduleorder.php index f71c8510..95f23a7b 100644 --- a/code/layouts/joomla/form/field/moduleorder.php +++ b/code/layouts/joomla/form/field/moduleorder.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('webcomponent.field-module-order'); + ->useScript('webcomponent.field-module-order'); ?> > diff --git a/code/layouts/joomla/form/field/number.php b/code/layouts/joomla/form/field/number.php index d10d61f3..ccc24f22 100644 --- a/code/layouts/joomla/form/field/number.php +++ b/code/layouts/joomla/form/field/number.php @@ -1,4 +1,5 @@ > + type="number" + inputmode="numeric" + name="" + id="" + value="" + > diff --git a/code/layouts/joomla/form/field/password.php b/code/layouts/joomla/form/field/password.php index a1bc52cd..d128aacb 100644 --- a/code/layouts/joomla/form/field/password.php +++ b/code/layouts/joomla/form/field/password.php @@ -1,4 +1,5 @@ getWebAssetManager(); -if ($meter) -{ - $wa->useScript('field.passwordstrength'); +if ($meter) { + $wa->useScript('field.passwordstrength'); - $class = 'js-password-strength ' . $class; + $class = 'js-password-strength ' . $class; - if ($forcePassword) - { - $class = $class . ' meteredPassword'; - } + if ($forcePassword) { + $class = $class . ' meteredPassword'; + } } $wa->useScript('field.passwordview'); @@ -75,92 +74,85 @@ Text::script('JSHOWPASSWORD'); Text::script('JHIDEPASSWORD'); -if ($lock) -{ - Text::script('JMODIFY'); - Text::script('JCANCEL'); +if ($lock) { + Text::script('JMODIFY'); + Text::script('JCANCEL'); - $disabled = true; - $hint = str_repeat('•', 10); - $value = ''; + $disabled = true; + $hint = str_repeat('•', 10); + $value = ''; } $ariaDescribedBy = $rules ? $name . '-rules ' : ''; $ariaDescribedBy .= !empty($description) ? (($id ?: $name) . '-desc') : ''; $attributes = array( - strlen($hint) ? 'placeholder="' . htmlspecialchars($hint, ENT_COMPAT, 'UTF-8') . '"' : '', - !empty($autocomplete) ? 'autocomplete="' . $autocomplete . '"' : '', - !empty($class) ? 'class="form-control ' . $class . '"' : 'class="form-control"', - !empty($ariaDescribedBy) ? 'aria-describedby="' . trim($ariaDescribedBy) . '"' : '', - $readonly ? 'readonly' : '', - $disabled ? 'disabled' : '', - !empty($size) ? 'size="' . $size . '"' : '', - !empty($maxLength) ? 'maxlength="' . $maxLength . '"' : '', - $required ? 'required' : '', - $autofocus ? 'autofocus' : '', - !empty($minLength) ? 'data-min-length="' . $minLength . '"' : '', - !empty($minIntegers) ? 'data-min-integers="' . $minIntegers . '"' : '', - !empty($minSymbols) ? 'data-min-symbols="' . $minSymbols . '"' : '', - !empty($minUppercase) ? 'data-min-uppercase="' . $minUppercase . '"' : '', - !empty($minLowercase) ? 'data-min-lowercase="' . $minLowercase . '"' : '', - !empty($forcePassword) ? 'data-min-force="' . $forcePassword . '"' : '', - $dataAttribute, + strlen($hint) ? 'placeholder="' . htmlspecialchars($hint, ENT_COMPAT, 'UTF-8') . '"' : '', + !empty($autocomplete) ? 'autocomplete="' . $autocomplete . '"' : '', + !empty($class) ? 'class="form-control ' . $class . '"' : 'class="form-control"', + !empty($ariaDescribedBy) ? 'aria-describedby="' . trim($ariaDescribedBy) . '"' : '', + $readonly ? 'readonly' : '', + $disabled ? 'disabled' : '', + !empty($size) ? 'size="' . $size . '"' : '', + !empty($maxLength) ? 'maxlength="' . $maxLength . '"' : '', + $required ? 'required' : '', + $autofocus ? 'autofocus' : '', + !empty($minLength) ? 'data-min-length="' . $minLength . '"' : '', + !empty($minIntegers) ? 'data-min-integers="' . $minIntegers . '"' : '', + !empty($minSymbols) ? 'data-min-symbols="' . $minSymbols . '"' : '', + !empty($minUppercase) ? 'data-min-uppercase="' . $minUppercase . '"' : '', + !empty($minLowercase) ? 'data-min-lowercase="' . $minLowercase . '"' : '', + !empty($forcePassword) ? 'data-min-force="' . $forcePassword . '"' : '', + $dataAttribute, ); -if ($rules) -{ - $requirements = []; - - if ($minLength) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_CHARACTERS', $minLength); - } - - if ($minIntegers) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_DIGITS', $minIntegers); - } - - if ($minSymbols) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_SYMBOLS', $minSymbols); - } - - if ($minUppercase) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_UPPERCASE', $minUppercase); - } - - if ($minLowercase) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_LOWERCASE', $minLowercase); - } +if ($rules) { + $requirements = []; + + if ($minLength) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_CHARACTERS', $minLength); + } + + if ($minIntegers) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_DIGITS', $minIntegers); + } + + if ($minSymbols) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_SYMBOLS', $minSymbols); + } + + if ($minUppercase) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_UPPERCASE', $minUppercase); + } + + if ($minLowercase) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_LOWERCASE', $minLowercase); + } } ?> -
    - -
    +
    + +
    -
    - > - - - - - -
    +
    + > + + + + + +
    diff --git a/code/layouts/joomla/form/field/radio/buttons.php b/code/layouts/joomla/form/field/radio/buttons.php index 96f36aa5..faf1d93f 100644 --- a/code/layouts/joomla/form/field/radio/buttons.php +++ b/code/layouts/joomla/form/field/radio/buttons.php @@ -1,4 +1,5 @@
    > - - - -
    - $option) : ?> - - disable) ? 'disabled' : ''; - $style = $disabled ? ' style="pointer-events: none"' : ''; + + + +
    + $option) : ?> + + disable) ? 'disabled' : ''; + $style = $disabled ? ' style="pointer-events: none"' : ''; - // Initialize some option attributes. - if ($isBtnYesNo) - { - // Set the button classes for the yes/no group - switch ($option->value) - { - case '0': - $btnClass = 'btn btn-outline-danger'; - break; - case '1': - $btnClass = 'btn btn-outline-success'; - break; - default: - $btnClass = 'btn btn-outline-secondary'; - break; - } - } + // Initialize some option attributes. + if ($isBtnYesNo) { + // Set the button classes for the yes/no group + switch ($option->value) { + case '0': + $btnClass = 'btn btn-outline-danger'; + break; + case '1': + $btnClass = 'btn btn-outline-success'; + break; + default: + $btnClass = 'btn btn-outline-secondary'; + break; + } + } - $optionClass = !empty($option->class) ? $option->class : $btnClass; - $optionClass = trim($optionClass . ' ' . $disabled); - $checked = ((string) $option->value === $value) ? 'checked="checked"' : ''; + $optionClass = !empty($option->class) ? $option->class : $btnClass; + $optionClass = trim($optionClass . ' ' . $disabled); + $checked = ((string) $option->value === $value) ? 'checked="checked"' : ''; - // Initialize some JavaScript option attributes. - $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; - $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; - $oid = $id . $i; - $ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); - $attributes = array_filter(array($checked, $disabled, ltrim($style), $onchange, $onclick)); - ?> - - - - > - - - -
    + // Initialize some JavaScript option attributes. + $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; + $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; + $oid = $id . $i; + $ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); + $attributes = array_filter(array($checked, $disabled, ltrim($style), $onchange, $onclick)); + ?> + + + + > + + + +
    diff --git a/code/layouts/joomla/form/field/radio/switcher.php b/code/layouts/joomla/form/field/radio/switcher.php index f1070fa6..191a5a07 100644 --- a/code/layouts/joomla/form/field/radio/switcher.php +++ b/code/layouts/joomla/form/field/radio/switcher.php @@ -1,4 +1,5 @@
    > - - - -
    - $option) : ?> - value == '0') - { - $value = '0'; - } + + + +
    + $option) : ?> + value == '0') { + $value = '0'; + } - // Initialize some option attributes. - $optionValue = (string) $option->value; - $optionId = $id . $i; - $attributes = $optionValue == $value ? 'checked class="active ' . $class . '"' : ($class ? 'class="' . $class . '"' : ''); - $attributes .= $optionValue != $value && $readonly || $disabled ? ' disabled' : ''; - ?> - escape($optionValue), $attributes); ?> - ' . $option->text . ''; ?> - - -
    + // Initialize some option attributes. + $optionValue = (string) $option->value; + $optionId = $id . $i; + $attributes = $optionValue == $value ? 'checked class="active ' . $class . '"' : ($class ? 'class="' . $class . '"' : ''); + $attributes .= $optionValue != $value && $readonly || $disabled ? ' disabled' : ''; + ?> + escape($optionValue), $attributes); ?> + ' . $option->text . ''; ?> + + +
    diff --git a/code/layouts/joomla/form/field/radiobasic.php b/code/layouts/joomla/form/field/radiobasic.php index e28959c7..45f1aa8a 100644 --- a/code/layouts/joomla/form/field/radiobasic.php +++ b/code/layouts/joomla/form/field/radiobasic.php @@ -1,4 +1,5 @@
    - - - > + + + + > - - $option) : ?> - value === $value) ? 'checked="checked"' : ''; - $optionClass = !empty($option->class) ? 'class="' . $option->class . '"' : ''; - $disabled = !empty($option->disable) || ($disabled && !$checked) ? 'disabled' : ''; + + $option) : ?> + value === $value) ? 'checked="checked"' : ''; + $optionClass = !empty($option->class) ? 'class="' . $option->class . '"' : ''; + $disabled = !empty($option->disable) || ($disabled && !$checked) ? 'disabled' : ''; - // Initialize some JavaScript option attributes. - $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; - $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; - $oid = $id . $i; - $ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); - $attributes = array_filter(array($checked, $optionClass, $disabled, $onchange, $onclick)); - ?> - - - -
    - -
    - - + // Initialize some JavaScript option attributes. + $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; + $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; + $oid = $id . $i; + $ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); + $attributes = array_filter(array($checked, $optionClass, $disabled, $onchange, $onclick)); + ?> + + + +
    + +
    + +
    diff --git a/code/layouts/joomla/form/field/range.php b/code/layouts/joomla/form/field/range.php index 3e07863d..9e5f0a31 100644 --- a/code/layouts/joomla/form/field/range.php +++ b/code/layouts/joomla/form/field/range.php @@ -1,4 +1,5 @@ > + type="range" + name="" + id="" + value="" + > diff --git a/code/layouts/joomla/form/field/rules.php b/code/layouts/joomla/form/field/rules.php index 90df4535..ee1c065b 100644 --- a/code/layouts/joomla/form/field/rules.php +++ b/code/layouts/joomla/form/field/rules.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useStyle('webcomponent.field-permissions') - ->useScript('webcomponent.field-permissions') - ->useStyle('webcomponent.joomla-tab') - ->useScript('webcomponent.joomla-tab'); + ->useStyle('webcomponent.field-permissions') + ->useScript('webcomponent.field-permissions') + ->useStyle('webcomponent.joomla-tab') + ->useScript('webcomponent.joomla-tab'); // Load JavaScript message titles Text::script('ERROR'); @@ -79,171 +80,155 @@
    - - - -
    - -
    + + + +
    + +
    > - - - - value === 1 ? ' active' : ''; ?> - name=" $group->level + 1)), ENT_COMPAT, 'utf-8') . $group->text; ?>" id="permission-value; ?>"> - - - - - - - - - - - - - - value, 'core.admin'); ?> - - - - - - - - - -
    - - - - - -
    - - description)) : ?> - - - -
    -   - -
    -
    - - - value, $action->name, $assetId); - $inheritedGroupParentAssetRule = !empty($parentAssetId) ? Access::checkGroup($group->value, $action->name, $parentAssetId) : null; - $inheritedParentGroupRule = !empty($group->parent_id) ? Access::checkGroup($group->parent_id, $action->name, $assetId) : null; - - // Current group is a Super User group, so calculated setting is "Allowed (Super User)". - if ($isSuperUserGroup) - { - $result['class'] = 'badge bg-success'; - $result['text'] = '' . Text::_('JLIB_RULES_ALLOWED_ADMIN'); - } - else - { - // First get the real recursive calculated setting and add (Inherited) to it. - - // If recursive calculated setting is "Denied" or null. Calculated permission is "Not Allowed (Inherited)". - if ($inheritedGroupRule === null || $inheritedGroupRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_INHERITED'); - } - // If recursive calculated setting is "Allowed". Calculated permission is "Allowed (Inherited)". - else - { - $result['class'] = 'badge bg-success'; - $result['text'] = Text::_('JLIB_RULES_ALLOWED_INHERITED'); - } - - // Second part: Overwrite the calculated permissions labels if there is an explicit permission in the current group. - - /** - * @todo: incorrect info - * If a component has a permission that doesn't exists in global config (ex: frontend editing in com_modules) by default - * we get "Not Allowed (Inherited)" when we should get "Not Allowed (Default)". - */ - - // If there is an explicit permission "Not Allowed". Calculated permission is "Not Allowed". - if ($assetRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED'); - } - // If there is an explicit permission is "Allowed". Calculated permission is "Allowed". - elseif ($assetRule === true) - { - $result['class'] = 'badge bg-success'; - $result['text'] = Text::_('JLIB_RULES_ALLOWED'); - } - - // Third part: Overwrite the calculated permissions labels for special cases. - - // Global configuration with "Not Set" permission. Calculated permission is "Not Allowed (Default)". - if (empty($group->parent_id) && $isGlobalConfig === true && $assetRule === null) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_DEFAULT'); - } - - /** - * Component/Item with explicit "Denied" permission at parent Asset (Category, Component or Global config) configuration. - * Or some parent group has an explicit "Denied". - * Calculated permission is "Not Allowed (Locked)". - */ - elseif ($inheritedGroupParentAssetRule === false || $inheritedParentGroupRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = ''. Text::_('JLIB_RULES_NOT_ALLOWED_LOCKED'); - } - } - ?> - -
    -
    - -
    + + + + value === 1 ? ' active' : ''; ?> + name=" $group->level + 1)), ENT_COMPAT, 'utf-8') . $group->text; ?>" id="permission-value; ?>"> + + + + + + + + + + + + + + value, 'core.admin'); ?> + + + + + + + + + +
    + + + + + +
    + + description)) : ?> + + + +
    +   + +
    +
    + + + value, $action->name, $assetId); + $inheritedGroupParentAssetRule = !empty($parentAssetId) ? Access::checkGroup($group->value, $action->name, $parentAssetId) : null; + $inheritedParentGroupRule = !empty($group->parent_id) ? Access::checkGroup($group->parent_id, $action->name, $assetId) : null; + + // Current group is a Super User group, so calculated setting is "Allowed (Super User)". + if ($isSuperUserGroup) { + $result['class'] = 'badge bg-success'; + $result['text'] = '' . Text::_('JLIB_RULES_ALLOWED_ADMIN'); + } else { + // First get the real recursive calculated setting and add (Inherited) to it. + + // If recursive calculated setting is "Denied" or null. Calculated permission is "Not Allowed (Inherited)". + if ($inheritedGroupRule === null || $inheritedGroupRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_INHERITED'); + } else { + // If recursive calculated setting is "Allowed". Calculated permission is "Allowed (Inherited)". + $result['class'] = 'badge bg-success'; + $result['text'] = Text::_('JLIB_RULES_ALLOWED_INHERITED'); + } + + // Second part: Overwrite the calculated permissions labels if there is an explicit permission in the current group. + + /** + * @todo: incorrect info + * If a component has a permission that doesn't exists in global config (ex: frontend editing in com_modules) by default + * we get "Not Allowed (Inherited)" when we should get "Not Allowed (Default)". + */ + + // If there is an explicit permission "Not Allowed". Calculated permission is "Not Allowed". + if ($assetRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED'); + } elseif ($assetRule === true) { + // If there is an explicit permission is "Allowed". Calculated permission is "Allowed". + $result['class'] = 'badge bg-success'; + $result['text'] = Text::_('JLIB_RULES_ALLOWED'); + } + + // Third part: Overwrite the calculated permissions labels for special cases. + + // Global configuration with "Not Set" permission. Calculated permission is "Not Allowed (Default)". + if (empty($group->parent_id) && $isGlobalConfig === true && $assetRule === null) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_DEFAULT'); + } elseif ($inheritedGroupParentAssetRule === false || $inheritedParentGroupRule === false) { + /** + * Component/Item with explicit "Denied" permission at parent Asset (Category, Component or Global config) configuration. + * Or some parent group has an explicit "Denied". + * Calculated permission is "Not Allowed (Locked)". + */ + $result['class'] = 'badge bg-danger'; + $result['text'] = '' . Text::_('JLIB_RULES_NOT_ALLOWED_LOCKED'); + } + } + ?> + +
    +
    + +
    diff --git a/code/layouts/joomla/form/field/subform/default.php b/code/layouts/joomla/form/field/subform/default.php index 6afbd05d..b9368728 100644 --- a/code/layouts/joomla/form/field/subform/default.php +++ b/code/layouts/joomla/form/field/subform/default.php @@ -1,4 +1,5 @@ getGroup('') as $field) : ?> - renderField(); ?> + renderField(); ?> diff --git a/code/layouts/joomla/form/field/subform/repeatable-table.php b/code/layouts/joomla/form/field/subform/repeatable-table.php index e2aa4a00..7625282a 100644 --- a/code/layouts/joomla/form/field/subform/repeatable-table.php +++ b/code/layouts/joomla/form/field/subform/repeatable-table.php @@ -1,4 +1,5 @@ getDocument() - ->getWebAssetManager() - ->useScript('webcomponent.field-subform'); +if ($multiple) { + // Add script + Factory::getApplication() + ->getDocument() + ->getWebAssetManager() + ->useScript('webcomponent.field-subform'); } $class = $class ? ' ' . $class : ''; @@ -47,82 +47,77 @@ // Build heading $table_head = ''; -if (!empty($groupByFieldset)) -{ - foreach ($tmpl->getFieldsets() as $fieldset) { - $table_head .= '' . Text::_($fieldset->label); +if (!empty($groupByFieldset)) { + foreach ($tmpl->getFieldsets() as $fieldset) { + $table_head .= '' . Text::_($fieldset->label); - if ($fieldset->description) - { - $table_head .= ''; - } + if ($fieldset->description) { + $table_head .= ''; + } - $table_head .= ''; - } + $table_head .= ''; + } - $sublayout = 'section-byfieldsets'; -} -else -{ - foreach ($tmpl->getGroup('') as $field) { - $table_head .= '' . strip_tags($field->label); + $sublayout = 'section-byfieldsets'; +} else { + foreach ($tmpl->getGroup('') as $field) { + $table_head .= '' . strip_tags($field->label); - if ($field->description) - { - $table_head .= ''; - } + if ($field->description) { + $table_head .= ''; + } - $table_head .= ''; - } + $table_head .= ''; + } - $sublayout = 'section'; + $sublayout = 'section'; - // Label will not be shown for sections layout, so reset the margin left - Factory::getApplication() - ->getDocument() - ->addStyleDeclaration('.subform-table-sublayout-section .controls { margin-left: 0px }'); + // Label will not be shown for sections layout, so reset the margin left + Factory::getApplication() + ->getDocument() + ->addStyleDeclaration('.subform-table-sublayout-section .controls { margin-left: 0px }'); } ?>
    - -
    - - - - - - - - - - - - $form) : - echo $this->sublayout($sublayout, array('form' => $form, 'basegroup' => $fieldname, 'group' => $fieldname . $k, 'buttons' => $buttons)); - endforeach; - ?> - -
    - -
    - -
    - -
    - -
    -
    - - - -
    + +
    + + + + + + + + + + + + $form) : + echo $this->sublayout($sublayout, array('form' => $form, 'basegroup' => $fieldname, 'group' => $fieldname . $k, 'buttons' => $buttons)); + endforeach; + ?> + +
    + +
    + +
    + +
    + +
    +
    + + + +
    diff --git a/code/layouts/joomla/form/field/subform/repeatable-table/section-byfieldsets.php b/code/layouts/joomla/form/field/subform/repeatable-table/section-byfieldsets.php index ba2730f2..a3ff486d 100644 --- a/code/layouts/joomla/form/field/subform/repeatable-table/section-byfieldsets.php +++ b/code/layouts/joomla/form/field/subform/repeatable-table/section-byfieldsets.php @@ -1,4 +1,5 @@ - getFieldsets() as $fieldset) : ?> - - getFieldset($fieldset->name) as $field) : ?> - renderField(); ?> - - - - - -
    - - - - - - - - - -
    - - + getFieldsets() as $fieldset) : ?> + + getFieldset($fieldset->name) as $field) : ?> + renderField(); ?> + + + + + +
    + + + + + + + + + +
    + + diff --git a/code/layouts/joomla/form/field/subform/repeatable-table/section.php b/code/layouts/joomla/form/field/subform/repeatable-table/section.php index 3435f1d4..ccef5fc9 100644 --- a/code/layouts/joomla/form/field/subform/repeatable-table/section.php +++ b/code/layouts/joomla/form/field/subform/repeatable-table/section.php @@ -1,4 +1,5 @@ - getGroup('') as $field) : ?> - - renderField(array('hiddenLabel' => true, 'hiddenDescription' => true)); ?> - - - - -
    - - - - - - - - - -
    - - + getGroup('') as $field) : ?> + + renderField(array('hiddenLabel' => true, 'hiddenDescription' => true)); ?> + + + + +
    + + + + + + + + + +
    + + diff --git a/code/layouts/joomla/form/field/subform/repeatable.php b/code/layouts/joomla/form/field/subform/repeatable.php index 0d272e72..a2504bbc 100644 --- a/code/layouts/joomla/form/field/subform/repeatable.php +++ b/code/layouts/joomla/form/field/subform/repeatable.php @@ -1,4 +1,5 @@ getDocument() - ->getWebAssetManager() - ->useScript('webcomponent.field-subform'); +if ($multiple) { + // Add script + Factory::getApplication() + ->getDocument() + ->getWebAssetManager() + ->useScript('webcomponent.field-subform'); } $class = $class ? ' ' . $class : ''; @@ -48,27 +48,27 @@ ?>
    - - -
    -
    - -
    -
    - - $form) : - echo $this->sublayout($sublayout, array('form' => $form, 'basegroup' => $fieldname, 'group' => $fieldname . $k, 'buttons' => $buttons)); - endforeach; - ?> - - - -
    + + +
    +
    + +
    +
    + + $form) : + echo $this->sublayout($sublayout, array('form' => $form, 'basegroup' => $fieldname, 'group' => $fieldname . $k, 'buttons' => $buttons)); + endforeach; + ?> + + + +
    diff --git a/code/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php b/code/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php index 0abf0645..4710dc0b 100644 --- a/code/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php +++ b/code/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php @@ -1,4 +1,5 @@
    - -
    -
    - - - -
    -
    - -
    - getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - getFieldset($fieldset->name) as $field) : ?> - renderField(); ?> - -
    - -
    + +
    +
    + + + +
    +
    + +
    + getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + getFieldset($fieldset->name) as $field) : ?> + renderField(); ?> + +
    + +
    diff --git a/code/layouts/joomla/form/field/subform/repeatable/section.php b/code/layouts/joomla/form/field/subform/repeatable/section.php index e3693ca5..7eb48262 100644 --- a/code/layouts/joomla/form/field/subform/repeatable/section.php +++ b/code/layouts/joomla/form/field/subform/repeatable/section.php @@ -1,4 +1,5 @@
    - -
    -
    - - - -
    -
    - + +
    +
    + + + +
    +
    + getGroup('') as $field) : ?> - renderField(); ?> + renderField(); ?>
    diff --git a/code/layouts/joomla/form/field/tag.php b/code/layouts/joomla/form/field/tag.php index 1c6de774..e7a586c6 100644 --- a/code/layouts/joomla/form/field/tag.php +++ b/code/layouts/joomla/form/field/tag.php @@ -1,4 +1,5 @@ escape($hint ?: Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_TAGS')) . '" '; $attr2 .= $dataAttribute; -if ($allowCustom) -{ - $attr2 .= $allowCustom ? ' allow-custom' : ''; - $attr2 .= $allowCustom ? ' new-item-prefix="#new#"' : ''; +if ($allowCustom) { + $attr2 .= $allowCustom ? ' allow-custom' : ''; + $attr2 .= $allowCustom ? ' new-item-prefix="#new#"' : ''; } -if ($remoteSearch) -{ - $attr2 .= ' remote-search'; - $attr2 .= ' url="' . Uri::root(true) . '/index.php?option=com_tags&task=tags.searchAjax"'; - $attr2 .= ' term-key="like"'; - $attr2 .= ' min-term-length="' . $minTermLength . '"'; +if ($remoteSearch) { + $attr2 .= ' remote-search'; + $attr2 .= ' url="' . Uri::root(true) . '/index.php?option=com_tags&task=tags.searchAjax"'; + $attr2 .= ' term-key="like"'; + $attr2 .= ' min-term-length="' . $minTermLength . '"'; } -if ($required) -{ - $attr .= ' required class="required"'; - $attr2 .= ' required'; +if ($required) { + $attr .= ' required class="required"'; + $attr2 .= ' required'; } // Create a read-only list (no name) with hidden input(s) to store the value(s). -if ($readonly) -{ - $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); - - // E.g. form field type tag sends $this->value as array - if ($multiple && is_array($value)) - { - if (!count($value)) - { - $value[] = ''; - } - - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} -else -// Create a regular list. +if ($readonly) { + $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); + + // E.g. form field type tag sends $this->value as array + if ($multiple && is_array($value)) { + if (!count($value)) { + $value[] = ''; + } + + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else // Create a regular list. { - $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); + $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/code/layouts/joomla/form/field/tel.php b/code/layouts/joomla/form/field/tel.php index ddf0735e..122b1e6d 100644 --- a/code/layouts/joomla/form/field/tel.php +++ b/code/layouts/joomla/form/field/tel.php @@ -1,4 +1,5 @@ - id="" - value="" - > + type="tel" + inputmode="tel" + name="" + + id="" + value="" + > diff --git a/code/layouts/joomla/form/field/text.php b/code/layouts/joomla/form/field/text.php index 300f2789..a01f4930 100644 --- a/code/layouts/joomla/form/field/text.php +++ b/code/layouts/joomla/form/field/text.php @@ -1,4 +1,5 @@ ' . Text::_($addonBefore) . ''; @@ -88,33 +88,33 @@
    - - - + + + - - > + + > - - - + + +
    - - - value) : ?> - - - - - + + + value) : ?> + + + + + diff --git a/code/layouts/joomla/form/field/textarea.php b/code/layouts/joomla/form/field/textarea.php index dcbd3f99..ceb57100 100644 --- a/code/layouts/joomla/form/field/textarea.php +++ b/code/layouts/joomla/form/field/textarea.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager(); - $wa->useScript('short-and-sweet'); +if ($charcounter) { + // Load the js file + /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + $wa->useScript('short-and-sweet'); - // Set the css class to be used as the trigger - $charcounter = ' charcount'; - // Set the text - $counterlabel = 'data-counter-label="' . $this->escape(Text::_('JFIELD_META_DESCRIPTION_COUNTER')) . '"'; + // Set the css class to be used as the trigger + $charcounter = ' charcount'; + // Set the text + $counterlabel = 'data-counter-label="' . $this->escape(Text::_('JFIELD_META_DESCRIPTION_COUNTER')) . '"'; } $attributes = array( - $columns ?: '', - $rows ?: '', - !empty($class) ? 'class="form-control ' . $class . $charcounter . '"' : 'class="form-control' . $charcounter . '"', - !empty($description) ? 'aria-describedby="' . ($id ?: $name) . '-desc"' : '', - strlen($hint) ? 'placeholder="' . htmlspecialchars($hint, ENT_COMPAT, 'UTF-8') . '"' : '', - $disabled ? 'disabled' : '', - $readonly ? 'readonly' : '', - $onchange ? 'onchange="' . $onchange . '"' : '', - $onclick ? 'onclick="' . $onclick . '"' : '', - $required ? 'required' : '', - !empty($autocomplete) ? 'autocomplete="' . $autocomplete . '"' : '', - $autofocus ? 'autofocus' : '', - $spellcheck ? '' : 'spellcheck="false"', - $maxlength ?: '', - !empty($counterlabel) ? $counterlabel : '', - $dataAttribute, + $columns ?: '', + $rows ?: '', + !empty($class) ? 'class="form-control ' . $class . $charcounter . '"' : 'class="form-control' . $charcounter . '"', + !empty($description) ? 'aria-describedby="' . ($id ?: $name) . '-desc"' : '', + strlen($hint) ? 'placeholder="' . htmlspecialchars($hint, ENT_COMPAT, 'UTF-8') . '"' : '', + $disabled ? 'disabled' : '', + $readonly ? 'readonly' : '', + $onchange ? 'onchange="' . $onchange . '"' : '', + $onclick ? 'onclick="' . $onclick . '"' : '', + $required ? 'required' : '', + !empty($autocomplete) ? 'autocomplete="' . $autocomplete . '"' : '', + $autofocus ? 'autofocus' : '', + $spellcheck ? '' : 'spellcheck="false"', + $maxlength ?: '', + !empty($counterlabel) ? $counterlabel : '', + $dataAttribute, ); ?> diff --git a/code/layouts/joomla/tinymce/togglebutton.php b/code/layouts/joomla/tinymce/togglebutton.php index 6b19fc1e..d963552b 100644 --- a/code/layouts/joomla/tinymce/togglebutton.php +++ b/code/layouts/joomla/tinymce/togglebutton.php @@ -1,4 +1,5 @@
    -
    - -
    +
    + +
    diff --git a/code/layouts/joomla/toolbar/base.php b/code/layouts/joomla/toolbar/base.php index b27e9209..dfba1abb 100644 --- a/code/layouts/joomla/toolbar/base.php +++ b/code/layouts/joomla/toolbar/base.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('core') - ->useScript('webcomponent.toolbar-button'); + ->useScript('core') + ->useScript('webcomponent.toolbar-button'); extract($displayData, EXTR_OVERWRITE); @@ -47,35 +48,31 @@ $validate = !empty($formValidation) ? ' form-validation' : ''; $msgAttr = !empty($message) ? ' confirm-message="' . $this->escape($message) . '"' : ''; -if ($id === 'toolbar-help') -{ - $title = ' title="' . Text::_('JGLOBAL_OPENS_IN_A_NEW_WINDOW') . '"'; +if ($id === 'toolbar-help') { + $title = ' title="' . Text::_('JGLOBAL_OPENS_IN_A_NEW_WINDOW') . '"'; } -if (!empty($task)) -{ - $taskAttr = ' task="' . $task . '"'; -} -elseif (!empty($onclick)) -{ - $htmlAttributes .= ' onclick="' . $onclick . '"'; +if (!empty($task)) { + $taskAttr = ' task="' . $task . '"'; +} elseif (!empty($onclick)) { + $htmlAttributes .= ' onclick="' . $onclick . '"'; } $direction = Factory::getLanguage()->isRtl() ? 'dropdown-menu-end' : ''; ?> > < - class="" - - - > - - + class="" + + + > + + > - - + + diff --git a/code/layouts/joomla/toolbar/batch.php b/code/layouts/joomla/toolbar/batch.php index f9cd16ed..a3d2cbc4 100644 --- a/code/layouts/joomla/toolbar/batch.php +++ b/code/layouts/joomla/toolbar/batch.php @@ -1,4 +1,5 @@ type="button" onclick="if (document.adminForm.boxchecked.value==0){}else{document.getElementById('collapseModal').open(); return true;}" class="btn btn-primary"> - - + + diff --git a/code/layouts/joomla/toolbar/containerclose.php b/code/layouts/joomla/toolbar/containerclose.php index 8c687c4a..aa9e8e5b 100644 --- a/code/layouts/joomla/toolbar/containerclose.php +++ b/code/layouts/joomla/toolbar/containerclose.php @@ -1,4 +1,5 @@ isRtl() ? 'dropdown-menu-end' : ''; +/** + * The dropdown class is also injected on the button from \Joomla\CMS\Toolbar\ToolbarButton::prepareOptions() and therefore we need the dropdown script whether we + * are in split toggle mode or not + */ +/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ +$wa = Factory::getApplication()->getDocument()->getWebAssetManager(); +$wa->useScript('bootstrap.dropdown'); + ?> + + + + + + + + diff --git a/code/layouts/joomla/toolbar/iconclass.php b/code/layouts/joomla/toolbar/iconclass.php index 28a101b0..4a26e574 100644 --- a/code/layouts/joomla/toolbar/iconclass.php +++ b/code/layouts/joomla/toolbar/iconclass.php @@ -1,4 +1,5 @@ getDocument() - ->getWebAssetManager()->useScript('inlinehelp'); + ->getWebAssetManager()->useScript('inlinehelp'); echo LayoutHelper::render('joomla.toolbar.standard', $displayData); diff --git a/code/layouts/joomla/toolbar/link.php b/code/layouts/joomla/toolbar/link.php index ff89f87f..6ee81013 100644 --- a/code/layouts/joomla/toolbar/link.php +++ b/code/layouts/joomla/toolbar/link.php @@ -1,4 +1,5 @@ - - > - - - + + > + + + diff --git a/code/layouts/joomla/toolbar/popup.php b/code/layouts/joomla/toolbar/popup.php index 25dcd392..3ee4f219 100644 --- a/code/layouts/joomla/toolbar/popup.php +++ b/code/layouts/joomla/toolbar/popup.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('core') - ->useScript('webcomponent.toolbar-button'); + ->useScript('core') + ->useScript('webcomponent.toolbar-button'); $tagName = $tagName ?? 'button'; @@ -43,12 +44,12 @@ ?> > < - value="" - class="" - - + value="" + class="" + + > - - + + > diff --git a/code/layouts/joomla/toolbar/separator.php b/code/layouts/joomla/toolbar/separator.php index dc5970af..2adc1fc2 100644 --- a/code/layouts/joomla/toolbar/separator.php +++ b/code/layouts/joomla/toolbar/separator.php @@ -1,4 +1,5 @@ - - - - - - + + + + + + diff --git a/code/layouts/joomla/toolbar/standard.php b/code/layouts/joomla/toolbar/standard.php index f7dbff43..a7d18c83 100644 --- a/code/layouts/joomla/toolbar/standard.php +++ b/code/layouts/joomla/toolbar/standard.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('core') - ->useScript('webcomponent.toolbar-button'); + ->useScript('core') + ->useScript('webcomponent.toolbar-button'); $tagName = $tagName ?? 'button'; @@ -43,13 +44,10 @@ $validate = !empty($formValidation) ? ' form-validation' : ''; $msgAttr = !empty($message) ? ' confirm-message="' . $this->escape($message) . '"' : ''; -if (!empty($task)) -{ - $taskAttr = ' task="' . $task . '"'; -} -elseif (!empty($onclick)) -{ - $htmlAttributes .= ' onclick="' . $onclick . '"'; +if (!empty($task)) { + $taskAttr = ' task="' . $task . '"'; +} elseif (!empty($onclick)) { + $htmlAttributes .= ' onclick="' . $onclick . '"'; } ?> @@ -57,16 +55,16 @@ > - - + + < - class="" - - > - - + class="" + + > + + > diff --git a/code/layouts/joomla/toolbar/title.php b/code/layouts/joomla/toolbar/title.php index ad2c9a60..2f2fb21c 100644 --- a/code/layouts/joomla/toolbar/title.php +++ b/code/layouts/joomla/toolbar/title.php @@ -1,4 +1,5 @@

    - $icon]); ?> - + $icon]); ?> +

    diff --git a/code/layouts/joomla/toolbar/versions.php b/code/layouts/joomla/toolbar/versions.php index 15d47b41..68a56dd7 100644 --- a/code/layouts/joomla/toolbar/versions.php +++ b/code/layouts/joomla/toolbar/versions.php @@ -1,4 +1,5 @@ getRegistry()->addExtensionRegistryFile('com_contenthistory'); $wa->useScript('core') - ->useScript('webcomponent.toolbar-button') - ->useScript('com_contenthistory.admin-history-versions'); + ->useScript('webcomponent.toolbar-button') + ->useScript('com_contenthistory.admin-history-versions'); echo HTMLHelper::_( - 'bootstrap.renderModal', - 'versionsModal', - array( - 'url' => 'index.php?' . http_build_query( - [ - 'option' => 'com_contenthistory', - 'view' => 'history', - 'layout' => 'modal', - 'tmpl' => 'component', - 'item_id' => $itemId, - Session::getFormToken() => 1 - ] - ), - 'title' => $title, - 'height' => '100%', - 'width' => '100%', - 'modalWidth' => '80', - 'bodyHeight' => '60', - 'footer' => '' - ) + 'bootstrap.renderModal', + 'versionsModal', + array( + 'url' => 'index.php?' . http_build_query( + [ + 'option' => 'com_contenthistory', + 'view' => 'history', + 'layout' => 'modal', + 'tmpl' => 'component', + 'item_id' => $itemId, + Session::getFormToken() => 1 + ] + ), + 'title' => $title, + 'height' => '100%', + 'width' => '100%', + 'modalWidth' => '80', + 'bodyHeight' => '60', + 'footer' => '' + ) ); ?> - + diff --git a/code/layouts/libraries/html/bootstrap/modal/body.php b/code/layouts/libraries/html/bootstrap/modal/body.php index 26dc032f..9bac638c 100644 --- a/code/layouts/libraries/html/bootstrap/modal/body.php +++ b/code/layouts/libraries/html/bootstrap/modal/body.php @@ -1,4 +1,5 @@ = 20 && $bodyHeight < 90) -{ - $bodyClass .= ' jviewport-height' . $bodyHeight; +if ($bodyHeight && $bodyHeight >= 20 && $bodyHeight < 90) { + $bodyClass .= ' jviewport-height' . $bodyHeight; } ?>
    - +
    diff --git a/code/layouts/libraries/html/bootstrap/modal/footer.php b/code/layouts/libraries/html/bootstrap/modal/footer.php index 78f11f45..8dd0f62c 100644 --- a/code/layouts/libraries/html/bootstrap/modal/footer.php +++ b/code/layouts/libraries/html/bootstrap/modal/footer.php @@ -1,4 +1,5 @@ diff --git a/code/layouts/libraries/html/bootstrap/modal/header.php b/code/layouts/libraries/html/bootstrap/modal/header.php index 61d7c726..0b039452 100644 --- a/code/layouts/libraries/html/bootstrap/modal/header.php +++ b/code/layouts/libraries/html/bootstrap/modal/header.php @@ -1,4 +1,5 @@ diff --git a/code/layouts/libraries/html/bootstrap/modal/iframe.php b/code/layouts/libraries/html/bootstrap/modal/iframe.php index 468b04ce..db4911b4 100644 --- a/code/layouts/libraries/html/bootstrap/modal/iframe.php +++ b/code/layouts/libraries/html/bootstrap/modal/iframe.php @@ -1,4 +1,5 @@ 'iframe', - 'src' => $params['url'] + 'class' => 'iframe', + 'src' => $params['url'] ); -if (isset($params['title'])) -{ - $iframeAttributes['name'] = addslashes($params['title']); - $iframeAttributes['title'] = addslashes($params['title']); +if (isset($params['title'])) { + $iframeAttributes['name'] = addslashes($params['title']); + $iframeAttributes['title'] = addslashes($params['title']); } -if (isset($params['height'])) -{ - $iframeAttributes['height'] = $params['height']; +if (isset($params['height'])) { + $iframeAttributes['height'] = $params['height']; } -if (isset($params['width'])) -{ - $iframeAttributes['width'] = $params['width']; +if (isset($params['width'])) { + $iframeAttributes['width'] = $params['width']; } ?> diff --git a/code/layouts/libraries/html/bootstrap/modal/main.php b/code/layouts/libraries/html/bootstrap/modal/main.php index 3cdbabb0..2f0fe32b 100644 --- a/code/layouts/libraries/html/bootstrap/modal/main.php +++ b/code/layouts/libraries/html/bootstrap/modal/main.php @@ -1,4 +1,5 @@ 0 && $modalWidth <= 100) -{ - $modalDialogClass = ' jviewport-width' . $modalWidth; +if ($modalWidth && $modalWidth > 0 && $modalWidth <= 100) { + $modalDialogClass = ' jviewport-width' . $modalWidth; } $modalAttributes = array( - 'tabindex' => '-1', - 'class' => 'joomla-modal ' .implode(' ', $modalClasses) + 'tabindex' => '-1', + 'class' => 'joomla-modal ' . implode(' ', $modalClasses) ); -if (isset($params['backdrop'])) -{ - $modalAttributes['data-bs-backdrop'] = (is_bool($params['backdrop']) ? ($params['backdrop'] ? 'true' : 'false') : $params['backdrop']); +if (isset($params['backdrop'])) { + $modalAttributes['data-bs-backdrop'] = (is_bool($params['backdrop']) ? ($params['backdrop'] ? 'true' : 'false') : $params['backdrop']); } -if (isset($params['keyboard'])) -{ - $modalAttributes['data-bs-keyboard'] = (is_bool($params['keyboard']) ? ($params['keyboard'] ? 'true' : 'false') : 'true'); +if (isset($params['keyboard'])) { + $modalAttributes['data-bs-keyboard'] = (is_bool($params['keyboard']) ? ($params['keyboard'] ? 'true' : 'false') : 'true'); } -if (isset($params['url'])) -{ - $url = 'data-url="' . $params['url'] . '"'; - $iframeHtml = htmlspecialchars(LayoutHelper::render('libraries.html.bootstrap.modal.iframe', $displayData), ENT_COMPAT, 'UTF-8'); +if (isset($params['url'])) { + $url = 'data-url="' . $params['url'] . '"'; + $iframeHtml = htmlspecialchars(LayoutHelper::render('libraries.html.bootstrap.modal.iframe', $displayData), ENT_COMPAT, 'UTF-8'); } ?> - diff --git a/code/layouts/libraries/html/bootstrap/tab/addtab.php b/code/layouts/libraries/html/bootstrap/tab/addtab.php index 286b32bc..a882acda 100644 --- a/code/layouts/libraries/html/bootstrap/tab/addtab.php +++ b/code/layouts/libraries/html/bootstrap/tab/addtab.php @@ -1,4 +1,5 @@
    + class="tab-pane" + data-active="" + data-id="" + data-title=""> diff --git a/code/layouts/libraries/html/bootstrap/tab/endtab.php b/code/layouts/libraries/html/bootstrap/tab/endtab.php index 84bdf84d..4b21cdf6 100644 --- a/code/layouts/libraries/html/bootstrap/tab/endtab.php +++ b/code/layouts/libraries/html/bootstrap/tab/endtab.php @@ -1,4 +1,5 @@ getWebAssetManager(); $wa->registerScript('tinymce', 'media/vendor/tinymce/tinymce.min.js', [], ['defer' => true]) - ->registerScript('plg_editors_tinymce', 'plg_editors_tinymce/tinymce.min.js', [], ['defer' => true], ['core', 'tinymce']) - ->registerAndUseStyle('tinymce.skin', 'media/vendor/tinymce/skins/ui/oxide/skin.min.css') - ->registerAndUseStyle('plg_editors_tinymce.builder', 'plg_editors_tinymce/tinymce-builder.css', [], [], ['tinymce.skin', 'dragula']) - ->registerScript('plg_editors_tinymce.builder', 'plg_editors_tinymce/tinymce-builder.js', [], ['type' => 'module'], ['dragula', 'plg_editors_tinymce']) - ->useScript('plg_editors_tinymce.builder') - ->useStyle('webcomponent.joomla-tab') - ->useScript('webcomponent.joomla-tab'); + ->registerScript('plg_editors_tinymce', 'plg_editors_tinymce/tinymce.min.js', [], ['defer' => true], ['core', 'tinymce']) + ->registerAndUseStyle('tinymce.skin', 'media/vendor/tinymce/skins/ui/oxide/skin.min.css') + ->registerAndUseStyle('plg_editors_tinymce.builder', 'plg_editors_tinymce/tinymce-builder.css', [], [], ['tinymce.skin', 'dragula']) + ->registerScript('plg_editors_tinymce.builder', 'plg_editors_tinymce/tinymce-builder.js', [], ['type' => 'module'], ['dragula', 'plg_editors_tinymce']) + ->useScript('plg_editors_tinymce.builder') + ->useStyle('webcomponent.joomla-tab') + ->useScript('webcomponent.joomla-tab'); // Add TinyMCE language file to translate the buttons -if ($languageFile) -{ - $wa->registerAndUseScript('tinymce.language', $languageFile, [], ['defer' => true], []); +if ($languageFile) { + $wa->registerAndUseScript('tinymce.language', $languageFile, [], ['defer' => true], []); } // Add the builder options -$doc->addScriptOptions('plg_editors_tinymce_builder', - [ - 'menus' => $menus, - 'buttons' => $buttons, - 'toolbarPreset' => $toolbarPreset, - 'formControl' => $name . '[toolbars]', - ] +$doc->addScriptOptions( + 'plg_editors_tinymce_builder', + [ + 'menus' => $menus, + 'buttons' => $buttons, + 'toolbarPreset' => $toolbarPreset, + 'formControl' => $name . '[toolbars]', + ] ); ?>
    -

    -

    -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - $title) : ?> - - name=""> - - 'btn-success', - 'medium' => 'btn-info', - 'advanced' => 'btn-warning', - ]; - // Check whether the values exists, and if empty then use from preset - if (empty($value['toolbars'][$num]['menu']) - && empty($value['toolbars'][$num]['toolbar1']) - && empty($value['toolbars'][$num]['toolbar2'])) - { - // Take the preset for default value - switch ($num) { - case 0: - $preset = $toolbarPreset['advanced']; - break; - case 1: - $preset = $toolbarPreset['medium']; - break; - default: - $preset = $toolbarPreset['simple']; - } - - $value['toolbars'][$num] = $preset; - } - - // Take existing values - $valMenu = empty($value['toolbars'][$num]['menu']) ? array() : $value['toolbars'][$num]['menu']; - $valBar1 = empty($value['toolbars'][$num]['toolbar1']) ? array() : $value['toolbars'][$num]['toolbar1']; - $valBar2 = empty($value['toolbars'][$num]['toolbar2']) ? array() : $value['toolbars'][$num]['toolbar2']; - - ?> - sublayout('setaccess', array('form' => $setsForms[$num])); ?> -
    -
    - - - - - - -
    -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - sublayout('setoptions', array('form' => $setsForms[$num])); ?> -
    - -
    +

    +

    +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + $title) : ?> + + name=""> + + 'btn-success', + 'medium' => 'btn-info', + 'advanced' => 'btn-warning', + ]; + // Check whether the values exists, and if empty then use from preset + if ( + empty($value['toolbars'][$num]['menu']) + && empty($value['toolbars'][$num]['toolbar1']) + && empty($value['toolbars'][$num]['toolbar2']) + ) { + // Take the preset for default value + switch ($num) { + case 0: + $preset = $toolbarPreset['advanced']; + break; + case 1: + $preset = $toolbarPreset['medium']; + break; + default: + $preset = $toolbarPreset['simple']; + } + + $value['toolbars'][$num] = $preset; + } + + // Take existing values + $valMenu = empty($value['toolbars'][$num]['menu']) ? array() : $value['toolbars'][$num]['menu']; + $valBar1 = empty($value['toolbars'][$num]['toolbar1']) ? array() : $value['toolbars'][$num]['toolbar1']; + $valBar2 = empty($value['toolbars'][$num]['toolbar2']) ? array() : $value['toolbars'][$num]['toolbar2']; + + ?> + sublayout('setaccess', array('form' => $setsForms[$num])); ?> +
    +
    + + + + + + +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + sublayout('setoptions', array('form' => $setsForms[$num])); ?> +
    + +
    diff --git a/code/layouts/plugins/editors/tinymce/field/tinymcebuilder/setaccess.php b/code/layouts/plugins/editors/tinymce/field/tinymcebuilder/setaccess.php index 17730067..c4be4b9e 100644 --- a/code/layouts/plugins/editors/tinymce/field/tinymcebuilder/setaccess.php +++ b/code/layouts/plugins/editors/tinymce/field/tinymcebuilder/setaccess.php @@ -1,4 +1,5 @@
    - renderField('access'); ?> + renderField('access'); ?>
    diff --git a/code/layouts/plugins/editors/tinymce/field/tinymcebuilder/setoptions.php b/code/layouts/plugins/editors/tinymce/field/tinymcebuilder/setoptions.php index d3df455a..4b51137d 100644 --- a/code/layouts/plugins/editors/tinymce/field/tinymcebuilder/setoptions.php +++ b/code/layouts/plugins/editors/tinymce/field/tinymcebuilder/setoptions.php @@ -1,4 +1,5 @@
    getFieldset('basic') as $field) : ?> - renderField(); ?> + renderField(); ?>
    diff --git a/code/layouts/plugins/system/privacyconsent/label.php b/code/layouts/plugins/system/privacyconsent/label.php index 655f1190..394f10f7 100644 --- a/code/layouts/plugins/system/privacyconsent/label.php +++ b/code/layouts/plugins/system/privacyconsent/label.php @@ -1,4 +1,5 @@ 'modal', - 'data-bs-target' => '#consentModal', - 'class' => 'required', - ]; +if ($privacyLink) { + $attribs = [ + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#consentModal', + 'class' => 'required', + ]; - $link = HTMLHelper::_('link', Route::_($privacyLink . '&tmpl=component'), $text, $attribs); + $link = HTMLHelper::_('link', Route::_($privacyLink . '&tmpl=component'), $text, $attribs); - echo HTMLHelper::_( - 'bootstrap.renderModal', - 'consentModal', - [ - 'url' => Route::_($privacyLink . '&tmpl=component'), - 'title' => $text, - 'height' => '100%', - 'width' => '100%', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ] - ); -} -else -{ - $link = '' . $text . ''; + echo HTMLHelper::_( + 'bootstrap.renderModal', + 'consentModal', + [ + 'url' => Route::_($privacyLink . '&tmpl=component'), + 'title' => $text, + 'height' => '100%', + 'width' => '100%', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ] + ); +} else { + $link = '' . $text . ''; } // Add the label text and star. diff --git a/code/layouts/plugins/system/privacyconsent/message.php b/code/layouts/plugins/system/privacyconsent/message.php index a9b10704..d9c991e4 100644 --- a/code/layouts/plugins/system/privacyconsent/message.php +++ b/code/layouts/plugins/system/privacyconsent/message.php @@ -1,4 +1,5 @@ ' . $privacynote . '
    '; - diff --git a/code/layouts/plugins/system/webauthn/manage.php b/code/layouts/plugins/system/webauthn/manage.php index 99c3180e..232424f3 100644 --- a/code/layouts/plugins/system/webauthn/manage.php +++ b/code/layouts/plugins/system/webauthn/manage.php @@ -1,4 +1,5 @@ getIdentity(); +try { + $app = Factory::getApplication(); + $loggedInUser = $app->getIdentity(); - $app->getDocument()->getWebAssetManager() - ->registerAndUseStyle('plg_system_webauthn.backend', 'plg_system_webauthn/backend.css'); -} -catch (Exception $e) -{ - $loggedInUser = new User; + $app->getDocument()->getWebAssetManager() + ->registerAndUseStyle('plg_system_webauthn.backend', 'plg_system_webauthn/backend.css'); +} catch (Exception $e) { + $loggedInUser = new User(); } $defaultDisplayData = [ - 'user' => $loggedInUser, - 'allow_add' => false, - 'credentials' => [], - 'error' => '', + 'user' => $loggedInUser, + 'allow_add' => false, + 'credentials' => [], + 'error' => '', + 'knownAuthenticators' => [], + 'attestationSupport' => true, ]; extract(array_merge($defaultDisplayData, $displayData)); -if ($displayData['allow_add'] === false) -{ - $error = Text::_('PLG_SYSTEM_WEBAUTHN_CANNOT_ADD_FOR_A_USER'); - $allow_add = false; +if ($displayData['allow_add'] === false) { + $error = Text::_('PLG_SYSTEM_WEBAUTHN_CANNOT_ADD_FOR_A_USER'); + $allow_add = false; } // Ensure the GMP or BCmath extension is loaded in PHP - as this is required by third party library -if ($allow_add && function_exists('gmp_intval') === false && function_exists('bccomp') === false) -{ - $error = Text::_('PLG_SYSTEM_WEBAUTHN_REQUIRES_GMP'); - $allow_add = false; +if ($allow_add && function_exists('gmp_intval') === false && function_exists('bccomp') === false) { + $error = Text::_('PLG_SYSTEM_WEBAUTHN_REQUIRES_GMP'); + $allow_add = false; } -/** - * Why not push these configuration variables directly to JavaScript? - * - * We need to reload them every time we return from an attempt to authorize an authenticator. Whenever that - * happens we push raw HTML to the page. However, any SCRIPT tags in that HTML do not get parsed, i.e. they - * do not replace existing values. This causes any retries to fail. By using a data storage object we circumvent - * that problem. - */ -$randomId = 'plg_system_webauthn_' . UserHelper::genRandomPassword(32); -// phpcs:ignore -$publicKey = $allow_add ? base64_encode(CredentialsCreation::createPublicKey($user)) : '{}'; -$postbackURL = base64_encode(rtrim(Uri::base(), '/') . '/index.php?' . Joomla::getToken() . '=1'); +Text::script('JGLOBAL_CONFIRM_DELETE'); +HTMLHelper::_('bootstrap.tooltip', '.plg_system_webauth-has-tooltip'); ?>
    - - - -
    - -
    - + +
    + +
    + - - - - - - - - - - - - - - - - - - - - - -
    - , -
    - - -
    - -
    + + + + + + + + + + + + getAaguid() : ''; + $authMetadata = $knownAuthenticators[$aaguid->toString()] ?? $knownAuthenticators['']; + ?> + + + + + + + + + + + + +
    + , +
    colspan="2" scope="col"> + +
    + <?php echo $authMetadata->description ?> + + + +
    + +
    - -

    - -

    - + +

    + +

    +
    diff --git a/code/layouts/plugins/user/profile/fields/dob.php b/code/layouts/plugins/user/profile/fields/dob.php deleted file mode 100644 index 2aa0f0e4..00000000 --- a/code/layouts/plugins/user/profile/fields/dob.php +++ /dev/null @@ -1,24 +0,0 @@ - - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -defined('_JEXEC') or die; - -extract($displayData); - -/** - * Layout variables - * ----------------- - * @var string $text infotext to be displayed - */ - -// Closing the opening .control-group and .control-label div so we can add our info text on own line ?> - -
    - -
    diff --git a/code/layouts/plugins/user/terms/label.php b/code/layouts/plugins/user/terms/label.php index 1b4d3278..61223ffe 100644 --- a/code/layouts/plugins/user/terms/label.php +++ b/code/layouts/plugins/user/terms/label.php @@ -1,4 +1,5 @@ 'modal', - 'data-bs-target' => '#tosModal', - 'class' => 'required', - ]; +if ($article) { + $attribs = [ + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#tosModal', + 'class' => 'required', + ]; - $link = HTMLHelper::_('link', Route::_($article->link . '&tmpl=component'), $text, $attribs); + $link = HTMLHelper::_('link', Route::_($article->link . '&tmpl=component'), $text, $attribs); - echo HTMLHelper::_( - 'bootstrap.renderModal', - 'tosModal', - [ - 'url' => Route::_($article->link . '&tmpl=component'), - 'title' => $text, - 'height' => '100%', - 'width' => '100%', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ] - ); -} -else -{ - $link = '' . $text . ''; + echo HTMLHelper::_( + 'bootstrap.renderModal', + 'tosModal', + [ + 'url' => Route::_($article->link . '&tmpl=component'), + 'title' => $text, + 'height' => '100%', + 'width' => '100%', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ] + ); +} else { + $link = '' . $text . ''; } // Add the label text and star. diff --git a/code/layouts/plugins/user/terms/message.php b/code/layouts/plugins/user/terms/message.php index e6284a23..f28f1c34 100644 --- a/code/layouts/plugins/user/terms/message.php +++ b/code/layouts/plugins/user/terms/message.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager() - ->registerAndUseScript('plg_user_token.token', 'plg_user_token/token.js', [], ['defer' => true], ['core']); + ->registerAndUseScript('plg_user_token.token', 'plg_user_token/token.js', [], ['defer' => true], ['core']); ?>
    - - + +
    diff --git a/code/libraries/bootstrap.php b/code/libraries/bootstrap.php index 54a59760..6f9f7491 100644 --- a/code/libraries/bootstrap.php +++ b/code/libraries/bootstrap.php @@ -1,4 +1,5 @@ withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor) + $behavior->withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor()) ); -if (in_array('phar', stream_get_wrappers())) -{ - stream_wrapper_unregister('phar'); - stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); +if (in_array('phar', stream_get_wrappers())) { + stream_wrapper_unregister('phar'); + stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); } // Define the Joomla version if not already defined. -defined('JVERSION') or define('JVERSION', (new \Joomla\CMS\Version)->getShortVersion()); +defined('JVERSION') or define('JVERSION', (new \Joomla\CMS\Version())->getShortVersion()); // Set up the message queue logger for web requests -if (array_key_exists('REQUEST_METHOD', $_SERVER)) -{ - \Joomla\CMS\Log\Log::addLogger(['logger' => 'messagequeue'], \Joomla\CMS\Log\Log::ALL, ['jerror']); +if (array_key_exists('REQUEST_METHOD', $_SERVER)) { + \Joomla\CMS\Log\Log::addLogger(['logger' => 'messagequeue'], \Joomla\CMS\Log\Log::ALL, ['jerror']); } // Register the Crypto lib diff --git a/code/libraries/classmap.php b/code/libraries/classmap.php index 1b43fa43..5b203ff2 100644 --- a/code/libraries/classmap.php +++ b/code/libraries/classmap.php @@ -1,4 +1,5 @@ withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor) + $behavior->withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor()) ); -if (in_array('phar', stream_get_wrappers())) -{ - stream_wrapper_unregister('phar'); - stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); +if (in_array('phar', stream_get_wrappers())) { + stream_wrapper_unregister('phar'); + stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); } // Define the Joomla version if not already defined -if (!defined('JVERSION')) -{ - define('JVERSION', (new \Joomla\CMS\Version)->getShortVersion()); +if (!defined('JVERSION')) { + define('JVERSION', (new \Joomla\CMS\Version())->getShortVersion()); } // Register a handler for uncaught exceptions that shows a pretty error page when possible set_exception_handler(array('Joomla\CMS\Exception\ExceptionHandler', 'handleException')); // Set up the message queue logger for web requests -if (array_key_exists('REQUEST_METHOD', $_SERVER)) -{ - \Joomla\CMS\Log\Log::addLogger(array('logger' => 'messagequeue'), \Joomla\CMS\Log\Log::ALL, ['jerror']); +if (array_key_exists('REQUEST_METHOD', $_SERVER)) { + \Joomla\CMS\Log\Log::addLogger(array('logger' => 'messagequeue'), \Joomla\CMS\Log\Log::ALL, ['jerror']); } // Register the Crypto lib diff --git a/code/libraries/extensions.classmap.php b/code/libraries/extensions.classmap.php index fd57dc93..efbd76a2 100644 --- a/code/libraries/extensions.classmap.php +++ b/code/libraries/extensions.classmap.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ defined('JPATH_PLATFORM') or die; @@ -15,761 +18,688 @@ */ abstract class JLoader { - /** - * Container for already imported library paths. - * - * @var array - * @since 1.7.0 - */ - protected static $classes = array(); - - /** - * Container for already imported library paths. - * - * @var array - * @since 1.7.0 - */ - protected static $imported = array(); - - /** - * Container for registered library class prefixes and path lookups. - * - * @var array - * @since 3.0.0 - */ - protected static $prefixes = array(); - - /** - * Holds proxy classes and the class names the proxy. - * - * @var array - * @since 3.2 - */ - protected static $classAliases = array(); - - /** - * Holds the inverse lookup for proxy classes and the class names the proxy. - * - * @var array - * @since 3.4 - */ - protected static $classAliasesInverse = array(); - - /** - * Container for namespace => path map. - * - * @var array - * @since 3.1.4 - */ - protected static $namespaces = array(); - - /** - * Holds a reference for all deprecated aliases (mainly for use by a logging platform). - * - * @var array - * @since 3.6.3 - */ - protected static $deprecatedAliases = array(); - - /** - * The root folders where extensions can be found. - * - * @var array - * @since 4.0.0 - */ - protected static $extensionRootFolders = array(); - - /** - * Method to discover classes of a given type in a given path. - * - * @param string $classPrefix The class name prefix to use for discovery. - * @param string $parentPath Full path to the parent folder for the classes to discover. - * @param boolean $force True to overwrite the autoload path value for the class if it already exists. - * @param boolean $recurse Recurse through all child directories as well as the parent path. - * - * @return void - * - * @since 1.7.0 - * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for - * your files. - */ - public static function discover($classPrefix, $parentPath, $force = true, $recurse = false) - { - try - { - if ($recurse) - { - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($parentPath), - RecursiveIteratorIterator::SELF_FIRST - ); - } - else - { - $iterator = new DirectoryIterator($parentPath); - } - - /** @type $file DirectoryIterator */ - foreach ($iterator as $file) - { - $fileName = $file->getFilename(); - - // Only load for php files. - if ($file->isFile() && $file->getExtension() === 'php') - { - // Get the class name and full path for each file. - $class = strtolower($classPrefix . preg_replace('#\.php$#', '', $fileName)); - - // Register the class with the autoloader if not already registered or the force flag is set. - if ($force || empty(self::$classes[$class])) - { - self::register($class, $file->getPath() . '/' . $fileName); - } - } - } - } - catch (UnexpectedValueException $e) - { - // Exception will be thrown if the path is not a directory. Ignore it. - } - } - - /** - * Method to get the list of registered classes and their respective file paths for the autoloader. - * - * @return array The array of class => path values for the autoloader. - * - * @since 1.7.0 - */ - public static function getClassList() - { - return self::$classes; - } - - /** - * Method to get the list of deprecated class aliases. - * - * @return array An associative array with deprecated class alias data. - * - * @since 3.6.3 - */ - public static function getDeprecatedAliases() - { - return self::$deprecatedAliases; - } - - /** - * Method to get the list of registered namespaces. - * - * @return array The array of namespace => path values for the autoloader. - * - * @since 3.1.4 - */ - public static function getNamespaces() - { - return self::$namespaces; - } - - /** - * Loads a class from specified directories. - * - * @param string $key The class name to look for (dot notation). - * @param string $base Search this directory for the class. - * - * @return boolean True on success. - * - * @since 1.7.0 - * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for - * your files. - */ - public static function import($key, $base = null) - { - // Only import the library if not already attempted. - if (!isset(self::$imported[$key])) - { - // Setup some variables. - $success = false; - $parts = explode('.', $key); - $class = array_pop($parts); - $base = (!empty($base)) ? $base : __DIR__; - $path = str_replace('.', DIRECTORY_SEPARATOR, $key); - - // Handle special case for helper classes. - if ($class === 'helper') - { - $class = ucfirst(array_pop($parts)) . ucfirst($class); - } - // Standard class. - else - { - $class = ucfirst($class); - } - - // If we are importing a library from the Joomla namespace set the class to autoload. - if (strpos($path, 'joomla') === 0) - { - // Since we are in the Joomla namespace prepend the classname with J. - $class = 'J' . $class; - - // Only register the class for autoloading if the file exists. - if (is_file($base . '/' . $path . '.php')) - { - self::$classes[strtolower($class)] = $base . '/' . $path . '.php'; - $success = true; - } - } - /* - * If we are not importing a library from the Joomla namespace directly include the - * file since we cannot assert the file/folder naming conventions. - */ - else - { - // If the file exists attempt to include it. - if (is_file($base . '/' . $path . '.php')) - { - $success = (bool) include_once $base . '/' . $path . '.php'; - } - } - - // Add the import key to the memory cache container. - self::$imported[$key] = $success; - } - - return self::$imported[$key]; - } - - /** - * Load the file for a class. - * - * @param string $class The class to be loaded. - * - * @return boolean True on success - * - * @since 1.7.0 - */ - public static function load($class) - { - // Sanitize class name. - $key = strtolower($class); - - // If the class already exists do nothing. - if (class_exists($class, false)) - { - return true; - } - - // If the class is registered include the file. - if (isset(self::$classes[$key])) - { - $found = (bool) include_once self::$classes[$key]; - - if ($found) - { - self::loadAliasFor($class); - } - - // If the class doesn't exists, we probably have a class alias available - if (!class_exists($class, false)) - { - // Search the alias class, first none namespaced and then namespaced - $original = array_search($class, self::$classAliases) ? : array_search('\\' . $class, self::$classAliases); - - // When we have an original and the class exists an alias should be created - if ($original && class_exists($original, false)) - { - class_alias($original, $class); - } - } - - return true; - } - - return false; - } - - /** - * Directly register a class to the autoload list. - * - * @param string $class The class name to register. - * @param string $path Full path to the file that holds the class to register. - * @param boolean $force True to overwrite the autoload path value for the class if it already exists. - * - * @return void - * - * @since 1.7.0 - * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for - * your files. - */ - public static function register($class, $path, $force = true) - { - // When an alias exists, register it as well - if (array_key_exists(strtolower($class), self::$classAliases)) - { - self::register(self::stripFirstBackslash(self::$classAliases[strtolower($class)]), $path, $force); - } - - // Sanitize class name. - $class = strtolower($class); - - // Only attempt to register the class if the name and file exist. - if (!empty($class) && is_file($path)) - { - // Register the class with the autoloader if not already registered or the force flag is set. - if ($force || empty(self::$classes[$class])) - { - self::$classes[$class] = $path; - } - } - } - - /** - * Register a class prefix with lookup path. This will allow developers to register library - * packages with different class prefixes to the system autoloader. More than one lookup path - * may be registered for the same class prefix, but if this method is called with the reset flag - * set to true then any registered lookups for the given prefix will be overwritten with the current - * lookup path. When loaded, prefix paths are searched in a "last in, first out" order. - * - * @param string $prefix The class prefix to register. - * @param string $path Absolute file path to the library root where classes with the given prefix can be found. - * @param boolean $reset True to reset the prefix with only the given lookup path. - * @param boolean $prepend If true, push the path to the beginning of the prefix lookup paths array. - * - * @return void - * - * @throws RuntimeException - * - * @since 3.0.0 - */ - public static function registerPrefix($prefix, $path, $reset = false, $prepend = false) - { - // Verify the library path exists. - if (!is_dir($path)) - { - $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); - - throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); - } - - // If the prefix is not yet registered or we have an explicit reset flag then set set the path. - if ($reset || !isset(self::$prefixes[$prefix])) - { - self::$prefixes[$prefix] = array($path); - } - // Otherwise we want to simply add the path to the prefix. - else - { - if ($prepend) - { - array_unshift(self::$prefixes[$prefix], $path); - } - else - { - self::$prefixes[$prefix][] = $path; - } - } - } - - /** - * Offers the ability for "just in time" usage of `class_alias()`. - * You cannot overwrite an existing alias. - * - * @param string $alias The alias name to register. - * @param string $original The original class to alias. - * @param string|boolean $version The version in which the alias will no longer be present. - * - * @return boolean True if registration was successful. False if the alias already exists. - * - * @since 3.2 - */ - public static function registerAlias($alias, $original, $version = false) - { - // PHP is case insensitive so support all kind of alias combination - $lowercasedAlias = strtolower($alias); - - if (!isset(self::$classAliases[$lowercasedAlias])) - { - self::$classAliases[$lowercasedAlias] = $original; - - $original = self::stripFirstBackslash($original); - - if (!isset(self::$classAliasesInverse[$original])) - { - self::$classAliasesInverse[$original] = array($lowercasedAlias); - } - else - { - self::$classAliasesInverse[$original][] = $lowercasedAlias; - } - - // If given a version, log this alias as deprecated - if ($version) - { - self::$deprecatedAliases[] = array('old' => $alias, 'new' => $original, 'version' => $version); - } - - return true; - } - - return false; - } - - /** - * Register a namespace to the autoloader. When loaded, namespace paths are searched in a "last in, first out" order. - * - * @param string $namespace A case sensitive Namespace to register. - * @param string $path A case sensitive absolute file path to the library root where classes of the given namespace can be found. - * @param boolean $reset True to reset the namespace with only the given lookup path. - * @param boolean $prepend If true, push the path to the beginning of the namespace lookup paths array. - * - * @return void - * - * @throws RuntimeException - * - * @since 3.1.4 - */ - public static function registerNamespace($namespace, $path, $reset = false, $prepend = false) - { - // Verify the library path exists. - if (!is_dir($path)) - { - $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); - - throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); - } - - // Trim leading and trailing backslashes from namespace, allowing "\Parent\Child", "Parent\Child\" and "\Parent\Child\" to be treated the same way. - $namespace = trim($namespace, '\\'); - - // If the namespace is not yet registered or we have an explicit reset flag then set the path. - if ($reset || !isset(self::$namespaces[$namespace])) - { - self::$namespaces[$namespace] = array($path); - } - - // Otherwise we want to simply add the path to the namespace. - else - { - if ($prepend) - { - array_unshift(self::$namespaces[$namespace], $path); - } - else - { - self::$namespaces[$namespace][] = $path; - } - } - } - - /** - * Method to setup the autoloaders for the Joomla Platform. - * Since the SPL autoloaders are called in a queue we will add our explicit - * class-registration based loader first, then fall back on the autoloader based on conventions. - * This will allow people to register a class in a specific location and override platform libraries - * as was previously possible. - * - * @param boolean $enablePsr True to enable autoloading based on PSR-0. - * @param boolean $enablePrefixes True to enable prefix based class loading (needed to auto load the Joomla core). - * @param boolean $enableClasses True to enable class map based class loading (needed to auto load the Joomla core). - * - * @return void - * - * @since 3.1.4 - */ - public static function setup($enablePsr = true, $enablePrefixes = true, $enableClasses = true) - { - if ($enableClasses) - { - // Register the class map based autoloader. - spl_autoload_register(array('JLoader', 'load')); - } - - if ($enablePrefixes) - { - // Register the prefix autoloader. - spl_autoload_register(array('JLoader', '_autoload')); - } - - if ($enablePsr) - { - // Register the PSR based autoloader. - spl_autoload_register(array('JLoader', 'loadByPsr')); - spl_autoload_register(array('JLoader', 'loadByAlias')); - } - } - - /** - * Method to autoload classes that are namespaced to the PSR-4 standard. - * - * @param string $class The fully qualified class name to autoload. - * - * @return boolean True on success, false otherwise. - * - * @since 3.7.0 - * @deprecated 5.0 Use JLoader::loadByPsr instead - */ - public static function loadByPsr4($class) - { - return self::loadByPsr($class); - } - - /** - * Method to autoload classes that are namespaced to the PSR-4 standard. - * - * @param string $class The fully qualified class name to autoload. - * - * @return boolean True on success, false otherwise. - * - * @since 4.0.0 - */ - public static function loadByPsr($class) - { - $class = self::stripFirstBackslash($class); - - // Find the location of the last NS separator. - $pos = strrpos($class, '\\'); - - // If one is found, we're dealing with a NS'd class. - if ($pos !== false) - { - $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR; - $className = substr($class, $pos + 1); - } - // If not, no need to parse path. - else - { - $classPath = null; - $className = $class; - } - - $classPath .= $className . '.php'; - - // Loop through registered namespaces until we find a match. - foreach (self::$namespaces as $ns => $paths) - { - if (strpos($class, "{$ns}\\") === 0) - { - $nsPath = trim(str_replace('\\', DIRECTORY_SEPARATOR, $ns), DIRECTORY_SEPARATOR); - - // Loop through paths registered to this namespace until we find a match. - foreach ($paths as $path) - { - $classFilePath = realpath($path . DIRECTORY_SEPARATOR . substr_replace($classPath, '', 0, strlen($nsPath) + 1)); - - // We do not allow files outside the namespace root to be loaded - if (strpos($classFilePath, realpath($path)) !== 0) - { - continue; - } - - // We check for class_exists to handle case-sensitive file systems - if (is_file($classFilePath) && !class_exists($class, false)) - { - $found = (bool) include_once $classFilePath; - - if ($found) - { - self::loadAliasFor($class); - } - - return $found; - } - } - } - } - - return false; - } - - /** - * Method to autoload classes that have been aliased using the registerAlias method. - * - * @param string $class The fully qualified class name to autoload. - * - * @return boolean True on success, false otherwise. - * - * @since 3.2 - */ - public static function loadByAlias($class) - { - $class = strtolower(self::stripFirstBackslash($class)); - - if (isset(self::$classAliases[$class])) - { - // Force auto-load of the regular class - class_exists(self::$classAliases[$class], true); - - // Normally this shouldn't execute as the autoloader will execute applyAliasFor when the regular class is - // auto-loaded above. - if (!class_exists($class, false) && !interface_exists($class, false)) - { - class_alias(self::$classAliases[$class], $class); - } - } - } - - /** - * Applies a class alias for an already loaded class, if a class alias was created for it. - * - * @param string $class We'll look for and register aliases for this (real) class name - * - * @return void - * - * @since 3.4 - */ - public static function applyAliasFor($class) - { - $class = self::stripFirstBackslash($class); - - if (isset(self::$classAliasesInverse[$class])) - { - foreach (self::$classAliasesInverse[$class] as $alias) - { - class_alias($class, $alias); - } - } - } - - /** - * Autoload a class based on name. - * - * @param string $class The class to be loaded. - * - * @return boolean True if the class was loaded, false otherwise. - * - * @since 1.7.3 - */ - public static function _autoload($class) - { - foreach (self::$prefixes as $prefix => $lookup) - { - $chr = strlen($prefix) < strlen($class) ? $class[strlen($prefix)] : 0; - - if (strpos($class, $prefix) === 0 && ($chr === strtoupper($chr))) - { - return self::_load(substr($class, strlen($prefix)), $lookup); - } - } - - return false; - } - - /** - * Load a class based on name and lookup array. - * - * @param string $class The class to be loaded (without prefix). - * @param array $lookup The array of base paths to use for finding the class file. - * - * @return boolean True if the class was loaded, false otherwise. - * - * @since 3.0.0 - */ - private static function _load($class, $lookup) - { - // Split the class name into parts separated by camelCase. - $parts = preg_split('/(?<=[a-z0-9])(?=[A-Z])/x', $class); - $partsCount = count($parts); - - foreach ($lookup as $base) - { - // Generate the path based on the class name parts. - $path = realpath($base . '/' . implode('/', array_map('strtolower', $parts)) . '.php'); - - // Load the file if it exists and is in the lookup path. - if (strpos($path, realpath($base)) === 0 && is_file($path)) - { - $found = (bool) include_once $path; - - if ($found) - { - self::loadAliasFor($class); - } - - return $found; - } - - // Backwards compatibility patch - - // If there is only one part we want to duplicate that part for generating the path. - if ($partsCount === 1) - { - // Generate the path based on the class name parts. - $path = realpath($base . '/' . implode('/', array_map('strtolower', array($parts[0], $parts[0]))) . '.php'); - - // Load the file if it exists and is in the lookup path. - if (strpos($path, realpath($base)) === 0 && is_file($path)) - { - $found = (bool) include_once $path; - - if ($found) - { - self::loadAliasFor($class); - } - - return $found; - } - } - } - - return false; - } - - /** - * Loads the aliases for the given class. - * - * @param string $class The class. - * - * @return void - * - * @since 3.8.0 - */ - private static function loadAliasFor($class) - { - if (!array_key_exists($class, self::$classAliasesInverse)) - { - return; - } - - foreach (self::$classAliasesInverse[$class] as $alias) - { - // Force auto-load of the alias class - class_exists($alias, true); - } - } - - /** - * Strips the first backslash from the given class if present. - * - * @param string $class The class to strip the first prefix from. - * - * @return string The striped class name. - * - * @since 3.8.0 - */ - private static function stripFirstBackslash($class) - { - return $class && $class[0] === '\\' ? substr($class, 1) : $class; - } + /** + * Container for already imported library paths. + * + * @var array + * @since 1.7.0 + */ + protected static $classes = array(); + + /** + * Container for already imported library paths. + * + * @var array + * @since 1.7.0 + */ + protected static $imported = array(); + + /** + * Container for registered library class prefixes and path lookups. + * + * @var array + * @since 3.0.0 + */ + protected static $prefixes = array(); + + /** + * Holds proxy classes and the class names the proxy. + * + * @var array + * @since 3.2 + */ + protected static $classAliases = array(); + + /** + * Holds the inverse lookup for proxy classes and the class names the proxy. + * + * @var array + * @since 3.4 + */ + protected static $classAliasesInverse = array(); + + /** + * Container for namespace => path map. + * + * @var array + * @since 3.1.4 + */ + protected static $namespaces = array(); + + /** + * Holds a reference for all deprecated aliases (mainly for use by a logging platform). + * + * @var array + * @since 3.6.3 + */ + protected static $deprecatedAliases = array(); + + /** + * The root folders where extensions can be found. + * + * @var array + * @since 4.0.0 + */ + protected static $extensionRootFolders = array(); + + /** + * Method to discover classes of a given type in a given path. + * + * @param string $classPrefix The class name prefix to use for discovery. + * @param string $parentPath Full path to the parent folder for the classes to discover. + * @param boolean $force True to overwrite the autoload path value for the class if it already exists. + * @param boolean $recurse Recurse through all child directories as well as the parent path. + * + * @return void + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ + public static function discover($classPrefix, $parentPath, $force = true, $recurse = false) + { + try { + if ($recurse) { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($parentPath), + RecursiveIteratorIterator::SELF_FIRST + ); + } else { + $iterator = new DirectoryIterator($parentPath); + } + + /** @type $file DirectoryIterator */ + foreach ($iterator as $file) { + $fileName = $file->getFilename(); + + // Only load for php files. + if ($file->isFile() && $file->getExtension() === 'php') { + // Get the class name and full path for each file. + $class = strtolower($classPrefix . preg_replace('#\.php$#', '', $fileName)); + + // Register the class with the autoloader if not already registered or the force flag is set. + if ($force || empty(self::$classes[$class])) { + self::register($class, $file->getPath() . '/' . $fileName); + } + } + } + } catch (UnexpectedValueException $e) { + // Exception will be thrown if the path is not a directory. Ignore it. + } + } + + /** + * Method to get the list of registered classes and their respective file paths for the autoloader. + * + * @return array The array of class => path values for the autoloader. + * + * @since 1.7.0 + */ + public static function getClassList() + { + return self::$classes; + } + + /** + * Method to get the list of deprecated class aliases. + * + * @return array An associative array with deprecated class alias data. + * + * @since 3.6.3 + */ + public static function getDeprecatedAliases() + { + return self::$deprecatedAliases; + } + + /** + * Method to get the list of registered namespaces. + * + * @return array The array of namespace => path values for the autoloader. + * + * @since 3.1.4 + */ + public static function getNamespaces() + { + return self::$namespaces; + } + + /** + * Loads a class from specified directories. + * + * @param string $key The class name to look for (dot notation). + * @param string $base Search this directory for the class. + * + * @return boolean True on success. + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ + public static function import($key, $base = null) + { + // Only import the library if not already attempted. + if (!isset(self::$imported[$key])) { + // Setup some variables. + $success = false; + $parts = explode('.', $key); + $class = array_pop($parts); + $base = (!empty($base)) ? $base : __DIR__; + $path = str_replace('.', DIRECTORY_SEPARATOR, $key); + + // Handle special case for helper classes. + if ($class === 'helper') { + $class = ucfirst(array_pop($parts)) . ucfirst($class); + } else { + // Standard class. + $class = ucfirst($class); + } + + // If we are importing a library from the Joomla namespace set the class to autoload. + if (strpos($path, 'joomla') === 0) { + // Since we are in the Joomla namespace prepend the classname with J. + $class = 'J' . $class; + + // Only register the class for autoloading if the file exists. + if (is_file($base . '/' . $path . '.php')) { + self::$classes[strtolower($class)] = $base . '/' . $path . '.php'; + $success = true; + } + } else { + /** + * If we are not importing a library from the Joomla namespace directly include the + * file since we cannot assert the file/folder naming conventions. + */ + // If the file exists attempt to include it. + if (is_file($base . '/' . $path . '.php')) { + $success = (bool) include_once $base . '/' . $path . '.php'; + } + } + + // Add the import key to the memory cache container. + self::$imported[$key] = $success; + } + + return self::$imported[$key]; + } + + /** + * Load the file for a class. + * + * @param string $class The class to be loaded. + * + * @return boolean True on success + * + * @since 1.7.0 + */ + public static function load($class) + { + // Sanitize class name. + $key = strtolower($class); + + // If the class already exists do nothing. + if (class_exists($class, false)) { + return true; + } + + // If the class is registered include the file. + if (isset(self::$classes[$key])) { + $found = (bool) include_once self::$classes[$key]; + + if ($found) { + self::loadAliasFor($class); + } + + // If the class doesn't exists, we probably have a class alias available + if (!class_exists($class, false)) { + // Search the alias class, first none namespaced and then namespaced + $original = array_search($class, self::$classAliases) ? : array_search('\\' . $class, self::$classAliases); + + // When we have an original and the class exists an alias should be created + if ($original && class_exists($original, false)) { + class_alias($original, $class); + } + } + + return true; + } + + return false; + } + + /** + * Directly register a class to the autoload list. + * + * @param string $class The class name to register. + * @param string $path Full path to the file that holds the class to register. + * @param boolean $force True to overwrite the autoload path value for the class if it already exists. + * + * @return void + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ + public static function register($class, $path, $force = true) + { + // When an alias exists, register it as well + if (array_key_exists(strtolower($class), self::$classAliases)) { + self::register(self::stripFirstBackslash(self::$classAliases[strtolower($class)]), $path, $force); + } + + // Sanitize class name. + $class = strtolower($class); + + // Only attempt to register the class if the name and file exist. + if (!empty($class) && is_file($path)) { + // Register the class with the autoloader if not already registered or the force flag is set. + if ($force || empty(self::$classes[$class])) { + self::$classes[$class] = $path; + } + } + } + + /** + * Register a class prefix with lookup path. This will allow developers to register library + * packages with different class prefixes to the system autoloader. More than one lookup path + * may be registered for the same class prefix, but if this method is called with the reset flag + * set to true then any registered lookups for the given prefix will be overwritten with the current + * lookup path. When loaded, prefix paths are searched in a "last in, first out" order. + * + * @param string $prefix The class prefix to register. + * @param string $path Absolute file path to the library root where classes with the given prefix can be found. + * @param boolean $reset True to reset the prefix with only the given lookup path. + * @param boolean $prepend If true, push the path to the beginning of the prefix lookup paths array. + * + * @return void + * + * @throws RuntimeException + * + * @since 3.0.0 + */ + public static function registerPrefix($prefix, $path, $reset = false, $prepend = false) + { + // Verify the library path exists. + if (!is_dir($path)) { + $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); + + throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); + } + + // If the prefix is not yet registered or we have an explicit reset flag then set set the path. + if ($reset || !isset(self::$prefixes[$prefix])) { + self::$prefixes[$prefix] = array($path); + } else { + // Otherwise we want to simply add the path to the prefix. + if ($prepend) { + array_unshift(self::$prefixes[$prefix], $path); + } else { + self::$prefixes[$prefix][] = $path; + } + } + } + + /** + * Offers the ability for "just in time" usage of `class_alias()`. + * You cannot overwrite an existing alias. + * + * @param string $alias The alias name to register. + * @param string $original The original class to alias. + * @param string|boolean $version The version in which the alias will no longer be present. + * + * @return boolean True if registration was successful. False if the alias already exists. + * + * @since 3.2 + */ + public static function registerAlias($alias, $original, $version = false) + { + // PHP is case insensitive so support all kind of alias combination + $lowercasedAlias = strtolower($alias); + + if (!isset(self::$classAliases[$lowercasedAlias])) { + self::$classAliases[$lowercasedAlias] = $original; + + $original = self::stripFirstBackslash($original); + + if (!isset(self::$classAliasesInverse[$original])) { + self::$classAliasesInverse[$original] = array($lowercasedAlias); + } else { + self::$classAliasesInverse[$original][] = $lowercasedAlias; + } + + // If given a version, log this alias as deprecated + if ($version) { + self::$deprecatedAliases[] = array('old' => $alias, 'new' => $original, 'version' => $version); + } + + return true; + } + + return false; + } + + /** + * Register a namespace to the autoloader. When loaded, namespace paths are searched in a "last in, first out" order. + * + * @param string $namespace A case sensitive Namespace to register. + * @param string $path A case sensitive absolute file path to the library root where classes of the given namespace can be found. + * @param boolean $reset True to reset the namespace with only the given lookup path. + * @param boolean $prepend If true, push the path to the beginning of the namespace lookup paths array. + * + * @return void + * + * @throws RuntimeException + * + * @since 3.1.4 + */ + public static function registerNamespace($namespace, $path, $reset = false, $prepend = false) + { + // Verify the library path exists. + if (!is_dir($path)) { + $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); + + throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); + } + + // Trim leading and trailing backslashes from namespace, allowing "\Parent\Child", "Parent\Child\" and "\Parent\Child\" to be treated the same way. + $namespace = trim($namespace, '\\'); + + // If the namespace is not yet registered or we have an explicit reset flag then set the path. + if ($reset || !isset(self::$namespaces[$namespace])) { + self::$namespaces[$namespace] = array($path); + } else { + // Otherwise we want to simply add the path to the namespace. + if ($prepend) { + array_unshift(self::$namespaces[$namespace], $path); + } else { + self::$namespaces[$namespace][] = $path; + } + } + } + + /** + * Method to setup the autoloaders for the Joomla Platform. + * Since the SPL autoloaders are called in a queue we will add our explicit + * class-registration based loader first, then fall back on the autoloader based on conventions. + * This will allow people to register a class in a specific location and override platform libraries + * as was previously possible. + * + * @param boolean $enablePsr True to enable autoloading based on PSR-0. + * @param boolean $enablePrefixes True to enable prefix based class loading (needed to auto load the Joomla core). + * @param boolean $enableClasses True to enable class map based class loading (needed to auto load the Joomla core). + * + * @return void + * + * @since 3.1.4 + */ + public static function setup($enablePsr = true, $enablePrefixes = true, $enableClasses = true) + { + if ($enableClasses) { + // Register the class map based autoloader. + spl_autoload_register(array('JLoader', 'load')); + } + + if ($enablePrefixes) { + // Register the prefix autoloader. + spl_autoload_register(array('JLoader', '_autoload')); + } + + if ($enablePsr) { + // Register the PSR based autoloader. + spl_autoload_register(array('JLoader', 'loadByPsr')); + spl_autoload_register(array('JLoader', 'loadByAlias')); + } + } + + /** + * Method to autoload classes that are namespaced to the PSR-4 standard. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 3.7.0 + * @deprecated 5.0 Use JLoader::loadByPsr instead + */ + public static function loadByPsr4($class) + { + return self::loadByPsr($class); + } + + /** + * Method to autoload classes that are namespaced to the PSR-4 standard. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 4.0.0 + */ + public static function loadByPsr($class) + { + $class = self::stripFirstBackslash($class); + + // Find the location of the last NS separator. + $pos = strrpos($class, '\\'); + + // If one is found, we're dealing with a NS'd class. + if ($pos !== false) { + $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR; + $className = substr($class, $pos + 1); + } else { + // If not, no need to parse path. + $classPath = null; + $className = $class; + } + + $classPath .= $className . '.php'; + + // Loop through registered namespaces until we find a match. + foreach (self::$namespaces as $ns => $paths) { + if (strpos($class, "{$ns}\\") === 0) { + $nsPath = trim(str_replace('\\', DIRECTORY_SEPARATOR, $ns), DIRECTORY_SEPARATOR); + + // Loop through paths registered to this namespace until we find a match. + foreach ($paths as $path) { + $classFilePath = realpath($path . DIRECTORY_SEPARATOR . substr_replace($classPath, '', 0, strlen($nsPath) + 1)); + + // We do not allow files outside the namespace root to be loaded + if (strpos($classFilePath, realpath($path)) !== 0) { + continue; + } + + // We check for class_exists to handle case-sensitive file systems + if (is_file($classFilePath) && !class_exists($class, false)) { + $found = (bool) include_once $classFilePath; + + if ($found) { + self::loadAliasFor($class); + } + + return $found; + } + } + } + } + + return false; + } + + /** + * Method to autoload classes that have been aliased using the registerAlias method. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 3.2 + */ + public static function loadByAlias($class) + { + $class = strtolower(self::stripFirstBackslash($class)); + + if (isset(self::$classAliases[$class])) { + // Force auto-load of the regular class + class_exists(self::$classAliases[$class], true); + + // Normally this shouldn't execute as the autoloader will execute applyAliasFor when the regular class is + // auto-loaded above. + if (!class_exists($class, false) && !interface_exists($class, false)) { + class_alias(self::$classAliases[$class], $class); + } + } + } + + /** + * Applies a class alias for an already loaded class, if a class alias was created for it. + * + * @param string $class We'll look for and register aliases for this (real) class name + * + * @return void + * + * @since 3.4 + */ + public static function applyAliasFor($class) + { + $class = self::stripFirstBackslash($class); + + if (isset(self::$classAliasesInverse[$class])) { + foreach (self::$classAliasesInverse[$class] as $alias) { + class_alias($class, $alias); + } + } + } + + /** + * Autoload a class based on name. + * + * @param string $class The class to be loaded. + * + * @return boolean True if the class was loaded, false otherwise. + * + * @since 1.7.3 + */ + public static function _autoload($class) + { + foreach (self::$prefixes as $prefix => $lookup) { + $chr = strlen($prefix) < strlen($class) ? $class[strlen($prefix)] : 0; + + if (strpos($class, $prefix) === 0 && ($chr === strtoupper($chr))) { + return self::_load(substr($class, strlen($prefix)), $lookup); + } + } + + return false; + } + + /** + * Load a class based on name and lookup array. + * + * @param string $class The class to be loaded (without prefix). + * @param array $lookup The array of base paths to use for finding the class file. + * + * @return boolean True if the class was loaded, false otherwise. + * + * @since 3.0.0 + */ + private static function _load($class, $lookup) + { + // Split the class name into parts separated by camelCase. + $parts = preg_split('/(?<=[a-z0-9])(?=[A-Z])/x', $class); + $partsCount = count($parts); + + foreach ($lookup as $base) { + // Generate the path based on the class name parts. + $path = realpath($base . '/' . implode('/', array_map('strtolower', $parts)) . '.php'); + + // Load the file if it exists and is in the lookup path. + if (strpos($path, realpath($base)) === 0 && is_file($path)) { + $found = (bool) include_once $path; + + if ($found) { + self::loadAliasFor($class); + } + + return $found; + } + + // Backwards compatibility patch + + // If there is only one part we want to duplicate that part for generating the path. + if ($partsCount === 1) { + // Generate the path based on the class name parts. + $path = realpath($base . '/' . implode('/', array_map('strtolower', array($parts[0], $parts[0]))) . '.php'); + + // Load the file if it exists and is in the lookup path. + if (strpos($path, realpath($base)) === 0 && is_file($path)) { + $found = (bool) include_once $path; + + if ($found) { + self::loadAliasFor($class); + } + + return $found; + } + } + } + + return false; + } + + /** + * Loads the aliases for the given class. + * + * @param string $class The class. + * + * @return void + * + * @since 3.8.0 + */ + private static function loadAliasFor($class) + { + if (!array_key_exists($class, self::$classAliasesInverse)) { + return; + } + + foreach (self::$classAliasesInverse[$class] as $alias) { + // Force auto-load of the alias class + class_exists($alias, true); + } + } + + /** + * Strips the first backslash from the given class if present. + * + * @param string $class The class to strip the first prefix from. + * + * @return string The striped class name. + * + * @since 3.8.0 + */ + private static function stripFirstBackslash($class) + { + return $class && $class[0] === '\\' ? substr($class, 1) : $class; + } } // Check if jexit is defined first (our unit tests mock this) -if (!function_exists('jexit')) -{ - /** - * Global application exit. - * - * This function provides a single exit point for the platform. - * - * @param mixed $message Exit code or string. Defaults to zero. - * - * @return void - * - * @codeCoverageIgnore - * @since 1.7.0 - */ - function jexit($message = 0) - { - exit($message); - } +if (!function_exists('jexit')) { + /** + * Global application exit. + * + * This function provides a single exit point for the platform. + * + * @param mixed $message Exit code or string. Defaults to zero. + * + * @return void + * + * @codeCoverageIgnore + * @since 1.7.0 + */ + function jexit($message = 0) + { + exit($message); + } } /** @@ -786,5 +716,5 @@ function jexit($message = 0) */ function jimport($path, $base = null) { - return JLoader::import($path, $base); + return JLoader::import($path, $base); } diff --git a/code/libraries/namespacemap.php b/code/libraries/namespacemap.php index 8b7668eb..c47fcdb9 100644 --- a/code/libraries/namespacemap.php +++ b/code/libraries/namespacemap.php @@ -1,16 +1,21 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ -use Joomla\CMS\Filesystem\File; -use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Log\Log; +use Joomla\Filesystem\File; +use Joomla\Filesystem\Folder; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects /** * Class JNamespacePsr4Map @@ -19,281 +24,267 @@ */ class JNamespacePsr4Map { - /** - * Path to the autoloader - * - * @var string - * @since 4.0.0 - */ - protected $file = JPATH_CACHE . '/autoload_psr4.php'; - - /** - * @var array|null - * @since 4.0.0 - */ - private $cachedMap = null; - - /** - * Check if the file exists - * - * @return boolean - * - * @since 4.0.0 - */ - public function exists() - { - return is_file($this->file); - } - - /** - * Check if the namespace mapping file exists, if not create it - * - * @return void - * - * @since 4.0.0 - */ - public function ensureMapFileExists() - { - if (!$this->exists()) - { - $this->create(); - } - } - - /** - * Create the namespace file - * - * @return boolean - * - * @since 4.0.0 - */ - public function create() - { - $extensions = array_merge( - $this->getNamespaces('component'), - $this->getNamespaces('module'), - $this->getNamespaces('plugin'), - $this->getNamespaces('library') - ); - - ksort($extensions); - - $this->writeNamespaceFile($extensions); - - return true; - } - - /** - * Load the PSR4 file - * - * @return boolean - * - * @since 4.0.0 - */ - public function load() - { - if (!$this->exists()) - { - $this->create(); - } - - $map = $this->cachedMap ?: require $this->file; - - $loader = include JPATH_LIBRARIES . '/vendor/autoload.php'; - - foreach ($map as $namespace => $path) - { - $loader->setPsr4($namespace, $path); - } - - return true; - } - - /** - * Write the Namespace mapping file - * - * @param array $elements Array of elements - * - * @return void - * - * @since 4.0.0 - */ - protected function writeNamespaceFile($elements) - { - $content = array(); - $content[] = " $path) - { - $content[] = "\t'" . $namespace . "'" . ' => [' . $path . '],'; - } - - $content[] = '];'; - - /** - * Backup the current error_reporting level and set a new level - * - * We do this because file_put_contents can raise a Warning if it cannot write the autoload_psr4.php file - * and this will output to the response BEFORE the session has started, causing the session start to fail - * and ultimately leading us to a 500 Internal Server Error page just because of the output warning, which - * we can safely ignore as we can use an in-memory autoload_psr4 map temporarily, and display real errors later. - */ - $error_reporting = error_reporting(0); - - if (!File::write($this->file, implode("\n", $content))) - { - Log::add('Could not save ' . $this->file, Log::WARNING); - - $map = []; - $constants = ['JPATH_ADMINISTRATOR', 'JPATH_API', 'JPATH_SITE', 'JPATH_PLUGINS']; - - foreach ($elements as $namespace => $path) - { - foreach ($constants as $constant) - { - $path = preg_replace(['/^(' . $constant . ")\s\.\s\'/", '/\'$/'], [constant($constant), ''], $path); - } - - $namespace = str_replace('\\\\', '\\', $namespace); - $map[$namespace] = [ $path ]; - } - - $this->cachedMap = $map; - } - - // Restore previous value of error_reporting - error_reporting($error_reporting); - } - - /** - * Get an array of namespaces with their respective path for the given extension type. - * - * @param string $type The extension type - * - * @return array - * - * @since 4.0.0 - */ - private function getNamespaces(string $type): array - { - if (!in_array($type, ['component', 'module', 'plugin', 'library'], true)) - { - return []; - } - - // Select directories containing extension manifest files. - if ($type === 'component') - { - $directories = [JPATH_ADMINISTRATOR . '/components']; - } - elseif ($type === 'module') - { - $directories = [JPATH_SITE . '/modules', JPATH_ADMINISTRATOR . '/modules']; - } - elseif ($type === 'plugin') - { - $directories = Folder::folders(JPATH_PLUGINS, '.', false, true); - } - else - { - $directories = [JPATH_LIBRARIES]; - } - - $extensions = []; - - foreach ($directories as $directory) - { - foreach (Folder::folders($directory) as $extension) - { - // Compile the extension path - $extensionPath = $directory . '/' . $extension . '/'; - - // Strip the com_ from the extension name for components - $name = str_replace('com_', '', $extension, $count); - $file = $extensionPath . $name . '.xml'; - - // If there is no manifest file, ignore. If it was a component check if the xml was named with the com_ prefix. - if (!is_file($file)) - { - if (!$count) - { - continue; - } - - $file = $extensionPath . $extension . '.xml'; - - if (!is_file($file)) - { - continue; - } - } - - // Load the manifest file - $xml = simplexml_load_file($file); - - // When invalid, ignore - if (!$xml) - { - continue; - } - - // The namespace node - $namespaceNode = $xml->namespace; - - // The namespace string - $namespace = (string) $namespaceNode; - - // Ignore when the string is empty - if (!$namespace) - { - continue; - } - - // Normalize the namespace string - $namespace = str_replace('\\', '\\\\', $namespace) . '\\\\'; - $namespacePath = rtrim($extensionPath . $namespaceNode->attributes()->path, '/'); - - if ($type === 'plugin' || $type === 'library') - { - $baseDir = $type === 'plugin' ? 'JPATH_PLUGINS . \'' : 'JPATH_LIBRARIES . \''; - $path = str_replace($type === 'plugin' ? JPATH_PLUGINS : JPATH_LIBRARIES, '', $namespacePath); - - // Set the namespace - $extensions[$namespace] = $baseDir . $path . '\''; - - continue; - } - - // Check if we need to use administrator path - $isAdministrator = strpos($namespacePath, JPATH_ADMINISTRATOR) === 0; - $path = str_replace($isAdministrator ? JPATH_ADMINISTRATOR : JPATH_SITE, '', $namespacePath); - - // Add the site path when a component - if ($type === 'component') - { - if (is_dir(JPATH_SITE . $path)) - { - $extensions[$namespace . 'Site\\\\'] = 'JPATH_SITE . \'' . $path . '\''; - } - - if (is_dir(JPATH_API . $path)) - { - $extensions[$namespace . 'Api\\\\'] = 'JPATH_API . \'' . $path . '\''; - } - } - - // Add the application specific segment when a component or module - $baseDir = $isAdministrator ? 'JPATH_ADMINISTRATOR . \'' : 'JPATH_SITE . \''; - $namespace .= $isAdministrator ? 'Administrator\\\\' : 'Site\\\\'; - - // Set the namespace - $extensions[$namespace] = $baseDir . $path . '\''; - } - } - - // Return the namespaces - return $extensions; - } + /** + * Path to the autoloader + * + * @var string + * @since 4.0.0 + */ + protected $file = JPATH_CACHE . '/autoload_psr4.php'; + + /** + * @var array|null + * @since 4.0.0 + */ + private $cachedMap = null; + + /** + * Check if the file exists + * + * @return boolean + * + * @since 4.0.0 + */ + public function exists() + { + return is_file($this->file); + } + + /** + * Check if the namespace mapping file exists, if not create it + * + * @return void + * + * @since 4.0.0 + */ + public function ensureMapFileExists() + { + if (!$this->exists()) { + $this->create(); + } + } + + /** + * Create the namespace file + * + * @return boolean + * + * @since 4.0.0 + */ + public function create() + { + $extensions = array_merge( + $this->getNamespaces('component'), + $this->getNamespaces('module'), + $this->getNamespaces('plugin'), + $this->getNamespaces('library') + ); + + ksort($extensions); + + $this->writeNamespaceFile($extensions); + + return true; + } + + /** + * Load the PSR4 file + * + * @return boolean + * + * @since 4.0.0 + */ + public function load() + { + if (!$this->exists()) { + $this->create(); + } + + $map = $this->cachedMap ?: require $this->file; + + $loader = include JPATH_LIBRARIES . '/vendor/autoload.php'; + + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + return true; + } + + /** + * Write the Namespace mapping file + * + * @param array $elements Array of elements + * + * @return void + * + * @since 4.0.0 + */ + protected function writeNamespaceFile($elements) + { + $content = array(); + $content[] = " $path) { + $content[] = "\t'" . $namespace . "'" . ' => [' . $path . '],'; + } + + $content[] = '];'; + + /** + * Backup the current error_reporting level and set a new level + * + * We do this because file_put_contents can raise a Warning if it cannot write the autoload_psr4.php file + * and this will output to the response BEFORE the session has started, causing the session start to fail + * and ultimately leading us to a 500 Internal Server Error page just because of the output warning, which + * we can safely ignore as we can use an in-memory autoload_psr4 map temporarily, and display real errors later. + */ + $error_reporting = error_reporting(0); + + try { + File::write($this->file, implode("\n", $content)); + } catch (Exception $e) { + Log::add('Could not save ' . $this->file, Log::WARNING); + + $map = []; + $constants = ['JPATH_ADMINISTRATOR', 'JPATH_API', 'JPATH_SITE', 'JPATH_PLUGINS']; + + foreach ($elements as $namespace => $path) { + foreach ($constants as $constant) { + $path = preg_replace(['/^(' . $constant . ")\s\.\s\'/", '/\'$/'], [constant($constant), ''], $path); + } + + $namespace = str_replace('\\\\', '\\', $namespace); + $map[$namespace] = [ $path ]; + } + + $this->cachedMap = $map; + } + + // Restore previous value of error_reporting + error_reporting($error_reporting); + } + + /** + * Get an array of namespaces with their respective path for the given extension type. + * + * @param string $type The extension type + * + * @return array + * + * @since 4.0.0 + */ + private function getNamespaces(string $type): array + { + if (!in_array($type, ['component', 'module', 'plugin', 'library'], true)) { + return []; + } + + // Select directories containing extension manifest files. + if ($type === 'component') { + $directories = [JPATH_ADMINISTRATOR . '/components']; + } elseif ($type === 'module') { + $directories = [JPATH_SITE . '/modules', JPATH_ADMINISTRATOR . '/modules']; + } elseif ($type === 'plugin') { + try { + $directories = Folder::folders(JPATH_PLUGINS, '.', false, true); + } catch (Exception $e) { + $directories = []; + } + } else { + $directories = [JPATH_LIBRARIES]; + } + + $extensions = []; + + foreach ($directories as $directory) { + try { + $extensionFolders = Folder::folders($directory); + } catch (Exception $e) { + continue; + } + + foreach ($extensionFolders as $extension) { + // Compile the extension path + $extensionPath = $directory . '/' . $extension . '/'; + + // Strip the com_ from the extension name for components + $name = str_replace('com_', '', $extension, $count); + $file = $extensionPath . $name . '.xml'; + + // If there is no manifest file, ignore. If it was a component check if the xml was named with the com_ prefix. + if (!is_file($file)) { + if (!$count) { + continue; + } + + $file = $extensionPath . $extension . '.xml'; + + if (!is_file($file)) { + continue; + } + } + + // Load the manifest file + $xml = simplexml_load_file($file); + + // When invalid, ignore + if (!$xml) { + continue; + } + + // The namespace node + $namespaceNode = $xml->namespace; + + // The namespace string + $namespace = (string) $namespaceNode; + + // Ignore when the string is empty + if (!$namespace) { + continue; + } + + // Normalize the namespace string + $namespace = str_replace('\\', '\\\\', $namespace) . '\\\\'; + $namespacePath = rtrim($extensionPath . $namespaceNode->attributes()->path, '/'); + + if ($type === 'plugin' || $type === 'library') { + $baseDir = $type === 'plugin' ? 'JPATH_PLUGINS . \'' : 'JPATH_LIBRARIES . \''; + $path = str_replace($type === 'plugin' ? JPATH_PLUGINS : JPATH_LIBRARIES, '', $namespacePath); + + // Set the namespace + $extensions[$namespace] = $baseDir . $path . '\''; + + continue; + } + + // Check if we need to use administrator path + $isAdministrator = strpos($namespacePath, JPATH_ADMINISTRATOR) === 0; + $path = str_replace($isAdministrator ? JPATH_ADMINISTRATOR : JPATH_SITE, '', $namespacePath); + + // Add the site path when a component + if ($type === 'component') { + if (is_dir(JPATH_SITE . $path)) { + $extensions[$namespace . 'Site\\\\'] = 'JPATH_SITE . \'' . $path . '\''; + } + + if (is_dir(JPATH_API . $path)) { + $extensions[$namespace . 'Api\\\\'] = 'JPATH_API . \'' . $path . '\''; + } + } + + // Add the application specific segment when a component or module + $baseDir = $isAdministrator ? 'JPATH_ADMINISTRATOR . \'' : 'JPATH_SITE . \''; + $namespace .= $isAdministrator ? 'Administrator\\\\' : 'Site\\\\'; + + // Set the namespace + $extensions[$namespace] = $baseDir . $path . '\''; + } + } + + // Return the namespaces + return $extensions; + } } diff --git a/code/libraries/src/Access/Access.php b/code/libraries/src/Access/Access.php index 2452c866..d270a8c5 100644 --- a/code/libraries/src/Access/Access.php +++ b/code/libraries/src/Access/Access.php @@ -1,4 +1,5 @@ allow($action, self::$identities[$userId]); - } - - /** - * Method to preload the Rules object for the given asset type. - * - * @param integer|string|array $assetTypes The type or name of the asset (e.g. 'com_content.article', 'com_menus.menu.2'). - * Also accepts the asset id. An array of asset type or a special - * 'components' string to load all component assets. - * @param boolean $reload Set to true to reload from database. - * - * @return boolean True on success. - * - * @since 1.6 - * @note This method will return void in 4.0. - */ - public static function preload($assetTypes = 'components', $reload = false) - { - // If sent an asset id, we first get the asset type for that asset id. - if (is_numeric($assetTypes)) - { - $assetTypes = self::getAssetType($assetTypes); - } - - // Check for default case: - $isDefault = \is_string($assetTypes) && \in_array($assetTypes, array('components', 'component')); - - // Preload the rules for all of the components. - if ($isDefault) - { - self::preloadComponents(); - - return true; - } - - // If we get to this point, this is a regular asset type and we'll proceed with the preloading process. - if (!\is_array($assetTypes)) - { - $assetTypes = (array) $assetTypes; - } - - foreach ($assetTypes as $assetType) - { - self::preloadPermissions($assetType, $reload); - } - - return true; - } - - /** - * Method to recursively retrieve the list of parent Asset IDs - * for a particular Asset. - * - * @param string $assetType The asset type, or the asset name, or the extension of the asset - * (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact'). - * @param integer $assetId The numeric asset id. - * - * @return array List of ancestor ids (includes original $assetId). - * - * @since 1.6 - */ - protected static function getAssetAncestors($assetType, $assetId) - { - // Get the extension name from the $assetType provided - $extensionName = self::getExtensionNameFromAsset($assetType); - - // Holds the list of ancestors for the Asset ID: - $ancestors = array(); - - // Add in our starting Asset ID: - $ancestors[] = (int) $assetId; - - // Initialize the variable we'll use in the loop: - $id = (int) $assetId; - - while ($id !== 0) - { - if (isset(self::$assetPermissionsParentIdMapping[$extensionName][$id])) - { - $id = (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->parent_id; - - if ($id !== 0) - { - $ancestors[] = $id; - } - } - else - { - // Add additional case to break out of the while loop automatically in - // the case that the ID is non-existent in our mapping variable above. - break; - } - } - - return $ancestors; - } - - /** - * Method to retrieve the Asset Rule strings for this particular - * Asset Type and stores them for later usage in getAssetRules(). - * Stores 2 arrays: one where the list has the Asset ID as the key - * and a second one where the Asset Name is the key. - * - * @param string $assetType The asset type, or the asset name, or the extension of the asset - * (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact'). - * @param boolean $reload Reload the preloaded assets. - * - * @return void - * - * @since 1.6 - */ - protected static function preloadPermissions($assetType, $reload = false) - { - // Get the extension name from the $assetType provided - $extensionName = self::getExtensionNameFromAsset($assetType); - - // If asset is a component, make sure that all the component assets are preloaded. - if ((isset(self::$preloadedAssetTypes[$extensionName]) || isset(self::$preloadedAssetTypes[$assetType])) && !$reload) - { - return; - } - - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadPermissions (' . $extensionName . ')'); - - // Get the database connection object. - $db = Factory::getDbo(); - $assetKey = $extensionName . '.%'; - - // Get a fresh query object. - $query = $db->getQuery(true) - ->select($db->quoteName(array('id', 'name', 'rules', 'parent_id'))) - ->from($db->quoteName('#__assets')) - ->where( - [ - $db->quoteName('name') . ' LIKE :asset', - $db->quoteName('name') . ' = :extension', - $db->quoteName('parent_id') . ' = 0', - ], - 'OR' - ) - ->bind(':extension', $extensionName) - ->bind(':asset', $assetKey); - - // Get the permission map for all assets in the asset extension. - $assets = $db->setQuery($query)->loadObjectList(); - - self::$assetPermissionsParentIdMapping[$extensionName] = array(); - - foreach ($assets as $asset) - { - self::$assetPermissionsParentIdMapping[$extensionName][$asset->id] = $asset; - self::$preloadedAssets[$asset->id] = $asset->name; - } - - // Mark asset type and it's extension name as preloaded. - self::$preloadedAssetTypes[$assetType] = true; - self::$preloadedAssetTypes[$extensionName] = true; - - !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadPermissions (' . $extensionName . ')'); - } - - /** - * Method to preload the Rules objects for all components. - * - * Note: This will only get the base permissions for the component. - * e.g. it will get 'com_content', but not 'com_content.article.1' or - * any more specific asset type rules. - * - * @return array Array of component names that were preloaded. - * - * @since 1.6 - */ - protected static function preloadComponents() - { - // If the components already been preloaded do nothing. - if (isset(self::$preloadedAssetTypes['components'])) - { - return array(); - } - - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadComponents (all components)'); - - // Add root to asset names list. - $components = array('root.1'); - - // Add enabled components to asset names list. - foreach (ComponentHelper::getComponents() as $component) - { - if ($component->enabled) - { - $components[] = $component->option; - } - } - - // Get the database connection object. - $db = Factory::getDbo(); - - // Get the asset info for all assets in asset names list. - $query = $db->getQuery(true) - ->select($db->quoteName(array('id', 'name', 'rules', 'parent_id'))) - ->from($db->quoteName('#__assets')) - ->whereIn($db->quoteName('name'), $components, ParameterType::STRING); - - // Get the Name Permission Map List - $assets = $db->setQuery($query)->loadObjectList(); - - $rootAsset = null; - - // First add the root asset and save it to preload memory and mark it as preloaded. - foreach ($assets as &$asset) - { - if ((int) $asset->parent_id === 0) - { - $rootAsset = $asset; - self::$rootAssetId = $asset->id; - self::$preloadedAssetTypes[$asset->name] = true; - self::$preloadedAssets[$asset->id] = $asset->name; - self::$assetPermissionsParentIdMapping[$asset->name][$asset->id] = $asset; - - unset($asset); - break; - } - } - - // Now create save the components asset tree to preload memory. - foreach ($assets as $asset) - { - if (!isset(self::$assetPermissionsParentIdMapping[$asset->name])) - { - self::$assetPermissionsParentIdMapping[$asset->name] = array($rootAsset->id => $rootAsset, $asset->id => $asset); - self::$preloadedAssets[$asset->id] = $asset->name; - } - } - - // Mark all components asset type as preloaded. - self::$preloadedAssetTypes['components'] = true; - - !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadComponents (all components)'); - - return $components; - } - - /** - * Method to check if a group is authorised to perform an action, optionally on an asset. - * - * @param integer $groupId The path to the group for which to check authorisation. - * @param string $action The name of the action to authorise. - * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. - * @param boolean $preload Indicates whether preloading should be used. - * - * @return boolean True if authorised. - * - * @since 1.7.0 - */ - public static function checkGroup($groupId, $action, $assetKey = null, $preload = true) - { - // Sanitize input. - $groupId = (int) $groupId; - $action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action))); - - return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::getGroupPath($groupId)); - } - - /** - * Gets the parent groups that a leaf group belongs to in its branch back to the root of the tree - * (including the leaf group id). - * - * @param mixed $groupId An integer or array of integers representing the identities to check. - * - * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. - * - * @since 1.7.0 - */ - protected static function getGroupPath($groupId) - { - // Load all the groups to improve performance on intensive groups checks - $groups = UserGroupsHelper::getInstance()->getAll(); - - if (!isset($groups[$groupId])) - { - return array(); - } - - return $groups[$groupId]->path; - } - - /** - * Method to return the Rules object for an asset. The returned object can optionally hold - * only the rules explicitly set for the asset or the summation of all inherited rules from - * parent assets and explicit rules. - * - * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. - * @param boolean $recursive True to return the rules object with inherited rules. - * @param boolean $recursiveParentAsset True to calculate the rule also based on inherited component/extension rules. - * @param boolean $preload Indicates whether preloading should be used. - * - * @return Rules Rules object for the asset. - * - * @since 1.7.0 - * @note The non preloading code will be removed in 4.0. All asset rules should use asset preloading. - */ - public static function getAssetRules($assetKey, $recursive = false, $recursiveParentAsset = true, $preload = true) - { - // Auto preloads the components assets and root asset (if chosen). - if ($preload) - { - self::preload('components'); - } - - // When asset key is null fallback to root asset. - $assetKey = self::cleanAssetKey($assetKey); - - // Auto preloads assets for the asset type (if chosen). - if ($preload) - { - self::preload(self::getAssetType($assetKey)); - } - - // Get the asset id and name. - $assetId = self::getAssetId($assetKey); - - // If asset rules already cached em memory return it (only in full recursive mode). - if ($recursive && $recursiveParentAsset && $assetId && isset(self::$assetRules[$assetId])) - { - return self::$assetRules[$assetId]; - } - - // Get the asset name and the extension name. - $assetName = self::getAssetName($assetKey); - $extensionName = self::getExtensionNameFromAsset($assetName); - - // If asset id does not exist fallback to extension asset, then root asset. - if (!$assetId) - { - if ($extensionName && $assetName !== $extensionName) - { - Log::add('No asset found for ' . $assetName . ', falling back to ' . $extensionName, Log::WARNING, 'assets'); - - return self::getAssetRules($extensionName, $recursive, $recursiveParentAsset, $preload); - } - - if (self::$rootAssetId !== null && $assetName !== self::$preloadedAssets[self::$rootAssetId]) - { - Log::add('No asset found for ' . $assetName . ', falling back to ' . self::$preloadedAssets[self::$rootAssetId], Log::WARNING, 'assets'); - - return self::getAssetRules(self::$preloadedAssets[self::$rootAssetId], $recursive, $recursiveParentAsset, $preload); - } - } - - // Almost all calls can take advantage of preloading. - if ($assetId && isset(self::$preloadedAssets[$assetId])) - { - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')'); - - // Collects permissions for each asset - $collected = array(); - - // If not in any recursive mode. We only want the asset rules. - if (!$recursive && !$recursiveParentAsset) - { - $collected = array(self::$assetPermissionsParentIdMapping[$extensionName][$assetId]->rules); - } - // If there is any type of recursive mode. - else - { - $ancestors = array_reverse(self::getAssetAncestors($extensionName, $assetId)); - - foreach ($ancestors as $id) - { - // There are no rules for this ancestor - if (!isset(self::$assetPermissionsParentIdMapping[$extensionName][$id])) - { - continue; - } - - // If full recursive mode, but not recursive parent mode, do not add the extension asset rules. - if ($recursive && !$recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name === $extensionName) - { - continue; - } - - // If not full recursive mode, but recursive parent mode, do not add other recursion rules. - if (!$recursive && $recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name !== $extensionName - && (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->id !== $assetId) - { - continue; - } - - // If empty asset to not add to rules. - if (self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules === '{}') - { - continue; - } - - $collected[] = self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules; - } - } - - /** - * Hashing the collected rules allows us to store - * only one instance of the Rules object for - * Assets that have the same exact permissions... - * it's a great way to save some memory. - */ - $hash = md5(implode(',', $collected)); - - if (!isset(self::$assetRulesIdentities[$hash])) - { - $rules = new Rules; - $rules->mergeCollection($collected); - - self::$assetRulesIdentities[$hash] = $rules; - } - - // Save asset rules to memory cache(only in full recursive mode). - if ($recursive && $recursiveParentAsset) - { - self::$assetRules[$assetId] = self::$assetRulesIdentities[$hash]; - } - - !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')'); - - return self::$assetRulesIdentities[$hash]; - } - - // Non preloading code. Use old slower method, slower. Only used in rare cases (if any) or without preloading chosen. - Log::add('Asset ' . $assetKey . ' permissions fetch without preloading (slower method).', Log::INFO, 'assets'); - - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (assetKey:' . $assetKey . ')'); - - // There's no need to process it with the recursive method for the Root Asset ID. - if ((int) $assetKey === 1) - { - $recursive = false; - } - - // Get the database connection object. - $db = Factory::getDbo(); - - // Build the database query to get the rules for the asset. - $query = $db->getQuery(true) - ->select($db->quoteName($recursive ? 'b.rules' : 'a.rules', 'rules')) - ->from($db->quoteName('#__assets', 'a')); - - // If the asset identifier is numeric assume it is a primary key, else lookup by name. - if (is_numeric($assetKey)) - { - $query->where($db->quoteName('a.id') . ' = :asset', 'OR') - ->bind(':asset', $assetKey, ParameterType::INTEGER); - } - else - { - $query->where($db->quoteName('a.name') . ' = :asset', 'OR') - ->bind(':asset', $assetKey); - } - - if ($recursiveParentAsset && ($extensionName !== $assetKey || is_numeric($assetKey))) - { - $query->where($db->quoteName('a.name') . ' = :extension') - ->bind(':extension', $extensionName); - } - - // If we want the rules cascading up to the global asset node we need a self-join. - if ($recursive) - { - $query->where($db->quoteName('a.parent_id') . ' = 0') - ->join( - 'LEFT', - $db->quoteName('#__assets', 'b'), - $db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt') - ) - ->order($db->quoteName('b.lft')); - } - - // Execute the query and load the rules from the result. - $result = $db->setQuery($query)->loadColumn(); - - // Get the root even if the asset is not found and in recursive mode - if (empty($result)) - { - $rootId = (new Asset($db))->getRootId(); - - $query->clear() - ->select($db->quoteName('rules')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('id') . ' = :rootId') - ->bind(':rootId', $rootId, ParameterType::INTEGER); - - $result = $db->setQuery($query)->loadColumn(); - } - - // Instantiate and return the Rules object for the asset rules. - $rules = new Rules; - $rules->mergeCollection($result); - - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules Slower (assetKey:' . $assetKey . ')'); - - return $rules; - } - - /** - * Method to clean the asset key to make sure we always have something. - * - * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. - * - * @return integer|string Asset id or asset name. - * - * @since 3.7.0 - */ - protected static function cleanAssetKey($assetKey = null) - { - // If it's a valid asset key, clean it and return it. - if ($assetKey) - { - return strtolower(preg_replace('#[\s\-]+#', '.', trim($assetKey))); - } - - // Return root asset id if already preloaded. - if (self::$rootAssetId !== null) - { - return self::$rootAssetId; - } - - // No preload. Return root asset id from Assets. - $assets = new Asset(Factory::getDbo()); - - return $assets->getRootId(); - } - - /** - * Method to get the asset id from the asset key. - * - * @param integer|string $assetKey The asset key (asset id or asset name). - * - * @return integer The asset id. - * - * @since 3.7.0 - */ - protected static function getAssetId($assetKey) - { - static $loaded = array(); - - // If the asset is already an id return it. - if (is_numeric($assetKey)) - { - return (int) $assetKey; - } - - if (!isset($loaded[$assetKey])) - { - // It's the root asset. - if (self::$rootAssetId !== null && $assetKey === self::$preloadedAssets[self::$rootAssetId]) - { - $loaded[$assetKey] = self::$rootAssetId; - } - else - { - $preloadedAssetsByName = array_flip(self::$preloadedAssets); - - // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table. - if (isset($preloadedAssetsByName[$assetKey])) - { - $loaded[$assetKey] = $preloadedAssetsByName[$assetKey]; - } - // Else we have to do an extra db query to fetch it from the table fetch it from table. - else - { - $table = new Asset(Factory::getDbo()); - $table->load(array('name' => $assetKey)); - $loaded[$assetKey] = $table->id; - } - } - } - - return (int) $loaded[$assetKey]; - } - - /** - * Method to get the asset name from the asset key. - * - * @param integer|string $assetKey The asset key (asset id or asset name). - * - * @return string The asset name (ex: com_content.article.8). - * - * @since 3.7.0 - */ - protected static function getAssetName($assetKey) - { - static $loaded = array(); - - // If the asset is already a string return it. - if (!is_numeric($assetKey)) - { - return $assetKey; - } - - if (!isset($loaded[$assetKey])) - { - // It's the root asset. - if (self::$rootAssetId !== null && $assetKey === self::$rootAssetId) - { - $loaded[$assetKey] = self::$preloadedAssets[self::$rootAssetId]; - } - // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table. - elseif (isset(self::$preloadedAssets[$assetKey])) - { - $loaded[$assetKey] = self::$preloadedAssets[$assetKey]; - } - // Else we have to do an extra db query to fetch it from the table fetch it from table. - else - { - $table = new Asset(Factory::getDbo()); - $table->load($assetKey); - $loaded[$assetKey] = $table->name; - } - } - - return $loaded[$assetKey]; - } - - /** - * Method to get the extension name from the asset name. - * - * @param integer|string $assetKey The asset key (asset id or asset name). - * - * @return string The extension name (ex: com_content). - * - * @since 1.6 - */ - public static function getExtensionNameFromAsset($assetKey) - { - static $loaded = array(); - - if (!isset($loaded[$assetKey])) - { - $assetName = self::getAssetName($assetKey); - $firstDot = strpos($assetName, '.'); - - if ($assetName !== 'root.1' && $firstDot !== false) - { - $assetName = substr($assetName, 0, $firstDot); - } - - $loaded[$assetKey] = $assetName; - } - - return $loaded[$assetKey]; - } - - /** - * Method to get the asset type from the asset name. - * - * For top level components this returns "components": - * 'com_content' returns 'components' - * - * For other types: - * 'com_content.article.1' returns 'com_content.article' - * 'com_content.category.1' returns 'com_content.category' - * - * @param integer|string $assetKey The asset key (asset id or asset name). - * - * @return string The asset type (ex: com_content.article). - * - * @since 1.6 - */ - public static function getAssetType($assetKey) - { - // If the asset is already a string return it. - $assetName = self::getAssetName($assetKey); - $lastDot = strrpos($assetName, '.'); - - if ($assetName !== 'root.1' && $lastDot !== false) - { - return substr($assetName, 0, $lastDot); - } - - return 'components'; - } - - /** - * Method to return the title of a user group - * - * @param integer $groupId Id of the group for which to get the title of. - * - * @return string The title of the group - * - * @since 3.5 - */ - public static function getGroupTitle($groupId) - { - // Cast as integer until method is typehinted. - $groupId = (int) $groupId; - - // Fetch the group title from the database - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('id') . ' = :groupId') - ->bind(':groupId', $groupId, ParameterType::INTEGER); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to return a list of user groups mapped to a user. The returned list can optionally hold - * only the groups explicitly mapped to the user or all groups both explicitly mapped and inherited - * by the user. - * - * @param integer $userId Id of the user for which to get the list of groups. - * @param boolean $recursive True to include inherited user groups. - * - * @return array List of user group ids to which the user is mapped. - * - * @since 1.7.0 - */ - public static function getGroupsByUser($userId, $recursive = true) - { - // Cast as integer until method is typehinted. - $userId = (int) $userId; - - // Creates a simple unique string for each parameter combination: - $storeId = $userId . ':' . (int) $recursive; - - if (!isset(self::$groupsByUser[$storeId])) - { - // @todo: Uncouple this from ComponentHelper and allow for a configuration setting or value injection. - $guestUsergroup = (int) ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); - - // Guest user (if only the actually assigned group is requested) - if (empty($userId) && !$recursive) - { - $result = array($guestUsergroup); - } - // Registered user and guest if all groups are requested - else - { - $db = Factory::getDbo(); - - // Build the database query to get the rules for the asset. - $query = $db->getQuery(true) - ->select($db->quoteName($recursive ? 'b.id' : 'a.id')); - - if (empty($userId)) - { - $query->from($db->quoteName('#__usergroups', 'a')) - ->where($db->quoteName('a.id') . ' = :guest') - ->bind(':guest', $guestUsergroup, ParameterType::INTEGER); - } - else - { - $query->from($db->quoteName('#__user_usergroup_map', 'map')) - ->where($db->quoteName('map.user_id') . ' = :userId') - ->join('LEFT', $db->quoteName('#__usergroups', 'a'), $db->quoteName('a.id') . ' = ' . $db->quoteName('map.group_id')) - ->bind(':userId', $userId, ParameterType::INTEGER); - } - - // If we want the rules cascading up to the global asset node we need a self-join. - if ($recursive) - { - $query->join( - 'LEFT', - $db->quoteName('#__usergroups', 'b'), - $db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt') - ); - } - - // Execute the query and load the rules from the result. - $db->setQuery($query); - $result = $db->loadColumn(); - - // Clean up any NULL or duplicate values, just in case - $result = ArrayHelper::toInteger($result); - - if (empty($result)) - { - $result = array(1); - } - else - { - $result = array_unique($result); - } - } - - self::$groupsByUser[$storeId] = $result; - } - - return self::$groupsByUser[$storeId]; - } - - /** - * Method to return a list of user Ids contained in a Group - * - * @param integer $groupId The group Id - * @param boolean $recursive Recursively include all child groups (optional) - * - * @return array - * - * @since 1.7.0 - * @todo This method should move somewhere else - */ - public static function getUsersByGroup($groupId, $recursive = false) - { - // Cast as integer until method is typehinted. - $groupId = (int) $groupId; - - // Get a database object. - $db = Factory::getDbo(); - - $test = $recursive ? ' >= ' : ' = '; - - // First find the users contained in the group - $query = $db->getQuery(true) - ->select('DISTINCT(' . $db->quoteName('user_id') . ')') - ->from($db->quoteName('#__usergroups', 'ug1')) - ->join( - 'INNER', - $db->quoteName('#__usergroups', 'ug2'), - $db->quoteName('ug2.lft') . $test . $db->quoteName('ug1.lft') . ' AND ' . $db->quoteName('ug1.rgt') . $test . $db->quoteName('ug2.rgt') - ) - ->join('INNER', $db->quoteName('#__user_usergroup_map', 'm'), $db->quoteName('ug2.id') . ' = ' . $db->quoteName('m.group_id')) - ->where($db->quoteName('ug1.id') . ' = :groupId') - ->bind(':groupId', $groupId, ParameterType::INTEGER); - - $db->setQuery($query); - - $result = $db->loadColumn(); - - // Clean up any NULL values, just in case - $result = ArrayHelper::toInteger($result); - - return $result; - } - - /** - * Method to return a list of view levels for which the user is authorised. - * - * @param integer $userId Id of the user for which to get the list of authorised view levels. - * - * @return array List of view levels for which the user is authorised. - * - * @since 1.7.0 - */ - public static function getAuthorisedViewLevels($userId) - { - // Only load the view levels once. - if (empty(self::$viewLevels)) - { - // Get a database object. - $db = Factory::getDbo(); - - // Build the base query. - $query = $db->getQuery(true) - ->select($db->quoteName(['id', 'rules'])) - ->from($db->quoteName('#__viewlevels')); - - // Set the query for execution. - $db->setQuery($query); - - // Build the view levels array. - foreach ($db->loadAssocList() as $level) - { - self::$viewLevels[$level['id']] = (array) json_decode($level['rules']); - } - } - - // Initialise the authorised array. - $authorised = array(1); - - // Check for the recovery mode setting and return early. - $user = User::getInstance($userId); - $root_user = Factory::getApplication()->get('root_user'); - - if (($user->username && $user->username == $root_user) || (is_numeric($root_user) && $user->id > 0 && $user->id == $root_user)) - { - // Find the super user levels. - foreach (self::$viewLevels as $level => $rule) - { - foreach ($rule as $id) - { - if ($id > 0 && self::checkGroup($id, 'core.admin')) - { - $authorised[] = $level; - break; - } - } - } - - return array_values(array_unique($authorised)); - } - - // Get all groups that the user is mapped to recursively. - $groups = self::getGroupsByUser($userId); - - // Find the authorised levels. - foreach (self::$viewLevels as $level => $rule) - { - foreach ($rule as $id) - { - if (($id < 0) && (($id * -1) == $userId)) - { - $authorised[] = $level; - break; - } - // Check to see if the group is mapped to the level. - elseif (($id >= 0) && \in_array($id, $groups)) - { - $authorised[] = $level; - break; - } - } - } - - return array_values(array_unique($authorised)); - } - - /** - * Method to return a list of actions from a file for which permissions can be set. - * - * @param string $file The path to the XML file. - * @param string $xpath An optional xpath to search for the fields. - * - * @return boolean|array False if case of error or the list of actions available. - * - * @since 3.0.0 - */ - public static function getActionsFromFile($file, $xpath = "/access/section[@name='component']/") - { - if (!is_file($file) || !is_readable($file)) - { - // If unable to find the file return false. - return false; - } - else - { - // Else return the actions from the xml. - $xml = simplexml_load_file($file); - - return self::getActionsFromData($xml, $xpath); - } - } - - /** - * Method to return a list of actions from a string or from an xml for which permissions can be set. - * - * @param string|\SimpleXMLElement $data The XML string or an XML element. - * @param string $xpath An optional xpath to search for the fields. - * - * @return boolean|array False if case of error or the list of actions available. - * - * @since 3.0.0 - */ - public static function getActionsFromData($data, $xpath = "/access/section[@name='component']/") - { - // If the data to load isn't already an XML element or string return false. - if ((!($data instanceof \SimpleXMLElement)) && (!\is_string($data))) - { - return false; - } - - // Attempt to load the XML if a string. - if (\is_string($data)) - { - try - { - $data = new \SimpleXMLElement($data); - } - catch (\Exception $e) - { - return false; - } - - // Make sure the XML loaded correctly. - if (!$data) - { - return false; - } - } - - // Initialise the actions array - $actions = array(); - - // Get the elements from the xpath - $elements = $data->xpath($xpath . 'action[@name][@title]'); - - // If there some elements, analyse them - if (!empty($elements)) - { - foreach ($elements as $element) - { - // Add the action to the actions array - $action = array( - 'name' => (string) $element['name'], - 'title' => (string) $element['title'], - ); - - if (isset($element['description'])) - { - $action['description'] = (string) $element['description']; - } - - $actions[] = (object) $action; - } - } - - // Finally return the actions array - return $actions; - } + /** + * Array of view levels + * + * @var array + * @since 1.7.0 + */ + protected static $viewLevels = array(); + + /** + * Array of rules for the asset + * + * @var array + * @since 1.7.0 + */ + protected static $assetRules = array(); + + /** + * Array of identities for asset rules + * + * @var array + * @since 1.7.0 + */ + protected static $assetRulesIdentities = array(); + + /** + * Array of the permission parent ID mappings + * + * @var array + * @since 1.7.0 + */ + protected static $assetPermissionsParentIdMapping = array(); + + /** + * Array of asset types that have been preloaded + * + * @var array + * @since 1.7.0 + */ + protected static $preloadedAssetTypes = array(); + + /** + * Array of loaded user identities + * + * @var array + * @since 1.7.0 + */ + protected static $identities = array(); + + /** + * Array of user groups. + * + * @var array + * @since 1.7.0 + */ + protected static $userGroups = array(); + + /** + * Array of user group paths. + * + * @var array + * @since 1.7.0 + */ + protected static $userGroupPaths = array(); + + /** + * Array of cached groups by user. + * + * @var array + * @since 1.7.0 + */ + protected static $groupsByUser = array(); + + /** + * Array of preloaded asset names and ids (key is the asset id). + * + * @var array + * @since 3.7.0 + */ + protected static $preloadedAssets = array(); + + /** + * The root asset id. + * + * @var integer + * @since 3.7.0 + */ + protected static $rootAssetId = null; + + /** + * Method for clearing static caches. + * + * @return void + * + * @since 1.7.3 + */ + public static function clearStatics() + { + self::$viewLevels = array(); + self::$assetRules = array(); + self::$assetRulesIdentities = array(); + self::$assetPermissionsParentIdMapping = array(); + self::$preloadedAssetTypes = array(); + self::$identities = array(); + self::$userGroups = array(); + self::$userGroupPaths = array(); + self::$groupsByUser = array(); + self::$preloadedAssets = array(); + self::$rootAssetId = null; + } + + /** + * Method to check if a user is authorised to perform an action, optionally on an asset. + * + * @param integer $userId Id of the user for which to check authorisation. + * @param string $action The name of the action to authorise. + * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. + * @param boolean $preload Indicates whether preloading should be used. + * + * @return boolean|null True if allowed, false for an explicit deny, null for an implicit deny. + * + * @since 1.7.0 + */ + public static function check($userId, $action, $assetKey = null, $preload = true) + { + // Sanitise inputs. + $userId = (int) $userId; + $action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action))); + + if (!isset(self::$identities[$userId])) { + // Get all groups against which the user is mapped. + self::$identities[$userId] = self::getGroupsByUser($userId); + array_unshift(self::$identities[$userId], $userId * -1); + } + + return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::$identities[$userId]); + } + + /** + * Method to preload the Rules object for the given asset type. + * + * @param integer|string|array $assetTypes The type or name of the asset (e.g. 'com_content.article', 'com_menus.menu.2'). + * Also accepts the asset id. An array of asset type or a special + * 'components' string to load all component assets. + * @param boolean $reload Set to true to reload from database. + * + * @return boolean True on success. + * + * @since 1.6 + * @note This method will return void in 4.0. + */ + public static function preload($assetTypes = 'components', $reload = false) + { + // If sent an asset id, we first get the asset type for that asset id. + if (is_numeric($assetTypes)) { + $assetTypes = self::getAssetType($assetTypes); + } + + // Check for default case: + $isDefault = \is_string($assetTypes) && \in_array($assetTypes, array('components', 'component')); + + // Preload the rules for all of the components. + if ($isDefault) { + self::preloadComponents(); + + return true; + } + + // If we get to this point, this is a regular asset type and we'll proceed with the preloading process. + if (!\is_array($assetTypes)) { + $assetTypes = (array) $assetTypes; + } + + foreach ($assetTypes as $assetType) { + self::preloadPermissions($assetType, $reload); + } + + return true; + } + + /** + * Method to recursively retrieve the list of parent Asset IDs + * for a particular Asset. + * + * @param string $assetType The asset type, or the asset name, or the extension of the asset + * (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact'). + * @param integer $assetId The numeric asset id. + * + * @return array List of ancestor ids (includes original $assetId). + * + * @since 1.6 + */ + protected static function getAssetAncestors($assetType, $assetId) + { + // Get the extension name from the $assetType provided + $extensionName = self::getExtensionNameFromAsset($assetType); + + // Holds the list of ancestors for the Asset ID: + $ancestors = array(); + + // Add in our starting Asset ID: + $ancestors[] = (int) $assetId; + + // Initialize the variable we'll use in the loop: + $id = (int) $assetId; + + while ($id !== 0) { + if (isset(self::$assetPermissionsParentIdMapping[$extensionName][$id])) { + $id = (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->parent_id; + + if ($id !== 0) { + $ancestors[] = $id; + } + } else { + // Add additional case to break out of the while loop automatically in + // the case that the ID is non-existent in our mapping variable above. + break; + } + } + + return $ancestors; + } + + /** + * Method to retrieve the Asset Rule strings for this particular + * Asset Type and stores them for later usage in getAssetRules(). + * Stores 2 arrays: one where the list has the Asset ID as the key + * and a second one where the Asset Name is the key. + * + * @param string $assetType The asset type, or the asset name, or the extension of the asset + * (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact'). + * @param boolean $reload Reload the preloaded assets. + * + * @return void + * + * @since 1.6 + */ + protected static function preloadPermissions($assetType, $reload = false) + { + // Get the extension name from the $assetType provided + $extensionName = self::getExtensionNameFromAsset($assetType); + + // If asset is a component, make sure that all the component assets are preloaded. + if ((isset(self::$preloadedAssetTypes[$extensionName]) || isset(self::$preloadedAssetTypes[$assetType])) && !$reload) { + return; + } + + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadPermissions (' . $extensionName . ')'); + + // Get the database connection object. + $db = Factory::getDbo(); + $assetKey = $extensionName . '.%'; + + // Get a fresh query object. + $query = $db->getQuery(true) + ->select($db->quoteName(array('id', 'name', 'rules', 'parent_id'))) + ->from($db->quoteName('#__assets')) + ->where( + [ + $db->quoteName('name') . ' LIKE :asset', + $db->quoteName('name') . ' = :extension', + $db->quoteName('parent_id') . ' = 0', + ], + 'OR' + ) + ->bind(':extension', $extensionName) + ->bind(':asset', $assetKey); + + // Get the permission map for all assets in the asset extension. + $assets = $db->setQuery($query)->loadObjectList(); + + self::$assetPermissionsParentIdMapping[$extensionName] = array(); + + foreach ($assets as $asset) { + self::$assetPermissionsParentIdMapping[$extensionName][$asset->id] = $asset; + self::$preloadedAssets[$asset->id] = $asset->name; + } + + // Mark asset type and it's extension name as preloaded. + self::$preloadedAssetTypes[$assetType] = true; + self::$preloadedAssetTypes[$extensionName] = true; + + !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadPermissions (' . $extensionName . ')'); + } + + /** + * Method to preload the Rules objects for all components. + * + * Note: This will only get the base permissions for the component. + * e.g. it will get 'com_content', but not 'com_content.article.1' or + * any more specific asset type rules. + * + * @return array Array of component names that were preloaded. + * + * @since 1.6 + */ + protected static function preloadComponents() + { + // If the components already been preloaded do nothing. + if (isset(self::$preloadedAssetTypes['components'])) { + return array(); + } + + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadComponents (all components)'); + + // Add root to asset names list. + $components = array('root.1'); + + // Add enabled components to asset names list. + foreach (ComponentHelper::getComponents() as $component) { + if ($component->enabled) { + $components[] = $component->option; + } + } + + // Get the database connection object. + $db = Factory::getDbo(); + + // Get the asset info for all assets in asset names list. + $query = $db->getQuery(true) + ->select($db->quoteName(array('id', 'name', 'rules', 'parent_id'))) + ->from($db->quoteName('#__assets')) + ->whereIn($db->quoteName('name'), $components, ParameterType::STRING); + + // Get the Name Permission Map List + $assets = $db->setQuery($query)->loadObjectList(); + + $rootAsset = null; + + // First add the root asset and save it to preload memory and mark it as preloaded. + foreach ($assets as &$asset) { + if ((int) $asset->parent_id === 0) { + $rootAsset = $asset; + self::$rootAssetId = $asset->id; + self::$preloadedAssetTypes[$asset->name] = true; + self::$preloadedAssets[$asset->id] = $asset->name; + self::$assetPermissionsParentIdMapping[$asset->name][$asset->id] = $asset; + + unset($asset); + break; + } + } + + // Now create save the components asset tree to preload memory. + foreach ($assets as $asset) { + if (!isset(self::$assetPermissionsParentIdMapping[$asset->name])) { + self::$assetPermissionsParentIdMapping[$asset->name] = array($rootAsset->id => $rootAsset, $asset->id => $asset); + self::$preloadedAssets[$asset->id] = $asset->name; + } + } + + // Mark all components asset type as preloaded. + self::$preloadedAssetTypes['components'] = true; + + !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadComponents (all components)'); + + return $components; + } + + /** + * Method to check if a group is authorised to perform an action, optionally on an asset. + * + * @param integer $groupId The path to the group for which to check authorisation. + * @param string $action The name of the action to authorise. + * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. + * @param boolean $preload Indicates whether preloading should be used. + * + * @return boolean True if authorised. + * + * @since 1.7.0 + */ + public static function checkGroup($groupId, $action, $assetKey = null, $preload = true) + { + // Sanitize input. + $groupId = (int) $groupId; + $action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action))); + + return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::getGroupPath($groupId)); + } + + /** + * Gets the parent groups that a leaf group belongs to in its branch back to the root of the tree + * (including the leaf group id). + * + * @param mixed $groupId An integer or array of integers representing the identities to check. + * + * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. + * + * @since 1.7.0 + */ + protected static function getGroupPath($groupId) + { + // Load all the groups to improve performance on intensive groups checks + $groups = UserGroupsHelper::getInstance()->getAll(); + + if (!isset($groups[$groupId])) { + return array(); + } + + return $groups[$groupId]->path; + } + + /** + * Method to return the Rules object for an asset. The returned object can optionally hold + * only the rules explicitly set for the asset or the summation of all inherited rules from + * parent assets and explicit rules. + * + * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. + * @param boolean $recursive True to return the rules object with inherited rules. + * @param boolean $recursiveParentAsset True to calculate the rule also based on inherited component/extension rules. + * @param boolean $preload Indicates whether preloading should be used. + * + * @return Rules Rules object for the asset. + * + * @since 1.7.0 + * @note The non preloading code will be removed in 4.0. All asset rules should use asset preloading. + */ + public static function getAssetRules($assetKey, $recursive = false, $recursiveParentAsset = true, $preload = true) + { + // Auto preloads the components assets and root asset (if chosen). + if ($preload) { + self::preload('components'); + } + + // When asset key is null fallback to root asset. + $assetKey = self::cleanAssetKey($assetKey); + + // Auto preloads assets for the asset type (if chosen). + if ($preload) { + self::preload(self::getAssetType($assetKey)); + } + + // Get the asset id and name. + $assetId = self::getAssetId($assetKey); + + // If asset rules already cached em memory return it (only in full recursive mode). + if ($recursive && $recursiveParentAsset && $assetId && isset(self::$assetRules[$assetId])) { + return self::$assetRules[$assetId]; + } + + // Get the asset name and the extension name. + $assetName = self::getAssetName($assetKey); + $extensionName = self::getExtensionNameFromAsset($assetName); + + // If asset id does not exist fallback to extension asset, then root asset. + if (!$assetId) { + if ($extensionName && $assetName !== $extensionName) { + Log::add('No asset found for ' . $assetName . ', falling back to ' . $extensionName, Log::WARNING, 'assets'); + + return self::getAssetRules($extensionName, $recursive, $recursiveParentAsset, $preload); + } + + if (self::$rootAssetId !== null && $assetName !== self::$preloadedAssets[self::$rootAssetId]) { + Log::add('No asset found for ' . $assetName . ', falling back to ' . self::$preloadedAssets[self::$rootAssetId], Log::WARNING, 'assets'); + + return self::getAssetRules(self::$preloadedAssets[self::$rootAssetId], $recursive, $recursiveParentAsset, $preload); + } + } + + // Almost all calls can take advantage of preloading. + if ($assetId && isset(self::$preloadedAssets[$assetId])) { + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')'); + + // Collects permissions for each asset + $collected = array(); + + // If not in any recursive mode. We only want the asset rules. + if (!$recursive && !$recursiveParentAsset) { + $collected = array(self::$assetPermissionsParentIdMapping[$extensionName][$assetId]->rules); + } else { + // If there is any type of recursive mode. + $ancestors = array_reverse(self::getAssetAncestors($extensionName, $assetId)); + + foreach ($ancestors as $id) { + // There are no rules for this ancestor + if (!isset(self::$assetPermissionsParentIdMapping[$extensionName][$id])) { + continue; + } + + // If full recursive mode, but not recursive parent mode, do not add the extension asset rules. + if ($recursive && !$recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name === $extensionName) { + continue; + } + + // If not full recursive mode, but recursive parent mode, do not add other recursion rules. + if ( + !$recursive && $recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name !== $extensionName + && (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->id !== $assetId + ) { + continue; + } + + // If empty asset to not add to rules. + if (self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules === '{}') { + continue; + } + + $collected[] = self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules; + } + } + + /** + * Hashing the collected rules allows us to store + * only one instance of the Rules object for + * Assets that have the same exact permissions... + * it's a great way to save some memory. + */ + $hash = md5(implode(',', $collected)); + + if (!isset(self::$assetRulesIdentities[$hash])) { + $rules = new Rules(); + $rules->mergeCollection($collected); + + self::$assetRulesIdentities[$hash] = $rules; + } + + // Save asset rules to memory cache(only in full recursive mode). + if ($recursive && $recursiveParentAsset) { + self::$assetRules[$assetId] = self::$assetRulesIdentities[$hash]; + } + + !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')'); + + return self::$assetRulesIdentities[$hash]; + } + + // Non preloading code. Use old slower method, slower. Only used in rare cases (if any) or without preloading chosen. + Log::add('Asset ' . $assetKey . ' permissions fetch without preloading (slower method).', Log::INFO, 'assets'); + + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (assetKey:' . $assetKey . ')'); + + // There's no need to process it with the recursive method for the Root Asset ID. + if ((int) $assetKey === 1) { + $recursive = false; + } + + // Get the database connection object. + $db = Factory::getDbo(); + + // Build the database query to get the rules for the asset. + $query = $db->getQuery(true) + ->select($db->quoteName($recursive ? 'b.rules' : 'a.rules', 'rules')) + ->from($db->quoteName('#__assets', 'a')); + + // If the asset identifier is numeric assume it is a primary key, else lookup by name. + if (is_numeric($assetKey)) { + $query->where($db->quoteName('a.id') . ' = :asset', 'OR') + ->bind(':asset', $assetKey, ParameterType::INTEGER); + } else { + $query->where($db->quoteName('a.name') . ' = :asset', 'OR') + ->bind(':asset', $assetKey); + } + + if ($recursiveParentAsset && ($extensionName !== $assetKey || is_numeric($assetKey))) { + $query->where($db->quoteName('a.name') . ' = :extension') + ->bind(':extension', $extensionName); + } + + // If we want the rules cascading up to the global asset node we need a self-join. + if ($recursive) { + $query->where($db->quoteName('a.parent_id') . ' = 0') + ->join( + 'LEFT', + $db->quoteName('#__assets', 'b'), + $db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt') + ) + ->order($db->quoteName('b.lft')); + } + + // Execute the query and load the rules from the result. + $result = $db->setQuery($query)->loadColumn(); + + // Get the root even if the asset is not found and in recursive mode + if (empty($result)) { + $rootId = (new Asset($db))->getRootId(); + + $query->clear() + ->select($db->quoteName('rules')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('id') . ' = :rootId') + ->bind(':rootId', $rootId, ParameterType::INTEGER); + + $result = $db->setQuery($query)->loadColumn(); + } + + // Instantiate and return the Rules object for the asset rules. + $rules = new Rules(); + $rules->mergeCollection($result); + + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules Slower (assetKey:' . $assetKey . ')'); + + return $rules; + } + + /** + * Method to clean the asset key to make sure we always have something. + * + * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. + * + * @return integer|string Asset id or asset name. + * + * @since 3.7.0 + */ + protected static function cleanAssetKey($assetKey = null) + { + // If it's a valid asset key, clean it and return it. + if ($assetKey) { + return strtolower(preg_replace('#[\s\-]+#', '.', trim($assetKey))); + } + + // Return root asset id if already preloaded. + if (self::$rootAssetId !== null) { + return self::$rootAssetId; + } + + // No preload. Return root asset id from Assets. + $assets = new Asset(Factory::getDbo()); + + return $assets->getRootId(); + } + + /** + * Method to get the asset id from the asset key. + * + * @param integer|string $assetKey The asset key (asset id or asset name). + * + * @return integer The asset id. + * + * @since 3.7.0 + */ + protected static function getAssetId($assetKey) + { + static $loaded = array(); + + // If the asset is already an id return it. + if (is_numeric($assetKey)) { + return (int) $assetKey; + } + + if (!isset($loaded[$assetKey])) { + // It's the root asset. + if (self::$rootAssetId !== null && $assetKey === self::$preloadedAssets[self::$rootAssetId]) { + $loaded[$assetKey] = self::$rootAssetId; + } else { + $preloadedAssetsByName = array_flip(self::$preloadedAssets); + + // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table. + if (isset($preloadedAssetsByName[$assetKey])) { + $loaded[$assetKey] = $preloadedAssetsByName[$assetKey]; + } else { + // Else we have to do an extra db query to fetch it from the table fetch it from table. + $table = new Asset(Factory::getDbo()); + $table->load(array('name' => $assetKey)); + $loaded[$assetKey] = $table->id; + } + } + } + + return (int) $loaded[$assetKey]; + } + + /** + * Method to get the asset name from the asset key. + * + * @param integer|string $assetKey The asset key (asset id or asset name). + * + * @return string The asset name (ex: com_content.article.8). + * + * @since 3.7.0 + */ + protected static function getAssetName($assetKey) + { + static $loaded = array(); + + // If the asset is already a string return it. + if (!is_numeric($assetKey)) { + return $assetKey; + } + + if (!isset($loaded[$assetKey])) { + // It's the root asset. + if (self::$rootAssetId !== null && $assetKey === self::$rootAssetId) { + $loaded[$assetKey] = self::$preloadedAssets[self::$rootAssetId]; + } elseif (isset(self::$preloadedAssets[$assetKey])) { + // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table. + $loaded[$assetKey] = self::$preloadedAssets[$assetKey]; + } else { + // Else we have to do an extra db query to fetch it from the table fetch it from table. + $table = new Asset(Factory::getDbo()); + $table->load($assetKey); + $loaded[$assetKey] = $table->name; + } + } + + return $loaded[$assetKey]; + } + + /** + * Method to get the extension name from the asset name. + * + * @param integer|string $assetKey The asset key (asset id or asset name). + * + * @return string The extension name (ex: com_content). + * + * @since 1.6 + */ + public static function getExtensionNameFromAsset($assetKey) + { + static $loaded = array(); + + if (!isset($loaded[$assetKey])) { + $assetName = self::getAssetName($assetKey); + $firstDot = strpos($assetName, '.'); + + if ($assetName !== 'root.1' && $firstDot !== false) { + $assetName = substr($assetName, 0, $firstDot); + } + + $loaded[$assetKey] = $assetName; + } + + return $loaded[$assetKey]; + } + + /** + * Method to get the asset type from the asset name. + * + * For top level components this returns "components": + * 'com_content' returns 'components' + * + * For other types: + * 'com_content.article.1' returns 'com_content.article' + * 'com_content.category.1' returns 'com_content.category' + * + * @param integer|string $assetKey The asset key (asset id or asset name). + * + * @return string The asset type (ex: com_content.article). + * + * @since 1.6 + */ + public static function getAssetType($assetKey) + { + // If the asset is already a string return it. + $assetName = self::getAssetName($assetKey); + $lastDot = strrpos($assetName, '.'); + + if ($assetName !== 'root.1' && $lastDot !== false) { + return substr($assetName, 0, $lastDot); + } + + return 'components'; + } + + /** + * Method to return the title of a user group + * + * @param integer $groupId Id of the group for which to get the title of. + * + * @return string The title of the group + * + * @since 3.5 + */ + public static function getGroupTitle($groupId) + { + // Cast as integer until method is typehinted. + $groupId = (int) $groupId; + + // Fetch the group title from the database + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :groupId') + ->bind(':groupId', $groupId, ParameterType::INTEGER); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to return a list of user groups mapped to a user. The returned list can optionally hold + * only the groups explicitly mapped to the user or all groups both explicitly mapped and inherited + * by the user. + * + * @param integer $userId Id of the user for which to get the list of groups. + * @param boolean $recursive True to include inherited user groups. + * + * @return array List of user group ids to which the user is mapped. + * + * @since 1.7.0 + */ + public static function getGroupsByUser($userId, $recursive = true) + { + // Cast as integer until method is typehinted. + $userId = (int) $userId; + + // Creates a simple unique string for each parameter combination: + $storeId = $userId . ':' . (int) $recursive; + + if (!isset(self::$groupsByUser[$storeId])) { + // @todo: Uncouple this from ComponentHelper and allow for a configuration setting or value injection. + $guestUsergroup = (int) ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); + + // Guest user (if only the actually assigned group is requested) + if (empty($userId) && !$recursive) { + $result = array($guestUsergroup); + } else { + // Registered user and guest if all groups are requested + $db = Factory::getDbo(); + + // Build the database query to get the rules for the asset. + $query = $db->getQuery(true) + ->select($db->quoteName($recursive ? 'b.id' : 'a.id')); + + if (empty($userId)) { + $query->from($db->quoteName('#__usergroups', 'a')) + ->where($db->quoteName('a.id') . ' = :guest') + ->bind(':guest', $guestUsergroup, ParameterType::INTEGER); + } else { + $query->from($db->quoteName('#__user_usergroup_map', 'map')) + ->where($db->quoteName('map.user_id') . ' = :userId') + ->join('LEFT', $db->quoteName('#__usergroups', 'a'), $db->quoteName('a.id') . ' = ' . $db->quoteName('map.group_id')) + ->bind(':userId', $userId, ParameterType::INTEGER); + } + + // If we want the rules cascading up to the global asset node we need a self-join. + if ($recursive) { + $query->join( + 'LEFT', + $db->quoteName('#__usergroups', 'b'), + $db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt') + ); + } + + // Execute the query and load the rules from the result. + $db->setQuery($query); + $result = $db->loadColumn(); + + // Clean up any NULL or duplicate values, just in case + $result = ArrayHelper::toInteger($result); + + if (empty($result)) { + $result = array(1); + } else { + $result = array_unique($result); + } + } + + self::$groupsByUser[$storeId] = $result; + } + + return self::$groupsByUser[$storeId]; + } + + /** + * Method to return a list of user Ids contained in a Group + * + * @param integer $groupId The group Id + * @param boolean $recursive Recursively include all child groups (optional) + * + * @return array + * + * @since 1.7.0 + * @todo This method should move somewhere else + */ + public static function getUsersByGroup($groupId, $recursive = false) + { + // Cast as integer until method is typehinted. + $groupId = (int) $groupId; + + // Get a database object. + $db = Factory::getDbo(); + + $test = $recursive ? ' >= ' : ' = '; + + // First find the users contained in the group + $query = $db->getQuery(true) + ->select('DISTINCT(' . $db->quoteName('user_id') . ')') + ->from($db->quoteName('#__usergroups', 'ug1')) + ->join( + 'INNER', + $db->quoteName('#__usergroups', 'ug2'), + $db->quoteName('ug2.lft') . $test . $db->quoteName('ug1.lft') . ' AND ' . $db->quoteName('ug1.rgt') . $test . $db->quoteName('ug2.rgt') + ) + ->join('INNER', $db->quoteName('#__user_usergroup_map', 'm'), $db->quoteName('ug2.id') . ' = ' . $db->quoteName('m.group_id')) + ->where($db->quoteName('ug1.id') . ' = :groupId') + ->bind(':groupId', $groupId, ParameterType::INTEGER); + + $db->setQuery($query); + + $result = $db->loadColumn(); + + // Clean up any NULL values, just in case + $result = ArrayHelper::toInteger($result); + + return $result; + } + + /** + * Method to return a list of view levels for which the user is authorised. + * + * @param integer $userId Id of the user for which to get the list of authorised view levels. + * + * @return array List of view levels for which the user is authorised. + * + * @since 1.7.0 + */ + public static function getAuthorisedViewLevels($userId) + { + // Only load the view levels once. + if (empty(self::$viewLevels)) { + // Get a database object. + $db = Factory::getDbo(); + + // Build the base query. + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'rules'])) + ->from($db->quoteName('#__viewlevels')); + + // Set the query for execution. + $db->setQuery($query); + + // Build the view levels array. + foreach ($db->loadAssocList() as $level) { + self::$viewLevels[$level['id']] = (array) json_decode($level['rules']); + } + } + + // Initialise the authorised array. + $authorised = array(1); + + // Check for the recovery mode setting and return early. + $user = User::getInstance($userId); + $root_user = Factory::getApplication()->get('root_user'); + + if (($user->username && $user->username == $root_user) || (is_numeric($root_user) && $user->id > 0 && $user->id == $root_user)) { + // Find the super user levels. + foreach (self::$viewLevels as $level => $rule) { + foreach ($rule as $id) { + if ($id > 0 && self::checkGroup($id, 'core.admin')) { + $authorised[] = $level; + break; + } + } + } + + return array_values(array_unique($authorised)); + } + + // Get all groups that the user is mapped to recursively. + $groups = self::getGroupsByUser($userId); + + // Find the authorised levels. + foreach (self::$viewLevels as $level => $rule) { + foreach ($rule as $id) { + if (($id < 0) && (($id * -1) == $userId)) { + $authorised[] = $level; + break; + } elseif (($id >= 0) && \in_array($id, $groups)) { + // Check to see if the group is mapped to the level. + $authorised[] = $level; + break; + } + } + } + + return array_values(array_unique($authorised)); + } + + /** + * Method to return a list of actions from a file for which permissions can be set. + * + * @param string $file The path to the XML file. + * @param string $xpath An optional xpath to search for the fields. + * + * @return boolean|array False if case of error or the list of actions available. + * + * @since 3.0.0 + */ + public static function getActionsFromFile($file, $xpath = "/access/section[@name='component']/") + { + if (!is_file($file) || !is_readable($file)) { + // If unable to find the file return false. + return false; + } else { + // Else return the actions from the xml. + $xml = simplexml_load_file($file); + + return self::getActionsFromData($xml, $xpath); + } + } + + /** + * Method to return a list of actions from a string or from an xml for which permissions can be set. + * + * @param string|\SimpleXMLElement $data The XML string or an XML element. + * @param string $xpath An optional xpath to search for the fields. + * + * @return boolean|array False if case of error or the list of actions available. + * + * @since 3.0.0 + */ + public static function getActionsFromData($data, $xpath = "/access/section[@name='component']/") + { + // If the data to load isn't already an XML element or string return false. + if ((!($data instanceof \SimpleXMLElement)) && (!\is_string($data))) { + return false; + } + + // Attempt to load the XML if a string. + if (\is_string($data)) { + try { + $data = new \SimpleXMLElement($data); + } catch (\Exception $e) { + return false; + } + + // Make sure the XML loaded correctly. + if (!$data) { + return false; + } + } + + // Initialise the actions array + $actions = array(); + + // Get the elements from the xpath + $elements = $data->xpath($xpath . 'action[@name][@title]'); + + // If there some elements, analyse them + if (!empty($elements)) { + foreach ($elements as $element) { + // Add the action to the actions array + $action = array( + 'name' => (string) $element['name'], + 'title' => (string) $element['title'], + ); + + if (isset($element['description'])) { + $action['description'] = (string) $element['description']; + } + + $actions[] = (object) $action; + } + } + + // Finally return the actions array + return $actions; + } } diff --git a/code/libraries/src/Access/Exception/AuthenticationFailed.php b/code/libraries/src/Access/Exception/AuthenticationFailed.php index 5703a129..568e7311 100644 --- a/code/libraries/src/Access/Exception/AuthenticationFailed.php +++ b/code/libraries/src/Access/Exception/AuthenticationFailed.php @@ -1,4 +1,5 @@ true, 3 => true, 4 => false) - * or an equivalent JSON encoded string. - * - * @param mixed $identities A JSON format string (probably from the database) or a named array. - * - * @since 1.7.0 - */ - public function __construct($identities) - { - // Convert string input to an array. - if (\is_string($identities)) - { - $identities = json_decode($identities, true); - } - - $this->mergeIdentities($identities); - } - - /** - * Get the data for the action. - * - * @return array A named array - * - * @since 1.7.0 - */ - public function getData() - { - return $this->data; - } - - /** - * Merges the identities - * - * @param mixed $identities An integer or array of integers representing the identities to check. - * - * @return void - * - * @since 1.7.0 - */ - public function mergeIdentities($identities) - { - if ($identities instanceof Rule) - { - $identities = $identities->getData(); - } - - if (\is_array($identities)) - { - foreach ($identities as $identity => $allow) - { - $this->mergeIdentity($identity, $allow); - } - } - } - - /** - * Merges the values for an identity. - * - * @param integer $identity The identity. - * @param boolean $allow The value for the identity (true == allow, false == deny). - * - * @return void - * - * @since 1.7.0 - */ - public function mergeIdentity($identity, $allow) - { - $identity = (int) $identity; - $allow = (int) ((boolean) $allow); - - // Check that the identity exists. - if (isset($this->data[$identity])) - { - // Explicit deny always wins a merge. - if ($this->data[$identity] !== 0) - { - $this->data[$identity] = $allow; - } - } - else - { - $this->data[$identity] = $allow; - } - } - - /** - * Checks that this action can be performed by an identity. - * - * The identity is an integer where +ve represents a user group, - * and -ve represents a user. - * - * @param mixed $identities An integer or array of integers representing the identities to check. - * - * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. - * - * @since 1.7.0 - */ - public function allow($identities) - { - // Implicit deny by default. - $result = null; - - // Check that the inputs are valid. - if (!empty($identities)) - { - if (!\is_array($identities)) - { - $identities = array($identities); - } - - foreach ($identities as $identity) - { - // Technically the identity just needs to be unique. - $identity = (int) $identity; - - // Check if the identity is known. - if (isset($this->data[$identity])) - { - $result = (boolean) $this->data[$identity]; - - // An explicit deny wins. - if ($result === false) - { - break; - } - } - } - } - - return $result; - } - - /** - * Convert this object into a JSON encoded string. - * - * @return string JSON encoded string - * - * @since 1.7.0 - */ - public function __toString() - { - return json_encode($this->data); - } + /** + * A named array + * + * @var array + * @since 1.7.0 + */ + protected $data = array(); + + /** + * Constructor. + * + * The input array must be in the form: array(-42 => true, 3 => true, 4 => false) + * or an equivalent JSON encoded string. + * + * @param mixed $identities A JSON format string (probably from the database) or a named array. + * + * @since 1.7.0 + */ + public function __construct($identities) + { + // Convert string input to an array. + if (\is_string($identities)) { + $identities = json_decode($identities, true); + } + + $this->mergeIdentities($identities); + } + + /** + * Get the data for the action. + * + * @return array A named array + * + * @since 1.7.0 + */ + public function getData() + { + return $this->data; + } + + /** + * Merges the identities + * + * @param mixed $identities An integer or array of integers representing the identities to check. + * + * @return void + * + * @since 1.7.0 + */ + public function mergeIdentities($identities) + { + if ($identities instanceof Rule) { + $identities = $identities->getData(); + } + + if (\is_array($identities)) { + foreach ($identities as $identity => $allow) { + $this->mergeIdentity($identity, $allow); + } + } + } + + /** + * Merges the values for an identity. + * + * @param integer $identity The identity. + * @param boolean $allow The value for the identity (true == allow, false == deny). + * + * @return void + * + * @since 1.7.0 + */ + public function mergeIdentity($identity, $allow) + { + $identity = (int) $identity; + $allow = (int) ((bool) $allow); + + // Check that the identity exists. + if (isset($this->data[$identity])) { + // Explicit deny always wins a merge. + if ($this->data[$identity] !== 0) { + $this->data[$identity] = $allow; + } + } else { + $this->data[$identity] = $allow; + } + } + + /** + * Checks that this action can be performed by an identity. + * + * The identity is an integer where +ve represents a user group, + * and -ve represents a user. + * + * @param mixed $identities An integer or array of integers representing the identities to check. + * + * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. + * + * @since 1.7.0 + */ + public function allow($identities) + { + // Implicit deny by default. + $result = null; + + // Check that the inputs are valid. + if (!empty($identities)) { + if (!\is_array($identities)) { + $identities = array($identities); + } + + foreach ($identities as $identity) { + // Technically the identity just needs to be unique. + $identity = (int) $identity; + + // Check if the identity is known. + if (isset($this->data[$identity])) { + $result = (bool) $this->data[$identity]; + + // An explicit deny wins. + if ($result === false) { + break; + } + } + } + } + + return $result; + } + + /** + * Convert this object into a JSON encoded string. + * + * @return string JSON encoded string + * + * @since 1.7.0 + */ + public function __toString() + { + return json_encode($this->data); + } } diff --git a/code/libraries/src/Access/Rules.php b/code/libraries/src/Access/Rules.php index 3f605ca6..39d5e551 100644 --- a/code/libraries/src/Access/Rules.php +++ b/code/libraries/src/Access/Rules.php @@ -1,4 +1,5 @@ array(-42 => true, 3 => true, 4 => false)) - * or an equivalent JSON encoded string, or an object where properties are arrays. - * - * @param mixed $input A JSON format string (probably from the database) or a nested array. - * - * @since 1.7.0 - */ - public function __construct($input = '') - { - // Convert in input to an array. - if (\is_string($input)) - { - $input = json_decode($input, true); - } - elseif (\is_object($input)) - { - $input = (array) $input; - } - - if (\is_array($input)) - { - // Top level keys represent the actions. - foreach ($input as $action => $identities) - { - $this->mergeAction($action, $identities); - } - } - } - - /** - * Get the data for the action. - * - * @return array A named array of Rule objects. - * - * @since 1.7.0 - */ - public function getData() - { - return $this->data; - } - - /** - * Method to merge a collection of Rules. - * - * @param mixed $input Rule or array of Rules - * - * @return void - * - * @since 1.7.0 - */ - public function mergeCollection($input) - { - // Check if the input is an array. - if (\is_array($input)) - { - foreach ($input as $actions) - { - $this->merge($actions); - } - } - } - - /** - * Method to merge actions with this object. - * - * @param mixed $actions Rule object, an array of actions or a JSON string array of actions. - * - * @return void - * - * @since 1.7.0 - */ - public function merge($actions) - { - if (\is_string($actions)) - { - $actions = json_decode($actions, true); - } - - if (\is_array($actions)) - { - foreach ($actions as $action => $identities) - { - $this->mergeAction($action, $identities); - } - } - elseif ($actions instanceof Rules) - { - $data = $actions->getData(); - - foreach ($data as $name => $identities) - { - $this->mergeAction($name, $identities); - } - } - } - - /** - * Merges an array of identities for an action. - * - * @param string $action The name of the action. - * @param array $identities An array of identities - * - * @return void - * - * @since 1.7.0 - */ - public function mergeAction($action, $identities) - { - if (isset($this->data[$action])) - { - // If exists, merge the action. - $this->data[$action]->mergeIdentities($identities); - } - else - { - // If new, add the action. - $this->data[$action] = new Rule($identities); - } - } - - /** - * Checks that an action can be performed by an identity. - * - * The identity is an integer where +ve represents a user group, - * and -ve represents a user. - * - * @param string $action The name of the action. - * @param mixed $identity An integer representing the identity, or an array of identities - * - * @return mixed Object or null if there is no information about the action. - * - * @since 1.7.0 - */ - public function allow($action, $identity) - { - // Check we have information about this action. - if (isset($this->data[$action])) - { - return $this->data[$action]->allow($identity); - } - } - - /** - * Get the allowed actions for an identity. - * - * @param mixed $identity An integer representing the identity or an array of identities - * - * @return CMSObject Allowed actions for the identity or identities - * - * @since 1.7.0 - */ - public function getAllowed($identity) - { - // Sweep for the allowed actions. - $allowed = new CMSObject; - - foreach ($this->data as $name => &$action) - { - if ($action->allow($identity)) - { - $allowed->set($name, true); - } - } - - return $allowed; - } - - /** - * Magic method to convert the object to JSON string representation. - * - * @return string JSON representation of the actions array - * - * @since 1.7.0 - */ - public function __toString() - { - $temp = array(); - - foreach ($this->data as $name => $rule) - { - if ($data = $rule->getData()) - { - $temp[$name] = $data; - } - } - - return json_encode($temp, JSON_FORCE_OBJECT); - } + /** + * A named array. + * + * @var array + * @since 1.7.0 + */ + protected $data = array(); + + /** + * Constructor. + * + * The input array must be in the form: array('action' => array(-42 => true, 3 => true, 4 => false)) + * or an equivalent JSON encoded string, or an object where properties are arrays. + * + * @param mixed $input A JSON format string (probably from the database) or a nested array. + * + * @since 1.7.0 + */ + public function __construct($input = '') + { + // Convert in input to an array. + if (\is_string($input)) { + $input = json_decode($input, true); + } elseif (\is_object($input)) { + $input = (array) $input; + } + + if (\is_array($input)) { + // Top level keys represent the actions. + foreach ($input as $action => $identities) { + $this->mergeAction($action, $identities); + } + } + } + + /** + * Get the data for the action. + * + * @return array A named array of Rule objects. + * + * @since 1.7.0 + */ + public function getData() + { + return $this->data; + } + + /** + * Method to merge a collection of Rules. + * + * @param mixed $input Rule or array of Rules + * + * @return void + * + * @since 1.7.0 + */ + public function mergeCollection($input) + { + // Check if the input is an array. + if (\is_array($input)) { + foreach ($input as $actions) { + $this->merge($actions); + } + } + } + + /** + * Method to merge actions with this object. + * + * @param mixed $actions Rule object, an array of actions or a JSON string array of actions. + * + * @return void + * + * @since 1.7.0 + */ + public function merge($actions) + { + if (\is_string($actions)) { + $actions = json_decode($actions, true); + } + + if (\is_array($actions)) { + foreach ($actions as $action => $identities) { + $this->mergeAction($action, $identities); + } + } elseif ($actions instanceof Rules) { + $data = $actions->getData(); + + foreach ($data as $name => $identities) { + $this->mergeAction($name, $identities); + } + } + } + + /** + * Merges an array of identities for an action. + * + * @param string $action The name of the action. + * @param array $identities An array of identities + * + * @return void + * + * @since 1.7.0 + */ + public function mergeAction($action, $identities) + { + if (isset($this->data[$action])) { + // If exists, merge the action. + $this->data[$action]->mergeIdentities($identities); + } else { + // If new, add the action. + $this->data[$action] = new Rule($identities); + } + } + + /** + * Checks that an action can be performed by an identity. + * + * The identity is an integer where +ve represents a user group, + * and -ve represents a user. + * + * @param string $action The name of the action. + * @param mixed $identity An integer representing the identity, or an array of identities + * + * @return mixed Object or null if there is no information about the action. + * + * @since 1.7.0 + */ + public function allow($action, $identity) + { + // Check we have information about this action. + if (isset($this->data[$action])) { + return $this->data[$action]->allow($identity); + } + } + + /** + * Get the allowed actions for an identity. + * + * @param mixed $identity An integer representing the identity or an array of identities + * + * @return CMSObject Allowed actions for the identity or identities + * + * @since 1.7.0 + */ + public function getAllowed($identity) + { + // Sweep for the allowed actions. + $allowed = new CMSObject(); + + foreach ($this->data as $name => &$action) { + if ($action->allow($identity)) { + $allowed->set($name, true); + } + } + + return $allowed; + } + + /** + * Magic method to convert the object to JSON string representation. + * + * @return string JSON representation of the actions array + * + * @since 1.7.0 + */ + public function __toString() + { + $temp = array(); + + foreach ($this->data as $name => $rule) { + if ($data = $rule->getData()) { + $temp[$name] = $data; + } + } + + return json_encode($temp, JSON_FORCE_OBJECT); + } } diff --git a/code/libraries/src/Adapter/Adapter.php b/code/libraries/src/Adapter/Adapter.php index 73877762..0242e4d2 100644 --- a/code/libraries/src/Adapter/Adapter.php +++ b/code/libraries/src/Adapter/Adapter.php @@ -1,4 +1,5 @@ _basepath = $basepath; - $this->_classprefix = $classprefix ?: 'J'; - $this->_adapterfolder = $adapterfolder ?: 'adapters'; - - $this->_db = Factory::getDbo(); - } - - /** - * Get the database connector object - * - * @return \Joomla\Database\DatabaseDriver Database connector object - * - * @since 1.6 - */ - public function getDbo() - { - return $this->_db; - } - - /** - * Return an adapter. - * - * @param string $name Name of adapter to return - * @param array $options Adapter options - * - * @return static|boolean Adapter of type 'name' or false - * - * @since 1.6 - */ - public function getAdapter($name, $options = array()) - { - if (array_key_exists($name, $this->_adapters)) - { - return $this->_adapters[$name]; - } - - if ($this->setAdapter($name, $options)) - { - return $this->_adapters[$name]; - } - - return false; - } - - /** - * Set an adapter by name - * - * @param string $name Adapter name - * @param object $adapter Adapter object - * @param array $options Adapter options - * - * @return boolean True if successful - * - * @since 1.6 - */ - public function setAdapter($name, &$adapter = null, $options = array()) - { - if (is_object($adapter)) - { - $this->_adapters[$name] = &$adapter; - - return true; - } - - $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name); - - if (class_exists($class)) - { - $this->_adapters[$name] = new $class($this, $this->_db, $options); - - return true; - } - - $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name) . 'Adapter'; - - if (class_exists($class)) - { - $this->_adapters[$name] = new $class($this, $this->_db, $options); - - return true; - } - - $fullpath = $this->_basepath . '/' . $this->_adapterfolder . '/' . strtolower($name) . '.php'; - - if (!is_file($fullpath)) - { - return false; - } - - // Try to load the adapter object - $class = $this->_classprefix . ucfirst($name); - - \JLoader::register($class, $fullpath); - - if (!class_exists($class)) - { - return false; - } - - $this->_adapters[$name] = new $class($this, $this->_db, $options); - - return true; - } - - /** - * Loads all adapters. - * - * @param array $options Adapter options - * - * @return void - * - * @since 1.6 - */ - public function loadAllAdapters($options = array()) - { - $files = new \DirectoryIterator($this->_basepath . '/' . $this->_adapterfolder); - - /** @type $file \DirectoryIterator */ - foreach ($files as $file) - { - $fileName = $file->getFilename(); - - // Only load for php files. - if (!$file->isFile() || $file->getExtension() != 'php') - { - continue; - } - - // Try to load the adapter object - require_once $this->_basepath . '/' . $this->_adapterfolder . '/' . $fileName; - - // Derive the class name from the filename. - $name = str_ireplace('.php', '', ucfirst(trim($fileName))); - $class = $this->_classprefix . ucfirst($name); - - if (!class_exists($class)) - { - // Skip to next one - continue; - } - - $adapter = new $class($this, $this->_db, $options); - $this->_adapters[$name] = clone $adapter; - } - } + /** + * Associative array of adapters + * + * @var static[] + * @since 1.6 + */ + protected $_adapters = array(); + + /** + * Adapter Folder + * + * @var string + * @since 1.6 + */ + protected $_adapterfolder = 'adapters'; + + /** + * Adapter Class Prefix + * + * @var string + * @since 1.6 + */ + protected $_classprefix = 'J'; + + /** + * Base Path for the adapter instance + * + * @var string + * @since 1.6 + */ + protected $_basepath = null; + + /** + * Database Connector Object + * + * @var \Joomla\Database\DatabaseDriver + * @since 1.6 + */ + protected $_db; + + /** + * Constructor + * + * @param string $basepath Base Path of the adapters + * @param string $classprefix Class prefix of adapters + * @param string $adapterfolder Name of folder to append to base path + * + * @since 1.6 + */ + public function __construct($basepath, $classprefix = null, $adapterfolder = null) + { + $this->_basepath = $basepath; + $this->_classprefix = $classprefix ?: 'J'; + $this->_adapterfolder = $adapterfolder ?: 'adapters'; + + $this->_db = Factory::getDbo(); + + // Ensure BC, when removed in 5, then the db must be set with setDatabase explicitly + if ($this instanceof DatabaseAwareInterface) { + $this->setDatabase($this->_db); + } + } + + /** + * Get the database connector object + * + * @return \Joomla\Database\DatabaseDriver Database connector object + * + * @since 1.6 + */ + public function getDbo() + { + return $this->_db; + } + + /** + * Return an adapter. + * + * @param string $name Name of adapter to return + * @param array $options Adapter options + * + * @return static|boolean Adapter of type 'name' or false + * + * @since 1.6 + */ + public function getAdapter($name, $options = array()) + { + if (array_key_exists($name, $this->_adapters)) { + return $this->_adapters[$name]; + } + + if ($this->setAdapter($name, $options)) { + return $this->_adapters[$name]; + } + + return false; + } + + /** + * Set an adapter by name + * + * @param string $name Adapter name + * @param object $adapter Adapter object + * @param array $options Adapter options + * + * @return boolean True if successful + * + * @since 1.6 + */ + public function setAdapter($name, &$adapter = null, $options = array()) + { + if (is_object($adapter)) { + $this->_adapters[$name] = &$adapter; + + return true; + } + + $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name); + + if (class_exists($class)) { + $this->_adapters[$name] = new $class($this, $this->_db, $options); + + return true; + } + + $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name) . 'Adapter'; + + if (class_exists($class)) { + $this->_adapters[$name] = new $class($this, $this->_db, $options); + + return true; + } + + $fullpath = $this->_basepath . '/' . $this->_adapterfolder . '/' . strtolower($name) . '.php'; + + if (!is_file($fullpath)) { + return false; + } + + // Try to load the adapter object + $class = $this->_classprefix . ucfirst($name); + + \JLoader::register($class, $fullpath); + + if (!class_exists($class)) { + return false; + } + + $this->_adapters[$name] = new $class($this, $this->_db, $options); + + return true; + } + + /** + * Loads all adapters. + * + * @param array $options Adapter options + * + * @return void + * + * @since 1.6 + */ + public function loadAllAdapters($options = array()) + { + $files = new \DirectoryIterator($this->_basepath . '/' . $this->_adapterfolder); + + /** @type $file \DirectoryIterator */ + foreach ($files as $file) { + $fileName = $file->getFilename(); + + // Only load for php files. + if (!$file->isFile() || $file->getExtension() != 'php') { + continue; + } + + // Try to load the adapter object + require_once $this->_basepath . '/' . $this->_adapterfolder . '/' . $fileName; + + // Derive the class name from the filename. + $name = str_ireplace('.php', '', ucfirst(trim($fileName))); + $class = $this->_classprefix . ucfirst($name); + + if (!class_exists($class)) { + // Skip to next one + continue; + } + + $adapter = new $class($this, $this->_db, $options); + $this->_adapters[$name] = clone $adapter; + } + } } diff --git a/code/libraries/src/Adapter/AdapterInstance.php b/code/libraries/src/Adapter/AdapterInstance.php index 297ba584..b3885380 100644 --- a/code/libraries/src/Adapter/AdapterInstance.php +++ b/code/libraries/src/Adapter/AdapterInstance.php @@ -1,4 +1,5 @@ setProperties($options); + /** + * Constructor + * + * @param Adapter $parent Parent object + * @param DatabaseDriver $db Database object + * @param array $options Configuration Options + * + * @since 1.6 + */ + public function __construct(Adapter $parent, DatabaseDriver $db, array $options = array()) + { + // Set the properties from the options array that is passed in + $this->setProperties($options); - // Set the parent and db in case $options for some reason overrides it. - $this->parent = $parent; + // Set the parent and db in case $options for some reason overrides it. + $this->parent = $parent; - // Pull in the global dbo in case something happened to it. - $this->db = $db ?: Factory::getDbo(); - } + // Pull in the global dbo in case something happened to it. + $this->db = $db ?: Factory::getDbo(); + } - /** - * Retrieves the parent object - * - * @return Adapter - * - * @since 1.6 - */ - public function getParent() - { - return $this->parent; - } + /** + * Retrieves the parent object + * + * @return Adapter + * + * @since 1.6 + */ + public function getParent() + { + return $this->parent; + } } diff --git a/code/libraries/src/Application/AdministratorApplication.php b/code/libraries/src/Application/AdministratorApplication.php index 29f4d735..4bcf41c9 100644 --- a/code/libraries/src/Application/AdministratorApplication.php +++ b/code/libraries/src/Application/AdministratorApplication.php @@ -1,4 +1,5 @@ name = 'administrator'; - - // Register the client ID - $this->clientId = 1; - - // Execute the parent constructor - parent::__construct($input, $config, $client, $container); - - // Set the root in the URI based on the application name - Uri::root(null, rtrim(\dirname(Uri::base(true)), '/\\')); - } - - /** - * Dispatch the application - * - * @param string $component The component which is being rendered. - * - * @return void - * - * @since 3.2 - */ - public function dispatch($component = null) - { - if ($component === null) - { - $component = $this->findOption(); - } - - // Load the document to the API - $this->loadDocument(); - - // Set up the params - $document = Factory::getDocument(); - - // Register the document object with Factory - Factory::$document = $document; - - switch ($document->getType()) - { - case 'html': - // Get the template - $template = $this->getTemplate(true); - $clientId = $this->getClientId(); - - // Store the template and its params to the config - $this->set('theme', $template->template); - $this->set('themeParams', $template->params); - - // Add Asset registry files - $wr = $document->getWebAssetManager()->getRegistry(); - - if ($component) - { - $wr->addExtensionRegistryFile($component); - } - - if (!empty($template->parent)) - { - $wr->addTemplateRegistryFile($template->parent, $clientId); - } - - $wr->addTemplateRegistryFile($template->template, $clientId); - - break; - - default: - break; - } - - $document->setTitle($this->get('sitename') . ' - ' . Text::_('JADMINISTRATION')); - $document->setDescription($this->get('MetaDesc')); - $document->setGenerator('Joomla! - Open Source Content Management'); - - $contents = ComponentHelper::renderComponent($component); - $document->setBuffer($contents, 'component'); - - // Trigger the onAfterDispatch event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterDispatch'); - } - - /** - * Method to run the Web application routines. - * - * @return void - * - * @since 3.2 - */ - protected function doExecute() - { - // Get the language from the (login) form or user state - $login_lang = ($this->input->get('option') === 'com_login') ? $this->input->get('lang') : ''; - $options = array('language' => $login_lang ?: $this->getUserState('application.lang')); - - // Initialise the application - $this->initialiseApp($options); - - // Mark afterInitialise in the profiler. - JDEBUG ? $this->profiler->mark('afterInitialise') : null; - - // Route the application - $this->route(); - - // Mark afterRoute in the profiler. - JDEBUG ? $this->profiler->mark('afterRoute') : null; - - /* - * Check if the user is required to reset their password - * - * Before $this->route(); "option" and "view" can't be safely read using: - * $this->input->getCmd('option'); or $this->input->getCmd('view'); - * ex: due of the sef urls - */ - $this->checkUserRequireReset('com_users', 'user', 'edit', 'com_users/user.edit,com_users/user.save,com_users/user.apply,com_login/logout'); - - // Dispatch the application - $this->dispatch(); - - // Mark afterDispatch in the profiler. - JDEBUG ? $this->profiler->mark('afterDispatch') : null; - } - - /** - * Return a reference to the Router object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return Router - * - * @since 3.2 - */ - public static function getRouter($name = 'administrator', array $options = array()) - { - return parent::getRouter($name, $options); - } - - /** - * Gets the name of the current template. - * - * @param boolean $params True to return the template parameters - * - * @return string The name of the template. - * - * @since 3.2 - * @throws \InvalidArgumentException - */ - public function getTemplate($params = false) - { - if (\is_object($this->template)) - { - if ($params) - { - return $this->template; - } - - return $this->template->template; - } - - $admin_style = (int) Factory::getUser()->getParam('admin_style'); - - // Load the template name from the database - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName(['s.template', 's.params', 's.inheritable', 's.parent'])) - ->from($db->quoteName('#__template_styles', 's')) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.type') . ' = ' . $db->quote('template') - . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template') - . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id') - ) - ->where( - [ - $db->quoteName('s.client_id') . ' = 1', - $db->quoteName('s.home') . ' = ' . $db->quote('1'), - ] - ); - - if ($admin_style) - { - $query->extendWhere( - 'OR', - [ - $db->quoteName('s.client_id') . ' = 1', - $db->quoteName('s.id') . ' = :style', - $db->quoteName('e.enabled') . ' = 1', - ] - ) - ->bind(':style', $admin_style, ParameterType::INTEGER); - } - - $query->order($db->quoteName('s.home')); - $db->setQuery($query); - $template = $db->loadObject(); - - $template->template = InputFilter::getInstance()->clean($template->template, 'cmd'); - $template->params = new Registry($template->params); - - // Fallback template - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php') - && !is_file(JPATH_THEMES . '/' . $template->parent . '/index.php')) - { - $this->getLogger()->error(Text::_('JERROR_ALERTNOTEMPLATE'), ['category' => 'system']); - $template->params = new Registry; - $template->template = 'atum'; - - // Check, the data were found and if template really exists - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $template->template)); - } - } - - // Cache the result - $this->template = $template; - - // Pass the parent template to the state - $this->set('themeInherits', $template->parent); - - if ($params) - { - return $template; - } - - return $template->template; - } - - /** - * Initialise the application. - * - * @param array $options An optional associative array of configuration settings. - * - * @return void - * - * @since 3.2 - */ - protected function initialiseApp($options = array()) - { - $user = Factory::getUser(); - - // If the user is a guest we populate it with the guest user group. - if ($user->guest) - { - $guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); - $user->groups = array($guestUsergroup); - } - - // If a language was specified it has priority, otherwise use user or default language settings - if (empty($options['language'])) - { - $lang = $user->getParam('admin_language'); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - else - { - $params = ComponentHelper::getParams('com_languages'); - $options['language'] = $params->get('administrator', $this->get('language', 'en-GB')); - } - } - - // One last check to make sure we have something - if (!LanguageHelper::exists($options['language'])) - { - $lang = $this->get('language', 'en-GB'); - - if (LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - else - { - // As a last ditch fail to english - $options['language'] = 'en-GB'; - } - } - - // Finish initialisation - parent::initialiseApp($options); - } - - /** - * Login authentication function - * - * @param array $credentials Array('username' => string, 'password' => string) - * @param array $options Array('remember' => boolean) - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function login($credentials, $options = array()) - { - // The minimum group - $options['group'] = 'Public Backend'; - - // Make sure users are not auto-registered - $options['autoregister'] = false; - - // Set the application login entry point - if (!\array_key_exists('entry_url', $options)) - { - $options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=login'; - } - - // Set the access control action to check. - $options['action'] = 'core.login.admin'; - - $result = parent::login($credentials, $options); - - if (!($result instanceof \Exception)) - { - $lang = $this->input->getCmd('lang', ''); - $lang = preg_replace('/[^A-Z-]/i', '', $lang); - - if ($lang) - { - $this->setUserState('application.lang', $lang); - } - - static::purgeMessages(); - } - - return $result; - } - - /** - * Purge the jos_messages table of old messages - * - * @return void - * - * @since 3.2 - */ - public static function purgeMessages() - { - $userId = Factory::getUser()->id; - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName(['cfg_name', 'cfg_value'])) - ->from($db->quoteName('#__messages_cfg')) - ->where( - [ - $db->quoteName('user_id') . ' = :userId', - $db->quoteName('cfg_name') . ' = ' . $db->quote('auto_purge'), - ] - ) - ->bind(':userId', $userId, ParameterType::INTEGER); - - $db->setQuery($query); - $config = $db->loadObject(); - - // Check if auto_purge value set - if (\is_object($config) && $config->cfg_name === 'auto_purge') - { - $purge = $config->cfg_value; - } - else - { - // If no value set, default is 7 days - $purge = 7; - } - - // If purge value is not 0, then allow purging of old messages - if ($purge > 0) - { - // Purge old messages at day set in message configuration - $past = Factory::getDate(time() - $purge * 86400)->toSql(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__messages')) - ->where( - [ - $db->quoteName('date_time') . ' < :past', - $db->quoteName('user_id_to') . ' = :userId', - ] - ) - ->bind(':past', $past) - ->bind(':userId', $userId, ParameterType::INTEGER); - - $db->setQuery($query); - $db->execute(); - } - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 3.2 - */ - protected function render() - { - // Get the \JInput object - $input = $this->input; - - $component = $input->getCmd('option', 'com_login'); - $file = $input->getCmd('tmpl', 'index'); - - if ($component === 'com_login') - { - $file = 'login'; - } - - $this->set('themeFile', $file . '.php'); - - // Safety check for when configuration.php root_user is in use. - $rootUser = $this->get('root_user'); - - if (property_exists('\JConfig', 'root_user')) - { - if (Factory::getUser()->get('username') === $rootUser || Factory::getUser()->id === (string) $rootUser) - { - $this->enqueueMessage( - Text::sprintf( - 'JWARNING_REMOVE_ROOT_USER', - 'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1' - ), - 'warning' - ); - } - // Show this message to superusers too - elseif (Factory::getUser()->authorise('core.admin')) - { - $this->enqueueMessage( - Text::sprintf( - 'JWARNING_REMOVE_ROOT_USER_ADMIN', - $rootUser, - 'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1' - ), - 'warning' - ); - } - } - - parent::render(); - } - - /** - * Route the application. - * - * Routing is the process of examining the request environment to determine which - * component should receive the request. The component optional parameters - * are then set in the request object to be processed when the application is being - * dispatched. - * - * @return void - * - * @since 3.2 - */ - protected function route() - { - $uri = Uri::getInstance(); - - if ($this->get('force_ssl') >= 1 && strtolower($uri->getScheme()) !== 'https') - { - // Forward to https - $uri->setScheme('https'); - $this->redirect((string) $uri, 301); - } - - if ($this->isTwoFactorAuthenticationRequired()) - { - $this->redirectIfTwoFactorAuthenticationRequired(); - } - - // Trigger the onAfterRoute event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterRoute'); - } - - /** - * Return the application option string [main component]. - * - * @return string The component to access. - * - * @since 4.0.0 - */ - public function findOption(): string - { - /** @var self $app */ - $app = Factory::getApplication(); - $option = strtolower($app->input->get('option', '')); - $user = $app->getIdentity(); - - /** - * Special handling for guest users and authenticated users without the Backend Login privilege. - * - * If the component they are trying to access is in the $this->allowedUnprivilegedOptions array we allow the - * request to go through. Otherwise we force com_login to be loaded, letting the user (re)try authenticating - * with a user account that has the Backend Login privilege. - */ - if ($user->get('guest') || !$user->authorise('core.login.admin')) - { - $option = in_array($option, $this->allowedUnprivilegedOptions) ? $option : 'com_login'; - } - - /** - * If no component is defined in the request we will try to load com_cpanel, the administrator Control Panel - * component. This allows the /administrator URL to display something meaningful after logging in instead of an - * error. - */ - if (empty($option)) - { - $option = 'com_cpanel'; - } - - /** - * Force the option to the input object. This is necessary because we might have force-changed the component in - * the two if-blocks above. - */ - $app->input->set('option', $option); - - return $option; - } + use MultiFactorAuthenticationHandler; + + /** + * List of allowed components for guests and users which do not have the core.login.admin privilege. + * + * By default we allow two core components: + * + * - com_login Absolutely necessary to let users log into the backend of the site. Do NOT remove! + * - com_ajax Handle AJAX requests or other administrative callbacks without logging in. Required for + * passwordless authentication using WebAuthn. + * + * @var array + */ + protected $allowedUnprivilegedOptions = [ + 'com_login', + 'com_ajax', + ]; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's config + * object. If the argument is a Registry object that object will become the + * application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's + * client object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container $container Dependency injection container. + * + * @since 3.2 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + // Register the application name + $this->name = 'administrator'; + + // Register the client ID + $this->clientId = 1; + + // Execute the parent constructor + parent::__construct($input, $config, $client, $container); + + // Set the root in the URI based on the application name + Uri::root(null, rtrim(\dirname(Uri::base(true)), '/\\')); + } + + /** + * Dispatch the application + * + * @param string $component The component which is being rendered. + * + * @return void + * + * @since 3.2 + */ + public function dispatch($component = null) + { + if ($component === null) { + $component = $this->findOption(); + } + + // Load the document to the API + $this->loadDocument(); + + // Set up the params + $document = Factory::getDocument(); + + // Register the document object with Factory + Factory::$document = $document; + + switch ($document->getType()) { + case 'html': + // Get the template + $template = $this->getTemplate(true); + $clientId = $this->getClientId(); + + // Store the template and its params to the config + $this->set('theme', $template->template); + $this->set('themeParams', $template->params); + + // Add Asset registry files + $wr = $document->getWebAssetManager()->getRegistry(); + + if ($component) { + $wr->addExtensionRegistryFile($component); + } + + if (!empty($template->parent)) { + $wr->addTemplateRegistryFile($template->parent, $clientId); + } + + $wr->addTemplateRegistryFile($template->template, $clientId); + + break; + + default: + break; + } + + $document->setTitle($this->get('sitename') . ' - ' . Text::_('JADMINISTRATION')); + $document->setDescription($this->get('MetaDesc')); + $document->setGenerator('Joomla! - Open Source Content Management'); + + $contents = ComponentHelper::renderComponent($component); + $document->setBuffer($contents, 'component'); + + // Trigger the onAfterDispatch event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterDispatch'); + } + + /** + * Method to run the Web application routines. + * + * @return void + * + * @since 3.2 + */ + protected function doExecute() + { + // Get the language from the (login) form or user state + $login_lang = ($this->input->get('option') === 'com_login') ? $this->input->get('lang') : ''; + $options = array('language' => $login_lang ?: $this->getUserState('application.lang')); + + // Initialise the application + $this->initialiseApp($options); + + // Mark afterInitialise in the profiler. + JDEBUG ? $this->profiler->mark('afterInitialise') : null; + + // Route the application + $this->route(); + + // Mark afterRoute in the profiler. + JDEBUG ? $this->profiler->mark('afterRoute') : null; + + /* + * Check if the user is required to reset their password + * + * Before $this->route(); "option" and "view" can't be safely read using: + * $this->input->getCmd('option'); or $this->input->getCmd('view'); + * ex: due of the sef urls + */ + $this->checkUserRequireReset('com_users', 'user', 'edit', 'com_users/user.edit,com_users/user.save,com_users/user.apply,com_login/logout'); + + // Dispatch the application + $this->dispatch(); + + // Mark afterDispatch in the profiler. + JDEBUG ? $this->profiler->mark('afterDispatch') : null; + } + + /** + * Return a reference to the Router object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return Router + * + * @since 3.2 + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public static function getRouter($name = 'administrator', array $options = array()) + { + return parent::getRouter($name, $options); + } + + /** + * Gets the name of the current template. + * + * @param boolean $params True to return the template parameters + * + * @return string The name of the template. + * + * @since 3.2 + * @throws \InvalidArgumentException + */ + public function getTemplate($params = false) + { + if (\is_object($this->template)) { + if ($params) { + return $this->template; + } + + return $this->template->template; + } + + $adminStyle = $this->getIdentity() ? (int) $this->getIdentity()->getParam('admin_style') : 0; + $template = $this->bootComponent('templates')->getMVCFactory() + ->createModel('Style', 'Administrator')->getAdminTemplate($adminStyle); + + $template->template = InputFilter::getInstance()->clean($template->template, 'cmd'); + $template->params = new Registry($template->params); + + // Fallback template + if ( + !is_file(JPATH_THEMES . '/' . $template->template . '/index.php') + && !is_file(JPATH_THEMES . '/' . $template->parent . '/index.php') + ) { + $this->getLogger()->error(Text::_('JERROR_ALERTNOTEMPLATE'), ['category' => 'system']); + $template->params = new Registry(); + $template->template = 'atum'; + + // Check, the data were found and if template really exists + if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $template->template)); + } + } + + // Cache the result + $this->template = $template; + + // Pass the parent template to the state + $this->set('themeInherits', $template->parent); + + if ($params) { + return $template; + } + + return $template->template; + } + + /** + * Initialise the application. + * + * @param array $options An optional associative array of configuration settings. + * + * @return void + * + * @since 3.2 + */ + protected function initialiseApp($options = array()) + { + $user = Factory::getUser(); + + // If the user is a guest we populate it with the guest user group. + if ($user->guest) { + $guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); + $user->groups = array($guestUsergroup); + } + + // If a language was specified it has priority, otherwise use user or default language settings + if (empty($options['language'])) { + $lang = $user->getParam('admin_language'); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } else { + $params = ComponentHelper::getParams('com_languages'); + $options['language'] = $params->get('administrator', $this->get('language', 'en-GB')); + } + } + + // One last check to make sure we have something + if (!LanguageHelper::exists($options['language'])) { + $lang = $this->get('language', 'en-GB'); + + if (LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } else { + // As a last ditch fail to english + $options['language'] = 'en-GB'; + } + } + + // Finish initialisation + parent::initialiseApp($options); + } + + /** + * Login authentication function + * + * @param array $credentials Array('username' => string, 'password' => string) + * @param array $options Array('remember' => boolean) + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function login($credentials, $options = array()) + { + // The minimum group + $options['group'] = 'Public Backend'; + + // Make sure users are not auto-registered + $options['autoregister'] = false; + + // Set the application login entry point + if (!\array_key_exists('entry_url', $options)) { + $options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=login'; + } + + // Set the access control action to check. + $options['action'] = 'core.login.admin'; + + $result = parent::login($credentials, $options); + + if (!($result instanceof \Exception)) { + $lang = $this->input->getCmd('lang', ''); + $lang = preg_replace('/[^A-Z-]/i', '', $lang); + + if ($lang) { + $this->setUserState('application.lang', $lang); + } + + $this->bootComponent('messages')->getMVCFactory() + ->createModel('Messages', 'Administrator')->purge($this->getIdentity() ? $this->getIdentity()->id : 0); + } + + return $result; + } + + /** + * Purge the jos_messages table of old messages + * + * @return void + * + * @since 3.2 + * + * @deprecated 5.0 Purge the messages through the model + */ + public static function purgeMessages() + { + Factory::getApplication()->bootComponent('messages')->getMVCFactory() + ->createModel('Messages', 'Administrator')->purge(Factory::getUser()->id); + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 3.2 + */ + protected function render() + { + // Get the \JInput object + $input = $this->input; + + $component = $input->getCmd('option', 'com_login'); + $file = $input->getCmd('tmpl', 'index'); + + if ($component === 'com_login') { + $file = 'login'; + } + + $this->set('themeFile', $file . '.php'); + + // Safety check for when configuration.php root_user is in use. + $rootUser = $this->get('root_user'); + + if (property_exists('\JConfig', 'root_user')) { + if (Factory::getUser()->get('username') === $rootUser || Factory::getUser()->id === (string) $rootUser) { + $this->enqueueMessage( + Text::sprintf( + 'JWARNING_REMOVE_ROOT_USER', + 'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1' + ), + 'warning' + ); + } elseif (Factory::getUser()->authorise('core.admin')) { + // Show this message to superusers too + $this->enqueueMessage( + Text::sprintf( + 'JWARNING_REMOVE_ROOT_USER_ADMIN', + $rootUser, + 'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1' + ), + 'warning' + ); + } + } + + parent::render(); + } + + /** + * Route the application. + * + * Routing is the process of examining the request environment to determine which + * component should receive the request. The component optional parameters + * are then set in the request object to be processed when the application is being + * dispatched. + * + * @return void + * + * @since 3.2 + */ + protected function route() + { + $uri = Uri::getInstance(); + + if ($this->get('force_ssl') >= 1 && strtolower($uri->getScheme()) !== 'https') { + // Forward to https + $uri->setScheme('https'); + $this->redirect((string) $uri, 301); + } + + $this->isHandlingMultiFactorAuthentication(); + + // Trigger the onAfterRoute event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterRoute'); + } + + /** + * Return the application option string [main component]. + * + * @return string The component to access. + * + * @since 4.0.0 + */ + public function findOption(): string + { + /** @var self $app */ + $app = Factory::getApplication(); + $option = strtolower($app->input->get('option', '')); + $user = $app->getIdentity(); + + /** + * Special handling for guest users and authenticated users without the Backend Login privilege. + * + * If the component they are trying to access is in the $this->allowedUnprivilegedOptions array we allow the + * request to go through. Otherwise we force com_login to be loaded, letting the user (re)try authenticating + * with a user account that has the Backend Login privilege. + */ + if ($user->get('guest') || !$user->authorise('core.login.admin')) { + $option = in_array($option, $this->allowedUnprivilegedOptions) ? $option : 'com_login'; + } + + /** + * If no component is defined in the request we will try to load com_cpanel, the administrator Control Panel + * component. This allows the /administrator URL to display something meaningful after logging in instead of an + * error. + */ + if (empty($option)) { + $option = 'com_cpanel'; + } + + /** + * Force the option to the input object. This is necessary because we might have force-changed the component in + * the two if-blocks above. + */ + $app->input->set('option', $option); + + return $option; + } } diff --git a/code/libraries/src/Application/ApiApplication.php b/code/libraries/src/Application/ApiApplication.php index 458791ce..6b8f338b 100644 --- a/code/libraries/src/Application/ApiApplication.php +++ b/code/libraries/src/Application/ApiApplication.php @@ -1,4 +1,5 @@ name = 'api'; - - // Register the client ID - $this->clientId = 3; - - // Execute the parent constructor - parent::__construct($input, $config, $client, $container); - - $this->addFormatMap('application/json', 'json'); - $this->addFormatMap('application/vnd.api+json', 'jsonapi'); - - // Set the root in the URI based on the application name - Uri::root(null, str_ireplace('/' . $this->getName(), '', Uri::base(true))); - } - - - /** - * Method to run the application routines. - * - * Most likely you will want to instantiate a controller and execute it, or perform some sort of task directly. - * - * @return void - * - * @since 4.0.0 - */ - protected function doExecute() - { - // Initialise the application - $this->initialiseApp(); - - // Mark afterInitialise in the profiler. - JDEBUG ? $this->profiler->mark('afterInitialise') : null; - - // Route the application - $this->route(); - - // Mark afterApiRoute in the profiler. - JDEBUG ? $this->profiler->mark('afterApiRoute') : null; - - // Dispatch the application - $this->dispatch(); - - // Mark afterDispatch in the profiler. - JDEBUG ? $this->profiler->mark('afterDispatch') : null; - } - - /** - * Adds a mapping from a content type to the format stored. Note the format type cannot be overwritten. - * - * @param string $contentHeader The content header - * @param string $format The content type format - * - * @return void - * - * @since 4.0.0 - */ - public function addFormatMap($contentHeader, $format) - { - if (!\array_key_exists($contentHeader, $this->formatMapper)) - { - $this->formatMapper[$contentHeader] = $format; - } - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 4.0.0 - * - * @note Rendering should be overridden to get rid of the theme files. - */ - protected function render() - { - // Render the document - $this->setBody($this->document->render($this->allowCache())); - } - - /** - * Method to send the application response to the client. All headers will be sent prior to the main application output data. - * - * @param array $options An optional argument to enable CORS. (Temporary) - * - * @return void - * - * @since 4.0.0 - */ - protected function respond($options = array()) - { - // Set the Joomla! API signature - $this->setHeader('X-Powered-By', 'JoomlaAPI/1.0', true); - - $forceCORS = (int) $this->get('cors'); - - if ($forceCORS) - { - /** - * Enable CORS (Cross-origin resource sharing) - * Obtain allowed CORS origin from Global Settings. - * Set to * (=all) if not set. - */ - $allowedOrigin = $this->get('cors_allow_origin', '*'); - $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin, true); - $this->setHeader('Access-Control-Allow-Headers', 'Authorization'); - - if ($this->input->server->getString('HTTP_ORIGIN', null) !== null) - { - $this->setHeader('Access-Control-Allow-Origin', $this->input->server->getString('HTTP_ORIGIN'), true); - $this->setHeader('Access-Control-Allow-Credentials', 'true', true); - } - } - - // Parent function can be overridden later on for debugging. - parent::respond(); - } - - /** - * Gets the name of the current template. - * - * @param boolean $params True to return the template parameters - * - * @return string|\stdClass - * - * @since 4.0.0 - */ - public function getTemplate($params = false) - { - // The API application should not need to use a template - if ($params) - { - $template = new \stdClass; - $template->template = 'system'; - $template->params = new Registry; - $template->inheritable = 0; - $template->parent = ''; - - return $template; - } - - return 'system'; - } - - /** - * Route the application. - * - * Routing is the process of examining the request environment to determine which - * component should receive the request. The component optional parameters - * are then set in the request object to be processed when the application is being - * dispatched. - * - * @return void - * - * @since 4.0.0 - */ - protected function route() - { - $router = $this->getApiRouter(); - - // Trigger the onBeforeApiRoute event. - PluginHelper::importPlugin('webservices'); - $this->triggerEvent('onBeforeApiRoute', array(&$router, $this)); - $caught404 = false; - $method = $this->input->getMethod(); - - try - { - $this->handlePreflight($method, $router); - - $route = $router->parseApiRoute($method); - } - catch (RouteNotFoundException $e) - { - $caught404 = true; - } - - /** - * Now we have an API perform content negotiation to ensure we have a valid header. Assume if the route doesn't - * tell us otherwise it uses the plain JSON API - */ - $priorities = array('application/vnd.api+json'); - - if (!$caught404 && \array_key_exists('format', $route['vars'])) - { - $priorities = $route['vars']['format']; - } - - $negotiator = new Negotiator; - - try - { - $mediaType = $negotiator->getBest($this->input->server->getString('HTTP_ACCEPT'), $priorities); - } - catch (InvalidArgument $e) - { - $mediaType = null; - } - - // If we can't find a match bail with a 406 - Not Acceptable - if ($mediaType === null) - { - throw new Exception\NotAcceptable('Could not match accept header', 406); - } - - /** @var $mediaType Accept */ - $format = $mediaType->getValue(); - - if (\array_key_exists($mediaType->getValue(), $this->formatMapper)) - { - $format = $this->formatMapper[$mediaType->getValue()]; - } - - $this->input->set('format', $format); - - if ($caught404) - { - throw $e; - } - - $this->input->set('option', $route['vars']['component']); - $this->input->set('controller', $route['controller']); - $this->input->set('task', $route['task']); - - foreach ($route['vars'] as $key => $value) - { - if ($key !== 'component') - { - if ($this->input->getMethod() === 'POST') - { - $this->input->post->set($key, $value); - } - else - { - $this->input->set($key, $value); - } - } - } - - $this->triggerEvent('onAfterApiRoute', array($this)); - - if (!isset($route['vars']['public']) || $route['vars']['public'] === false) - { - if (!$this->login(array('username' => ''), array('silent' => true, 'action' => 'core.login.api'))) - { - throw new AuthenticationFailed; - } - } - } - - /** - * Handles preflight requests. - * - * @param String $method The REST verb - * - * @param ApiRouter $router The API Routing object - * - * @return void - * - * @since 4.0.0 - */ - protected function handlePreflight($method, $router) - { - /** - * If not an OPTIONS request or CORS is not enabled, - * there's nothing useful to do here. - */ - if ($method !== 'OPTIONS' || !(int) $this->get('cors')) - { - return; - } - - // Extract routes matching current route from all known routes. - $matchingRoutes = $router->getMatchingRoutes(); - - // Extract exposed methods from matching routes. - $matchingRoutesMethods = array_unique( - array_reduce($matchingRoutes, - function ($carry, $route) { - return array_merge($carry, $route->getMethods()); - }, - [] - ) - ); - - /** - * Obtain allowed CORS origin from Global Settings. - * Set to * (=all) if not set. - */ - $allowedOrigin = $this->get('cors_allow_origin', '*'); - - /** - * Obtain allowed CORS headers from Global Settings. - * Set to sensible default if not set. - */ - $allowedHeaders = $this->get('cors_allow_headers', 'Content-Type,X-Joomla-Token'); - - /** - * Obtain allowed CORS methods from Global Settings. - * Set to methods exposed by current route if not set. - */ - $allowedMethods = $this->get('cors_allow_methods', implode(',', $matchingRoutesMethods)); - - // No use to go through the regular route handling hassle, - // so let's simply output the headers and exit. - $this->setHeader('status', '204'); - $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin); - $this->setHeader('Access-Control-Allow-Headers', $allowedHeaders); - $this->setHeader('Access-Control-Allow-Methods', $allowedMethods); - $this->sendHeaders(); - - $this->close(); - } - - /** - * Returns the application Router object. - * - * @return ApiRouter - * - * @since 4.0.0 - */ - public function getApiRouter() - { - return $this->getContainer()->get('ApiRouter'); - } - - /** - * Dispatch the application - * - * @param string $component The component which is being rendered. - * - * @return void - * - * @since 4.0.0 - */ - public function dispatch($component = null) - { - // Get the component if not set. - if (!$component) - { - $component = $this->input->get('option', null); - } - - // Load the document to the API - $this->loadDocument(); - - // Set up the params - $document = Factory::getDocument(); - - // Register the document object with Factory - Factory::$document = $document; - - $contents = ComponentHelper::renderComponent($component); - $document->setBuffer($contents, 'component'); - - // Trigger the onAfterDispatch event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterDispatch'); - } + /** + * Maps extension types to their + * + * @var array + * @since 4.0.0 + */ + protected $formatMapper = array(); + + /** + * The authentication plugin type + * + * @var string + * @since 4.0.0 + */ + protected $authenticationPluginType = 'api-authentication'; + + /** + * Class constructor. + * + * @param JInputJson $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's config + * object. If the argument is a Registry object that object will become the + * application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's client + * object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container $container Dependency injection container. + * + * @since 4.0.0 + */ + public function __construct(JInputJson $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + // Register the application name + $this->name = 'api'; + + // Register the client ID + $this->clientId = 3; + + // Execute the parent constructor + parent::__construct($input, $config, $client, $container); + + $this->addFormatMap('application/json', 'json'); + $this->addFormatMap('application/vnd.api+json', 'jsonapi'); + + // Set the root in the URI based on the application name + Uri::root(null, str_ireplace('/' . $this->getName(), '', Uri::base(true))); + } + + + /** + * Method to run the application routines. + * + * Most likely you will want to instantiate a controller and execute it, or perform some sort of task directly. + * + * @return void + * + * @since 4.0.0 + */ + protected function doExecute() + { + // Initialise the application + $this->initialiseApp(); + + // Mark afterInitialise in the profiler. + JDEBUG ? $this->profiler->mark('afterInitialise') : null; + + // Route the application + $this->route(); + + // Mark afterApiRoute in the profiler. + JDEBUG ? $this->profiler->mark('afterApiRoute') : null; + + // Dispatch the application + $this->dispatch(); + + // Mark afterDispatch in the profiler. + JDEBUG ? $this->profiler->mark('afterDispatch') : null; + } + + /** + * Adds a mapping from a content type to the format stored. Note the format type cannot be overwritten. + * + * @param string $contentHeader The content header + * @param string $format The content type format + * + * @return void + * + * @since 4.0.0 + */ + public function addFormatMap($contentHeader, $format) + { + if (!\array_key_exists($contentHeader, $this->formatMapper)) { + $this->formatMapper[$contentHeader] = $format; + } + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 4.0.0 + * + * @note Rendering should be overridden to get rid of the theme files. + */ + protected function render() + { + // Render the document + $this->setBody($this->document->render($this->allowCache())); + } + + /** + * Method to send the application response to the client. All headers will be sent prior to the main application output data. + * + * @param array $options An optional argument to enable CORS. (Temporary) + * + * @return void + * + * @since 4.0.0 + */ + protected function respond($options = array()) + { + // Set the Joomla! API signature + $this->setHeader('X-Powered-By', 'JoomlaAPI/1.0', true); + + $forceCORS = (int) $this->get('cors'); + + if ($forceCORS) { + /** + * Enable CORS (Cross-origin resource sharing) + * Obtain allowed CORS origin from Global Settings. + * Set to * (=all) if not set. + */ + $allowedOrigin = $this->get('cors_allow_origin', '*'); + $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin, true); + $this->setHeader('Access-Control-Allow-Headers', 'Authorization'); + + if ($this->input->server->getString('HTTP_ORIGIN', null) !== null) { + $this->setHeader('Access-Control-Allow-Origin', $this->input->server->getString('HTTP_ORIGIN'), true); + $this->setHeader('Access-Control-Allow-Credentials', 'true', true); + } + } + + // Parent function can be overridden later on for debugging. + parent::respond(); + } + + /** + * Gets the name of the current template. + * + * @param boolean $params True to return the template parameters + * + * @return string|\stdClass + * + * @since 4.0.0 + */ + public function getTemplate($params = false) + { + // The API application should not need to use a template + if ($params) { + $template = new \stdClass(); + $template->template = 'system'; + $template->params = new Registry(); + $template->inheritable = 0; + $template->parent = ''; + + return $template; + } + + return 'system'; + } + + /** + * Route the application. + * + * Routing is the process of examining the request environment to determine which + * component should receive the request. The component optional parameters + * are then set in the request object to be processed when the application is being + * dispatched. + * + * @return void + * + * @since 4.0.0 + */ + protected function route() + { + $router = $this->getContainer()->get(ApiRouter::class); + + // Trigger the onBeforeApiRoute event. + PluginHelper::importPlugin('webservices'); + $this->triggerEvent('onBeforeApiRoute', array(&$router, $this)); + $caught404 = false; + $method = $this->input->getMethod(); + + try { + $this->handlePreflight($method, $router); + + $route = $router->parseApiRoute($method); + } catch (RouteNotFoundException $e) { + $caught404 = true; + } + + /** + * Now we have an API perform content negotiation to ensure we have a valid header. Assume if the route doesn't + * tell us otherwise it uses the plain JSON API + */ + $priorities = array('application/vnd.api+json'); + + if (!$caught404 && \array_key_exists('format', $route['vars'])) { + $priorities = $route['vars']['format']; + } + + $negotiator = new Negotiator(); + + try { + $mediaType = $negotiator->getBest($this->input->server->getString('HTTP_ACCEPT'), $priorities); + } catch (InvalidArgument $e) { + $mediaType = null; + } + + // If we can't find a match bail with a 406 - Not Acceptable + if ($mediaType === null) { + throw new Exception\NotAcceptable('Could not match accept header', 406); + } + + /** @var $mediaType Accept */ + $format = $mediaType->getValue(); + + if (\array_key_exists($mediaType->getValue(), $this->formatMapper)) { + $format = $this->formatMapper[$mediaType->getValue()]; + } + + $this->input->set('format', $format); + + if ($caught404) { + throw $e; + } + + $this->input->set('option', $route['vars']['component']); + $this->input->set('controller', $route['controller']); + $this->input->set('task', $route['task']); + + foreach ($route['vars'] as $key => $value) { + if ($key !== 'component') { + if ($this->input->getMethod() === 'POST') { + $this->input->post->set($key, $value); + } else { + $this->input->set($key, $value); + } + } + } + + $this->triggerEvent('onAfterApiRoute', array($this)); + + if (!isset($route['vars']['public']) || $route['vars']['public'] === false) { + if (!$this->login(array('username' => ''), array('silent' => true, 'action' => 'core.login.api'))) { + throw new AuthenticationFailed(); + } + } + } + + /** + * Handles preflight requests. + * + * @param String $method The REST verb + * + * @param ApiRouter $router The API Routing object + * + * @return void + * + * @since 4.0.0 + */ + protected function handlePreflight($method, $router) + { + /** + * If not an OPTIONS request or CORS is not enabled, + * there's nothing useful to do here. + */ + if ($method !== 'OPTIONS' || !(int) $this->get('cors')) { + return; + } + + // Extract routes matching current route from all known routes. + $matchingRoutes = $router->getMatchingRoutes(); + + // Extract exposed methods from matching routes. + $matchingRoutesMethods = array_unique( + array_reduce( + $matchingRoutes, + function ($carry, $route) { + return array_merge($carry, $route->getMethods()); + }, + [] + ) + ); + + /** + * Obtain allowed CORS origin from Global Settings. + * Set to * (=all) if not set. + */ + $allowedOrigin = $this->get('cors_allow_origin', '*'); + + /** + * Obtain allowed CORS headers from Global Settings. + * Set to sensible default if not set. + */ + $allowedHeaders = $this->get('cors_allow_headers', 'Content-Type,X-Joomla-Token'); + + /** + * Obtain allowed CORS methods from Global Settings. + * Set to methods exposed by current route if not set. + */ + $allowedMethods = $this->get('cors_allow_methods', implode(',', $matchingRoutesMethods)); + + // No use to go through the regular route handling hassle, + // so let's simply output the headers and exit. + $this->setHeader('status', '204'); + $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin); + $this->setHeader('Access-Control-Allow-Headers', $allowedHeaders); + $this->setHeader('Access-Control-Allow-Methods', $allowedMethods); + $this->sendHeaders(); + + $this->close(); + } + + /** + * Returns the application Router object. + * + * @return ApiRouter + * + * @since 4.0.0 + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public function getApiRouter() + { + return $this->getContainer()->get(ApiRouter::class); + } + + /** + * Dispatch the application + * + * @param string $component The component which is being rendered. + * + * @return void + * + * @since 4.0.0 + */ + public function dispatch($component = null) + { + // Get the component if not set. + if (!$component) { + $component = $this->input->get('option', null); + } + + // Load the document to the API + $this->loadDocument(); + + // Set up the params + $document = Factory::getDocument(); + + // Register the document object with Factory + Factory::$document = $document; + + $contents = ComponentHelper::renderComponent($component); + $document->setBuffer($contents, 'component'); + + // Trigger the onAfterDispatch event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterDispatch'); + } } diff --git a/code/libraries/src/Application/ApplicationHelper.php b/code/libraries/src/Application/ApplicationHelper.php index 454e5978..e3271a04 100644 --- a/code/libraries/src/Application/ApplicationHelper.php +++ b/code/libraries/src/Application/ApplicationHelper.php @@ -1,4 +1,5 @@ input; - $option = strtolower($input->get('option', '')); - - if (empty($option)) - { - $option = $default; - } - - $input->set('option', $option); - - return $option; - } - - /** - * Provides a secure hash based on a seed - * - * @param string $seed Seed string. - * - * @return string A secure hash - * - * @since 3.2 - */ - public static function getHash($seed) - { - return md5(Factory::getApplication()->get('secret') . $seed); - } - - /** - * This method transliterates a string into a URL - * safe string or returns a URL safe UTF-8 string - * based on the global configuration - * - * @param string $string String to process - * @param string $language Language to transliterate to if unicode slugs are disabled - * - * @return string Processed string - * - * @since 3.2 - */ - public static function stringURLSafe($string, $language = '') - { - if (Factory::getApplication()->get('unicodeslugs') == 1) - { - $output = OutputFilter::stringUrlUnicodeSlug($string); - } - else - { - if ($language === '*' || $language === '') - { - $languageParams = ComponentHelper::getParams('com_languages'); - $language = $languageParams->get('site'); - } - - $output = OutputFilter::stringURLSafe($string, $language); - } - - return $output; - } - - /** - * Gets information on a specific client id. This method will be useful in - * future versions when we start mapping applications in the database. - * - * This method will return a client information array if called - * with no arguments which can be used to add custom application information. - * - * @param integer|string|null $id A client identifier - * @param boolean $byName If true, find the client by its name - * - * @return \stdClass|array|void Object describing the client, array containing all the clients or void if $id not known - * - * @since 1.5 - */ - public static function getClientInfo($id = null, $byName = false) - { - // Only create the array if it is empty - if (empty(self::$_clients)) - { - $obj = new \stdClass; - - // Site Client - $obj->id = 0; - $obj->name = 'site'; - $obj->path = JPATH_SITE; - self::$_clients[0] = clone $obj; - - // Administrator Client - $obj->id = 1; - $obj->name = 'administrator'; - $obj->path = JPATH_ADMINISTRATOR; - self::$_clients[1] = clone $obj; - - // Installation Client - $obj->id = 2; - $obj->name = 'installation'; - $obj->path = JPATH_INSTALLATION; - self::$_clients[2] = clone $obj; - - // API Client - $obj->id = 3; - $obj->name = 'api'; - $obj->path = JPATH_API; - self::$_clients[3] = clone $obj; - - // CLI Client - $obj->id = 4; - $obj->name = 'cli'; - $obj->path = JPATH_CLI; - self::$_clients[4] = clone $obj; - } - - // If no client id has been passed return the whole array - if ($id === null) - { - return self::$_clients; - } - - // Are we looking for client information by id or by name? - if (!$byName) - { - if (isset(self::$_clients[$id])) - { - return self::$_clients[$id]; - } - } - else - { - foreach (self::$_clients as $client) - { - if ($client->name == strtolower($id)) - { - return $client; - } - } - } - } - - /** - * Adds information for a client. - * - * @param mixed $client A client identifier either an array or object - * - * @return boolean True if the information is added. False on error - * - * @since 1.6 - */ - public static function addClientInfo($client) - { - if (\is_array($client)) - { - $client = (object) $client; - } - - if (!\is_object($client)) - { - return false; - } - - $info = self::getClientInfo(); - - if (!isset($client->id)) - { - $client->id = \count($info); - } - - self::$_clients[$client->id] = clone $client; - - return true; - } + /** + * Client information array + * + * @var array + * @since 1.6 + */ + protected static $_clients = array(); + + /** + * Return the name of the request component [main component] + * + * @param string $default The default option + * + * @return string Option (e.g. com_something) + * + * @since 1.6 + */ + public static function getComponentName($default = null) + { + static $option; + + if ($option) { + return $option; + } + + $input = Factory::getApplication()->input; + $option = strtolower($input->get('option', '')); + + if (empty($option)) { + $option = $default; + } + + $input->set('option', $option); + + return $option; + } + + /** + * Provides a secure hash based on a seed + * + * @param string $seed Seed string. + * + * @return string A secure hash + * + * @since 3.2 + */ + public static function getHash($seed) + { + return md5(Factory::getApplication()->get('secret') . $seed); + } + + /** + * This method transliterates a string into a URL + * safe string or returns a URL safe UTF-8 string + * based on the global configuration + * + * @param string $string String to process + * @param string $language Language to transliterate to if unicode slugs are disabled + * + * @return string Processed string + * + * @since 3.2 + */ + public static function stringURLSafe($string, $language = '') + { + if (Factory::getApplication()->get('unicodeslugs') == 1) { + $output = OutputFilter::stringUrlUnicodeSlug($string); + } else { + if ($language === '*' || $language === '') { + $languageParams = ComponentHelper::getParams('com_languages'); + $language = $languageParams->get('site'); + } + + $output = OutputFilter::stringURLSafe($string, $language); + } + + return $output; + } + + /** + * Gets information on a specific client id. This method will be useful in + * future versions when we start mapping applications in the database. + * + * This method will return a client information array if called + * with no arguments which can be used to add custom application information. + * + * @param integer|string|null $id A client identifier + * @param boolean $byName If true, find the client by its name + * + * @return \stdClass|array|void Object describing the client, array containing all the clients or void if $id not known + * + * @since 1.5 + */ + public static function getClientInfo($id = null, $byName = false) + { + // Only create the array if it is empty + if (empty(self::$_clients)) { + $obj = new \stdClass(); + + // Site Client + $obj->id = 0; + $obj->name = 'site'; + $obj->path = JPATH_SITE; + self::$_clients[0] = clone $obj; + + // Administrator Client + $obj->id = 1; + $obj->name = 'administrator'; + $obj->path = JPATH_ADMINISTRATOR; + self::$_clients[1] = clone $obj; + + // Installation Client + $obj->id = 2; + $obj->name = 'installation'; + $obj->path = JPATH_INSTALLATION; + self::$_clients[2] = clone $obj; + + // API Client + $obj->id = 3; + $obj->name = 'api'; + $obj->path = JPATH_API; + self::$_clients[3] = clone $obj; + + // CLI Client + $obj->id = 4; + $obj->name = 'cli'; + $obj->path = JPATH_CLI; + self::$_clients[4] = clone $obj; + } + + // If no client id has been passed return the whole array + if ($id === null) { + return self::$_clients; + } + + // Are we looking for client information by id or by name? + if (!$byName) { + if (isset(self::$_clients[$id])) { + return self::$_clients[$id]; + } + } else { + foreach (self::$_clients as $client) { + if ($client->name == strtolower($id)) { + return $client; + } + } + } + } + + /** + * Adds information for a client. + * + * @param mixed $client A client identifier either an array or object + * + * @return boolean True if the information is added. False on error + * + * @since 1.6 + */ + public static function addClientInfo($client) + { + if (\is_array($client)) { + $client = (object) $client; + } + + if (!\is_object($client)) { + return false; + } + + $info = self::getClientInfo(); + + if (!isset($client->id)) { + $client->id = \count($info); + } + + self::$_clients[$client->id] = clone $client; + + return true; + } } diff --git a/code/libraries/src/Application/BaseApplication.php b/code/libraries/src/Application/BaseApplication.php index f4e63fa7..a58cd079 100644 --- a/code/libraries/src/Application/BaseApplication.php +++ b/code/libraries/src/Application/BaseApplication.php @@ -1,4 +1,5 @@ input = $input instanceof Input ? $input : new Input; - $this->config = $config instanceof Registry ? $config : new Registry; + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's + * input object. If the argument is a \JInput object that object will become + * the application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * + * @since 3.0.0 + */ + public function __construct(Input $input = null, Registry $config = null) + { + $this->input = $input instanceof Input ? $input : new Input(); + $this->config = $config instanceof Registry ? $config : new Registry(); - $this->initialise(); - } + $this->initialise(); + } } diff --git a/code/libraries/src/Application/CLI/CliInput.php b/code/libraries/src/Application/CLI/CliInput.php index 0f214306..bfdc402d 100644 --- a/code/libraries/src/Application/CLI/CliInput.php +++ b/code/libraries/src/Application/CLI/CliInput.php @@ -1,4 +1,5 @@ setProcessor($processor ?: new Output\Processor\ColorProcessor); - } + /** + * Constructor + * + * @param ProcessorInterface $processor The output processor. + * + * @since 4.0.0 + */ + public function __construct(ProcessorInterface $processor = null) + { + $this->setProcessor($processor ?: new Output\Processor\ColorProcessor()); + } - /** - * Set a processor - * - * @param ProcessorInterface $processor The output processor. - * - * @return $this - * - * @since 4.0.0 - */ - public function setProcessor(ProcessorInterface $processor) - { - $this->processor = $processor; + /** + * Set a processor + * + * @param ProcessorInterface $processor The output processor. + * + * @return $this + * + * @since 4.0.0 + */ + public function setProcessor(ProcessorInterface $processor) + { + $this->processor = $processor; - return $this; - } + return $this; + } - /** - * Get a processor - * - * @return ProcessorInterface - * - * @since 4.0.0 - * @throws \RuntimeException - */ - public function getProcessor() - { - if ($this->processor) - { - return $this->processor; - } + /** + * Get a processor + * + * @return ProcessorInterface + * + * @since 4.0.0 + * @throws \RuntimeException + */ + public function getProcessor() + { + if ($this->processor) { + return $this->processor; + } - throw new \RuntimeException('A ProcessorInterface object has not been set.'); - } + throw new \RuntimeException('A ProcessorInterface object has not been set.'); + } - /** - * Write a string to an output handler. - * - * @param string $text The text to display. - * @param boolean $nl True (default) to append a new line at the end of the output string. - * - * @return $this - * - * @since 4.0.0 - * @codeCoverageIgnore - */ - abstract public function out($text = '', $nl = true); + /** + * Write a string to an output handler. + * + * @param string $text The text to display. + * @param boolean $nl True (default) to append a new line at the end of the output string. + * + * @return $this + * + * @since 4.0.0 + * @codeCoverageIgnore + */ + abstract public function out($text = '', $nl = true); } diff --git a/code/libraries/src/Application/CLI/ColorStyle.php b/code/libraries/src/Application/CLI/ColorStyle.php index 0b1050af..521940b1 100644 --- a/code/libraries/src/Application/CLI/ColorStyle.php +++ b/code/libraries/src/Application/CLI/ColorStyle.php @@ -1,4 +1,5 @@ 0, - 'red' => 1, - 'green' => 2, - 'yellow' => 3, - 'blue' => 4, - 'magenta' => 5, - 'cyan' => 6, - 'white' => 7, - ]; - - /** - * Known styles - * - * @var array - * @since 4.0.0 - */ - private static $knownOptions = [ - 'bold' => 1, - 'underscore' => 4, - 'blink' => 5, - 'reverse' => 7, - ]; - - /** - * Foreground base value - * - * @var integer - * @since 4.0.0 - */ - private static $fgBase = 30; - - /** - * Background base value - * - * @var integer - * @since 4.0.0 - */ - private static $bgBase = 40; - - /** - * Foreground color - * - * @var integer - * @since 4.0.0 - */ - private $fgColor = 0; - - /** - * Background color - * - * @var integer - * @since 4.0.0 - */ - private $bgColor = 0; - - /** - * Array of style options - * - * @var array - * @since 4.0.0 - */ - private $options = []; - - /** - * Constructor - * - * @param string $fg Foreground color. - * @param string $bg Background color. - * @param array $options Style options. - * - * @since 4.0.0 - * @throws \InvalidArgumentException - */ - public function __construct(string $fg = '', string $bg = '', array $options = []) - { - if ($fg) - { - if (\array_key_exists($fg, static::$knownColors) == false) - { - throw new \InvalidArgumentException( - sprintf( - 'Invalid foreground color "%1$s" [%2$s]', - $fg, - implode(', ', $this->getKnownColors()) - ) - ); - } - - $this->fgColor = static::$fgBase + static::$knownColors[$fg]; - } - - if ($bg) - { - if (\array_key_exists($bg, static::$knownColors) == false) - { - throw new \InvalidArgumentException( - sprintf( - 'Invalid background color "%1$s" [%2$s]', - $bg, - implode(', ', $this->getKnownColors()) - ) - ); - } - - $this->bgColor = static::$bgBase + static::$knownColors[$bg]; - } - - foreach ($options as $option) - { - if (\array_key_exists($option, static::$knownOptions) == false) - { - throw new \InvalidArgumentException( - sprintf( - 'Invalid option "%1$s" [%2$s]', - $option, - implode(', ', $this->getKnownOptions()) - ) - ); - } - - $this->options[] = $option; - } - } - - /** - * Convert to a string. - * - * @return string - * - * @since 4.0.0 - */ - public function __toString() - { - return $this->getStyle(); - } - - /** - * Create a color style from a parameter string. - * - * Example: fg=red;bg=blue;options=bold,blink - * - * @param string $string The parameter string. - * - * @return $this - * - * @since 4.0.0 - * @throws \RuntimeException - */ - public static function fromString(string $string): self - { - $fg = ''; - $bg = ''; - $options = []; - - $parts = explode(';', $string); - - foreach ($parts as $part) - { - $subParts = explode('=', $part); - - if (\count($subParts) < 2) - { - continue; - } - - switch ($subParts[0]) - { - case 'fg': - $fg = $subParts[1]; - - break; - - case 'bg': - $bg = $subParts[1]; - - break; - - case 'options': - $options = explode(',', $subParts[1]); - - break; - - default: - throw new \RuntimeException('Invalid option: ' . $subParts[0]); - } - } - - return new self($fg, $bg, $options); - } - - /** - * Get the translated color code. - * - * @return string - * - * @since 4.0.0 - */ - public function getStyle(): string - { - $values = []; - - if ($this->fgColor) - { - $values[] = $this->fgColor; - } - - if ($this->bgColor) - { - $values[] = $this->bgColor; - } - - foreach ($this->options as $option) - { - $values[] = static::$knownOptions[$option]; - } - - return implode(';', $values); - } - - /** - * Get the known colors. - * - * @return string[] - * - * @since 4.0.0 - */ - public function getKnownColors(): array - { - return array_keys(static::$knownColors); - } - - /** - * Get the known options. - * - * @return string[] - * - * @since 4.0.0 - */ - public function getKnownOptions(): array - { - return array_keys(static::$knownOptions); - } + /** + * Known colors + * + * @var array + * @since 4.0.0 + */ + private static $knownColors = [ + 'black' => 0, + 'red' => 1, + 'green' => 2, + 'yellow' => 3, + 'blue' => 4, + 'magenta' => 5, + 'cyan' => 6, + 'white' => 7, + ]; + + /** + * Known styles + * + * @var array + * @since 4.0.0 + */ + private static $knownOptions = [ + 'bold' => 1, + 'underscore' => 4, + 'blink' => 5, + 'reverse' => 7, + ]; + + /** + * Foreground base value + * + * @var integer + * @since 4.0.0 + */ + private static $fgBase = 30; + + /** + * Background base value + * + * @var integer + * @since 4.0.0 + */ + private static $bgBase = 40; + + /** + * Foreground color + * + * @var integer + * @since 4.0.0 + */ + private $fgColor = 0; + + /** + * Background color + * + * @var integer + * @since 4.0.0 + */ + private $bgColor = 0; + + /** + * Array of style options + * + * @var array + * @since 4.0.0 + */ + private $options = []; + + /** + * Constructor + * + * @param string $fg Foreground color. + * @param string $bg Background color. + * @param array $options Style options. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + */ + public function __construct(string $fg = '', string $bg = '', array $options = []) + { + if ($fg) { + if (\array_key_exists($fg, static::$knownColors) == false) { + throw new \InvalidArgumentException( + sprintf( + 'Invalid foreground color "%1$s" [%2$s]', + $fg, + implode(', ', $this->getKnownColors()) + ) + ); + } + + $this->fgColor = static::$fgBase + static::$knownColors[$fg]; + } + + if ($bg) { + if (\array_key_exists($bg, static::$knownColors) == false) { + throw new \InvalidArgumentException( + sprintf( + 'Invalid background color "%1$s" [%2$s]', + $bg, + implode(', ', $this->getKnownColors()) + ) + ); + } + + $this->bgColor = static::$bgBase + static::$knownColors[$bg]; + } + + foreach ($options as $option) { + if (\array_key_exists($option, static::$knownOptions) == false) { + throw new \InvalidArgumentException( + sprintf( + 'Invalid option "%1$s" [%2$s]', + $option, + implode(', ', $this->getKnownOptions()) + ) + ); + } + + $this->options[] = $option; + } + } + + /** + * Convert to a string. + * + * @return string + * + * @since 4.0.0 + */ + public function __toString() + { + return $this->getStyle(); + } + + /** + * Create a color style from a parameter string. + * + * Example: fg=red;bg=blue;options=bold,blink + * + * @param string $string The parameter string. + * + * @return $this + * + * @since 4.0.0 + * @throws \RuntimeException + */ + public static function fromString(string $string): self + { + $fg = ''; + $bg = ''; + $options = []; + + $parts = explode(';', $string); + + foreach ($parts as $part) { + $subParts = explode('=', $part); + + if (\count($subParts) < 2) { + continue; + } + + switch ($subParts[0]) { + case 'fg': + $fg = $subParts[1]; + + break; + + case 'bg': + $bg = $subParts[1]; + + break; + + case 'options': + $options = explode(',', $subParts[1]); + + break; + + default: + throw new \RuntimeException('Invalid option: ' . $subParts[0]); + } + } + + return new self($fg, $bg, $options); + } + + /** + * Get the translated color code. + * + * @return string + * + * @since 4.0.0 + */ + public function getStyle(): string + { + $values = []; + + if ($this->fgColor) { + $values[] = $this->fgColor; + } + + if ($this->bgColor) { + $values[] = $this->bgColor; + } + + foreach ($this->options as $option) { + $values[] = static::$knownOptions[$option]; + } + + return implode(';', $values); + } + + /** + * Get the known colors. + * + * @return string[] + * + * @since 4.0.0 + */ + public function getKnownColors(): array + { + return array_keys(static::$knownColors); + } + + /** + * Get the known options. + * + * @return string[] + * + * @since 4.0.0 + */ + public function getKnownOptions(): array + { + return array_keys(static::$knownOptions); + } } diff --git a/code/libraries/src/Application/CLI/Output/Processor/ColorProcessor.php b/code/libraries/src/Application/CLI/Output/Processor/ColorProcessor.php index 894d96f6..00009204 100644 --- a/code/libraries/src/Application/CLI/Output/Processor/ColorProcessor.php +++ b/code/libraries/src/Application/CLI/Output/Processor/ColorProcessor.php @@ -1,4 +1,5 @@ (.*?)<\/\\1>/s'; - - /** - * Regex used for removing color codes - * - * @var string - * @since 4.0.0 - */ - protected static $stripFilter = '/<[\/]?[a-z=;]+>/'; - - /** - * Array of ColorStyle objects - * - * @var ColorStyle[] - * @since 4.0.0 - */ - protected $styles = []; - - /** - * Class constructor - * - * @param boolean $noColors Defines non-colored mode on construct - * - * @since 4.0.0 - */ - public function __construct($noColors = null) - { - if ($noColors === null) - { - /* - * By default windows cmd.exe and PowerShell does not support ANSI-colored output - * if the variable is not set explicitly colors should be disabled on Windows - */ - $noColors = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'); - } - - $this->noColors = $noColors; - - $this->addPredefinedStyles(); - } - - /** - * Add a style. - * - * @param string $name The style name. - * @param ColorStyle $style The color style. - * - * @return $this - * - * @since 4.0.0 - */ - public function addStyle($name, ColorStyle $style) - { - $this->styles[$name] = $style; - - return $this; - } - - /** - * Strip color tags from a string. - * - * @param string $string The string. - * - * @return string - * - * @since 4.0.0 - */ - public static function stripColors($string) - { - return preg_replace(static::$stripFilter, '', $string); - } - - /** - * Process a string. - * - * @param string $string The string to process. - * - * @return string - * - * @since 4.0.0 - */ - public function process($string) - { - preg_match_all($this->tagFilter, $string, $matches); - - if (!$matches) - { - return $string; - } - - foreach ($matches[0] as $i => $m) - { - if (\array_key_exists($matches[1][$i], $this->styles)) - { - $string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], $this->styles[$matches[1][$i]]); - } - // Custom format - elseif (strpos($matches[1][$i], '=')) - { - $string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], ColorStyle::fromString($matches[1][$i])); - } - } - - return $string; - } - - /** - * Replace color tags in a string. - * - * @param string $text The original text. - * @param string $tag The matched tag. - * @param string $match The match. - * @param ColorStyle $style The color style to apply. - * - * @return mixed - * - * @since 4.0.0 - */ - private function replaceColors($text, $tag, $match, ColorStyle $style) - { - $replace = $this->noColors - ? $match - : "\033[" . $style . 'm' . $match . "\033[0m"; - - return str_replace('<' . $tag . '>' . $match . '', $replace, $text); - } - - /** - * Adds predefined color styles to the ColorProcessor object - * - * @return $this - * - * @since 4.0.0 - */ - private function addPredefinedStyles() - { - $this->addStyle( - 'info', - new ColorStyle('green', '', ['bold']) - ); - - $this->addStyle( - 'comment', - new ColorStyle('yellow', '', ['bold']) - ); - - $this->addStyle( - 'question', - new ColorStyle('black', 'cyan') - ); - - $this->addStyle( - 'error', - new ColorStyle('white', 'red') - ); - - return $this; - } + /** + * Flag to remove color codes from the output + * + * @var boolean + * @since 4.0.0 + */ + public $noColors = false; + + /** + * Regex to match tags + * + * @var string + * @since 4.0.0 + */ + protected $tagFilter = '/<([a-z=;]+)>(.*?)<\/\\1>/s'; + + /** + * Regex used for removing color codes + * + * @var string + * @since 4.0.0 + */ + protected static $stripFilter = '/<[\/]?[a-z=;]+>/'; + + /** + * Array of ColorStyle objects + * + * @var ColorStyle[] + * @since 4.0.0 + */ + protected $styles = []; + + /** + * Class constructor + * + * @param boolean $noColors Defines non-colored mode on construct + * + * @since 4.0.0 + */ + public function __construct($noColors = null) + { + if ($noColors === null) { + /* + * By default windows cmd.exe and PowerShell does not support ANSI-colored output + * if the variable is not set explicitly colors should be disabled on Windows + */ + $noColors = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'); + } + + $this->noColors = $noColors; + + $this->addPredefinedStyles(); + } + + /** + * Add a style. + * + * @param string $name The style name. + * @param ColorStyle $style The color style. + * + * @return $this + * + * @since 4.0.0 + */ + public function addStyle($name, ColorStyle $style) + { + $this->styles[$name] = $style; + + return $this; + } + + /** + * Strip color tags from a string. + * + * @param string $string The string. + * + * @return string + * + * @since 4.0.0 + */ + public static function stripColors($string) + { + return preg_replace(static::$stripFilter, '', $string); + } + + /** + * Process a string. + * + * @param string $string The string to process. + * + * @return string + * + * @since 4.0.0 + */ + public function process($string) + { + preg_match_all($this->tagFilter, $string, $matches); + + if (!$matches) { + return $string; + } + + foreach ($matches[0] as $i => $m) { + if (\array_key_exists($matches[1][$i], $this->styles)) { + $string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], $this->styles[$matches[1][$i]]); + } elseif (strpos($matches[1][$i], '=')) { + // Custom format + $string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], ColorStyle::fromString($matches[1][$i])); + } + } + + return $string; + } + + /** + * Replace color tags in a string. + * + * @param string $text The original text. + * @param string $tag The matched tag. + * @param string $match The match. + * @param ColorStyle $style The color style to apply. + * + * @return mixed + * + * @since 4.0.0 + */ + private function replaceColors($text, $tag, $match, ColorStyle $style) + { + $replace = $this->noColors + ? $match + : "\033[" . $style . 'm' . $match . "\033[0m"; + + return str_replace('<' . $tag . '>' . $match . '', $replace, $text); + } + + /** + * Adds predefined color styles to the ColorProcessor object + * + * @return $this + * + * @since 4.0.0 + */ + private function addPredefinedStyles() + { + $this->addStyle( + 'info', + new ColorStyle('green', '', ['bold']) + ); + + $this->addStyle( + 'comment', + new ColorStyle('yellow', '', ['bold']) + ); + + $this->addStyle( + 'question', + new ColorStyle('black', 'cyan') + ); + + $this->addStyle( + 'error', + new ColorStyle('white', 'red') + ); + + return $this; + } } diff --git a/code/libraries/src/Application/CLI/Output/Processor/ProcessorInterface.php b/code/libraries/src/Application/CLI/Output/Processor/ProcessorInterface.php index 0e54f3e2..508bb1c4 100644 --- a/code/libraries/src/Application/CLI/Output/Processor/ProcessorInterface.php +++ b/code/libraries/src/Application/CLI/Output/Processor/ProcessorInterface.php @@ -1,4 +1,5 @@ getProcessor()->process($text) . ($nl ? "\n" : null)); + /** + * Write a string to standard output + * + * @param string $text The text to display. + * @param boolean $nl True (default) to append a new line at the end of the output string. + * + * @return $this + * + * @codeCoverageIgnore + * @since 4.0.0 + */ + public function out($text = '', $nl = true) + { + fwrite(STDOUT, $this->getProcessor()->process($text) . ($nl ? "\n" : null)); - return $this; - } + return $this; + } } diff --git a/code/libraries/src/Application/CLI/Output/Xml.php b/code/libraries/src/Application/CLI/Output/Xml.php index a5d09c19..50f14fda 100644 --- a/code/libraries/src/Application/CLI/Output/Xml.php +++ b/code/libraries/src/Application/CLI/Output/Xml.php @@ -1,4 +1,5 @@ setContainer($container); - - parent::__construct($input, $config, $client); - - // If JDEBUG is defined, load the profiler instance - if (\defined('JDEBUG') && JDEBUG) - { - $this->profiler = Profiler::getInstance('Application'); - } - - // Enable sessions by default. - if ($this->config->get('session') === null) - { - $this->config->set('session', true); - } - - // Set the session default name. - if ($this->config->get('session_name') === null) - { - $this->config->set('session_name', $this->getName()); - } - } - - /** - * Checks the user session. - * - * If the session record doesn't exist, initialise it. - * If session is new, create session variables - * - * @return void - * - * @since 3.2 - * @throws \RuntimeException - */ - public function checkSession() - { - $this->getContainer()->get(MetadataManager::class)->createOrUpdateRecord($this->getSession(), $this->getIdentity()); - } - - /** - * Enqueue a system message. - * - * @param string $msg The message to enqueue. - * @param string $type The message type. Default is message. - * - * @return void - * - * @since 3.2 - */ - public function enqueueMessage($msg, $type = self::MSG_INFO) - { - // Don't add empty messages. - if ($msg === null || trim($msg) === '') - { - return; - } - - $inputFilter = InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ); - - // Build the message array and apply the HTML InputFilter with the default blacklist to the message - $message = array( - 'message' => $inputFilter->clean($msg, 'html'), - 'type' => $inputFilter->clean(strtolower($type), 'cmd'), - ); - - // For empty queue, if messages exists in the session, enqueue them first. - $messages = $this->getMessageQueue(); - - if (!\in_array($message, $this->messageQueue)) - { - // Enqueue the message. - $this->messageQueue[] = $message; - } - } - - /** - * Ensure several core system input variables are not arrays. - * - * @return void - * - * @since 3.9 - */ - private function sanityCheckSystemVariables() - { - $input = $this->input; - - // Get invalid input variables - $invalidInputVariables = array_filter( - array('option', 'view', 'format', 'lang', 'Itemid', 'template', 'templateStyle', 'task'), - function ($systemVariable) use ($input) { - return $input->exists($systemVariable) && is_array($input->getRaw($systemVariable)); - } - ); - - // Unset invalid system variables - foreach ($invalidInputVariables as $systemVariable) - { - $input->set($systemVariable, null); - } - - // Abort when there are invalid variables - if ($invalidInputVariables) - { - throw new \RuntimeException('Invalid input, aborting application.'); - } - } - - /** - * Execute the application. - * - * @return void - * - * @since 3.2 - */ - public function execute() - { - try - { - $this->sanityCheckSystemVariables(); - $this->setupLogging(); - $this->createExtensionNamespaceMap(); - - // Perform application routines. - $this->doExecute(); - - // If we have an application document object, render it. - if ($this->document instanceof \Joomla\CMS\Document\Document) - { - // Render the application output. - $this->render(); - } - - // If gzip compression is enabled in configuration and the server is compliant, compress the output. - if ($this->get('gzip') && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') - { - $this->compress(); - - // Trigger the onAfterCompress event. - $this->triggerEvent('onAfterCompress'); - } - } - catch (\Throwable $throwable) - { - /** @var ErrorEvent $event */ - $event = AbstractEvent::create( - 'onError', - [ - 'subject' => $throwable, - 'eventClass' => ErrorEvent::class, - 'application' => $this, - ] - ); - - // Trigger the onError event. - $this->triggerEvent('onError', $event); - - ExceptionHandler::handleException($event->getError()); - } - - // Trigger the onBeforeRespond event. - $this->getDispatcher()->dispatch('onBeforeRespond'); - - // Send the application response. - $this->respond(); - - // Trigger the onAfterRespond event. - $this->getDispatcher()->dispatch('onAfterRespond'); - } - - /** - * Check if the user is required to reset their password. - * - * If the user is required to reset their password will be redirected to the page that manage the password reset. - * - * @param string $option The option that manage the password reset - * @param string $view The view that manage the password reset - * @param string $layout The layout of the view that manage the password reset - * @param string $tasks Permitted tasks - * - * @return void - * - * @throws \Exception - */ - protected function checkUserRequireReset($option, $view, $layout, $tasks) - { - if (Factory::getUser()->get('requireReset', 0)) - { - $redirect = false; - - /* - * By default user profile edit page is used. - * That page allows you to change more than just the password and might not be the desired behavior. - * This allows a developer to override the page that manage the password reset. - * (can be configured using the file: configuration.php, or if extended, through the global configuration form) - */ - $name = $this->getName(); - - if ($this->get($name . '_reset_password_override', 0)) - { - $option = $this->get($name . '_reset_password_option', ''); - $view = $this->get($name . '_reset_password_view', ''); - $layout = $this->get($name . '_reset_password_layout', ''); - $tasks = $this->get($name . '_reset_password_tasks', ''); - } - - $task = $this->input->getCmd('task', ''); - - // Check task or option/view/layout - if (!empty($task)) - { - $tasks = explode(',', $tasks); - - // Check full task version "option/task" - if (array_search($this->input->getCmd('option', '') . '/' . $task, $tasks) === false) - { - // Check short task version, must be on the same option of the view - if ($this->input->getCmd('option', '') !== $option || array_search($task, $tasks) === false) - { - // Not permitted task - $redirect = true; - } - } - } - else - { - if ($this->input->getCmd('option', '') !== $option || $this->input->getCmd('view', '') !== $view - || $this->input->getCmd('layout', '') !== $layout) - { - // Requested a different option/view/layout - $redirect = true; - } - } - - if ($redirect) - { - // Redirect to the profile edit page - $this->enqueueMessage(Text::_('JGLOBAL_PASSWORD_RESET_REQUIRED'), 'notice'); - - $url = Route::_('index.php?option=' . $option . '&view=' . $view . '&layout=' . $layout, false); - - // In the administrator we need a different URL - if (strtolower($name) === 'administrator') - { - $user = Factory::getApplication()->getIdentity(); - $url = Route::_('index.php?option=' . $option . '&task=' . $view . '.' . $layout . '&id=' . $user->id, false); - } - - $this->redirect($url); - } - } - } - - /** - * Gets a configuration value. - * - * @param string $varname The name of the value to get. - * @param string $default Default value to return - * - * @return mixed The user state. - * - * @since 3.2 - * @deprecated 5.0 Use get() instead - */ - public function getCfg($varname, $default = null) - { - try - { - Log::add( - sprintf('%s() is deprecated and will be removed in 5.0. Use JFactory->getApplication()->get() instead.', __METHOD__), - Log::WARNING, - 'deprecated' - ); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - return $this->get($varname, $default); - } - - /** - * Gets the client id of the current running application. - * - * @return integer A client identifier. - * - * @since 3.2 - */ - public function getClientId() - { - return $this->clientId; - } - - /** - * Returns a reference to the global CmsApplication object, only creating it if it doesn't already exist. - * - * This method must be invoked as: $web = CmsApplication::getInstance(); - * - * @param string $name The name (optional) of the CmsApplication class to instantiate. - * @param string $prefix The class name prefix of the object. - * @param Container $container An optional dependency injection container to inject into the application. - * - * @return CmsApplication - * - * @since 3.2 - * @throws \RuntimeException - * @deprecated 5.0 Use \Joomla\CMS\Factory::getContainer()->get($name) instead - */ - public static function getInstance($name = null, $prefix = '\JApplication', Container $container = null) - { - if (empty(static::$instances[$name])) - { - // Create a CmsApplication object. - $classname = $prefix . ucfirst($name); - - if (!$container) - { - $container = Factory::getContainer(); - } - - if ($container->has($classname)) - { - static::$instances[$name] = $container->get($classname); - } - elseif (class_exists($classname)) - { - // @todo This creates an implicit hard requirement on the ApplicationCms constructor - static::$instances[$name] = new $classname(null, null, null, $container); - } - else - { - throw new \RuntimeException(Text::sprintf('JLIB_APPLICATION_ERROR_APPLICATION_LOAD', $name), 500); - } - - static::$instances[$name]->loadIdentity(Factory::getUser()); - } - - return static::$instances[$name]; - } - - /** - * Returns the application \JMenu object. - * - * @param string $name The name of the application/client. - * @param array $options An optional associative array of configuration settings. - * - * @return AbstractMenu - * - * @since 3.2 - */ - public function getMenu($name = null, $options = array()) - { - if (!isset($name)) - { - $name = $this->getName(); - } - - // Inject this application object into the \JMenu tree if one isn't already specified - if (!isset($options['app'])) - { - $options['app'] = $this; - } - - return AbstractMenu::getInstance($name, $options); - } - - /** - * Get the system message queue. - * - * @param boolean $clear Clear the messages currently attached to the application object - * - * @return array The system message queue. - * - * @since 3.2 - */ - public function getMessageQueue($clear = false) - { - // For empty queue, if messages exists in the session, enqueue them. - if (!\count($this->messageQueue)) - { - $sessionQueue = $this->getSession()->get('application.queue', []); - - if ($sessionQueue) - { - $this->messageQueue = $sessionQueue; - $this->getSession()->set('application.queue', []); - } - } - - $messageQueue = $this->messageQueue; - - if ($clear) - { - $this->messageQueue = array(); - } - - return $messageQueue; - } - - /** - * Gets the name of the current running application. - * - * @return string The name of the application. - * - * @since 3.2 - */ - public function getName() - { - return $this->name; - } - - /** - * Returns the application Pathway object. - * - * @return Pathway - * - * @since 3.2 - */ - public function getPathway() - { - if (!$this->pathway) - { - $resourceName = ucfirst($this->getName()) . 'Pathway'; - - if (!$this->getContainer()->has($resourceName)) - { - throw new \RuntimeException( - Text::sprintf('JLIB_APPLICATION_ERROR_PATHWAY_LOAD', $this->getName()), - 500 - ); - } - - $this->pathway = $this->getContainer()->get($resourceName); - } - - return $this->pathway; - } - - /** - * Returns the application Router object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return Router - * - * @since 3.2 - */ - public static function getRouter($name = null, array $options = array()) - { - $app = Factory::getApplication(); - - if (!isset($name)) - { - $name = $app->getName(); - } - - $options['mode'] = $app->get('sef'); - - return Router::getInstance($name, $options); - } - - /** - * Gets the name of the current template. - * - * @param boolean $params An optional associative array of configuration settings - * - * @return mixed System is the fallback. - * - * @since 3.2 - */ - public function getTemplate($params = false) - { - if ($params) - { - $template = new \stdClass; - - $template->template = 'system'; - $template->params = new Registry; - $template->inheritable = 0; - $template->parent = ''; - - return $template; - } - - return 'system'; - } - - /** - * Gets a user state. - * - * @param string $key The path of the state. - * @param mixed $default Optional default value, returned if the internal value is null. - * - * @return mixed The user state or null. - * - * @since 3.2 - */ - public function getUserState($key, $default = null) - { - $registry = $this->getSession()->get('registry'); - - if ($registry !== null) - { - return $registry->get($key, $default); - } - - return $default; - } - - /** - * Gets the value of a user state variable. - * - * @param string $key The key of the user state variable. - * @param string $request The name of the variable passed in a request. - * @param string $default The default value for the variable if not found. Optional. - * @param string $type Filter for the variable, for valid values see {@link InputFilter::clean()}. Optional. - * - * @return mixed The request user state. - * - * @since 3.2 - */ - public function getUserStateFromRequest($key, $request, $default = null, $type = 'none') - { - $cur_state = $this->getUserState($key, $default); - $new_state = $this->input->get($request, null, $type); - - if ($new_state === null) - { - return $cur_state; - } - - // Save the new value only if it was set in this request. - $this->setUserState($key, $new_state); - - return $new_state; - } - - /** - * Initialise the application. - * - * @param array $options An optional associative array of configuration settings. - * - * @return void - * - * @since 3.2 - */ - protected function initialiseApp($options = array()) - { - // Check that we were given a language in the array (since by default may be blank). - if (isset($options['language'])) - { - $this->set('language', $options['language']); - } - - // Build our language object - $lang = Language::getInstance($this->get('language'), $this->get('debug_lang')); - - // Load the language to the API - $this->loadLanguage($lang); - - // Register the language object with Factory - Factory::$language = $this->getLanguage(); - - // Load the library language files - $this->loadLibraryLanguage(); - - // Set user specific editor. - $user = Factory::getUser(); - $editor = $user->getParam('editor', $this->get('editor')); - - if (!PluginHelper::isEnabled('editors', $editor)) - { - $editor = $this->get('editor'); - - if (!PluginHelper::isEnabled('editors', $editor)) - { - $editor = 'none'; - } - } - - $this->set('editor', $editor); - - // Load the behaviour plugins - PluginHelper::importPlugin('behaviour'); - - // Trigger the onAfterInitialise event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterInitialise'); - } - - /** - * Checks if HTTPS is forced in the client configuration. - * - * @param integer $clientId An optional client id (defaults to current application client). - * - * @return boolean True if is forced for the client, false otherwise. - * - * @since 3.7.3 - */ - public function isHttpsForced($clientId = null) - { - $clientId = (int) ($clientId !== null ? $clientId : $this->getClientId()); - $forceSsl = (int) $this->get('force_ssl'); - - if ($clientId === 0 && $forceSsl === 2) - { - return true; - } - - if ($clientId === 1 && $forceSsl >= 1) - { - return true; - } - - return false; - } - - /** - * Check the client interface by name. - * - * @param string $identifier String identifier for the application interface - * - * @return boolean True if this application is of the given type client interface. - * - * @since 3.7.0 - */ - public function isClient($identifier) - { - return $this->getName() === $identifier; - } - - /** - * Load the library language files for the application - * - * @return void - * - * @since 3.6.3 - */ - protected function loadLibraryLanguage() - { - $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR); - } - - /** - * Login authentication function. - * - * Username and encoded password are passed the onUserLogin event which - * is responsible for the user validation. A successful validation updates - * the current session record with the user's details. - * - * Username and encoded password are sent as credentials (along with other - * possibilities) to each observer (authentication plugin) for user - * validation. Successful validation will update the current session with - * the user details. - * - * @param array $credentials Array('username' => string, 'password' => string) - * @param array $options Array('remember' => boolean) - * - * @return boolean|\Exception True on success, false if failed or silent handling is configured, or a \Exception object on authentication error. - * - * @since 3.2 - */ - public function login($credentials, $options = array()) - { - // Get the global Authentication object. - $authenticate = Authentication::getInstance($this->authenticationPluginType); - $response = $authenticate->authenticate($credentials, $options); - - // Import the user plugin group. - PluginHelper::importPlugin('user'); - - if ($response->status === Authentication::STATUS_SUCCESS) - { - /* - * Validate that the user should be able to login (different to being authenticated). - * This permits authentication plugins blocking the user. - */ - $authorisations = $authenticate->authorise($response, $options); - $denied_states = Authentication::STATUS_EXPIRED | Authentication::STATUS_DENIED; - - foreach ($authorisations as $authorisation) - { - if ((int) $authorisation->status & $denied_states) - { - // Trigger onUserAuthorisationFailure Event. - $this->triggerEvent('onUserAuthorisationFailure', array((array) $authorisation)); - - // If silent is set, just return false. - if (isset($options['silent']) && $options['silent']) - { - return false; - } - - // Return the error. - switch ($authorisation->status) - { - case Authentication::STATUS_EXPIRED: - Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_EXPIRED'), 'error'); - - return false; - - case Authentication::STATUS_DENIED: - Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_DENIED'), 'error'); - - return false; - - default: - Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_AUTHORISATION'), 'error'); - - return false; - } - } - } - - // OK, the credentials are authenticated and user is authorised. Let's fire the onLogin event. - $results = $this->triggerEvent('onUserLogin', array((array) $response, $options)); - - /* - * If any of the user plugins did not successfully complete the login routine - * then the whole method fails. - * - * Any errors raised should be done in the plugin as this provides the ability - * to provide much more information about why the routine may have failed. - */ - $user = Factory::getUser(); - - if ($response->type === 'Cookie') - { - $user->set('cookieLogin', true); - } - - if (\in_array(false, $results, true) == false) - { - $options['user'] = $user; - $options['responseType'] = $response->type; - - // The user is successfully logged in. Run the after login events - $this->triggerEvent('onUserAfterLogin', array($options)); - - return true; - } - } - - // Trigger onUserLoginFailure Event. - $this->triggerEvent('onUserLoginFailure', array((array) $response)); - - // If silent is set, just return false. - if (isset($options['silent']) && $options['silent']) - { - return false; - } - - // If status is success, any error will have been raised by the user plugin - if ($response->status !== Authentication::STATUS_SUCCESS) - { - $this->getLogger()->warning($response->error_message, array('category' => 'jerror')); - } - - return false; - } - - /** - * Logout authentication function. - * - * Passed the current user information to the onUserLogout event and reverts the current - * session record back to 'anonymous' parameters. - * If any of the authentication plugins did not successfully complete - * the logout routine then the whole method fails. Any errors raised - * should be done in the plugin as this provides the ability to give - * much more information about why the routine may have failed. - * - * @param integer $userid The user to load - Can be an integer or string - If string, it is converted to ID automatically - * @param array $options Array('clientid' => array of client id's) - * - * @return boolean True on success - * - * @since 3.2 - */ - public function logout($userid = null, $options = array()) - { - // Get a user object from the Application. - $user = Factory::getUser($userid); - - // Build the credentials array. - $parameters['username'] = $user->get('username'); - $parameters['id'] = $user->get('id'); - - // Set clientid in the options array if it hasn't been set already and shared sessions are not enabled. - if (!$this->get('shared_session', '0') && !isset($options['clientid'])) - { - $options['clientid'] = $this->getClientId(); - } - - // Import the user plugin group. - PluginHelper::importPlugin('user'); - - // OK, the credentials are built. Lets fire the onLogout event. - $results = $this->triggerEvent('onUserLogout', array($parameters, $options)); - - // Check if any of the plugins failed. If none did, success. - if (!\in_array(false, $results, true)) - { - $options['username'] = $user->get('username'); - $this->triggerEvent('onUserAfterLogout', array($options)); - - return true; - } - - // Trigger onUserLogoutFailure Event. - $this->triggerEvent('onUserLogoutFailure', array($parameters)); - - return false; - } - - /** - * Redirect to another URL. - * - * If the headers have not been sent the redirect will be accomplished using a "301 Moved Permanently" - * or "303 See Other" code in the header pointing to the new location. If the headers have already been - * sent this will be accomplished using a JavaScript statement. - * - * @param string $url The URL to redirect to. Can only be http/https URL - * @param integer $status The HTTP 1.1 status code to be provided. 303 is assumed by default. - * - * @return void - * - * @since 3.2 - */ - public function redirect($url, $status = 303) - { - // Persist messages if they exist. - if (\count($this->messageQueue)) - { - $this->getSession()->set('application.queue', $this->messageQueue); - } - - // Hand over processing to the parent now - parent::redirect($url, $status); - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 3.2 - */ - protected function render() - { - // Setup the document options. - $this->docOptions['template'] = $this->get('theme'); - $this->docOptions['file'] = $this->get('themeFile', 'index.php'); - $this->docOptions['params'] = $this->get('themeParams'); - $this->docOptions['csp_nonce'] = $this->get('csp_nonce'); - $this->docOptions['templateInherits'] = $this->get('themeInherits'); - - if ($this->get('themes.base')) - { - $this->docOptions['directory'] = $this->get('themes.base'); - } - // Fall back to constants. - else - { - $this->docOptions['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes'; - } - - // Parse the document. - $this->document->parse($this->docOptions); - - // Trigger the onBeforeRender event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onBeforeRender'); - - $caching = false; - - if ($this->isClient('site') && $this->get('caching') && $this->get('caching', 2) == 2 && !Factory::getUser()->get('id')) - { - $caching = true; - } - - // Render the document. - $data = $this->document->render($caching, $this->docOptions); - - // Set the application output data. - $this->setBody($data); - - // Trigger the onAfterRender event. - $this->triggerEvent('onAfterRender'); - - // Mark afterRender in the profiler. - JDEBUG ? $this->profiler->mark('afterRender') : null; - } - - /** - * Route the application. - * - * Routing is the process of examining the request environment to determine which - * component should receive the request. The component optional parameters - * are then set in the request object to be processed when the application is being - * dispatched. - * - * @return void - * - * @since 3.2 - */ - protected function route() - { - // Get the full request URI. - $uri = clone Uri::getInstance(); - - $router = static::getRouter(); - $result = $router->parse($uri, true); - - $active = $this->getMenu()->getActive(); - - if ($active !== null - && $active->type === 'alias' - && $active->getParams()->get('alias_redirect') - && \in_array($this->input->getMethod(), array('GET', 'HEAD'), true)) - { - $item = $this->getMenu()->getItem($active->getParams()->get('aliasoptions')); - - if ($item !== null) - { - $oldUri = clone Uri::getInstance(); - - if ($oldUri->getVar('Itemid') == $active->id) - { - $oldUri->setVar('Itemid', $item->id); - } - - $base = Uri::base(true); - $oldPath = StringHelper::strtolower(substr($oldUri->getPath(), \strlen($base) + 1)); - $activePathPrefix = StringHelper::strtolower($active->route); - - $position = strpos($oldPath, $activePathPrefix); - - if ($position !== false) - { - $oldUri->setPath($base . '/' . substr_replace($oldPath, $item->route, $position, \strlen($activePathPrefix))); - - $this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true); - $this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); - $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false); - $this->sendHeaders(); - - $this->redirect((string) $oldUri, 301); - } - } - } - - foreach ($result as $key => $value) - { - $this->input->def($key, $value); - } - - if ($this->isTwoFactorAuthenticationRequired()) - { - $this->redirectIfTwoFactorAuthenticationRequired(); - } - - // Trigger the onAfterRoute event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterRoute'); - } - - /** - * Sets the value of a user state variable. - * - * @param string $key The path of the state. - * @param mixed $value The value of the variable. - * - * @return mixed|void The previous state, if one existed. - * - * @since 3.2 - */ - public function setUserState($key, $value) - { - $session = Factory::getSession(); - $registry = $session->get('registry'); - - if ($registry !== null) - { - return $registry->set($key, $value); - } - } - - /** - * Sends all headers prior to returning the string - * - * @param boolean $compress If true, compress the data - * - * @return string - * - * @since 3.2 - */ - public function toString($compress = false) - { - // Don't compress something if the server is going to do it anyway. Waste of time. - if ($compress && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') - { - $this->compress(); - } - - if ($this->allowCache() === false) - { - $this->setHeader('Cache-Control', 'no-cache', false); - } - - $this->sendHeaders(); - - return $this->getBody(); - } - - /** - * Method to determine a hash for anti-spoofing variable names - * - * @param boolean $forceNew If true, force a new token to be created - * - * @return string Hashed var name - * - * @since 4.0.0 - */ - public function getFormToken($forceNew = false) - { - /** @var Session $session */ - $session = $this->getSession(); - - return $session->getFormToken($forceNew); - } - - /** - * Checks for a form token in the request. - * - * Use in conjunction with getFormToken. - * - * @param string $method The request method in which to look for the token key. - * - * @return boolean True if found and valid, false otherwise. - * - * @since 4.0.0 - */ - public function checkToken($method = 'post') - { - /** @var Session $session */ - $session = $this->getSession(); - - return $session->checkToken($method); - } - - /** - * Flag if the application instance is a CLI or web based application. - * - * Helper function, you should use the native PHP functions to detect if it is a CLI application. - * - * @return boolean - * - * @since 4.0.0 - * @deprecated 5.0 Will be removed without replacements - */ - public function isCli() - { - return false; - } - - /** - * Checks if 2fa needs to be enforced - * if so returns true, else returns false - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception - */ - protected function isTwoFactorAuthenticationRequired(): bool - { - $user = $this->getIdentity(); - - if (!$user->id) - { - return false; - } - - // Check session if user has set up 2fa - if ($this->getSession()->has('has2fa')) - { - return false; - } - - $comUsersParams = ComponentHelper::getComponent('com_users')->getParams(); - - // Check if 2fa is enforced for the logged in user. - $forced2faGroups = (array) $comUsersParams->get('enforce_2fa_usergroups', []); - - if (!empty($forced2faGroups)) - { - $userGroups = (array) $user->get('groups', []); - - if (!array_intersect($forced2faGroups, $userGroups)) - { - return false; - } - } - - $enforce2faOptions = $comUsersParams->get('enforce_2fa_options', 0); - - if ($enforce2faOptions == 0 || !$enforce2faOptions) - { - return false; - } - - if (!PluginHelper::isEnabled('twofactorauth')) - { - return false; - } - - $pluginsSiteEnable = false; - $pluginsAdministratorEnable = false; - $pluginOptions = PluginHelper::getPlugin('twofactorauth'); - - // Sets and checks pluginOptions for Site and Administrator view depending on if any 2fa plugin is enabled for that view - array_walk($pluginOptions, - static function ($pluginOption) use (&$pluginsSiteEnable, &$pluginsAdministratorEnable) - { - $option = new Registry($pluginOption->params); - $section = $option->get('section', 3); - - switch ($section) - { - case 1: - $pluginsSiteEnable = true; - break; - case 2: - $pluginsAdministratorEnable = true; - break; - case 3: - default: - $pluginsAdministratorEnable = true; - $pluginsSiteEnable = true; - } - } - ); - - if ($pluginsSiteEnable && $this->isClient('site')) - { - if (\in_array($enforce2faOptions, [1, 3])) - { - return !$this->hasUserConfiguredTwoFactorAuthentication(); - } - } - - if ($pluginsAdministratorEnable && $this->isClient('administrator')) - { - if (\in_array($enforce2faOptions, [2, 3])) - { - return !$this->hasUserConfiguredTwoFactorAuthentication(); - } - } - - return false; - } - - /** - * Redirects user to his Two Factor Authentication setup page - * - * @return void - * - * @since 4.0.0 - */ - protected function redirectIfTwoFactorAuthenticationRequired(): void - { - $option = $this->input->get('option'); - $task = $this->input->get('task'); - $view = $this->input->get('view', null, 'STRING'); - $layout = $this->input->get('layout', null, 'STRING'); - - if ($this->isClient('site')) - { - // If user is already on edit profile screen or press update/apply button, do nothing to avoid infinite redirect - if (($option === 'com_users' && \in_array($task, ['profile.edit', 'profile.save', 'profile.apply', 'user.logout', 'user.menulogout'], true)) - || $option === 'com_users' && $view === 'profile' && $layout === 'edit') - { - return; - } - - // Redirect to com_users profile edit - $this->enqueueMessage(Text::_('JENFORCE_2FA_REDIRECT_MESSAGE'), 'notice'); - $this->redirect('index.php?option=com_users&view=profile&layout=edit'); - } - - if (($option === 'com_users' && \in_array($task, ['user.save', 'user.edit', 'user.apply', 'user.logout', 'user.menulogout'], true)) - || ($option === 'com_users' && $view === 'user' && $layout === 'edit') - || ($option === 'com_login' && \in_array($task, ['save', 'edit', 'apply', 'logout', 'menulogout'], true))) - { - return; - } - - // Redirect to com_admin profile edit - $this->enqueueMessage(Text::_('JENFORCE_2FA_REDIRECT_MESSAGE'), 'notice'); - $this->redirect('index.php?option=com_users&task=user.edit&id=' . $this->getIdentity()->id); - } - - /** - * Checks if otpKey and otep for the user are not empty - * if any one is empty returns false, else returns true - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception - */ - private function hasUserConfiguredTwoFactorAuthentication(): bool - { - $user = $this->getIdentity(); - - if (empty($user->otpKey) || empty($user->otep)) - { - return false; - } - - // Set session to user has configured 2fa - $this->getSession()->set('has2fa', 1); - - return true; - } - - /** - * Setup logging functionality. - * - * @return void - * - * @since 4.0.0 - */ - private function setupLogging(): void - { - // Add InMemory logger that will collect all log entries to allow to display them later by extensions - if ($this->get('debug')) - { - Log::addLogger(['logger' => 'inmemory']); - } - - // Log the deprecated API. - if ($this->get('log_deprecated')) - { - Log::addLogger(['text_file' => 'deprecated.php'], Log::ALL, ['deprecated']); - } - - // We only log errors unless Site Debug is enabled - $logLevels = Log::ERROR | Log::CRITICAL | Log::ALERT | Log::EMERGENCY; - - if ($this->get('debug')) - { - $logLevels = Log::ALL; - } - - Log::addLogger(['text_file' => 'joomla_core_errors.php'], $logLevels, ['system']); - - // Log everything (except deprecated APIs, these are logged separately with the option above). - if ($this->get('log_everything')) - { - Log::addLogger(['text_file' => 'everything.php'], Log::ALL, ['deprecated', 'deprecation-notes', 'databasequery'], true); - } - - if ($this->get('log_categories')) - { - $priority = 0; - - foreach ($this->get('log_priorities', ['all']) as $p) - { - $const = '\\Joomla\\CMS\\Log\\Log::' . strtoupper($p); - - if (defined($const)) - { - $priority |= constant($const); - } - } - - // Split into an array at any character other than alphabet, numbers, _, ., or - - $categories = preg_split('/[^\w.-]+/', $this->get('log_categories', ''), -1, PREG_SPLIT_NO_EMPTY); - $mode = (bool) $this->get('log_category_mode', false); - - if (!$categories) - { - return; - } - - Log::addLogger(['text_file' => 'custom-logging.php'], $priority, $categories, $mode); - } - } + use ContainerAwareTrait; + use ExtensionManagerTrait; + use ExtensionNamespaceMapper; + use SessionAwareWebApplicationTrait; + + /** + * Array of options for the \JDocument object + * + * @var array + * @since 3.2 + */ + protected $docOptions = array(); + + /** + * Application instances container. + * + * @var CmsApplication[] + * @since 3.2 + */ + protected static $instances = array(); + + /** + * The scope of the application. + * + * @var string + * @since 3.2 + */ + public $scope = null; + + /** + * The client identifier. + * + * @var integer + * @since 4.0.0 + */ + protected $clientId = null; + + /** + * The application message queue. + * + * @var array + * @since 4.0.0 + */ + protected $messageQueue = array(); + + /** + * The name of the application. + * + * @var string + * @since 4.0.0 + */ + protected $name = null; + + /** + * The profiler instance + * + * @var Profiler + * @since 3.2 + */ + protected $profiler = null; + + /** + * Currently active template + * + * @var object + * @since 3.2 + */ + protected $template = null; + + /** + * The pathway object + * + * @var Pathway + * @since 4.0.0 + */ + protected $pathway = null; + + /** + * The authentication plugin type + * + * @var string + * @since 4.0.0 + */ + protected $authenticationPluginType = 'authentication'; + + /** + * Menu instances container. + * + * @var AbstractMenu[] + * @since 4.2.0 + */ + protected $menus = []; + + /** + * The menu factory + * + * @var MenuFactoryInterface + * + * @since 4.2.0 + */ + private $menuFactory; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's config + * object. If the argument is a Registry object that object will become the + * application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's client + * object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container $container Dependency injection container. + * + * @since 3.2 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + $container = $container ?: new Container(); + $this->setContainer($container); + + parent::__construct($input, $config, $client); + + // If JDEBUG is defined, load the profiler instance + if (\defined('JDEBUG') && JDEBUG) { + $this->profiler = Profiler::getInstance('Application'); + } + + // Enable sessions by default. + if ($this->config->get('session') === null) { + $this->config->set('session', true); + } + + // Set the session default name. + if ($this->config->get('session_name') === null) { + $this->config->set('session_name', $this->getName()); + } + } + + /** + * Checks the user session. + * + * If the session record doesn't exist, initialise it. + * If session is new, create session variables + * + * @return void + * + * @since 3.2 + * @throws \RuntimeException + */ + public function checkSession() + { + $this->getContainer()->get(MetadataManager::class)->createOrUpdateRecord($this->getSession(), $this->getIdentity()); + } + + /** + * Enqueue a system message. + * + * @param string $msg The message to enqueue. + * @param string $type The message type. Default is message. + * + * @return void + * + * @since 3.2 + */ + public function enqueueMessage($msg, $type = self::MSG_INFO) + { + // Don't add empty messages. + if ($msg === null || trim($msg) === '') { + return; + } + + $inputFilter = InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ); + + // Build the message array and apply the HTML InputFilter with the default blacklist to the message + $message = array( + 'message' => $inputFilter->clean($msg, 'html'), + 'type' => $inputFilter->clean(strtolower($type), 'cmd'), + ); + + // For empty queue, if messages exists in the session, enqueue them first. + $messages = $this->getMessageQueue(); + + if (!\in_array($message, $this->messageQueue)) { + // Enqueue the message. + $this->messageQueue[] = $message; + } + } + + /** + * Ensure several core system input variables are not arrays. + * + * @return void + * + * @since 3.9 + */ + private function sanityCheckSystemVariables() + { + $input = $this->input; + + // Get invalid input variables + $invalidInputVariables = array_filter( + array('option', 'view', 'format', 'lang', 'Itemid', 'template', 'templateStyle', 'task'), + function ($systemVariable) use ($input) { + return $input->exists($systemVariable) && is_array($input->getRaw($systemVariable)); + } + ); + + // Unset invalid system variables + foreach ($invalidInputVariables as $systemVariable) { + $input->set($systemVariable, null); + } + + // Abort when there are invalid variables + if ($invalidInputVariables) { + throw new \RuntimeException('Invalid input, aborting application.'); + } + } + + /** + * Execute the application. + * + * @return void + * + * @since 3.2 + */ + public function execute() + { + try { + $this->sanityCheckSystemVariables(); + $this->setupLogging(); + $this->createExtensionNamespaceMap(); + + // Perform application routines. + $this->doExecute(); + + // If we have an application document object, render it. + if ($this->document instanceof \Joomla\CMS\Document\Document) { + // Render the application output. + $this->render(); + } + + // If gzip compression is enabled in configuration and the server is compliant, compress the output. + if ($this->get('gzip') && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') { + $this->compress(); + + // Trigger the onAfterCompress event. + $this->triggerEvent('onAfterCompress'); + } + } catch (\Throwable $throwable) { + /** @var ErrorEvent $event */ + $event = AbstractEvent::create( + 'onError', + [ + 'subject' => $throwable, + 'eventClass' => ErrorEvent::class, + 'application' => $this, + ] + ); + + // Trigger the onError event. + $this->triggerEvent('onError', $event); + + ExceptionHandler::handleException($event->getError()); + } + + // Trigger the onBeforeRespond event. + $this->getDispatcher()->dispatch('onBeforeRespond'); + + // Send the application response. + $this->respond(); + + // Trigger the onAfterRespond event. + $this->getDispatcher()->dispatch('onAfterRespond'); + } + + /** + * Check if the user is required to reset their password. + * + * If the user is required to reset their password will be redirected to the page that manage the password reset. + * + * @param string $option The option that manage the password reset + * @param string $view The view that manage the password reset + * @param string $layout The layout of the view that manage the password reset + * @param string $tasks Permitted tasks + * + * @return void + * + * @throws \Exception + */ + protected function checkUserRequireReset($option, $view, $layout, $tasks) + { + if (Factory::getUser()->get('requireReset', 0)) { + $redirect = false; + + /* + * By default user profile edit page is used. + * That page allows you to change more than just the password and might not be the desired behavior. + * This allows a developer to override the page that manage the password reset. + * (can be configured using the file: configuration.php, or if extended, through the global configuration form) + */ + $name = $this->getName(); + + if ($this->get($name . '_reset_password_override', 0)) { + $option = $this->get($name . '_reset_password_option', ''); + $view = $this->get($name . '_reset_password_view', ''); + $layout = $this->get($name . '_reset_password_layout', ''); + $tasks = $this->get($name . '_reset_password_tasks', ''); + } + + $task = $this->input->getCmd('task', ''); + + // Check task or option/view/layout + if (!empty($task)) { + $tasks = explode(',', $tasks); + + // Check full task version "option/task" + if (array_search($this->input->getCmd('option', '') . '/' . $task, $tasks) === false) { + // Check short task version, must be on the same option of the view + if ($this->input->getCmd('option', '') !== $option || array_search($task, $tasks) === false) { + // Not permitted task + $redirect = true; + } + } + } else { + if ( + $this->input->getCmd('option', '') !== $option || $this->input->getCmd('view', '') !== $view + || $this->input->getCmd('layout', '') !== $layout + ) { + // Requested a different option/view/layout + $redirect = true; + } + } + + if ($redirect) { + // Redirect to the profile edit page + $this->enqueueMessage(Text::_('JGLOBAL_PASSWORD_RESET_REQUIRED'), 'notice'); + + $url = Route::_('index.php?option=' . $option . '&view=' . $view . '&layout=' . $layout, false); + + // In the administrator we need a different URL + if (strtolower($name) === 'administrator') { + $user = Factory::getApplication()->getIdentity(); + $url = Route::_('index.php?option=' . $option . '&task=' . $view . '.' . $layout . '&id=' . $user->id, false); + } + + $this->redirect($url); + } + } + } + + /** + * Gets a configuration value. + * + * @param string $varname The name of the value to get. + * @param string $default Default value to return + * + * @return mixed The user state. + * + * @since 3.2 + * @deprecated 5.0 Use get() instead + */ + public function getCfg($varname, $default = null) + { + try { + Log::add( + sprintf('%s() is deprecated and will be removed in 5.0. Use JFactory->getApplication()->get() instead.', __METHOD__), + Log::WARNING, + 'deprecated' + ); + } catch (\RuntimeException $exception) { + // Informational log only + } + + return $this->get($varname, $default); + } + + /** + * Gets the client id of the current running application. + * + * @return integer A client identifier. + * + * @since 3.2 + */ + public function getClientId() + { + return $this->clientId; + } + + /** + * Returns a reference to the global CmsApplication object, only creating it if it doesn't already exist. + * + * This method must be invoked as: $web = CmsApplication::getInstance(); + * + * @param string $name The name (optional) of the CmsApplication class to instantiate. + * @param string $prefix The class name prefix of the object. + * @param Container $container An optional dependency injection container to inject into the application. + * + * @return CmsApplication + * + * @since 3.2 + * @throws \RuntimeException + * @deprecated 5.0 Use \Joomla\CMS\Factory::getContainer()->get($name) instead + */ + public static function getInstance($name = null, $prefix = '\JApplication', Container $container = null) + { + if (empty(static::$instances[$name])) { + // Create a CmsApplication object. + $classname = $prefix . ucfirst($name); + + if (!$container) { + $container = Factory::getContainer(); + } + + if ($container->has($classname)) { + static::$instances[$name] = $container->get($classname); + } elseif (class_exists($classname)) { + // @todo This creates an implicit hard requirement on the ApplicationCms constructor + static::$instances[$name] = new $classname(null, null, null, $container); + } else { + throw new \RuntimeException(Text::sprintf('JLIB_APPLICATION_ERROR_APPLICATION_LOAD', $name), 500); + } + + static::$instances[$name]->loadIdentity(Factory::getUser()); + } + + return static::$instances[$name]; + } + + /** + * Returns the application \JMenu object. + * + * @param string $name The name of the application/client. + * @param array $options An optional associative array of configuration settings. + * + * @return AbstractMenu + * + * @since 3.2 + */ + public function getMenu($name = null, $options = array()) + { + if (!isset($name)) { + $name = $this->getName(); + } + + // Inject this application object into the \JMenu tree if one isn't already specified + if (!isset($options['app'])) { + $options['app'] = $this; + } + + if (array_key_exists($name, $this->menus)) { + return $this->menus[$name]; + } + + if ($this->menuFactory === null) { + @trigger_error('Menu factory must be set in 5.0', E_USER_DEPRECATED); + $this->menuFactory = $this->getContainer()->get(MenuFactoryInterface::class); + } + + $this->menus[$name] = $this->menuFactory->createMenu($name, $options); + + // Make sure the abstract menu has the instance too, is needed for BC and will be removed with version 5 + AbstractMenu::$instances[$name] = $this->menus[$name]; + + return $this->menus[$name]; + } + + /** + * Get the system message queue. + * + * @param boolean $clear Clear the messages currently attached to the application object + * + * @return array The system message queue. + * + * @since 3.2 + */ + public function getMessageQueue($clear = false) + { + // For empty queue, if messages exists in the session, enqueue them. + if (!\count($this->messageQueue)) { + $sessionQueue = $this->getSession()->get('application.queue', []); + + if ($sessionQueue) { + $this->messageQueue = $sessionQueue; + $this->getSession()->set('application.queue', []); + } + } + + $messageQueue = $this->messageQueue; + + if ($clear) { + $this->messageQueue = array(); + } + + return $messageQueue; + } + + /** + * Gets the name of the current running application. + * + * @return string The name of the application. + * + * @since 3.2 + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the application Pathway object. + * + * @return Pathway + * + * @since 3.2 + */ + public function getPathway() + { + if (!$this->pathway) { + $resourceName = ucfirst($this->getName()) . 'Pathway'; + + if (!$this->getContainer()->has($resourceName)) { + throw new \RuntimeException( + Text::sprintf('JLIB_APPLICATION_ERROR_PATHWAY_LOAD', $this->getName()), + 500 + ); + } + + $this->pathway = $this->getContainer()->get($resourceName); + } + + return $this->pathway; + } + + /** + * Returns the application Router object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return Router + * + * @since 3.2 + * + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public static function getRouter($name = null, array $options = array()) + { + $app = Factory::getApplication(); + + if (!isset($name)) { + $name = $app->getName(); + } + + $options['mode'] = $app->get('sef'); + + return Router::getInstance($name, $options); + } + + /** + * Gets the name of the current template. + * + * @param boolean $params An optional associative array of configuration settings + * + * @return mixed System is the fallback. + * + * @since 3.2 + */ + public function getTemplate($params = false) + { + if ($params) { + $template = new \stdClass(); + + $template->template = 'system'; + $template->params = new Registry(); + $template->inheritable = 0; + $template->parent = ''; + + return $template; + } + + return 'system'; + } + + /** + * Gets a user state. + * + * @param string $key The path of the state. + * @param mixed $default Optional default value, returned if the internal value is null. + * + * @return mixed The user state or null. + * + * @since 3.2 + */ + public function getUserState($key, $default = null) + { + $registry = $this->getSession()->get('registry'); + + if ($registry !== null) { + return $registry->get($key, $default); + } + + return $default; + } + + /** + * Gets the value of a user state variable. + * + * @param string $key The key of the user state variable. + * @param string $request The name of the variable passed in a request. + * @param string $default The default value for the variable if not found. Optional. + * @param string $type Filter for the variable, for valid values see {@link InputFilter::clean()}. Optional. + * + * @return mixed The request user state. + * + * @since 3.2 + */ + public function getUserStateFromRequest($key, $request, $default = null, $type = 'none') + { + $cur_state = $this->getUserState($key, $default); + $new_state = $this->input->get($request, null, $type); + + if ($new_state === null) { + return $cur_state; + } + + // Save the new value only if it was set in this request. + $this->setUserState($key, $new_state); + + return $new_state; + } + + /** + * Initialise the application. + * + * @param array $options An optional associative array of configuration settings. + * + * @return void + * + * @since 3.2 + */ + protected function initialiseApp($options = array()) + { + // Check that we were given a language in the array (since by default may be blank). + if (isset($options['language'])) { + $this->set('language', $options['language']); + } + + // Build our language object + $lang = Language::getInstance($this->get('language'), $this->get('debug_lang')); + + // Load the language to the API + $this->loadLanguage($lang); + + // Register the language object with Factory + Factory::$language = $this->getLanguage(); + + // Load the library language files + $this->loadLibraryLanguage(); + + // Set user specific editor. + $user = Factory::getUser(); + $editor = $user->getParam('editor', $this->get('editor')); + + if (!PluginHelper::isEnabled('editors', $editor)) { + $editor = $this->get('editor'); + + if (!PluginHelper::isEnabled('editors', $editor)) { + $editor = 'none'; + } + } + + $this->set('editor', $editor); + + // Load the behaviour plugins + PluginHelper::importPlugin('behaviour'); + + // Trigger the onAfterInitialise event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterInitialise'); + } + + /** + * Checks if HTTPS is forced in the client configuration. + * + * @param integer $clientId An optional client id (defaults to current application client). + * + * @return boolean True if is forced for the client, false otherwise. + * + * @since 3.7.3 + */ + public function isHttpsForced($clientId = null) + { + $clientId = (int) ($clientId !== null ? $clientId : $this->getClientId()); + $forceSsl = (int) $this->get('force_ssl'); + + if ($clientId === 0 && $forceSsl === 2) { + return true; + } + + if ($clientId === 1 && $forceSsl >= 1) { + return true; + } + + return false; + } + + /** + * Check the client interface by name. + * + * @param string $identifier String identifier for the application interface + * + * @return boolean True if this application is of the given type client interface. + * + * @since 3.7.0 + */ + public function isClient($identifier) + { + return $this->getName() === $identifier; + } + + /** + * Load the library language files for the application + * + * @return void + * + * @since 3.6.3 + */ + protected function loadLibraryLanguage() + { + $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR); + } + + /** + * Login authentication function. + * + * Username and encoded password are passed the onUserLogin event which + * is responsible for the user validation. A successful validation updates + * the current session record with the user's details. + * + * Username and encoded password are sent as credentials (along with other + * possibilities) to each observer (authentication plugin) for user + * validation. Successful validation will update the current session with + * the user details. + * + * @param array $credentials Array('username' => string, 'password' => string) + * @param array $options Array('remember' => boolean) + * + * @return boolean|\Exception True on success, false if failed or silent handling is configured, or a \Exception object on authentication error. + * + * @since 3.2 + */ + public function login($credentials, $options = array()) + { + // Get the global Authentication object. + $authenticate = Authentication::getInstance($this->authenticationPluginType); + $response = $authenticate->authenticate($credentials, $options); + + // Import the user plugin group. + PluginHelper::importPlugin('user'); + + if ($response->status === Authentication::STATUS_SUCCESS) { + /* + * Validate that the user should be able to login (different to being authenticated). + * This permits authentication plugins blocking the user. + */ + $authorisations = $authenticate->authorise($response, $options); + $denied_states = Authentication::STATUS_EXPIRED | Authentication::STATUS_DENIED; + + foreach ($authorisations as $authorisation) { + if ((int) $authorisation->status & $denied_states) { + // Trigger onUserAuthorisationFailure Event. + $this->triggerEvent('onUserAuthorisationFailure', array((array) $authorisation)); + + // If silent is set, just return false. + if (isset($options['silent']) && $options['silent']) { + return false; + } + + // Return the error. + switch ($authorisation->status) { + case Authentication::STATUS_EXPIRED: + Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_EXPIRED'), 'error'); + + return false; + + case Authentication::STATUS_DENIED: + Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_DENIED'), 'error'); + + return false; + + default: + Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_AUTHORISATION'), 'error'); + + return false; + } + } + } + + // OK, the credentials are authenticated and user is authorised. Let's fire the onLogin event. + $results = $this->triggerEvent('onUserLogin', array((array) $response, $options)); + + /* + * If any of the user plugins did not successfully complete the login routine + * then the whole method fails. + * + * Any errors raised should be done in the plugin as this provides the ability + * to provide much more information about why the routine may have failed. + */ + $user = Factory::getUser(); + + if ($response->type === 'Cookie') { + $user->set('cookieLogin', true); + } + + if (\in_array(false, $results, true) == false) { + $options['user'] = $user; + $options['responseType'] = $response->type; + + // The user is successfully logged in. Run the after login events + $this->triggerEvent('onUserAfterLogin', array($options)); + + return true; + } + } + + // Trigger onUserLoginFailure Event. + $this->triggerEvent('onUserLoginFailure', array((array) $response)); + + // If silent is set, just return false. + if (isset($options['silent']) && $options['silent']) { + return false; + } + + // If status is success, any error will have been raised by the user plugin + if ($response->status !== Authentication::STATUS_SUCCESS) { + $this->getLogger()->warning($response->error_message, array('category' => 'jerror')); + } + + return false; + } + + /** + * Logout authentication function. + * + * Passed the current user information to the onUserLogout event and reverts the current + * session record back to 'anonymous' parameters. + * If any of the authentication plugins did not successfully complete + * the logout routine then the whole method fails. Any errors raised + * should be done in the plugin as this provides the ability to give + * much more information about why the routine may have failed. + * + * @param integer $userid The user to load - Can be an integer or string - If string, it is converted to ID automatically + * @param array $options Array('clientid' => array of client id's) + * + * @return boolean True on success + * + * @since 3.2 + */ + public function logout($userid = null, $options = array()) + { + // Get a user object from the Application. + $user = Factory::getUser($userid); + + // Build the credentials array. + $parameters['username'] = $user->get('username'); + $parameters['id'] = $user->get('id'); + + // Set clientid in the options array if it hasn't been set already and shared sessions are not enabled. + if (!$this->get('shared_session', '0') && !isset($options['clientid'])) { + $options['clientid'] = $this->getClientId(); + } + + // Import the user plugin group. + PluginHelper::importPlugin('user'); + + // OK, the credentials are built. Lets fire the onLogout event. + $results = $this->triggerEvent('onUserLogout', array($parameters, $options)); + + // Check if any of the plugins failed. If none did, success. + if (!\in_array(false, $results, true)) { + $options['username'] = $user->get('username'); + $this->triggerEvent('onUserAfterLogout', array($options)); + + return true; + } + + // Trigger onUserLogoutFailure Event. + $this->triggerEvent('onUserLogoutFailure', array($parameters)); + + return false; + } + + /** + * Redirect to another URL. + * + * If the headers have not been sent the redirect will be accomplished using a "301 Moved Permanently" + * or "303 See Other" code in the header pointing to the new location. If the headers have already been + * sent this will be accomplished using a JavaScript statement. + * + * @param string $url The URL to redirect to. Can only be http/https URL + * @param integer $status The HTTP 1.1 status code to be provided. 303 is assumed by default. + * + * @return void + * + * @since 3.2 + */ + public function redirect($url, $status = 303) + { + // Persist messages if they exist. + if (\count($this->messageQueue)) { + $this->getSession()->set('application.queue', $this->messageQueue); + } + + // Hand over processing to the parent now + parent::redirect($url, $status); + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 3.2 + */ + protected function render() + { + // Setup the document options. + $this->docOptions['template'] = $this->get('theme'); + $this->docOptions['file'] = $this->get('themeFile', 'index.php'); + $this->docOptions['params'] = $this->get('themeParams'); + $this->docOptions['csp_nonce'] = $this->get('csp_nonce'); + $this->docOptions['templateInherits'] = $this->get('themeInherits'); + + if ($this->get('themes.base')) { + $this->docOptions['directory'] = $this->get('themes.base'); + } else { + // Fall back to constants. + $this->docOptions['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes'; + } + + // Parse the document. + $this->document->parse($this->docOptions); + + // Trigger the onBeforeRender event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onBeforeRender'); + + $caching = false; + + if ($this->isClient('site') && $this->get('caching') && $this->get('caching', 2) == 2 && !Factory::getUser()->get('id')) { + $caching = true; + } + + // Render the document. + $data = $this->document->render($caching, $this->docOptions); + + // Set the application output data. + $this->setBody($data); + + // Trigger the onAfterRender event. + $this->triggerEvent('onAfterRender'); + + // Mark afterRender in the profiler. + JDEBUG ? $this->profiler->mark('afterRender') : null; + } + + /** + * Route the application. + * + * Routing is the process of examining the request environment to determine which + * component should receive the request. The component optional parameters + * are then set in the request object to be processed when the application is being + * dispatched. + * + * @return void + * + * @since 3.2 + * + * @deprecated 5.0 Implement the route functionality in the extending class, this here will be removed without replacement + */ + protected function route() + { + // Get the full request URI. + $uri = clone Uri::getInstance(); + + $router = static::getRouter(); + $result = $router->parse($uri, true); + + $active = $this->getMenu()->getActive(); + + if ( + $active !== null + && $active->type === 'alias' + && $active->getParams()->get('alias_redirect') + && \in_array($this->input->getMethod(), array('GET', 'HEAD'), true) + ) { + $item = $this->getMenu()->getItem($active->getParams()->get('aliasoptions')); + + if ($item !== null) { + $oldUri = clone Uri::getInstance(); + + if ($oldUri->getVar('Itemid') == $active->id) { + $oldUri->setVar('Itemid', $item->id); + } + + $base = Uri::base(true); + $oldPath = StringHelper::strtolower(substr($oldUri->getPath(), \strlen($base) + 1)); + $activePathPrefix = StringHelper::strtolower($active->route); + + $position = strpos($oldPath, $activePathPrefix); + + if ($position !== false) { + $oldUri->setPath($base . '/' . substr_replace($oldPath, $item->route, $position, \strlen($activePathPrefix))); + + $this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true); + $this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); + $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false); + $this->sendHeaders(); + + $this->redirect((string) $oldUri, 301); + } + } + } + + foreach ($result as $key => $value) { + $this->input->def($key, $value); + } + + // Trigger the onAfterRoute event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterRoute'); + } + + /** + * Sets the value of a user state variable. + * + * @param string $key The path of the state. + * @param mixed $value The value of the variable. + * + * @return mixed|void The previous state, if one existed. + * + * @since 3.2 + */ + public function setUserState($key, $value) + { + $session = $this->getSession(); + $registry = $session->get('registry'); + + if ($registry !== null) { + return $registry->set($key, $value); + } + } + + /** + * Sends all headers prior to returning the string + * + * @param boolean $compress If true, compress the data + * + * @return string + * + * @since 3.2 + */ + public function toString($compress = false) + { + // Don't compress something if the server is going to do it anyway. Waste of time. + if ($compress && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') { + $this->compress(); + } + + if ($this->allowCache() === false) { + $this->setHeader('Cache-Control', 'no-cache', false); + } + + $this->sendHeaders(); + + return $this->getBody(); + } + + /** + * Method to determine a hash for anti-spoofing variable names + * + * @param boolean $forceNew If true, force a new token to be created + * + * @return string Hashed var name + * + * @since 4.0.0 + */ + public function getFormToken($forceNew = false) + { + /** @var Session $session */ + $session = $this->getSession(); + + return $session->getFormToken($forceNew); + } + + /** + * Checks for a form token in the request. + * + * Use in conjunction with getFormToken. + * + * @param string $method The request method in which to look for the token key. + * + * @return boolean True if found and valid, false otherwise. + * + * @since 4.0.0 + */ + public function checkToken($method = 'post') + { + /** @var Session $session */ + $session = $this->getSession(); + + return $session->checkToken($method); + } + + /** + * Flag if the application instance is a CLI or web based application. + * + * Helper function, you should use the native PHP functions to detect if it is a CLI application. + * + * @return boolean + * + * @since 4.0.0 + * @deprecated 5.0 Will be removed without replacements + */ + public function isCli() + { + return false; + } + + /** + * No longer used + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception + * @deprecated 4.2.0 Will be removed in 5.0 without replacement. + */ + protected function isTwoFactorAuthenticationRequired(): bool + { + return false; + } + + /** + * No longer used + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception + * @deprecated 4.2.0 Will be removed in 5.0 without replacement. + */ + private function hasUserConfiguredTwoFactorAuthentication(): bool + { + return false; + } + + /** + * Setup logging functionality. + * + * @return void + * + * @since 4.0.0 + */ + private function setupLogging(): void + { + // Add InMemory logger that will collect all log entries to allow to display them later by extensions + if ($this->get('debug')) { + Log::addLogger(['logger' => 'inmemory']); + } + + // Log the deprecated API. + if ($this->get('log_deprecated')) { + Log::addLogger(['text_file' => 'deprecated.php'], Log::ALL, ['deprecated']); + } + + // We only log errors unless Site Debug is enabled + $logLevels = Log::ERROR | Log::CRITICAL | Log::ALERT | Log::EMERGENCY; + + if ($this->get('debug')) { + $logLevels = Log::ALL; + } + + Log::addLogger(['text_file' => 'joomla_core_errors.php'], $logLevels, ['system']); + + // Log everything (except deprecated APIs, these are logged separately with the option above). + if ($this->get('log_everything')) { + Log::addLogger(['text_file' => 'everything.php'], Log::ALL, ['deprecated', 'deprecation-notes', 'databasequery'], true); + } + + if ($this->get('log_categories')) { + $priority = 0; + + foreach ($this->get('log_priorities', ['all']) as $p) { + $const = '\\Joomla\\CMS\\Log\\Log::' . strtoupper($p); + + if (defined($const)) { + $priority |= constant($const); + } + } + + // Split into an array at any character other than alphabet, numbers, _, ., or - + $categories = preg_split('/[^\w.-]+/', $this->get('log_categories', ''), -1, PREG_SPLIT_NO_EMPTY); + $mode = (bool) $this->get('log_category_mode', false); + + if (!$categories) { + return; + } + + Log::addLogger(['text_file' => 'custom-logging.php'], $priority, $categories, $mode); + } + } + + /** + * Sets the internal menu factory. + * + * @param MenuFactoryInterface $menuFactory The menu factory + * + * @return void + * + * @since 4.2.0 + */ + public function setMenuFactory(MenuFactoryInterface $menuFactory): void + { + $this->menuFactory = $menuFactory; + } } diff --git a/code/libraries/src/Application/CMSApplicationInterface.php b/code/libraries/src/Application/CMSApplicationInterface.php index ebab368d..95cadc56 100644 --- a/code/libraries/src/Application/CMSApplicationInterface.php +++ b/code/libraries/src/Application/CMSApplicationInterface.php @@ -1,4 +1,5 @@ close(); - } - - $container = $container ?: Factory::getContainer(); - $this->setContainer($container); - $this->setDispatcher($dispatcher ?: $container->get(\Joomla\Event\DispatcherInterface::class)); - - if (!$container->has('session')) - { - $container->alias('session', 'session.cli') - ->alias('JSession', 'session.cli') - ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); - } - - $this->input = new \Joomla\CMS\Input\Cli; - $this->language = Factory::getLanguage(); - $this->output = $output ?: new Stdout; - $this->cliInput = $cliInput ?: new CliInput; - - parent::__construct($config); - - // Set the current directory. - $this->set('cwd', getcwd()); - - // Set up the environment - $this->input->set('format', 'cli'); - } - - /** - * Magic method to access properties of the application. - * - * @param string $name The name of the property. - * - * @return mixed A value if the property name is valid, null otherwise. - * - * @since 4.0.0 - * @deprecated 5.0 This is a B/C proxy for deprecated read accesses - */ - public function __get($name) - { - switch ($name) - { - case 'input': - @trigger_error( - 'Accessing the input property of the application is deprecated, use the getInput() method instead.', - E_USER_DEPRECATED - ); - - return $this->getInput(); - - default: - $trace = debug_backtrace(); - trigger_error( - sprintf( - 'Undefined property via __get(): %1$s in %2$s on line %3$s', - $name, - $trace[0]['file'], - $trace[0]['line'] - ), - E_USER_NOTICE - ); - } - } - - /** - * Method to get the application input object. - * - * @return Input - * - * @since 4.0.0 - */ - public function getInput(): Input - { - return $this->input; - } - - /** - * Method to get the application language object. - * - * @return Language The language object - * - * @since 4.0.0 - */ - public function getLanguage() - { - return $this->language; - } - - /** - * Returns a reference to the global CliApplication object, only creating it if it doesn't already exist. - * - * This method must be invoked as: $cli = CliApplication::getInstance(); - * - * @param string $name The name (optional) of the Application Cli class to instantiate. - * - * @return CliApplication - * - * @since 1.7.0 - * @deprecated 5.0 Load the app through the container - * @throws \RuntimeException - */ - public static function getInstance($name = null) - { - // Only create the object if it doesn't exist. - if (empty(static::$instance)) - { - if (!class_exists($name)) - { - throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500); - } - - static::$instance = new $name; - } - - return static::$instance; - } - - /** - * Execute the application. - * - * @return void - * - * @since 1.7.0 - */ - public function execute() - { - $this->createExtensionNamespaceMap(); - - // Trigger the onBeforeExecute event - $this->triggerEvent('onBeforeExecute'); - - // Perform application routines. - $this->doExecute(); - - // Trigger the onAfterExecute event. - $this->triggerEvent('onAfterExecute'); - } - - /** - * Get an output object. - * - * @return CliOutput - * - * @since 4.0.0 - */ - public function getOutput() - { - return $this->output; - } - - /** - * Get a CLI input object. - * - * @return CliInput - * - * @since 4.0.0 - */ - public function getCliInput() - { - return $this->cliInput; - } - - /** - * Write a string to standard output. - * - * @param string $text The text to display. - * @param boolean $nl True (default) to append a new line at the end of the output string. - * - * @return $this - * - * @since 4.0.0 - */ - public function out($text = '', $nl = true) - { - $this->getOutput()->out($text, $nl); - - return $this; - } - - /** - * Get a value from standard input. - * - * @return string The input string from standard input. - * - * @codeCoverageIgnore - * @since 4.0.0 - */ - public function in() - { - return $this->getCliInput()->in(); - } - - /** - * Set an output object. - * - * @param CliOutput $output CliOutput object - * - * @return $this - * - * @since 3.3 - */ - public function setOutput(CliOutput $output) - { - $this->output = $output; - - return $this; - } - - /** - * Enqueue a system message. - * - * @param string $msg The message to enqueue. - * @param string $type The message type. - * - * @return void - * - * @since 4.0.0 - */ - public function enqueueMessage($msg, $type = self::MSG_INFO) - { - if (!\array_key_exists($type, $this->messages)) - { - $this->messages[$type] = []; - } - - $this->messages[$type][] = $msg; - } - - /** - * Get the system message queue. - * - * @return array The system message queue. - * - * @since 4.0.0 - */ - public function getMessageQueue() - { - return $this->messages; - } - - /** - * Check the client interface by name. - * - * @param string $identifier String identifier for the application interface - * - * @return boolean True if this application is of the given type client interface. - * - * @since 4.0.0 - */ - public function isClient($identifier) - { - return $identifier === 'cli'; - } - - /** - * Method to get the application session object. - * - * @return SessionInterface The session object - * - * @since 4.0.0 - */ - public function getSession() - { - return $this->container->get(SessionInterface::class); - } - - /** - * Retrieve the application configuration object. - * - * @return Registry - * - * @since 4.0.0 - */ - public function getConfig() - { - return $this->config; - } - - /** - * Flag if the application instance is a CLI or web based application. - * - * Helper function, you should use the native PHP functions to detect if it is a CLI application. - * - * @return boolean - * - * @since 4.0.0 - * @deprecated 5.0 Will be removed without replacements - */ - public function isCli() - { - return true; - } + use DispatcherAwareTrait; + use EventAware; + use IdentityAware; + use ContainerAwareTrait; + use ExtensionManagerTrait; + use ExtensionNamespaceMapper; + + /** + * Output object + * + * @var CliOutput + * @since 4.0.0 + */ + protected $output; + + /** + * The input. + * + * @var \Joomla\Input\Input + * @since 4.0.0 + */ + protected $input = null; + + /** + * CLI Input object + * + * @var CliInput + * @since 4.0.0 + */ + protected $cliInput; + + /** + * The application language object. + * + * @var Language + * @since 4.0.0 + */ + protected $language; + + /** + * The application message queue. + * + * @var array + * @since 4.0.0 + */ + protected $messages = []; + + /** + * The application instance. + * + * @var CliApplication + * @since 1.7.0 + */ + protected static $instance; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's + * input object. If the argument is a JInputCli object that object will become + * the application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * @param CliOutput $output The output handler. + * @param CliInput $cliInput The CLI input handler. + * @param DispatcherInterface $dispatcher An optional argument to provide dependency injection for the application's + * event dispatcher. If the argument is a DispatcherInterface object that object will become + * the application's event dispatcher, if it is null then the default event dispatcher + * will be created based on the application's loadDispatcher() method. + * @param Container $container Dependency injection container. + * + * @since 1.7.0 + */ + public function __construct( + Input $input = null, + Registry $config = null, + CliOutput $output = null, + CliInput $cliInput = null, + DispatcherInterface $dispatcher = null, + Container $container = null + ) { + // Close the application if we are not executed from the command line. + if (!\defined('STDOUT') || !\defined('STDIN') || !isset($_SERVER['argv'])) { + $this->close(); + } + + $container = $container ?: Factory::getContainer(); + $this->setContainer($container); + $this->setDispatcher($dispatcher ?: $container->get(\Joomla\Event\DispatcherInterface::class)); + + if (!$container->has('session')) { + $container->alias('session', 'session.cli') + ->alias('JSession', 'session.cli') + ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); + } + + $this->input = new \Joomla\CMS\Input\Cli(); + $this->language = Factory::getLanguage(); + $this->output = $output ?: new Stdout(); + $this->cliInput = $cliInput ?: new CliInput(); + + parent::__construct($config); + + // Set the current directory. + $this->set('cwd', getcwd()); + + // Set up the environment + $this->input->set('format', 'cli'); + } + + /** + * Magic method to access properties of the application. + * + * @param string $name The name of the property. + * + * @return mixed A value if the property name is valid, null otherwise. + * + * @since 4.0.0 + * @deprecated 5.0 This is a B/C proxy for deprecated read accesses + */ + public function __get($name) + { + switch ($name) { + case 'input': + @trigger_error( + 'Accessing the input property of the application is deprecated, use the getInput() method instead.', + E_USER_DEPRECATED + ); + + return $this->getInput(); + + default: + $trace = debug_backtrace(); + trigger_error( + sprintf( + 'Undefined property via __get(): %1$s in %2$s on line %3$s', + $name, + $trace[0]['file'], + $trace[0]['line'] + ), + E_USER_NOTICE + ); + } + } + + /** + * Method to get the application input object. + * + * @return Input + * + * @since 4.0.0 + */ + public function getInput(): Input + { + return $this->input; + } + + /** + * Method to get the application language object. + * + * @return Language The language object + * + * @since 4.0.0 + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Returns a reference to the global CliApplication object, only creating it if it doesn't already exist. + * + * This method must be invoked as: $cli = CliApplication::getInstance(); + * + * @param string $name The name (optional) of the Application Cli class to instantiate. + * + * @return CliApplication + * + * @since 1.7.0 + * @deprecated 5.0 Load the app through the container + * @throws \RuntimeException + */ + public static function getInstance($name = null) + { + // Only create the object if it doesn't exist. + if (empty(static::$instance)) { + if (!class_exists($name)) { + throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500); + } + + static::$instance = new $name(); + } + + return static::$instance; + } + + /** + * Execute the application. + * + * @return void + * + * @since 1.7.0 + */ + public function execute() + { + $this->createExtensionNamespaceMap(); + + // Trigger the onBeforeExecute event + $this->triggerEvent('onBeforeExecute'); + + // Perform application routines. + $this->doExecute(); + + // Trigger the onAfterExecute event. + $this->triggerEvent('onAfterExecute'); + } + + /** + * Get an output object. + * + * @return CliOutput + * + * @since 4.0.0 + */ + public function getOutput() + { + return $this->output; + } + + /** + * Get a CLI input object. + * + * @return CliInput + * + * @since 4.0.0 + */ + public function getCliInput() + { + return $this->cliInput; + } + + /** + * Write a string to standard output. + * + * @param string $text The text to display. + * @param boolean $nl True (default) to append a new line at the end of the output string. + * + * @return $this + * + * @since 4.0.0 + */ + public function out($text = '', $nl = true) + { + $this->getOutput()->out($text, $nl); + + return $this; + } + + /** + * Get a value from standard input. + * + * @return string The input string from standard input. + * + * @codeCoverageIgnore + * @since 4.0.0 + */ + public function in() + { + return $this->getCliInput()->in(); + } + + /** + * Set an output object. + * + * @param CliOutput $output CliOutput object + * + * @return $this + * + * @since 3.3 + */ + public function setOutput(CliOutput $output) + { + $this->output = $output; + + return $this; + } + + /** + * Enqueue a system message. + * + * @param string $msg The message to enqueue. + * @param string $type The message type. + * + * @return void + * + * @since 4.0.0 + */ + public function enqueueMessage($msg, $type = self::MSG_INFO) + { + if (!\array_key_exists($type, $this->messages)) { + $this->messages[$type] = []; + } + + $this->messages[$type][] = $msg; + } + + /** + * Get the system message queue. + * + * @return array The system message queue. + * + * @since 4.0.0 + */ + public function getMessageQueue() + { + return $this->messages; + } + + /** + * Check the client interface by name. + * + * @param string $identifier String identifier for the application interface + * + * @return boolean True if this application is of the given type client interface. + * + * @since 4.0.0 + */ + public function isClient($identifier) + { + return $identifier === 'cli'; + } + + /** + * Method to get the application session object. + * + * @return SessionInterface The session object + * + * @since 4.0.0 + */ + public function getSession() + { + return $this->container->get(SessionInterface::class); + } + + /** + * Retrieve the application configuration object. + * + * @return Registry + * + * @since 4.0.0 + */ + public function getConfig() + { + return $this->config; + } + + /** + * Flag if the application instance is a CLI or web based application. + * + * Helper function, you should use the native PHP functions to detect if it is a CLI application. + * + * @return boolean + * + * @since 4.0.0 + * @deprecated 5.0 Will be removed without replacements + */ + public function isCli() + { + return true; + } } diff --git a/code/libraries/src/Application/ConsoleApplication.php b/code/libraries/src/Application/ConsoleApplication.php index a4d9b854..be55ffd0 100644 --- a/code/libraries/src/Application/ConsoleApplication.php +++ b/code/libraries/src/Application/ConsoleApplication.php @@ -1,4 +1,5 @@ close(); - } - - // Set up a Input object for Controllers etc to use - $this->input = new \Joomla\CMS\Input\Cli; - $this->language = $language; - - parent::__construct($input, $output, $config); - - $this->setVersion(JVERSION); - - // Register the client name as cli - $this->name = 'cli'; - - $this->setContainer($container); - $this->setDispatcher($dispatcher); - - // Set the execution datetime and timestamp; - $this->set('execution.datetime', gmdate('Y-m-d H:i:s')); - $this->set('execution.timestamp', time()); - $this->set('execution.microtimestamp', microtime(true)); - - // Set the current directory. - $this->set('cwd', getcwd()); - - // Set up the environment - $this->input->set('format', 'cli'); - } - - /** - * Magic method to access properties of the application. - * - * @param string $name The name of the property. - * - * @return mixed A value if the property name is valid, null otherwise. - * - * @since 4.0.0 - * @deprecated 5.0 This is a B/C proxy for deprecated read accesses - */ - public function __get($name) - { - switch ($name) - { - case 'input': - @trigger_error( - 'Accessing the input property of the application is deprecated, use the getInput() method instead.', - E_USER_DEPRECATED - ); - - return $this->getInput(); - - default: - $trace = debug_backtrace(); - trigger_error( - sprintf( - 'Undefined property via __get(): %1$s in %2$s on line %3$s', - $name, - $trace[0]['file'], - $trace[0]['line'] - ), - E_USER_NOTICE - ); - } - } - - /** - * Method to run the application routines. - * - * @return integer The exit code for the application - * - * @since 4.0.0 - * @throws \Throwable - */ - protected function doExecute(): int - { - $exitCode = parent::doExecute(); - - $style = new SymfonyStyle($this->getConsoleInput(), $this->getConsoleOutput()); - - $methodMap = [ - self::MSG_ALERT => 'error', - self::MSG_CRITICAL => 'caution', - self::MSG_DEBUG => 'comment', - self::MSG_EMERGENCY => 'caution', - self::MSG_ERROR => 'error', - self::MSG_INFO => 'note', - self::MSG_NOTICE => 'note', - self::MSG_WARNING => 'warning', - ]; - - // Output any enqueued messages before the app exits - foreach ($this->getMessageQueue() as $type => $messages) - { - $method = $methodMap[$type] ?? 'comment'; - - $style->$method($messages); - } - - return $exitCode; - } - - /** - * Execute the application. - * - * @return void - * - * @since 4.0.0 - * @throws \Throwable - */ - public function execute() - { - // Load extension namespaces - $this->createExtensionNamespaceMap(); - - // Import CMS plugin groups to be able to subscribe to events - PluginHelper::importPlugin('system'); - PluginHelper::importPlugin('console'); - - parent::execute(); - } - - /** - * Enqueue a system message. - * - * @param string $msg The message to enqueue. - * @param string $type The message type. - * - * @return void - * - * @since 4.0.0 - */ - public function enqueueMessage($msg, $type = self::MSG_INFO) - { - if (!array_key_exists($type, $this->messages)) - { - $this->messages[$type] = []; - } - - $this->messages[$type][] = $msg; - } - - /** - * Gets the name of the current running application. - * - * @return string The name of the application. - * - * @since 4.0.0 - */ - public function getName(): string - { - return $this->name; - } - - /** - * Get the commands which should be registered by default to the application. - * - * @return \Joomla\Console\Command\AbstractCommand[] - * - * @since 4.0.0 - */ - protected function getDefaultCommands(): array - { - return array_merge( - parent::getDefaultCommands(), - [ - new Console\CleanCacheCommand, - new Console\CheckUpdatesCommand, - new Console\RemoveOldFilesCommand, - new Console\AddUserCommand, - new Console\AddUserToGroupCommand, - new Console\RemoveUserFromGroupCommand, - new Console\DeleteUserCommand, - new Console\ChangeUserPasswordCommand, - new Console\ListUserCommand, - ] - ); - } - - /** - * Retrieve the application configuration object. - * - * @return Registry - * - * @since 4.0.0 - */ - public function getConfig() - { - return $this->config; - } - - /** - * Method to get the application input object. - * - * @return Input - * - * @since 4.0.0 - */ - public function getInput(): Input - { - return $this->input; - } - - /** - * Method to get the application language object. - * - * @return Language The language object - * - * @since 4.0.0 - */ - public function getLanguage() - { - return $this->language; - } - - /** - * Get the system message queue. - * - * @return array The system message queue. - * - * @since 4.0.0 - */ - public function getMessageQueue() - { - return $this->messages; - } - - /** - * Method to get the application session object. - * - * @return SessionInterface The session object - * - * @since 4.0.0 - */ - public function getSession() - { - return $this->session; - } - - /** - * Check the client interface by name. - * - * @param string $identifier String identifier for the application interface - * - * @return boolean True if this application is of the given type client interface. - * - * @since 4.0.0 - */ - public function isClient($identifier) - { - return $this->getName() === $identifier; - } - - /** - * Flag if the application instance is a CLI or web based application. - * - * Helper function, you should use the native PHP functions to detect if it is a CLI application. - * - * @return boolean - * - * @since 4.0.0 - * @deprecated 5.0 Will be removed without replacements - */ - public function isCli() - { - return true; - } - - /** - * Sets the session for the application to use, if required. - * - * @param SessionInterface $session A session object. - * - * @return $this - * - * @since 4.0.0 - */ - public function setSession(SessionInterface $session): self - { - $this->session = $session; - - return $this; - } - - /** - * Flush the media version to refresh versionable assets - * - * @return void - * - * @since 4.0.0 - */ - public function flushAssets() - { - (new Version)->refreshMediaVersion(); - } - - /** - * Get the long version string for the application. - * - * Overrides the parent method due to conflicting use of the getName method between the console application and - * the CMS application interface. - * - * @return string - * - * @since 4.0.0 - */ - public function getLongVersion(): string - { - return sprintf('Joomla! %s (debug: %s)', (new Version)->getShortVersion(), (\defined('JDEBUG') && JDEBUG ? 'Yes' : 'No')); - } - - /** - * Set the name of the application. - * - * @param string $name The new application name. - * - * @return void - * - * @since 4.0.0 - * @throws \RuntimeException because the application name cannot be changed - */ - public function setName(string $name): void - { - throw new \RuntimeException('The console application name cannot be changed'); - } - - /** - * Returns the application Router object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return Router - * - * @since 4.0.6 - * @throws \InvalidArgumentException - */ - public static function getRouter($name = null, array $options = array()) - { - if (empty($name)) - { - throw new InvalidArgumentException('A router name must be set in console application.'); - } - - $options['mode'] = Factory::getApplication()->get('sef'); - - return Router::getInstance($name, $options); - } + use DispatcherAwareTrait; + use EventAware; + use IdentityAware; + use ContainerAwareTrait; + use ExtensionManagerTrait; + use ExtensionNamespaceMapper; + use DatabaseAwareTrait; + + /** + * The input. + * + * @var Input + * @since 4.0.0 + */ + protected $input = null; + + /** + * The name of the application. + * + * @var string + * @since 4.0.0 + */ + protected $name = null; + + /** + * The application language object. + * + * @var Language + * @since 4.0.0 + */ + protected $language; + + /** + * The application message queue. + * + * @var array + * @since 4.0.0 + */ + private $messages = []; + + /** + * The application session object. + * + * @var SessionInterface + * @since 4.0.0 + */ + private $session; + + /** + * Class constructor. + * + * @param Registry $config An optional argument to provide dependency injection for the application's config object. If the + * argument is a Registry object that object will become the application's config object, + * otherwise a default config object is created. + * @param DispatcherInterface $dispatcher An optional argument to provide dependency injection for the application's event dispatcher. If the + * argument is a DispatcherInterface object that object will become the application's event dispatcher, + * if it is null then the default event dispatcher will be created based on the application's + * loadDispatcher() method. + * @param Container $container Dependency injection container. + * @param Language $language The language object provisioned for the application. + * @param InputInterface|null $input An optional argument to provide dependency injection for the application's input object. If the + * argument is an InputInterface object that object will become the application's input object, + * otherwise a default input object is created. + * @param OutputInterface|null $output An optional argument to provide dependency injection for the application's output object. If the + * argument is an OutputInterface object that object will become the application's output object, + * otherwise a default output object is created. + * + * @since 4.0.0 + */ + public function __construct( + Registry $config, + DispatcherInterface $dispatcher, + Container $container, + Language $language, + ?InputInterface $input = null, + ?OutputInterface $output = null + ) { + // Close the application if it is not executed from the command line. + if (!\defined('STDOUT') || !\defined('STDIN') || !isset($_SERVER['argv'])) { + $this->close(); + } + + // Set up a Input object for Controllers etc to use + $this->input = new \Joomla\CMS\Input\Cli(); + $this->language = $language; + + parent::__construct($input, $output, $config); + + $this->setVersion(JVERSION); + + // Register the client name as cli + $this->name = 'cli'; + + $this->setContainer($container); + $this->setDispatcher($dispatcher); + + // Set the execution datetime and timestamp; + $this->set('execution.datetime', gmdate('Y-m-d H:i:s')); + $this->set('execution.timestamp', time()); + $this->set('execution.microtimestamp', microtime(true)); + + // Set the current directory. + $this->set('cwd', getcwd()); + + // Set up the environment + $this->input->set('format', 'cli'); + } + + /** + * Magic method to access properties of the application. + * + * @param string $name The name of the property. + * + * @return mixed A value if the property name is valid, null otherwise. + * + * @since 4.0.0 + * @deprecated 5.0 This is a B/C proxy for deprecated read accesses + */ + public function __get($name) + { + switch ($name) { + case 'input': + @trigger_error( + 'Accessing the input property of the application is deprecated, use the getInput() method instead.', + E_USER_DEPRECATED + ); + + return $this->getInput(); + + default: + $trace = debug_backtrace(); + trigger_error( + sprintf( + 'Undefined property via __get(): %1$s in %2$s on line %3$s', + $name, + $trace[0]['file'], + $trace[0]['line'] + ), + E_USER_NOTICE + ); + } + } + + /** + * Method to run the application routines. + * + * @return integer The exit code for the application + * + * @since 4.0.0 + * @throws \Throwable + */ + protected function doExecute(): int + { + $exitCode = parent::doExecute(); + + $style = new SymfonyStyle($this->getConsoleInput(), $this->getConsoleOutput()); + + $methodMap = [ + self::MSG_ALERT => 'error', + self::MSG_CRITICAL => 'caution', + self::MSG_DEBUG => 'comment', + self::MSG_EMERGENCY => 'caution', + self::MSG_ERROR => 'error', + self::MSG_INFO => 'note', + self::MSG_NOTICE => 'note', + self::MSG_WARNING => 'warning', + ]; + + // Output any enqueued messages before the app exits + foreach ($this->getMessageQueue() as $type => $messages) { + $method = $methodMap[$type] ?? 'comment'; + + $style->$method($messages); + } + + return $exitCode; + } + + /** + * Execute the application. + * + * @return void + * + * @since 4.0.0 + * @throws \Throwable + */ + public function execute() + { + // Load extension namespaces + $this->createExtensionNamespaceMap(); + + /** + * Address issues with instantiating WebApplication descendants under CLI. + * + * IMPORTANT! This code must be always be executed **before** the first use of + * PluginHelper::importPlugin(). Some plugins will attempt to register an MVCFactory for a + * component in their service provider. This will in turn try to get the SiteRouter service + * for the component which tries to get an instance of SiteApplication which will fail with + * a RuntimeException if the populateHttpHost() method has not already executed. + */ + $this->populateHttpHost(); + + // Import CMS plugin groups to be able to subscribe to events + PluginHelper::importPlugin('system'); + PluginHelper::importPlugin('console'); + + parent::execute(); + } + + /** + * Enqueue a system message. + * + * @param string $msg The message to enqueue. + * @param string $type The message type. + * + * @return void + * + * @since 4.0.0 + */ + public function enqueueMessage($msg, $type = self::MSG_INFO) + { + if (!array_key_exists($type, $this->messages)) { + $this->messages[$type] = []; + } + + $this->messages[$type][] = $msg; + } + + /** + * Gets the name of the current running application. + * + * @return string The name of the application. + * + * @since 4.0.0 + */ + public function getName(): string + { + return $this->name; + } + + /** + * Get the commands which should be registered by default to the application. + * + * @return \Joomla\Console\Command\AbstractCommand[] + * + * @since 4.0.0 + */ + protected function getDefaultCommands(): array + { + return array_merge( + parent::getDefaultCommands(), + [ + new Console\CleanCacheCommand(), + new Console\CheckUpdatesCommand(), + new Console\RemoveOldFilesCommand(), + new Console\AddUserCommand($this->getDatabase()), + new Console\AddUserToGroupCommand($this->getDatabase()), + new Console\RemoveUserFromGroupCommand($this->getDatabase()), + new Console\DeleteUserCommand($this->getDatabase()), + new Console\ChangeUserPasswordCommand(), + new Console\ListUserCommand($this->getDatabase()), + ] + ); + } + + /** + * Retrieve the application configuration object. + * + * @return Registry + * + * @since 4.0.0 + */ + public function getConfig() + { + return $this->config; + } + + /** + * Method to get the application input object. + * + * @return Input + * + * @since 4.0.0 + */ + public function getInput(): Input + { + return $this->input; + } + + /** + * Method to get the application language object. + * + * @return Language The language object + * + * @since 4.0.0 + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Get the system message queue. + * + * @return array The system message queue. + * + * @since 4.0.0 + */ + public function getMessageQueue() + { + return $this->messages; + } + + /** + * Method to get the application session object. + * + * @return SessionInterface The session object + * + * @since 4.0.0 + */ + public function getSession() + { + return $this->session; + } + + /** + * Check the client interface by name. + * + * @param string $identifier String identifier for the application interface + * + * @return boolean True if this application is of the given type client interface. + * + * @since 4.0.0 + */ + public function isClient($identifier) + { + return $this->getName() === $identifier; + } + + /** + * Flag if the application instance is a CLI or web based application. + * + * Helper function, you should use the native PHP functions to detect if it is a CLI application. + * + * @return boolean + * + * @since 4.0.0 + * @deprecated 5.0 Will be removed without replacements + */ + public function isCli() + { + return true; + } + + /** + * Sets the session for the application to use, if required. + * + * @param SessionInterface $session A session object. + * + * @return $this + * + * @since 4.0.0 + */ + public function setSession(SessionInterface $session): self + { + $this->session = $session; + + return $this; + } + + /** + * Flush the media version to refresh versionable assets + * + * @return void + * + * @since 4.0.0 + */ + public function flushAssets() + { + (new Version())->refreshMediaVersion(); + } + + /** + * Get the long version string for the application. + * + * Overrides the parent method due to conflicting use of the getName method between the console application and + * the CMS application interface. + * + * @return string + * + * @since 4.0.0 + */ + public function getLongVersion(): string + { + return sprintf('Joomla! %s (debug: %s)', (new Version())->getShortVersion(), (\defined('JDEBUG') && JDEBUG ? 'Yes' : 'No')); + } + + /** + * Set the name of the application. + * + * @param string $name The new application name. + * + * @return void + * + * @since 4.0.0 + * @throws \RuntimeException because the application name cannot be changed + */ + public function setName(string $name): void + { + throw new \RuntimeException('The console application name cannot be changed'); + } + + /** + * Returns the application Router object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return Router + * + * @since 4.0.6 + * + * @throws \InvalidArgumentException + * + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public static function getRouter($name = null, array $options = array()) + { + if (empty($name)) { + throw new InvalidArgumentException('A router name must be set in console application.'); + } + + $options['mode'] = Factory::getApplication()->get('sef'); + + return Router::getInstance($name, $options); + } + + /** + * Populates the HTTP_HOST and REQUEST_URI from the URL provided in the --live-site parameter. + * + * If the URL provided is empty or invalid we will use the URL + * https://joomla.invalid/set/by/console/application just so that the CLI application doesn't + * crash when a WebApplication descendant is instantiated in it. + * + * This is a practical workaround for using any service depending on a WebApplication + * descendant under CLI. + * + * Practical example: using a component's MVCFactory which instantiates the SiteRouter + * service for that component which in turn relies on an instance of SiteApplication. + * + * @return void + * @since 4.2.1 + * @see https://github.com/joomla/joomla-cms/issues/38518 + */ + protected function populateHttpHost() + { + // First check for the --live-site command line option. + $input = $this->getConsoleInput(); + $liveSite = ''; + + if ($input->hasParameterOption(['--live-site', false])) { + $liveSite = $input->getParameterOption(['--live-site'], ''); + } + + // Fallback to the $live_site global configuration option in configuration.php + $liveSite = $liveSite ?: $this->get('live_site', 'https://joomla.invalid/set/by/console/application'); + + /** + * Try to use the live site URL we were given. If all else fails, fall back to + * https://joomla.invalid/set/by/console/application. + */ + try { + $uri = Uri::getInstance($liveSite); + } catch (\RuntimeException $e) { + $uri = Uri::getInstance('https://joomla.invalid/set/by/console/application'); + } + + /** + * Yes, this is icky but it is the only way to trick WebApplication into compliance. + * + * @see \Joomla\Application\AbstractWebApplication::detectRequestUri + */ + $_SERVER['HTTP_HOST'] = $uri->toString(['host', 'port']); + $_SERVER['REQUEST_URI'] = $uri->getPath(); + $_SERVER['HTTPS'] = $uri->getScheme() === 'https' ? 'on' : 'off'; + } + + /** + * Builds the default input definition. + * + * @return InputDefinition + * + * @since 4.2.1 + */ + protected function getDefaultInputDefinition(): InputDefinition + { + return new InputDefinition( + [ + new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), + new InputOption( + '--live-site', + null, + InputOption::VALUE_OPTIONAL, + 'The URL to your site, e.g. https://www.example.com' + ), + new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display the help information'), + new InputOption( + '--quiet', + '-q', + InputOption::VALUE_NONE, + 'Flag indicating that all output should be silenced' + ), + new InputOption( + '--verbose', + '-v|vv|vvv', + InputOption::VALUE_NONE, + 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug' + ), + new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Displays the application version'), + new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'), + new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'), + new InputOption( + '--no-interaction', + '-n', + InputOption::VALUE_NONE, + 'Flag to disable interacting with the user' + ), + ] + ); + } } diff --git a/code/libraries/src/Application/DaemonApplication.php b/code/libraries/src/Application/DaemonApplication.php index f56ff88a..3f9dfefa 100644 --- a/code/libraries/src/Application/DaemonApplication.php +++ b/code/libraries/src/Application/DaemonApplication.php @@ -1,4 +1,5 @@ config->get('max_execution_time', 0)); - - if ($this->config->get('max_memory_limit') !== null) - { - ini_set('memory_limit', $this->config->get('max_memory_limit', '256M')); - } - - // Flush content immediately. - ob_implicit_flush(); - } - - /** - * Method to handle POSIX signals. - * - * @param integer $signal The received POSIX signal. - * - * @return void - * - * @since 1.7.0 - * @see pcntl_signal() - * @throws \RuntimeException - */ - public static function signal($signal) - { - // Log all signals sent to the daemon. - Log::add('Received signal: ' . $signal, Log::DEBUG); - - // Let's make sure we have an application instance. - if (!is_subclass_of(static::$instance, CliApplication::class)) - { - Log::add('Cannot find the application instance.', Log::EMERGENCY); - throw new \RuntimeException('Cannot find the application instance.'); - } - - // Fire the onReceiveSignal event. - static::$instance->triggerEvent('onReceiveSignal', array($signal)); - - switch ($signal) - { - case SIGINT: - case SIGTERM: - // Handle shutdown tasks - if (static::$instance->running && static::$instance->isActive()) - { - static::$instance->shutdown(); - } - else - { - static::$instance->close(); - } - break; - case SIGHUP: - // Handle restart tasks - if (static::$instance->running && static::$instance->isActive()) - { - static::$instance->shutdown(true); - } - else - { - static::$instance->close(); - } - break; - case SIGCHLD: - // A child process has died - while (static::$instance->pcntlWait($signal, WNOHANG || WUNTRACED) > 0) - { - usleep(1000); - } - break; - case SIGCLD: - while (static::$instance->pcntlWait($signal, WNOHANG) > 0) - { - $signal = static::$instance->pcntlChildExitStatus($signal); - } - break; - default: - break; - } - } - - /** - * Check to see if the daemon is active. This does not assume that $this daemon is active, but - * only if an instance of the application is active as a daemon. - * - * @return boolean True if daemon is active. - * - * @since 1.7.0 - */ - public function isActive() - { - // Get the process id file location for the application. - $pidFile = $this->config->get('application_pid_file'); - - // If the process id file doesn't exist then the daemon is obviously not running. - if (!is_file($pidFile)) - { - return false; - } - - // Read the contents of the process id file as an integer. - $fp = fopen($pidFile, 'r'); - $pid = fread($fp, filesize($pidFile)); - $pid = (int) $pid; - fclose($fp); - - // Check to make sure that the process id exists as a positive integer. - if (!$pid) - { - return false; - } - - // Check to make sure the process is active by pinging it and ensure it responds. - if (!posix_kill($pid, 0)) - { - // No response so remove the process id file and log the situation. - @ unlink($pidFile); - Log::add('The process found based on PID file was unresponsive.', Log::WARNING); - - return false; - } - - return true; - } - - /** - * Load an object or array into the application configuration object. - * - * @param mixed $data Either an array or object to be loaded into the configuration object. - * - * @return DaemonApplication Instance of $this to allow chaining. - * - * @since 1.7.0 - */ - public function loadConfiguration($data) - { - /* - * Setup some application metadata options. This is useful if we ever want to write out startup scripts - * or just have some sort of information available to share about things. - */ - - // The application author name. This string is used in generating startup scripts and has - // a maximum of 50 characters. - $tmp = (string) $this->config->get('author_name', 'Joomla Platform'); - $this->config->set('author_name', (\strlen($tmp) > 50) ? substr($tmp, 0, 50) : $tmp); - - // The application author email. This string is used in generating startup scripts. - $tmp = (string) $this->config->get('author_email', 'admin@joomla.org'); - $this->config->set('author_email', filter_var($tmp, FILTER_VALIDATE_EMAIL)); - - // The application name. This string is used in generating startup scripts. - $tmp = (string) $this->config->get('application_name', 'DaemonApplication'); - $this->config->set('application_name', (string) preg_replace('/[^A-Z0-9_-]/i', '', $tmp)); - - // The application description. This string is used in generating startup scripts. - $tmp = (string) $this->config->get('application_description', 'A generic Joomla Platform application.'); - $this->config->set('application_description', filter_var($tmp, FILTER_SANITIZE_STRING)); - - /* - * Setup the application path options. This defines the default executable name, executable directory, - * and also the path to the daemon process id file. - */ - - // The application executable daemon. This string is used in generating startup scripts. - $tmp = (string) $this->config->get('application_executable', basename($this->input->executable)); - $this->config->set('application_executable', $tmp); - - // The home directory of the daemon. - $tmp = (string) $this->config->get('application_directory', \dirname($this->input->executable)); - $this->config->set('application_directory', $tmp); - - // The pid file location. This defaults to a path inside the /tmp directory. - $name = $this->config->get('application_name'); - $tmp = (string) $this->config->get('application_pid_file', strtolower('/tmp/' . $name . '/' . $name . '.pid')); - $this->config->set('application_pid_file', $tmp); - - /* - * Setup the application identity options. It is important to remember if the default of 0 is set for - * either UID or GID then changing that setting will not be attempted as there is no real way to "change" - * the identity of a process from some user to root. - */ - - // The user id under which to run the daemon. - $tmp = (int) $this->config->get('application_uid', 0); - $options = array('options' => array('min_range' => 0, 'max_range' => 65000)); - $this->config->set('application_uid', filter_var($tmp, FILTER_VALIDATE_INT, $options)); - - // The group id under which to run the daemon. - $tmp = (int) $this->config->get('application_gid', 0); - $options = array('options' => array('min_range' => 0, 'max_range' => 65000)); - $this->config->set('application_gid', filter_var($tmp, FILTER_VALIDATE_INT, $options)); - - // Option to kill the daemon if it cannot switch to the chosen identity. - $tmp = (bool) $this->config->get('application_require_identity', 1); - $this->config->set('application_require_identity', $tmp); - - /* - * Setup the application runtime options. By default our execution time limit is infinite obviously - * because a daemon should be constantly running unless told otherwise. The default limit for memory - * usage is 256M, which admittedly is a little high, but remember it is a "limit" and PHP's memory - * management leaves a bit to be desired :-) - */ - - // The maximum execution time of the application in seconds. Zero is infinite. - $tmp = $this->config->get('max_execution_time'); - - if ($tmp !== null) - { - $this->config->set('max_execution_time', (int) $tmp); - } - - // The maximum amount of memory the application can use. - $tmp = $this->config->get('max_memory_limit', '256M'); - - if ($tmp !== null) - { - $this->config->set('max_memory_limit', (string) $tmp); - } - - return $this; - } - - /** - * Execute the daemon. - * - * @return void - * - * @since 1.7.0 - */ - public function execute() - { - // Trigger the onBeforeExecute event - $this->triggerEvent('onBeforeExecute'); - - // Enable basic garbage collection. - gc_enable(); - - Log::add('Starting ' . $this->name, Log::INFO); - - // Set off the process for becoming a daemon. - if ($this->daemonize()) - { - // Declare ticks to start signal monitoring. When you declare ticks, PCNTL will monitor - // incoming signals after each tick and call the relevant signal handler automatically. - declare (ticks = 1); - - // Start the main execution loop. - while (true) - { - // Perform basic garbage collection. - $this->gc(); - - // Don't completely overload the CPU. - usleep(1000); - - // Execute the main application logic. - $this->doExecute(); - } - } - // We were not able to daemonize the application so log the failure and die gracefully. - else - { - Log::add('Starting ' . $this->name . ' failed', Log::INFO); - } - - // Trigger the onAfterExecute event. - $this->triggerEvent('onAfterExecute'); - } - - /** - * Restart daemon process. - * - * @return void - * - * @since 1.7.0 - */ - public function restart() - { - Log::add('Stopping ' . $this->name, Log::INFO); - $this->shutdown(true); - } - - /** - * Stop daemon process. - * - * @return void - * - * @since 1.7.0 - */ - public function stop() - { - Log::add('Stopping ' . $this->name, Log::INFO); - $this->shutdown(); - } - - /** - * Method to change the identity of the daemon process and resources. - * - * @return boolean True if identity successfully changed - * - * @since 1.7.0 - * @see posix_setuid() - */ - protected function changeIdentity() - { - // Get the group and user ids to set for the daemon. - $uid = (int) $this->config->get('application_uid', 0); - $gid = (int) $this->config->get('application_gid', 0); - - // Get the application process id file path. - $file = $this->config->get('application_pid_file'); - - // Change the user id for the process id file if necessary. - if ($uid && (fileowner($file) != $uid) && (!@ chown($file, $uid))) - { - Log::add('Unable to change user ownership of the process id file.', Log::ERROR); - - return false; - } - - // Change the group id for the process id file if necessary. - if ($gid && (filegroup($file) != $gid) && (!@ chgrp($file, $gid))) - { - Log::add('Unable to change group ownership of the process id file.', Log::ERROR); - - return false; - } - - // Set the correct home directory for the process. - if ($uid && ($info = posix_getpwuid($uid)) && is_dir($info['dir'])) - { - system('export HOME="' . $info['dir'] . '"'); - } - - // Change the user id for the process necessary. - if ($uid && (posix_getuid() != $uid) && (!@ posix_setuid($uid))) - { - Log::add('Unable to change user ownership of the process.', Log::ERROR); - - return false; - } - - // Change the group id for the process necessary. - if ($gid && (posix_getgid() != $gid) && (!@ posix_setgid($gid))) - { - Log::add('Unable to change group ownership of the process.', Log::ERROR); - - return false; - } - - // Get the user and group information based on uid and gid. - $user = posix_getpwuid($uid); - $group = posix_getgrgid($gid); - - Log::add('Changed daemon identity to ' . $user['name'] . ':' . $group['name'], Log::INFO); - - return true; - } - - /** - * Method to put the application into the background. - * - * @return boolean - * - * @since 1.7.0 - * @throws \RuntimeException - */ - protected function daemonize() - { - // Is there already an active daemon running? - if ($this->isActive()) - { - Log::add($this->name . ' daemon is still running. Exiting the application.', Log::EMERGENCY); - - return false; - } - - // Reset Process Information - $this->safeMode = !!@ ini_get('safe_mode'); - $this->processId = 0; - $this->running = false; - - // Detach process! - try - { - // Check if we should run in the foreground. - if (!$this->input->get('f')) - { - // Detach from the terminal. - $this->detach(); - } - else - { - // Setup running values. - $this->exiting = false; - $this->running = true; - - // Set the process id. - $this->processId = (int) posix_getpid(); - $this->parentId = $this->processId; - } - } - catch (\RuntimeException $e) - { - Log::add('Unable to fork.', Log::EMERGENCY); - - return false; - } - - // Verify the process id is valid. - if ($this->processId < 1) - { - Log::add('The process id is invalid; the fork failed.', Log::EMERGENCY); - - return false; - } - - // Clear the umask. - @ umask(0); - - // Write out the process id file for concurrency management. - if (!$this->writeProcessIdFile()) - { - Log::add('Unable to write the pid file at: ' . $this->config->get('application_pid_file'), Log::EMERGENCY); - - return false; - } - - // Attempt to change the identity of user running the process. - if (!$this->changeIdentity()) - { - // If the identity change was required then we need to return false. - if ($this->config->get('application_require_identity')) - { - Log::add('Unable to change process owner.', Log::CRITICAL); - - return false; - } - else - { - Log::add('Unable to change process owner.', Log::WARNING); - } - } - - // Setup the signal handlers for the daemon. - if (!$this->setupSignalHandlers()) - { - return false; - } - - // Change the current working directory to the application working directory. - @ chdir($this->config->get('application_directory')); - - return true; - } - - /** - * This is truly where the magic happens. This is where we fork the process and kill the parent - * process, which is essentially what turns the application into a daemon. - * - * @return void - * - * @since 3.0.0 - * @throws \RuntimeException - */ - protected function detach() - { - Log::add('Detaching the ' . $this->name . ' daemon.', Log::DEBUG); - - // Attempt to fork the process. - $pid = $this->fork(); - - // If the pid is positive then we successfully forked, and can close this application. - if ($pid) - { - // Add the log entry for debugging purposes and exit gracefully. - Log::add('Ending ' . $this->name . ' parent process', Log::DEBUG); - $this->close(); - } - // We are in the forked child process. - else - { - // Setup some protected values. - $this->exiting = false; - $this->running = true; - - // Set the parent to self. - $this->parentId = $this->processId; - } - } - - /** - * Method to fork the process. - * - * @return integer The child process id to the parent process, zero to the child process. - * - * @since 1.7.0 - * @throws \RuntimeException - */ - protected function fork() - { - // Attempt to fork the process. - $pid = $this->pcntlFork(); - - // If the fork failed, throw an exception. - if ($pid === -1) - { - throw new \RuntimeException('The process could not be forked.'); - } - // Update the process id for the child. - elseif ($pid === 0) - { - $this->processId = (int) posix_getpid(); - } - // Log the fork in the parent. - else - { - // Log the fork. - Log::add('Process forked ' . $pid, Log::DEBUG); - } - - // Trigger the onFork event. - $this->postFork(); - - return $pid; - } - - /** - * Method to perform basic garbage collection and memory management in the sense of clearing the - * stat cache. We will probably call this method pretty regularly in our main loop. - * - * @return void - * - * @since 1.7.0 - */ - protected function gc() - { - // Perform generic garbage collection. - gc_collect_cycles(); - - // Clear the stat cache so it doesn't blow up memory. - clearstatcache(); - } - - /** - * Method to attach the DaemonApplication signal handler to the known signals. Applications - * can override these handlers by using the pcntl_signal() function and attaching a different - * callback method. - * - * @return boolean - * - * @since 1.7.0 - * @see pcntl_signal() - */ - protected function setupSignalHandlers() - { - // We add the error suppression for the loop because on some platforms some constants are not defined. - foreach (self::$signals as $signal) - { - // Ignore signals that are not defined. - if (!\defined($signal) || !\is_int(\constant($signal)) || (\constant($signal) === 0)) - { - // Define the signal to avoid notices. - Log::add('Signal "' . $signal . '" not defined. Defining it as null.', Log::DEBUG); - \define($signal, null); - - // Don't listen for signal. - continue; - } - - // Attach the signal handler for the signal. - if (!$this->pcntlSignal(\constant($signal), array('DaemonApplication', 'signal'))) - { - Log::add(sprintf('Unable to reroute signal handler: %s', $signal), Log::EMERGENCY); - - return false; - } - } - - return true; - } - - /** - * Method to shut down the daemon and optionally restart it. - * - * @param boolean $restart True to restart the daemon on exit. - * - * @return void - * - * @since 1.7.0 - */ - protected function shutdown($restart = false) - { - // If we are already exiting, chill. - if ($this->exiting) - { - return; - } - // If not, now we are. - else - { - $this->exiting = true; - } - - // If we aren't already daemonized then just kill the application. - if (!$this->running && !$this->isActive()) - { - Log::add('Process was not daemonized yet, just halting current process', Log::INFO); - $this->close(); - } - - // Only read the pid for the parent file. - if ($this->parentId == $this->processId) - { - // Read the contents of the process id file as an integer. - $fp = fopen($this->config->get('application_pid_file'), 'r'); - $pid = fread($fp, filesize($this->config->get('application_pid_file'))); - $pid = (int) $pid; - fclose($fp); - - // Remove the process id file. - @ unlink($this->config->get('application_pid_file')); - - // If we are supposed to restart the daemon we need to execute the same command. - if ($restart) - { - $this->close(exec(implode(' ', $GLOBALS['argv']) . ' > /dev/null &')); - } - // If we are not supposed to restart the daemon let's just kill -9. - else - { - passthru('kill -9 ' . $pid); - $this->close(); - } - } - } - - /** - * Method to write the process id file out to disk. - * - * @return boolean - * - * @since 1.7.0 - */ - protected function writeProcessIdFile() - { - // Verify the process id is valid. - if ($this->processId < 1) - { - Log::add('The process id is invalid.', Log::EMERGENCY); - - return false; - } - - // Get the application process id file path. - $file = $this->config->get('application_pid_file'); - - if (empty($file)) - { - Log::add('The process id file path is empty.', Log::ERROR); - - return false; - } - - // Make sure that the folder where we are writing the process id file exists. - $folder = \dirname($file); - - if (!is_dir($folder) && !Folder::create($folder)) - { - Log::add('Unable to create directory: ' . $folder, Log::ERROR); - - return false; - } - - // Write the process id file out to disk. - if (!file_put_contents($file, $this->processId)) - { - Log::add('Unable to write process id file: ' . $file, Log::ERROR); - - return false; - } - - // Make sure the permissions for the process id file are accurate. - if (!chmod($file, 0644)) - { - Log::add('Unable to adjust permissions for the process id file: ' . $file, Log::ERROR); - - return false; - } - - return true; - } - - /** - * Method to handle post-fork triggering of the onFork event. - * - * @return void - * - * @since 3.0.0 - */ - protected function postFork() - { - // Trigger the onFork event. - $this->triggerEvent('onFork'); - } - - /** - * Method to return the exit code of a terminated child process. - * - * @param integer $status The status parameter is the status parameter supplied to a successful call to pcntl_waitpid(). - * - * @return integer The child process exit code. - * - * @see pcntl_wexitstatus() - * @since 1.7.3 - */ - protected function pcntlChildExitStatus($status) - { - return pcntl_wexitstatus($status); - } - - /** - * Method to return the exit code of a terminated child process. - * - * @return integer On success, the PID of the child process is returned in the parent's thread - * of execution, and a 0 is returned in the child's thread of execution. On - * failure, a -1 will be returned in the parent's context, no child process - * will be created, and a PHP error is raised. - * - * @see pcntl_fork() - * @since 1.7.3 - */ - protected function pcntlFork() - { - return pcntl_fork(); - } - - /** - * Method to install a signal handler. - * - * @param integer $signal The signal number. - * @param callable $handler The signal handler which may be the name of a user created function, - * or method, or either of the two global constants SIG_IGN or SIG_DFL. - * @param boolean $restart Specifies whether system call restarting should be used when this - * signal arrives. - * - * @return boolean True on success. - * - * @see pcntl_signal() - * @since 1.7.3 - */ - protected function pcntlSignal($signal, $handler, $restart = true) - { - return pcntl_signal($signal, $handler, $restart); - } - - /** - * Method to wait on or return the status of a forked child. - * - * @param integer &$status Status information. - * @param integer $options If wait3 is available on your system (mostly BSD-style systems), - * you can provide the optional options parameter. - * - * @return integer The process ID of the child which exited, -1 on error or zero if WNOHANG - * was provided as an option (on wait3-available systems) and no child was available. - * - * @see pcntl_wait() - * @since 1.7.3 - */ - protected function pcntlWait(&$status, $options = 0) - { - return pcntl_wait($status, $options); - } + /** + * @var array The available POSIX signals to be caught by default. + * @link https://www.php.net/manual/pcntl.constants.php + * @since 1.7.0 + */ + protected static $signals = array( + 'SIGHUP', + 'SIGINT', + 'SIGQUIT', + 'SIGILL', + 'SIGTRAP', + 'SIGABRT', + 'SIGIOT', + 'SIGBUS', + 'SIGFPE', + 'SIGUSR1', + 'SIGSEGV', + 'SIGUSR2', + 'SIGPIPE', + 'SIGALRM', + 'SIGTERM', + 'SIGSTKFLT', + 'SIGCLD', + 'SIGCHLD', + 'SIGCONT', + 'SIGTSTP', + 'SIGTTIN', + 'SIGTTOU', + 'SIGURG', + 'SIGXCPU', + 'SIGXFSZ', + 'SIGVTALRM', + 'SIGPROF', + 'SIGWINCH', + 'SIGPOLL', + 'SIGIO', + 'SIGPWR', + 'SIGSYS', + 'SIGBABY', + 'SIG_BLOCK', + 'SIG_UNBLOCK', + 'SIG_SETMASK', + ); + + /** + * @var boolean True if the daemon is in the process of exiting. + * @since 1.7.0 + */ + protected $exiting = false; + + /** + * @var integer The parent process id. + * @since 3.0.0 + */ + protected $parentId = 0; + + /** + * @var integer The process id of the daemon. + * @since 1.7.0 + */ + protected $processId = 0; + + /** + * @var boolean True if the daemon is currently running. + * @since 1.7.0 + */ + protected $running = false; + + /** + * Class constructor. + * + * @param Cli $input An optional argument to provide dependency injection for the application's + * input object. If the argument is a JInputCli object that object will become + * the application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * @param DispatcherInterface $dispatcher An optional argument to provide dependency injection for the application's + * event dispatcher. If the argument is a DispatcherInterface object that object will become + * the application's event dispatcher, if it is null then the default event dispatcher + * will be created based on the application's loadDispatcher() method. + * + * @since 1.7.0 + */ + public function __construct(Cli $input = null, Registry $config = null, DispatcherInterface $dispatcher = null) + { + // Verify that the process control extension for PHP is available. + if (!\defined('SIGHUP')) { + Log::add('The PCNTL extension for PHP is not available.', Log::ERROR); + throw new \RuntimeException('The PCNTL extension for PHP is not available.'); + } + + // Verify that POSIX support for PHP is available. + if (!\function_exists('posix_getpid')) { + Log::add('The POSIX extension for PHP is not available.', Log::ERROR); + throw new \RuntimeException('The POSIX extension for PHP is not available.'); + } + + // Call the parent constructor. + parent::__construct($input, $config, null, null, $dispatcher); + + // Set some system limits. + @set_time_limit($this->config->get('max_execution_time', 0)); + + if ($this->config->get('max_memory_limit') !== null) { + ini_set('memory_limit', $this->config->get('max_memory_limit', '256M')); + } + + // Flush content immediately. + ob_implicit_flush(); + } + + /** + * Method to handle POSIX signals. + * + * @param integer $signal The received POSIX signal. + * + * @return void + * + * @since 1.7.0 + * @see pcntl_signal() + * @throws \RuntimeException + */ + public static function signal($signal) + { + // Log all signals sent to the daemon. + Log::add('Received signal: ' . $signal, Log::DEBUG); + + // Let's make sure we have an application instance. + if (!is_subclass_of(static::$instance, CliApplication::class)) { + Log::add('Cannot find the application instance.', Log::EMERGENCY); + throw new \RuntimeException('Cannot find the application instance.'); + } + + // Fire the onReceiveSignal event. + static::$instance->triggerEvent('onReceiveSignal', array($signal)); + + switch ($signal) { + case SIGINT: + case SIGTERM: + // Handle shutdown tasks + if (static::$instance->running && static::$instance->isActive()) { + static::$instance->shutdown(); + } else { + static::$instance->close(); + } + break; + case SIGHUP: + // Handle restart tasks + if (static::$instance->running && static::$instance->isActive()) { + static::$instance->shutdown(true); + } else { + static::$instance->close(); + } + break; + case SIGCHLD: + // A child process has died + while (static::$instance->pcntlWait($signal, WNOHANG || WUNTRACED) > 0) { + usleep(1000); + } + break; + case SIGCLD: + while (static::$instance->pcntlWait($signal, WNOHANG) > 0) { + $signal = static::$instance->pcntlChildExitStatus($signal); + } + break; + default: + break; + } + } + + /** + * Check to see if the daemon is active. This does not assume that $this daemon is active, but + * only if an instance of the application is active as a daemon. + * + * @return boolean True if daemon is active. + * + * @since 1.7.0 + */ + public function isActive() + { + // Get the process id file location for the application. + $pidFile = $this->config->get('application_pid_file'); + + // If the process id file doesn't exist then the daemon is obviously not running. + if (!is_file($pidFile)) { + return false; + } + + // Read the contents of the process id file as an integer. + $fp = fopen($pidFile, 'r'); + $pid = fread($fp, filesize($pidFile)); + $pid = (int) $pid; + fclose($fp); + + // Check to make sure that the process id exists as a positive integer. + if (!$pid) { + return false; + } + + // Check to make sure the process is active by pinging it and ensure it responds. + if (!posix_kill($pid, 0)) { + // No response so remove the process id file and log the situation. + @ unlink($pidFile); + Log::add('The process found based on PID file was unresponsive.', Log::WARNING); + + return false; + } + + return true; + } + + /** + * Load an object or array into the application configuration object. + * + * @param mixed $data Either an array or object to be loaded into the configuration object. + * + * @return DaemonApplication Instance of $this to allow chaining. + * + * @since 1.7.0 + */ + public function loadConfiguration($data) + { + /* + * Setup some application metadata options. This is useful if we ever want to write out startup scripts + * or just have some sort of information available to share about things. + */ + + // The application author name. This string is used in generating startup scripts and has + // a maximum of 50 characters. + $tmp = (string) $this->config->get('author_name', 'Joomla Platform'); + $this->config->set('author_name', (\strlen($tmp) > 50) ? substr($tmp, 0, 50) : $tmp); + + // The application author email. This string is used in generating startup scripts. + $tmp = (string) $this->config->get('author_email', 'admin@joomla.org'); + $this->config->set('author_email', filter_var($tmp, FILTER_VALIDATE_EMAIL)); + + // The application name. This string is used in generating startup scripts. + $tmp = (string) $this->config->get('application_name', 'DaemonApplication'); + $this->config->set('application_name', (string) preg_replace('/[^A-Z0-9_-]/i', '', $tmp)); + + // The application description. This string is used in generating startup scripts. + $tmp = (string) $this->config->get('application_description', 'A generic Joomla Platform application.'); + $this->config->set('application_description', filter_var($tmp, FILTER_SANITIZE_STRING)); + + /* + * Setup the application path options. This defines the default executable name, executable directory, + * and also the path to the daemon process id file. + */ + + // The application executable daemon. This string is used in generating startup scripts. + $tmp = (string) $this->config->get('application_executable', basename($this->input->executable)); + $this->config->set('application_executable', $tmp); + + // The home directory of the daemon. + $tmp = (string) $this->config->get('application_directory', \dirname($this->input->executable)); + $this->config->set('application_directory', $tmp); + + // The pid file location. This defaults to a path inside the /tmp directory. + $name = $this->config->get('application_name'); + $tmp = (string) $this->config->get('application_pid_file', strtolower('/tmp/' . $name . '/' . $name . '.pid')); + $this->config->set('application_pid_file', $tmp); + + /* + * Setup the application identity options. It is important to remember if the default of 0 is set for + * either UID or GID then changing that setting will not be attempted as there is no real way to "change" + * the identity of a process from some user to root. + */ + + // The user id under which to run the daemon. + $tmp = (int) $this->config->get('application_uid', 0); + $options = array('options' => array('min_range' => 0, 'max_range' => 65000)); + $this->config->set('application_uid', filter_var($tmp, FILTER_VALIDATE_INT, $options)); + + // The group id under which to run the daemon. + $tmp = (int) $this->config->get('application_gid', 0); + $options = array('options' => array('min_range' => 0, 'max_range' => 65000)); + $this->config->set('application_gid', filter_var($tmp, FILTER_VALIDATE_INT, $options)); + + // Option to kill the daemon if it cannot switch to the chosen identity. + $tmp = (bool) $this->config->get('application_require_identity', 1); + $this->config->set('application_require_identity', $tmp); + + /* + * Setup the application runtime options. By default our execution time limit is infinite obviously + * because a daemon should be constantly running unless told otherwise. The default limit for memory + * usage is 256M, which admittedly is a little high, but remember it is a "limit" and PHP's memory + * management leaves a bit to be desired :-) + */ + + // The maximum execution time of the application in seconds. Zero is infinite. + $tmp = $this->config->get('max_execution_time'); + + if ($tmp !== null) { + $this->config->set('max_execution_time', (int) $tmp); + } + + // The maximum amount of memory the application can use. + $tmp = $this->config->get('max_memory_limit', '256M'); + + if ($tmp !== null) { + $this->config->set('max_memory_limit', (string) $tmp); + } + + return $this; + } + + /** + * Execute the daemon. + * + * @return void + * + * @since 1.7.0 + */ + public function execute() + { + // Trigger the onBeforeExecute event + $this->triggerEvent('onBeforeExecute'); + + // Enable basic garbage collection. + gc_enable(); + + Log::add('Starting ' . $this->name, Log::INFO); + + // Set off the process for becoming a daemon. + if ($this->daemonize()) { + // Declare ticks to start signal monitoring. When you declare ticks, PCNTL will monitor + // incoming signals after each tick and call the relevant signal handler automatically. + declare(ticks=1); + + // Start the main execution loop. + while (true) { + // Perform basic garbage collection. + $this->gc(); + + // Don't completely overload the CPU. + usleep(1000); + + // Execute the main application logic. + $this->doExecute(); + } + } else { + // We were not able to daemonize the application so log the failure and die gracefully. + Log::add('Starting ' . $this->name . ' failed', Log::INFO); + } + + // Trigger the onAfterExecute event. + $this->triggerEvent('onAfterExecute'); + } + + /** + * Restart daemon process. + * + * @return void + * + * @since 1.7.0 + */ + public function restart() + { + Log::add('Stopping ' . $this->name, Log::INFO); + $this->shutdown(true); + } + + /** + * Stop daemon process. + * + * @return void + * + * @since 1.7.0 + */ + public function stop() + { + Log::add('Stopping ' . $this->name, Log::INFO); + $this->shutdown(); + } + + /** + * Method to change the identity of the daemon process and resources. + * + * @return boolean True if identity successfully changed + * + * @since 1.7.0 + * @see posix_setuid() + */ + protected function changeIdentity() + { + // Get the group and user ids to set for the daemon. + $uid = (int) $this->config->get('application_uid', 0); + $gid = (int) $this->config->get('application_gid', 0); + + // Get the application process id file path. + $file = $this->config->get('application_pid_file'); + + // Change the user id for the process id file if necessary. + if ($uid && (fileowner($file) != $uid) && (!@ chown($file, $uid))) { + Log::add('Unable to change user ownership of the process id file.', Log::ERROR); + + return false; + } + + // Change the group id for the process id file if necessary. + if ($gid && (filegroup($file) != $gid) && (!@ chgrp($file, $gid))) { + Log::add('Unable to change group ownership of the process id file.', Log::ERROR); + + return false; + } + + // Set the correct home directory for the process. + if ($uid && ($info = posix_getpwuid($uid)) && is_dir($info['dir'])) { + system('export HOME="' . $info['dir'] . '"'); + } + + // Change the user id for the process necessary. + if ($uid && (posix_getuid() != $uid) && (!@ posix_setuid($uid))) { + Log::add('Unable to change user ownership of the process.', Log::ERROR); + + return false; + } + + // Change the group id for the process necessary. + if ($gid && (posix_getgid() != $gid) && (!@ posix_setgid($gid))) { + Log::add('Unable to change group ownership of the process.', Log::ERROR); + + return false; + } + + // Get the user and group information based on uid and gid. + $user = posix_getpwuid($uid); + $group = posix_getgrgid($gid); + + Log::add('Changed daemon identity to ' . $user['name'] . ':' . $group['name'], Log::INFO); + + return true; + } + + /** + * Method to put the application into the background. + * + * @return boolean + * + * @since 1.7.0 + * @throws \RuntimeException + */ + protected function daemonize() + { + // Is there already an active daemon running? + if ($this->isActive()) { + Log::add($this->name . ' daemon is still running. Exiting the application.', Log::EMERGENCY); + + return false; + } + + // Reset Process Information + $this->safeMode = !!@ ini_get('safe_mode'); + $this->processId = 0; + $this->running = false; + + // Detach process! + try { + // Check if we should run in the foreground. + if (!$this->input->get('f')) { + // Detach from the terminal. + $this->detach(); + } else { + // Setup running values. + $this->exiting = false; + $this->running = true; + + // Set the process id. + $this->processId = (int) posix_getpid(); + $this->parentId = $this->processId; + } + } catch (\RuntimeException $e) { + Log::add('Unable to fork.', Log::EMERGENCY); + + return false; + } + + // Verify the process id is valid. + if ($this->processId < 1) { + Log::add('The process id is invalid; the fork failed.', Log::EMERGENCY); + + return false; + } + + // Clear the umask. + @ umask(0); + + // Write out the process id file for concurrency management. + if (!$this->writeProcessIdFile()) { + Log::add('Unable to write the pid file at: ' . $this->config->get('application_pid_file'), Log::EMERGENCY); + + return false; + } + + // Attempt to change the identity of user running the process. + if (!$this->changeIdentity()) { + // If the identity change was required then we need to return false. + if ($this->config->get('application_require_identity')) { + Log::add('Unable to change process owner.', Log::CRITICAL); + + return false; + } else { + Log::add('Unable to change process owner.', Log::WARNING); + } + } + + // Setup the signal handlers for the daemon. + if (!$this->setupSignalHandlers()) { + return false; + } + + // Change the current working directory to the application working directory. + @ chdir($this->config->get('application_directory')); + + return true; + } + + /** + * This is truly where the magic happens. This is where we fork the process and kill the parent + * process, which is essentially what turns the application into a daemon. + * + * @return void + * + * @since 3.0.0 + * @throws \RuntimeException + */ + protected function detach() + { + Log::add('Detaching the ' . $this->name . ' daemon.', Log::DEBUG); + + // Attempt to fork the process. + $pid = $this->fork(); + + // If the pid is positive then we successfully forked, and can close this application. + if ($pid) { + // Add the log entry for debugging purposes and exit gracefully. + Log::add('Ending ' . $this->name . ' parent process', Log::DEBUG); + $this->close(); + } else { + // We are in the forked child process. + // Setup some protected values. + $this->exiting = false; + $this->running = true; + + // Set the parent to self. + $this->parentId = $this->processId; + } + } + + /** + * Method to fork the process. + * + * @return integer The child process id to the parent process, zero to the child process. + * + * @since 1.7.0 + * @throws \RuntimeException + */ + protected function fork() + { + // Attempt to fork the process. + $pid = $this->pcntlFork(); + + // If the fork failed, throw an exception. + if ($pid === -1) { + throw new \RuntimeException('The process could not be forked.'); + } elseif ($pid === 0) { + // Update the process id for the child. + $this->processId = (int) posix_getpid(); + } else { + // Log the fork in the parent. + // Log the fork. + Log::add('Process forked ' . $pid, Log::DEBUG); + } + + // Trigger the onFork event. + $this->postFork(); + + return $pid; + } + + /** + * Method to perform basic garbage collection and memory management in the sense of clearing the + * stat cache. We will probably call this method pretty regularly in our main loop. + * + * @return void + * + * @since 1.7.0 + */ + protected function gc() + { + // Perform generic garbage collection. + gc_collect_cycles(); + + // Clear the stat cache so it doesn't blow up memory. + clearstatcache(); + } + + /** + * Method to attach the DaemonApplication signal handler to the known signals. Applications + * can override these handlers by using the pcntl_signal() function and attaching a different + * callback method. + * + * @return boolean + * + * @since 1.7.0 + * @see pcntl_signal() + */ + protected function setupSignalHandlers() + { + // We add the error suppression for the loop because on some platforms some constants are not defined. + foreach (self::$signals as $signal) { + // Ignore signals that are not defined. + if (!\defined($signal) || !\is_int(\constant($signal)) || (\constant($signal) === 0)) { + // Define the signal to avoid notices. + Log::add('Signal "' . $signal . '" not defined. Defining it as null.', Log::DEBUG); + \define($signal, null); + + // Don't listen for signal. + continue; + } + + // Attach the signal handler for the signal. + if (!$this->pcntlSignal(\constant($signal), array('DaemonApplication', 'signal'))) { + Log::add(sprintf('Unable to reroute signal handler: %s', $signal), Log::EMERGENCY); + + return false; + } + } + + return true; + } + + /** + * Method to shut down the daemon and optionally restart it. + * + * @param boolean $restart True to restart the daemon on exit. + * + * @return void + * + * @since 1.7.0 + */ + protected function shutdown($restart = false) + { + // If we are already exiting, chill. + if ($this->exiting) { + return; + } else { + // If not, now we are. + $this->exiting = true; + } + + // If we aren't already daemonized then just kill the application. + if (!$this->running && !$this->isActive()) { + Log::add('Process was not daemonized yet, just halting current process', Log::INFO); + $this->close(); + } + + // Only read the pid for the parent file. + if ($this->parentId == $this->processId) { + // Read the contents of the process id file as an integer. + $fp = fopen($this->config->get('application_pid_file'), 'r'); + $pid = fread($fp, filesize($this->config->get('application_pid_file'))); + $pid = (int) $pid; + fclose($fp); + + // Remove the process id file. + @ unlink($this->config->get('application_pid_file')); + + // If we are supposed to restart the daemon we need to execute the same command. + if ($restart) { + $this->close(exec(implode(' ', $GLOBALS['argv']) . ' > /dev/null &')); + } else { + // If we are not supposed to restart the daemon let's just kill -9. + passthru('kill -9 ' . $pid); + $this->close(); + } + } + } + + /** + * Method to write the process id file out to disk. + * + * @return boolean + * + * @since 1.7.0 + */ + protected function writeProcessIdFile() + { + // Verify the process id is valid. + if ($this->processId < 1) { + Log::add('The process id is invalid.', Log::EMERGENCY); + + return false; + } + + // Get the application process id file path. + $file = $this->config->get('application_pid_file'); + + if (empty($file)) { + Log::add('The process id file path is empty.', Log::ERROR); + + return false; + } + + // Make sure that the folder where we are writing the process id file exists. + $folder = \dirname($file); + + if (!is_dir($folder) && !Folder::create($folder)) { + Log::add('Unable to create directory: ' . $folder, Log::ERROR); + + return false; + } + + // Write the process id file out to disk. + if (!file_put_contents($file, $this->processId)) { + Log::add('Unable to write process id file: ' . $file, Log::ERROR); + + return false; + } + + // Make sure the permissions for the process id file are accurate. + if (!chmod($file, 0644)) { + Log::add('Unable to adjust permissions for the process id file: ' . $file, Log::ERROR); + + return false; + } + + return true; + } + + /** + * Method to handle post-fork triggering of the onFork event. + * + * @return void + * + * @since 3.0.0 + */ + protected function postFork() + { + // Trigger the onFork event. + $this->triggerEvent('onFork'); + } + + /** + * Method to return the exit code of a terminated child process. + * + * @param integer $status The status parameter is the status parameter supplied to a successful call to pcntl_waitpid(). + * + * @return integer The child process exit code. + * + * @see pcntl_wexitstatus() + * @since 1.7.3 + */ + protected function pcntlChildExitStatus($status) + { + return pcntl_wexitstatus($status); + } + + /** + * Method to return the exit code of a terminated child process. + * + * @return integer On success, the PID of the child process is returned in the parent's thread + * of execution, and a 0 is returned in the child's thread of execution. On + * failure, a -1 will be returned in the parent's context, no child process + * will be created, and a PHP error is raised. + * + * @see pcntl_fork() + * @since 1.7.3 + */ + protected function pcntlFork() + { + return pcntl_fork(); + } + + /** + * Method to install a signal handler. + * + * @param integer $signal The signal number. + * @param callable $handler The signal handler which may be the name of a user created function, + * or method, or either of the two global constants SIG_IGN or SIG_DFL. + * @param boolean $restart Specifies whether system call restarting should be used when this + * signal arrives. + * + * @return boolean True on success. + * + * @see pcntl_signal() + * @since 1.7.3 + */ + protected function pcntlSignal($signal, $handler, $restart = true) + { + return pcntl_signal($signal, $handler, $restart); + } + + /** + * Method to wait on or return the status of a forked child. + * + * @param integer &$status Status information. + * @param integer $options If wait3 is available on your system (mostly BSD-style systems), + * you can provide the optional options parameter. + * + * @return integer The process ID of the child which exited, -1 on error or zero if WNOHANG + * was provided as an option (on wait3-available systems) and no child was available. + * + * @see pcntl_wait() + * @since 1.7.3 + */ + protected function pcntlWait(&$status, $options = 0) + { + return pcntl_wait($status, $options); + } } diff --git a/code/libraries/src/Application/EventAware.php b/code/libraries/src/Application/EventAware.php index 4237e636..7190a6fb 100644 --- a/code/libraries/src/Application/EventAware.php +++ b/code/libraries/src/Application/EventAware.php @@ -1,4 +1,5 @@ getDispatcher()->addListener($event, $handler); - } - catch (\UnexpectedValueException $e) - { - // No dispatcher is registered, don't throw an error (mimics old behavior) - } + /** + * Registers a handler to a particular event group. + * + * @param string $event The event name. + * @param callable $handler The handler, a function or an instance of an event object. + * + * @return $this + * + * @since 4.0.0 + */ + public function registerEvent($event, callable $handler) + { + try { + $this->getDispatcher()->addListener($event, $handler); + } catch (\UnexpectedValueException $e) { + // No dispatcher is registered, don't throw an error (mimics old behavior) + } - return $this; - } + return $this; + } - /** - * Calls all handlers associated with an event group. - * - * This is a legacy method, implementing old-style (Joomla! 3.x) plugin calls. It's best to go directly through the - * Dispatcher and handle the returned EventInterface object instead of going through this method. This method is - * deprecated and will be removed in Joomla! 5.x. - * - * This method will only return the 'result' argument of the event - * - * @param string $eventName The event name. - * @param array|Event $args An array of arguments or an Event object (optional). - * - * @return array An array of results from each function call. Note this will be an empty array if no dispatcher is set. - * - * @since 4.0.0 - * @throws \InvalidArgumentException - * @deprecated 5.0 - */ - public function triggerEvent($eventName, $args = []) - { - try - { - $dispatcher = $this->getDispatcher(); - } - catch (\UnexpectedValueException $exception) - { - $this->getLogger()->error(sprintf('Dispatcher not set in %s, cannot trigger events.', \get_class($this))); + /** + * Calls all handlers associated with an event group. + * + * This is a legacy method, implementing old-style (Joomla! 3.x) plugin calls. It's best to go directly through the + * Dispatcher and handle the returned EventInterface object instead of going through this method. This method is + * deprecated and will be removed in Joomla! 5.x. + * + * This method will only return the 'result' argument of the event + * + * @param string $eventName The event name. + * @param array|Event $args An array of arguments or an Event object (optional). + * + * @return array An array of results from each function call. Note this will be an empty array if no dispatcher is set. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + * @deprecated 5.0 + */ + public function triggerEvent($eventName, $args = []) + { + try { + $dispatcher = $this->getDispatcher(); + } catch (\UnexpectedValueException $exception) { + $this->getLogger()->error(sprintf('Dispatcher not set in %s, cannot trigger events.', \get_class($this))); - return []; - } + return []; + } - if ($args instanceof Event) - { - $event = $args; - } - elseif (\is_array($args)) - { - $event = new Event($eventName, $args); - } - else - { - throw new \InvalidArgumentException('The arguments must either be an event or an array'); - } + if ($args instanceof Event) { + $event = $args; + } elseif (\is_array($args)) { + $className = self::getEventClassByEventName($eventName); + $event = new $className($eventName, $args); + } else { + throw new \InvalidArgumentException('The arguments must either be an event or an array'); + } - $result = $dispatcher->dispatch($eventName, $event); + $result = $dispatcher->dispatch($eventName, $event); - // @todo - There are still test cases where the result isn't defined, temporarily leave the isset check in place - return !isset($result['result']) || \is_null($result['result']) ? [] : $result['result']; - } + // @todo - There are still test cases where the result isn't defined, temporarily leave the isset check in place + return !isset($result['result']) || \is_null($result['result']) ? [] : $result['result']; + } } diff --git a/code/libraries/src/Application/EventAwareInterface.php b/code/libraries/src/Application/EventAwareInterface.php index f8bb1681..296633ef 100644 --- a/code/libraries/src/Application/EventAwareInterface.php +++ b/code/libraries/src/Application/EventAwareInterface.php @@ -1,4 +1,5 @@ load(); - } + /** + * Allows the application to load a custom or default identity. + * + * @return void + * + * @since 4.0.0 + */ + public function createExtensionNamespaceMap() + { + JLoader::register('JNamespacePsr4Map', JPATH_LIBRARIES . '/namespacemap.php'); + $extensionPsr4Loader = new \JNamespacePsr4Map(); + $extensionPsr4Loader->load(); + } } diff --git a/code/libraries/src/Application/IdentityAware.php b/code/libraries/src/Application/IdentityAware.php index 03152ac5..9e4135c0 100644 --- a/code/libraries/src/Application/IdentityAware.php +++ b/code/libraries/src/Application/IdentityAware.php @@ -1,4 +1,5 @@ identity; - } + /** + * Get the application identity. + * + * @return User + * + * @since 4.0.0 + */ + public function getIdentity() + { + return $this->identity; + } - /** - * Allows the application to load a custom or default identity. - * - * @param User $identity An optional identity object. If omitted, a null user object is created. - * - * @return $this - * - * @since 4.0.0 - */ - public function loadIdentity(User $identity = null) - { - $this->identity = $identity ?: $this->userFactory->loadUserById(0); + /** + * Allows the application to load a custom or default identity. + * + * @param User $identity An optional identity object. If omitted, a null user object is created. + * + * @return $this + * + * @since 4.0.0 + */ + public function loadIdentity(User $identity = null) + { + $this->identity = $identity ?: $this->userFactory->loadUserById(0); - return $this; - } + return $this; + } - /** - * Set the user factory to use. - * - * @param UserFactoryInterface $userFactory The user factory to use - * - * @return void - * - * @since 4.0.0 - */ - public function setUserFactory(UserFactoryInterface $userFactory) - { - $this->userFactory = $userFactory; - } + /** + * Set the user factory to use. + * + * @param UserFactoryInterface $userFactory The user factory to use + * + * @return void + * + * @since 4.0.0 + */ + public function setUserFactory(UserFactoryInterface $userFactory) + { + $this->userFactory = $userFactory; + } } diff --git a/code/libraries/src/Application/MultiFactorAuthenticationHandler.php b/code/libraries/src/Application/MultiFactorAuthenticationHandler.php new file mode 100644 index 00000000..7ce1508c --- /dev/null +++ b/code/libraries/src/Application/MultiFactorAuthenticationHandler.php @@ -0,0 +1,516 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Application; + +use Exception; +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Date\Date; +use Joomla\CMS\Encrypt\Aes; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Table\User as UserTable; +use Joomla\CMS\Uri\Uri; +use Joomla\CMS\User\User; +use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper; +use Joomla\Component\Users\Administrator\Table\MfaTable; +use Joomla\Database\DatabaseDriver; +use Joomla\Database\ParameterType; +use RuntimeException; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * Implements the code required for integrating with Joomla's Multi-factor Authentication. + * + * Please keep in mind that Joomla's MFA, like any MFA method, is designed to be user-interactive. + * Moreover, it's meant to be used in an HTML- and JavaScript-aware execution environment i.e. a web + * browser, web view or similar. + * + * If your application is designed to work non-interactively (e.g. a JSON API application) or + * outside and HTML- and JavaScript-aware execution environments (e.g. CLI) you MUST NOT use this + * trait. Authentication should be either implicit (e.g. CLI) or using sufficiently secure non- + * interactive methods (tokens, certificates, ...). + * + * Regarding the Joomla CMS itself, only the SiteApplication (frontend) and AdministratorApplication + * (backend) applications use this trait because of this reason. The CLI application is implicitly + * authorised at the highest level, whereas the ApiApplication encourages the use of tokens for + * authentication. + * + * @since 4.2.0 + */ +trait MultiFactorAuthenticationHandler +{ + /** + * Handle the redirection to the Multi-factor Authentication captive login or setup page. + * + * @return boolean True if we are currently handling a Multi-factor Authentication captive page. + * @throws Exception + * @since 4.2.0 + */ + protected function isHandlingMultiFactorAuthentication(): bool + { + // Multi-factor Authentication checks take place only for logged in users. + try { + $user = $this->getIdentity() ?? null; + } catch (Exception $e) { + return false; + } + + if (!($user instanceof User) || $user->guest) { + return false; + } + + // If there is no need for a redirection I must not proceed + if (!$this->needsMultiFactorAuthenticationRedirection()) { + return false; + } + + /** + * Automatically migrate from legacy MFA, if needed. + * + * We prefer to do a user-by-user migration instead of migrating everybody on Joomla update + * for practical reasons. On a site with hundreds or thousands of users the migration could + * take several minutes, causing Joomla Update to time out. + * + * Instead, every time we are in a captive Multi-factor Authentication page (captive MFA login + * or captive forced MFA setup) we spend a few milliseconds to check if a migration is + * necessary. If it's necessary, we perform it. + * + * The captive pages don't load any content or modules, therefore the few extra milliseconds + * we spend here are not a big deal. A failed all-users migration which would stop Joomla + * Update dead in its tracks would, however, be a big deal (broken sites). Moreover, a + * migration that has to be initiated by the site owner would also be a big deal — if they + * did not know they need to do it none of their users who had previously enabled MFA would + * now have it enabled! + * + * To paraphrase Otto von Bismarck: programming, like politics, is the art of the possible, + * the attainable -- the art of the next best. + */ + $this->migrateFromLegacyMFA(); + + // We only kick in when the user has actually set up MFA or must definitely enable MFA. + $userOptions = ComponentHelper::getParams('com_users'); + $neverMFAUserGroups = $userOptions->get('neverMFAUserGroups', []); + $forceMFAUserGroups = $userOptions->get('forceMFAUserGroups', []); + $isMFADisallowed = count( + array_intersect( + is_array($neverMFAUserGroups) ? $neverMFAUserGroups : [], + $user->getAuthorisedGroups() + ) + ) >= 1; + $isMFAMandatory = count( + array_intersect( + is_array($forceMFAUserGroups) ? $forceMFAUserGroups : [], + $user->getAuthorisedGroups() + ) + ) >= 1; + $isMFADisallowed = $isMFADisallowed && !$isMFAMandatory; + $isMFAPending = $this->isMultiFactorAuthenticationPending(); + $session = $this->getSession(); + $isNonHtml = $this->input->getCmd('format', 'html') !== 'html'; + + // Prevent non-interactive (non-HTML) content from being loaded until MFA is validated. + if ($isMFAPending && $isNonHtml) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + if ($isMFAPending && !$isMFADisallowed) { + /** + * Saves the current URL as the return URL if all of the following conditions apply + * - It is not a URL to com_users' MFA feature itself + * - A return URL does not already exist, is imperfect or external to the site + * + * If no return URL has been set up and the current URL is com_users' MFA feature + * we will save the home page as the redirect target. + */ + $returnUrl = $session->get('com_users.return_url', ''); + + if (empty($returnUrl) || !Uri::isInternal($returnUrl)) { + $returnUrl = $this->isMultiFactorAuthenticationPage() + ? Uri::base() + : Uri::getInstance()->toString(['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment']); + $session->set('com_users.return_url', $returnUrl); + } + + // Redirect + $this->redirect(Route::_('index.php?option=com_users&view=captive', false), 307); + } + + // If we're here someone just logged in but does not have MFA set up. Just flag him as logged in and continue. + $session->set('com_users.mfa_checked', 1); + + // If the user is in a group that requires MFA we will redirect them to the setup page. + if (!$isMFAPending && $isMFAMandatory) { + // First unset the flag to make sure the redirection will apply until they conform to the mandatory MFA + $session->set('com_users.mfa_checked', 0); + + // Now set a flag which forces rechecking MFA for this user + $session->set('com_users.mandatory_mfa_setup', 1); + + // Then redirect them to the setup page + if (!$this->isMultiFactorAuthenticationPage()) { + $url = Route::_('index.php?option=com_users&view=methods', false); + $this->redirect($url, 307); + } + } + + // Do I need to redirect the user to the MFA setup page after they have fully logged in? + $hasRejectedMultiFactorAuthenticationSetup = $this->hasRejectedMultiFactorAuthenticationSetup() && !$isMFAMandatory; + + if ( + !$isMFAPending && !$isMFADisallowed && ($userOptions->get('mfaredirectonlogin', 0) == 1) + && !$user->guest && !$hasRejectedMultiFactorAuthenticationSetup && !empty(MfaHelper::getMfaMethods()) + ) { + $this->redirect( + $userOptions->get('mfaredirecturl', '') ?: + Route::_('index.php?option=com_users&view=methods&layout=firsttime', false) + ); + } + + return true; + } + + /** + * Does the current user need to complete MFA authentication before being allowed to access the site? + * + * @return boolean + * @throws Exception + * @since 4.2.0 + */ + private function isMultiFactorAuthenticationPending(): bool + { + $user = $this->getIdentity(); + + if (empty($user) || $user->guest) { + return false; + } + + // Get the user's MFA records + $records = MfaHelper::getUserMfaRecords($user->id); + + // No MFA Methods? Then we obviously don't need to display a Captive login page. + if (count($records) < 1) { + return false; + } + + // Let's get a list of all currently active MFA Methods + $mfaMethods = MfaHelper::getMfaMethods(); + + // If no MFA Method is active we can't really display a Captive login page. + if (empty($mfaMethods)) { + return false; + } + + // Get a list of just the Method names + $methodNames = []; + + foreach ($mfaMethods as $mfaMethod) { + $methodNames[] = $mfaMethod['name']; + } + + // Filter the records based on currently active MFA Methods + foreach ($records as $record) { + if (in_array($record->method, $methodNames)) { + // We found an active Method. Show the Captive page. + return true; + } + } + + // No viable MFA Method found. We won't show the Captive page. + return false; + } + + /** + * Check whether we'll need to do a redirection to the Multi-factor Authentication captive page. + * + * @return boolean + * @since 4.2.0 + */ + private function needsMultiFactorAuthenticationRedirection(): bool + { + $isAdmin = $this->isClient('administrator'); + + /** + * We only kick in if the session flag is not set AND the user is not flagged for monitoring of their MFA status + * + * In case a user belongs to a group which requires MFA to be always enabled and they logged in without having + * MFA enabled we have the recheck flag. This prevents the user from enabling and immediately disabling MFA, + * circumventing the requirement for MFA. + */ + $session = $this->getSession(); + $isMFAComplete = $session->get('com_users.mfa_checked', 0) != 0; + $isMFASetupMandatory = $session->get('com_users.mandatory_mfa_setup', 0) != 0; + + if ($isMFAComplete && !$isMFASetupMandatory) { + return false; + } + + // Make sure we are logged in + try { + $user = $this->getIdentity(); + } catch (Exception $e) { + // This would happen if we are in CLI or under an old Joomla! version. Either case is not supported. + return false; + } + + // The plugin only needs to kick in when you have logged in + if (empty($user) || $user->guest) { + return false; + } + + // If we are in the administrator section we only kick in when the user has backend access privileges + if ($isAdmin && !$user->authorise('core.login.admin')) { + // @todo How exactly did you end up here if you didn't have the core.login.admin privilege to begin with?! + return false; + } + + // Do not redirect if we are already in a MFA management or captive page + if ($this->isMultiFactorAuthenticationPage()) { + return false; + } + + $option = strtolower($this->input->getCmd('option', '')); + $task = strtolower($this->input->getCmd('task', '')); + + // Allow the frontend user to log out (in case they forgot their MFA code or something) + if (!$isAdmin && ($option == 'com_users') && in_array($task, ['user.logout', 'user.menulogout'])) { + return false; + } + + // Allow the backend user to log out (in case they forgot their MFA code or something) + if ($isAdmin && ($option == 'com_login') && ($task == 'logout')) { + return false; + } + + // Allow the Joomla update finalisation to run + if ($isAdmin && $option === 'com_joomlaupdate' && in_array($task, ['update.finalise', 'update.cleanup', 'update.finaliseconfirm'])) { + return false; + } + + return true; + } + + /** + * Is this a page concerning the Multi-factor Authentication feature? + * + * @param bool $onlyCaptive Should I only check for the MFA captive page? + * + * @return boolean + * @since 4.2.0 + */ + public function isMultiFactorAuthenticationPage(bool $onlyCaptive = false): bool + { + $option = $this->input->get('option'); + $task = $this->input->get('task'); + $view = $this->input->get('view'); + + if ($option !== 'com_users') { + return false; + } + + $allowedViews = ['captive', 'method', 'methods', 'callback']; + $allowedTasks = [ + 'captive.display', 'captive.captive', 'captive.validate', + 'methods.display', + ]; + + if (!$onlyCaptive) { + $allowedTasks = array_merge( + $allowedTasks, + [ + 'method.display', 'method.add', 'method.edit', 'method.regenerateBackupCodes', + 'method.delete', 'method.save', 'methods.disable', 'methods.doNotShowThisAgain', + ] + ); + } + + return in_array($view, $allowedViews) || in_array($task, $allowedTasks); + } + + /** + * Does the user have a "don't show this again" flag? + * + * @return boolean + * @since 4.2.0 + */ + private function hasRejectedMultiFactorAuthenticationSetup(): bool + { + $user = $this->getIdentity(); + $profileKey = 'mfa.dontshow'; + /** @var DatabaseDriver $db */ + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true) + ->select($db->quoteName('profile_value')) + ->from($db->quoteName('#__user_profiles')) + ->where($db->quoteName('user_id') . ' = :userId') + ->where($db->quoteName('profile_key') . ' = :profileKey') + ->bind(':userId', $user->id, ParameterType::INTEGER) + ->bind(':profileKey', $profileKey); + + try { + $result = $db->setQuery($query)->loadResult(); + } catch (Exception $e) { + $result = 1; + } + + return $result == 1; + } + + /** + * Automatically migrates a user's legacy MFA records into the new Captive MFA format. + * + * @return void + * @since 4.2.0 + */ + private function migrateFromLegacyMFA(): void + { + $user = $this->getIdentity(); + + if (!($user instanceof User) || $user->guest || $user->id <= 0) { + return; + } + + /** @var DatabaseDriver $db */ + $db = Factory::getContainer()->get('DatabaseDriver'); + + $userTable = new UserTable($db); + + if (!$userTable->load($user->id) || empty($userTable->otpKey)) { + return; + } + + [$otpMethod, $otpKey] = explode(':', $userTable->otpKey, 2); + $secret = $this->get('secret'); + $otpKey = $this->decryptLegacyTFAString($secret, $otpKey); + $otep = $this->decryptLegacyTFAString($secret, $userTable->otep); + $config = @json_decode($otpKey, true); + $hasConverted = true; + + if (!empty($config)) { + switch ($otpMethod) { + case 'totp': + $this->getLanguage()->load('plg_multifactorauth_totp', JPATH_ADMINISTRATOR); + + (new MfaTable($db))->save( + [ + 'user_id' => $user->id, + 'title' => Text::_('PLG_MULTIFACTORAUTH_TOTP_METHOD_TITLE'), + 'method' => 'totp', + 'default' => 0, + 'created_on' => Date::getInstance()->toSql(), + 'last_used' => null, + 'options' => ['key' => $config['code']], + ] + ); + break; + + case 'yubikey': + $this->getLanguage()->load('plg_multifactorauth_yubikey', JPATH_ADMINISTRATOR); + + (new MfaTable($db))->save( + [ + 'user_id' => $user->id, + 'title' => sprintf("%s %s", Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_METHOD_TITLE'), $config['yubikey']), + 'method' => 'yubikey', + 'default' => 0, + 'created_on' => Date::getInstance()->toSql(), + 'last_used' => null, + 'options' => ['id' => $config['yubikey']], + ] + ); + break; + + default: + $hasConverted = false; + break; + } + } + + // Convert the emergency codes + if ($hasConverted && !empty(@json_decode($otep, true))) { + // Delete any other record with the same user_id and Method. + $method = 'emergencycodes'; + $userId = $user->id; + $query = $db->getQuery(true) + ->delete($db->qn('#__user_mfa')) + ->where($db->qn('user_id') . ' = :user_id') + ->where($db->qn('method') . ' = :method') + ->bind(':user_id', $userId, ParameterType::INTEGER) + ->bind(':method', $method); + $db->setQuery($query)->execute(); + + // Migrate data + (new MfaTable($db))->save( + [ + 'user_id' => $user->id, + 'title' => Text::_('COM_USERS_USER_BACKUPCODES'), + 'method' => 'backupcodes', + 'default' => 0, + 'created_on' => Date::getInstance()->toSql(), + 'last_used' => null, + 'options' => @json_decode($otep, true), + ] + ); + } + + // Remove the legacy MFA + $update = (object) [ + 'id' => $user->id, + 'otpKey' => '', + 'otep' => '', + ]; + $db->updateObject('#__users', $update, ['id']); + } + + /** + * Tries to decrypt the legacy MFA configuration. + * + * @param string $secret Site's secret key + * @param string $stringToDecrypt Base64-encoded and encrypted, JSON-encoded information + * + * @return string Decrypted, but JSON-encoded, information + * + * @see https://github.com/joomla/joomla-cms/pull/12497 + * @since 4.2.0 + */ + private function decryptLegacyTFAString(string $secret, string $stringToDecrypt): string + { + // Is this already decrypted? + try { + $decrypted = @json_decode($stringToDecrypt, true); + } catch (Exception $e) { + $decrypted = null; + } + + if (!empty($decrypted)) { + return $stringToDecrypt; + } + + // No, we need to decrypt the string + $aes = new Aes($secret, 256); + $decrypted = $aes->decryptString($stringToDecrypt); + + if (!is_string($decrypted) || empty($decrypted)) { + $aes->setPassword($secret, true); + + $decrypted = $aes->decryptString($stringToDecrypt); + } + + if (!is_string($decrypted) || empty($decrypted)) { + return ''; + } + + // Remove the null padding added during encryption + return rtrim($decrypted, "\0"); + } +} diff --git a/code/libraries/src/Application/SiteApplication.php b/code/libraries/src/Application/SiteApplication.php index dfbba499..7b384af1 100644 --- a/code/libraries/src/Application/SiteApplication.php +++ b/code/libraries/src/Application/SiteApplication.php @@ -1,4 +1,5 @@ name = 'site'; - - // Register the client ID - $this->clientId = 0; - - // Execute the parent constructor - parent::__construct($input, $config, $client, $container); - } - - /** - * Check if the user can access the application - * - * @param integer $itemid The item ID to check authorisation for - * - * @return void - * - * @since 3.2 - * - * @throws \Exception When you are not authorised to view the home page menu item - */ - protected function authorise($itemid) - { - $menus = $this->getMenu(); - $user = Factory::getUser(); - - if (!$menus->authorise($itemid)) - { - if ($user->get('id') == 0) - { - // Set the data - $this->setUserState('users.login.form.data', array('return' => Uri::getInstance()->toString())); - - $url = Route::_('index.php?option=com_users&view=login', false); - - $this->enqueueMessage(Text::_('JGLOBAL_YOU_MUST_LOGIN_FIRST'), 'error'); - $this->redirect($url); - } - else - { - // Get the home page menu item - $home_item = $menus->getDefault($this->getLanguage()->getTag()); - - // If we are already in the homepage raise an exception - if ($menus->getActive()->id == $home_item->id) - { - throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Otherwise redirect to the homepage and show an error - $this->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $this->redirect(Route::_('index.php?Itemid=' . $home_item->id, false)); - } - } - } - - /** - * Dispatch the application - * - * @param string $component The component which is being rendered. - * - * @return void - * - * @since 3.2 - */ - public function dispatch($component = null) - { - // Get the component if not set. - if (!$component) - { - $component = $this->input->getCmd('option', null); - } - - // Load the document to the API - $this->loadDocument(); - - // Set up the params - $document = $this->getDocument(); - $params = $this->getParams(); - - // Register the document object with Factory - Factory::$document = $document; - - switch ($document->getType()) - { - case 'html': - // Set up the language - LanguageHelper::getLanguages('lang_code'); - - // Set metadata - $document->setMetaData('rights', $this->get('MetaRights')); - - // Get the template - $template = $this->getTemplate(true); - - // Store the template and its params to the config - $this->set('theme', $template->template); - $this->set('themeParams', $template->params); - - // Add Asset registry files - $wr = $document->getWebAssetManager()->getRegistry(); - - if ($component) - { - $wr->addExtensionRegistryFile($component); - } - - if ($template->parent) - { - $wr->addTemplateRegistryFile($template->parent, $this->getClientId()); - } - - $wr->addTemplateRegistryFile($template->template, $this->getClientId()); - - break; - - case 'feed': - $document->setBase(htmlspecialchars(Uri::current())); - break; - } - - $document->setTitle($params->get('page_title')); - $document->setDescription($params->get('page_description')); - - // Add version number or not based on global configuration - if ($this->get('MetaVersion', 0)) - { - $document->setGenerator('Joomla! - Open Source Content Management - Version ' . JVERSION); - } - else - { - $document->setGenerator('Joomla! - Open Source Content Management'); - } - - $contents = ComponentHelper::renderComponent($component); - $document->setBuffer($contents, 'component'); - - // Trigger the onAfterDispatch event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterDispatch'); - } - - /** - * Method to run the Web application routines. - * - * @return void - * - * @since 3.2 - */ - protected function doExecute() - { - // Initialise the application - $this->initialiseApp(); - - // Mark afterInitialise in the profiler. - JDEBUG ? $this->profiler->mark('afterInitialise') : null; - - // Route the application - $this->route(); - - // Mark afterRoute in the profiler. - JDEBUG ? $this->profiler->mark('afterRoute') : null; - - /* - * Check if the user is required to reset their password - * - * Before $this->route(); "option" and "view" can't be safely read using: - * $this->input->getCmd('option'); or $this->input->getCmd('view'); - * ex: due of the sef urls - */ - $this->checkUserRequireReset('com_users', 'profile', 'edit', 'com_users/profile.save,com_users/profile.apply,com_users/user.logout'); - - // Dispatch the application - $this->dispatch(); - - // Mark afterDispatch in the profiler. - JDEBUG ? $this->profiler->mark('afterDispatch') : null; - } - - /** - * Return the current state of the detect browser option. - * - * @return boolean - * - * @since 3.2 - */ - public function getDetectBrowser() - { - return $this->detect_browser; - } - - /** - * Return the current state of the language filter. - * - * @return boolean - * - * @since 3.2 - */ - public function getLanguageFilter() - { - return $this->language_filter; - } - - /** - * Get the application parameters - * - * @param string $option The component option - * - * @return Registry The parameters object - * - * @since 3.2 - */ - public function getParams($option = null) - { - static $params = array(); - - $hash = '__default'; - - if (!empty($option)) - { - $hash = $option; - } - - if (!isset($params[$hash])) - { - // Get component parameters - if (!$option) - { - $option = $this->input->getCmd('option', null); - } - - // Get new instance of component global parameters - $params[$hash] = clone ComponentHelper::getParams($option); - - // Get menu parameters - $menus = $this->getMenu(); - $menu = $menus->getActive(); - - // Get language - $lang_code = $this->getLanguage()->getTag(); - $languages = LanguageHelper::getLanguages('lang_code'); - - $title = $this->get('sitename'); - - if (isset($languages[$lang_code]) && $languages[$lang_code]->metadesc) - { - $description = $languages[$lang_code]->metadesc; - } - else - { - $description = $this->get('MetaDesc'); - } - - $rights = $this->get('MetaRights'); - $robots = $this->get('robots'); - - // Retrieve com_menu global settings - $temp = clone ComponentHelper::getParams('com_menus'); - - // Lets cascade the parameters if we have menu item parameters - if (\is_object($menu)) - { - // Get show_page_heading from com_menu global settings - $params[$hash]->def('show_page_heading', $temp->get('show_page_heading')); - - $params[$hash]->merge($menu->getParams()); - $title = $menu->title; - } - else - { - // Merge com_menu global settings - $params[$hash]->merge($temp); - - // If supplied, use page title - $title = $temp->get('page_title', $title); - } - - $params[$hash]->def('page_title', $title); - $params[$hash]->def('page_description', $description); - $params[$hash]->def('page_rights', $rights); - $params[$hash]->def('robots', $robots); - } - - return $params[$hash]; - } - - /** - * Return a reference to the Pathway object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return Pathway A Pathway object - * - * @since 3.2 - */ - public function getPathway($name = 'site', $options = array()) - { - return parent::getPathway($name, $options); - } - - /** - * Return a reference to the Router object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return \Joomla\CMS\Router\Router - * - * @since 3.2 - */ - public static function getRouter($name = 'site', array $options = array()) - { - return parent::getRouter($name, $options); - } - - /** - * Gets the name of the current template. - * - * @param boolean $params True to return the template parameters - * - * @return string The name of the template. - * - * @since 3.2 - * @throws \InvalidArgumentException - */ - public function getTemplate($params = false) - { - if (\is_object($this->template)) - { - if ($this->template->parent) - { - if (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) - { - if (!is_file(JPATH_THEMES . '/' . $this->template->parent . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template)); - } - } - } - elseif (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template)); - } - - if ($params) - { - return $this->template; - } - - return $this->template->template; - } - - // Get the id of the active menu item - $menu = $this->getMenu(); - $item = $menu->getActive(); - - if (!$item) - { - $item = $menu->getItem($this->input->getInt('Itemid', null)); - } - - $id = 0; - - if (\is_object($item)) - { - // Valid item retrieved - $id = $item->template_style_id; - } - - $tid = $this->input->getUint('templateStyle', 0); - - if (is_numeric($tid) && (int) $tid > 0) - { - $id = (int) $tid; - } - - /** @var OutputController $cache */ - $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController('output', ['defaultgroup' => 'com_templates']); - - if ($this->getLanguageFilter()) - { - $tag = $this->getLanguage()->getTag(); - } - else - { - $tag = ''; - } - - $cacheId = 'templates0' . $tag; - - if ($cache->contains($cacheId)) - { - $templates = $cache->get($cacheId); - } - else - { - // Load styles - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName(['id', 'home', 'template', 's.params', 'inheritable', 'parent'])) - ->from($db->quoteName('#__template_styles', 's')) - ->where( - [ - $db->quoteName('s.client_id') . ' = 0', - $db->quoteName('e.enabled') . ' = 1', - ] - ) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template') - . ' AND ' . $db->quoteName('e.type') . ' = ' . $db->quote('template') - . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id') - ); - - $db->setQuery($query); - $templates = $db->loadObjectList('id'); - - foreach ($templates as &$template) - { - // Create home element - if ($template->home == 1 && !isset($template_home) || $this->getLanguageFilter() && $template->home == $tag) - { - $template_home = clone $template; - } - - $template->params = new Registry($template->params); - } - - // Unset the $template reference to the last $templates[n] item cycled in the foreach above to avoid editing it later - unset($template); - - // Add home element, after loop to avoid double execution - if (isset($template_home)) - { - $template_home->params = new Registry($template_home->params); - $templates[0] = $template_home; - } - - $cache->store($templates, $cacheId); - } - - if (isset($templates[$id])) - { - $template = $templates[$id]; - } - else - { - $template = $templates[0]; - } - - // Allows for overriding the active template from the request - $template_override = $this->input->getCmd('template', ''); - - // Only set template override if it is a valid template (= it exists and is enabled) - if (!empty($template_override)) - { - if (is_file(JPATH_THEMES . '/' . $template_override . '/index.php')) - { - foreach ($templates as $tmpl) - { - if ($tmpl->template === $template_override) - { - $template = $tmpl; - break; - } - } - } - } - - // Need to filter the default value as well - $template->template = InputFilter::getInstance()->clean($template->template, 'cmd'); - - // Fallback template - if (!empty($template->parent)) - { - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - if (!is_file(JPATH_THEMES . '/' . $template->parent . '/index.php')) - { - $this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error'); - - // Try to find data for 'cassiopeia' template - $original_tmpl = $template->template; - - foreach ($templates as $tmpl) - { - if ($tmpl->template === 'cassiopeia') - { - $template = $tmpl; - break; - } - } - - // Check, the data were found and if template really exists - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl)); - } - } - } - } - elseif (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - $this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error'); - - // Try to find data for 'cassiopeia' template - $original_tmpl = $template->template; - - foreach ($templates as $tmpl) - { - if ($tmpl->template === 'cassiopeia') - { - $template = $tmpl; - break; - } - } - - // Check, the data were found and if template really exists - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl)); - } - } - - // Cache the result - $this->template = $template; - - if ($params) - { - return $template; - } - - return $template->template; - } - - /** - * Initialise the application. - * - * @param array $options An optional associative array of configuration settings. - * - * @return void - * - * @since 3.2 - */ - protected function initialiseApp($options = array()) - { - $user = Factory::getUser(); - - // If the user is a guest we populate it with the guest user group. - if ($user->guest) - { - $guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); - $user->groups = array($guestUsergroup); - } - - if ($plugin = PluginHelper::getPlugin('system', 'languagefilter')) - { - $pluginParams = new Registry($plugin->params); - $this->setLanguageFilter(true); - $this->setDetectBrowser($pluginParams->get('detect_browser', 1) == 1); - } - - if (empty($options['language'])) - { - // Detect the specified language - $lang = $this->input->getString('language', null); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - } - - if (empty($options['language']) && $this->getLanguageFilter()) - { - // Detect cookie language - $lang = $this->input->cookie->get(md5($this->get('secret') . 'language'), null, 'string'); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - } - - if (empty($options['language'])) - { - // Detect user language - $lang = $user->getParam('language'); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - } - - if (empty($options['language']) && $this->getDetectBrowser()) - { - // Detect browser language - $lang = LanguageHelper::detectLanguage(); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - } - - if (empty($options['language'])) - { - // Detect default language - $params = ComponentHelper::getParams('com_languages'); - $options['language'] = $params->get('site', $this->get('language', 'en-GB')); - } - - // One last check to make sure we have something - if (!LanguageHelper::exists($options['language'])) - { - $lang = $this->config->get('language', 'en-GB'); - - if (LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - else - { - // As a last ditch fail to english - $options['language'] = 'en-GB'; - } - } - - // Finish initialisation - parent::initialiseApp($options); - } - - /** - * Load the library language files for the application - * - * @return void - * - * @since 3.6.3 - */ - protected function loadLibraryLanguage() - { - /* - * Try the lib_joomla file in the current language (without allowing the loading of the file in the default language) - * Fallback to the default language if necessary - */ - $this->getLanguage()->load('lib_joomla', JPATH_SITE) - || $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR); - } - - /** - * Login authentication function - * - * @param array $credentials Array('username' => string, 'password' => string) - * @param array $options Array('remember' => boolean) - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function login($credentials, $options = array()) - { - // Set the application login entry point - if (!\array_key_exists('entry_url', $options)) - { - $options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=user.login'; - } - - // Set the access control action to check. - $options['action'] = 'core.login.site'; - - return parent::login($credentials, $options); - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 3.2 - */ - protected function render() - { - switch ($this->document->getType()) - { - case 'feed': - // No special processing for feeds - break; - - case 'html': - default: - $template = $this->getTemplate(true); - $file = $this->input->get('tmpl', 'index'); - - if ($file === 'offline' && !$this->get('offline')) - { - $this->set('themeFile', 'index.php'); - } - - if ($this->get('offline') && !Factory::getUser()->authorise('core.login.offline')) - { - $this->setUserState('users.login.form.data', array('return' => Uri::getInstance()->toString())); - $this->set('themeFile', 'offline.php'); - $this->setHeader('Status', '503 Service Temporarily Unavailable', 'true'); - } - - if (!is_dir(JPATH_THEMES . '/' . $template->template) && !$this->get('offline')) - { - $this->set('themeFile', 'component.php'); - } - - // Ensure themeFile is set by now - if ($this->get('themeFile') == '') - { - $this->set('themeFile', $file . '.php'); - } - - // Pass the parent template to the state - $this->set('themeInherits', $template->parent); - - break; - } - - parent::render(); - } - - /** - * Route the application. - * - * Routing is the process of examining the request environment to determine which - * component should receive the request. The component optional parameters - * are then set in the request object to be processed when the application is being - * dispatched. - * - * @return void - * - * @since 3.2 - */ - protected function route() - { - // Execute the parent method - parent::route(); - - $Itemid = $this->input->getInt('Itemid', null); - $this->authorise($Itemid); - } - - /** - * Set the current state of the detect browser option. - * - * @param boolean $state The new state of the detect browser option - * - * @return boolean The previous state - * - * @since 3.2 - */ - public function setDetectBrowser($state = false) - { - $old = $this->getDetectBrowser(); - $this->detect_browser = $state; - - return $old; - } - - /** - * Set the current state of the language filter. - * - * @param boolean $state The new state of the language filter - * - * @return boolean The previous state - * - * @since 3.2 - */ - public function setLanguageFilter($state = false) - { - $old = $this->getLanguageFilter(); - $this->language_filter = $state; - - return $old; - } - - /** - * Overrides the default template that would be used - * - * @param \stdClass|string $template The template name or definition - * @param mixed $styleParams The template style parameters - * - * @return void - * - * @since 3.2 - */ - public function setTemplate($template, $styleParams = null) - { - if (is_object($template)) - { - $templateName = empty($template->template) - ? '' - : $template->template; - $templateInheritable = empty($template->inheritable) - ? 0 - : $template->inheritable; - $templateParent = empty($template->parent) - ? '' - : $template->parent; - $templateParams = empty($template->params) - ? $styleParams - : $template->params; - } - else - { - $templateName = $template; - $templateInheritable = 0; - $templateParent = ''; - $templateParams = $styleParams; - } - - if (is_dir(JPATH_THEMES . '/' . $templateName)) - { - $this->template = new \stdClass; - $this->template->template = $templateName; - - if ($templateParams instanceof Registry) - { - $this->template->params = $templateParams; - } - else - { - $this->template->params = new Registry($templateParams); - } - - $this->template->inheritable = $templateInheritable; - $this->template->parent = $templateParent; - - // Store the template and its params to the config - $this->set('theme', $this->template->template); - $this->set('themeParams', $this->template->params); - } - } + use CacheControllerFactoryAwareTrait; + use MultiFactorAuthenticationHandler; + + /** + * Option to filter by language + * + * @var boolean + * @since 4.0.0 + */ + protected $language_filter = false; + + /** + * Option to detect language by the browser + * + * @var boolean + * @since 4.0.0 + */ + protected $detect_browser = false; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's config + * object. If the argument is a Registry object that object will become the + * application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's client + * object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container $container Dependency injection container. + * + * @since 3.2 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + // Register the application name + $this->name = 'site'; + + // Register the client ID + $this->clientId = 0; + + // Execute the parent constructor + parent::__construct($input, $config, $client, $container); + } + + /** + * Check if the user can access the application + * + * @param integer $itemid The item ID to check authorisation for + * + * @return void + * + * @since 3.2 + * + * @throws \Exception When you are not authorised to view the home page menu item + */ + protected function authorise($itemid) + { + $menus = $this->getMenu(); + $user = Factory::getUser(); + + if (!$menus->authorise($itemid)) { + if ($user->get('id') == 0) { + // Set the data + $this->setUserState('users.login.form.data', array('return' => Uri::getInstance()->toString())); + + $url = Route::_('index.php?option=com_users&view=login', false); + + $this->enqueueMessage(Text::_('JGLOBAL_YOU_MUST_LOGIN_FIRST'), 'error'); + $this->redirect($url); + } else { + // Get the home page menu item + $home_item = $menus->getDefault($this->getLanguage()->getTag()); + + // If we are already in the homepage raise an exception + if ($menus->getActive()->id == $home_item->id) { + throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Otherwise redirect to the homepage and show an error + $this->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $this->redirect(Route::_('index.php?Itemid=' . $home_item->id, false)); + } + } + } + + /** + * Dispatch the application + * + * @param string $component The component which is being rendered. + * + * @return void + * + * @since 3.2 + */ + public function dispatch($component = null) + { + // Get the component if not set. + if (!$component) { + $component = $this->input->getCmd('option', null); + } + + // Load the document to the API + $this->loadDocument(); + + // Set up the params + $document = $this->getDocument(); + $params = $this->getParams(); + + // Register the document object with Factory + Factory::$document = $document; + + switch ($document->getType()) { + case 'html': + // Set up the language + LanguageHelper::getLanguages('lang_code'); + + // Set metadata + $document->setMetaData('rights', $this->get('MetaRights')); + + // Get the template + $template = $this->getTemplate(true); + + // Store the template and its params to the config + $this->set('theme', $template->template); + $this->set('themeParams', $template->params); + + // Add Asset registry files + $wr = $document->getWebAssetManager()->getRegistry(); + + if ($component) { + $wr->addExtensionRegistryFile($component); + } + + if ($template->parent) { + $wr->addTemplateRegistryFile($template->parent, $this->getClientId()); + } + + $wr->addTemplateRegistryFile($template->template, $this->getClientId()); + + break; + + case 'feed': + $document->setBase(htmlspecialchars(Uri::current())); + break; + } + + $document->setTitle($params->get('page_title')); + $document->setDescription($params->get('page_description')); + + // Add version number or not based on global configuration + if ($this->get('MetaVersion', 0)) { + $document->setGenerator('Joomla! - Open Source Content Management - Version ' . JVERSION); + } else { + $document->setGenerator('Joomla! - Open Source Content Management'); + } + + $contents = ComponentHelper::renderComponent($component); + $document->setBuffer($contents, 'component'); + + // Trigger the onAfterDispatch event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterDispatch'); + } + + /** + * Method to run the Web application routines. + * + * @return void + * + * @since 3.2 + */ + protected function doExecute() + { + // Initialise the application + $this->initialiseApp(); + + // Mark afterInitialise in the profiler. + JDEBUG ? $this->profiler->mark('afterInitialise') : null; + + // Route the application + $this->route(); + + // Mark afterRoute in the profiler. + JDEBUG ? $this->profiler->mark('afterRoute') : null; + + if (!$this->isHandlingMultiFactorAuthentication()) { + /* + * Check if the user is required to reset their password + * + * Before $this->route(); "option" and "view" can't be safely read using: + * $this->input->getCmd('option'); or $this->input->getCmd('view'); + * ex: due of the sef urls + */ + $this->checkUserRequireReset('com_users', 'profile', 'edit', 'com_users/profile.save,com_users/profile.apply,com_users/user.logout'); + } + + // Dispatch the application + $this->dispatch(); + + // Mark afterDispatch in the profiler. + JDEBUG ? $this->profiler->mark('afterDispatch') : null; + } + + /** + * Return the current state of the detect browser option. + * + * @return boolean + * + * @since 3.2 + */ + public function getDetectBrowser() + { + return $this->detect_browser; + } + + /** + * Return the current state of the language filter. + * + * @return boolean + * + * @since 3.2 + */ + public function getLanguageFilter() + { + return $this->language_filter; + } + + /** + * Get the application parameters + * + * @param string $option The component option + * + * @return Registry The parameters object + * + * @since 3.2 + */ + public function getParams($option = null) + { + static $params = array(); + + $hash = '__default'; + + if (!empty($option)) { + $hash = $option; + } + + if (!isset($params[$hash])) { + // Get component parameters + if (!$option) { + $option = $this->input->getCmd('option', null); + } + + // Get new instance of component global parameters + $params[$hash] = clone ComponentHelper::getParams($option); + + // Get menu parameters + $menus = $this->getMenu(); + $menu = $menus->getActive(); + + // Get language + $lang_code = $this->getLanguage()->getTag(); + $languages = LanguageHelper::getLanguages('lang_code'); + + $title = $this->get('sitename'); + + if (isset($languages[$lang_code]) && $languages[$lang_code]->metadesc) { + $description = $languages[$lang_code]->metadesc; + } else { + $description = $this->get('MetaDesc'); + } + + $rights = $this->get('MetaRights'); + $robots = $this->get('robots'); + + // Retrieve com_menu global settings + $temp = clone ComponentHelper::getParams('com_menus'); + + // Lets cascade the parameters if we have menu item parameters + if (\is_object($menu)) { + // Get show_page_heading from com_menu global settings + $params[$hash]->def('show_page_heading', $temp->get('show_page_heading')); + + $params[$hash]->merge($menu->getParams()); + $title = $menu->title; + } else { + // Merge com_menu global settings + $params[$hash]->merge($temp); + + // If supplied, use page title + $title = $temp->get('page_title', $title); + } + + $params[$hash]->def('page_title', $title); + $params[$hash]->def('page_description', $description); + $params[$hash]->def('page_rights', $rights); + $params[$hash]->def('robots', $robots); + } + + return $params[$hash]; + } + + /** + * Return a reference to the Pathway object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return Pathway A Pathway object + * + * @since 3.2 + */ + public function getPathway($name = 'site', $options = array()) + { + return parent::getPathway($name, $options); + } + + /** + * Return a reference to the Router object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return \Joomla\CMS\Router\Router + * + * @since 3.2 + * + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public static function getRouter($name = 'site', array $options = array()) + { + return parent::getRouter($name, $options); + } + + /** + * Gets the name of the current template. + * + * @param boolean $params True to return the template parameters + * + * @return string The name of the template. + * + * @since 3.2 + * @throws \InvalidArgumentException + */ + public function getTemplate($params = false) + { + if (\is_object($this->template)) { + if ($this->template->parent) { + if (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) { + if (!is_file(JPATH_THEMES . '/' . $this->template->parent . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template)); + } + } + } elseif (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template)); + } + + if ($params) { + return $this->template; + } + + return $this->template->template; + } + + // Get the id of the active menu item + $menu = $this->getMenu(); + $item = $menu->getActive(); + + if (!$item) { + $item = $menu->getItem($this->input->getInt('Itemid', null)); + } + + $id = 0; + + if (\is_object($item)) { + // Valid item retrieved + $id = $item->template_style_id; + } + + $tid = $this->input->getUint('templateStyle', 0); + + if (is_numeric($tid) && (int) $tid > 0) { + $id = (int) $tid; + } + + /** @var OutputController $cache */ + $cache = $this->getCacheControllerFactory()->createCacheController('output', ['defaultgroup' => 'com_templates']); + + if ($this->getLanguageFilter()) { + $tag = $this->getLanguage()->getTag(); + } else { + $tag = ''; + } + + $cacheId = 'templates0' . $tag; + + if ($cache->contains($cacheId)) { + $templates = $cache->get($cacheId); + } else { + $templates = $this->bootComponent('templates')->getMVCFactory() + ->createModel('Style', 'Administrator')->getSiteTemplates(); + + foreach ($templates as &$template) { + // Create home element + if ($template->home == 1 && !isset($template_home) || $this->getLanguageFilter() && $template->home == $tag) { + $template_home = clone $template; + } + + $template->params = new Registry($template->params); + } + + // Unset the $template reference to the last $templates[n] item cycled in the foreach above to avoid editing it later + unset($template); + + // Add home element, after loop to avoid double execution + if (isset($template_home)) { + $template_home->params = new Registry($template_home->params); + $templates[0] = $template_home; + } + + $cache->store($templates, $cacheId); + } + + if (isset($templates[$id])) { + $template = $templates[$id]; + } else { + $template = $templates[0]; + } + + // Allows for overriding the active template from the request + $template_override = $this->input->getCmd('template', ''); + + // Only set template override if it is a valid template (= it exists and is enabled) + if (!empty($template_override)) { + if (is_file(JPATH_THEMES . '/' . $template_override . '/index.php')) { + foreach ($templates as $tmpl) { + if ($tmpl->template === $template_override) { + $template = $tmpl; + break; + } + } + } + } + + // Need to filter the default value as well + $template->template = InputFilter::getInstance()->clean($template->template, 'cmd'); + + // Fallback template + if (!empty($template->parent)) { + if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + if (!is_file(JPATH_THEMES . '/' . $template->parent . '/index.php')) { + $this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error'); + + // Try to find data for 'cassiopeia' template + $original_tmpl = $template->template; + + foreach ($templates as $tmpl) { + if ($tmpl->template === 'cassiopeia') { + $template = $tmpl; + break; + } + } + + // Check, the data were found and if template really exists + if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl)); + } + } + } + } elseif (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + $this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error'); + + // Try to find data for 'cassiopeia' template + $original_tmpl = $template->template; + + foreach ($templates as $tmpl) { + if ($tmpl->template === 'cassiopeia') { + $template = $tmpl; + break; + } + } + + // Check, the data were found and if template really exists + if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl)); + } + } + + // Cache the result + $this->template = $template; + + if ($params) { + return $template; + } + + return $template->template; + } + + /** + * Initialise the application. + * + * @param array $options An optional associative array of configuration settings. + * + * @return void + * + * @since 3.2 + */ + protected function initialiseApp($options = array()) + { + $user = Factory::getUser(); + + // If the user is a guest we populate it with the guest user group. + if ($user->guest) { + $guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); + $user->groups = array($guestUsergroup); + } + + if ($plugin = PluginHelper::getPlugin('system', 'languagefilter')) { + $pluginParams = new Registry($plugin->params); + $this->setLanguageFilter(true); + $this->setDetectBrowser($pluginParams->get('detect_browser', 1) == 1); + } + + if (empty($options['language'])) { + // Detect the specified language + $lang = $this->input->getString('language', null); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } + } + + if (empty($options['language']) && $this->getLanguageFilter()) { + // Detect cookie language + $lang = $this->input->cookie->get(md5($this->get('secret') . 'language'), null, 'string'); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } + } + + if (empty($options['language'])) { + // Detect user language + $lang = $user->getParam('language'); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } + } + + if (empty($options['language']) && $this->getDetectBrowser()) { + // Detect browser language + $lang = LanguageHelper::detectLanguage(); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } + } + + if (empty($options['language'])) { + // Detect default language + $params = ComponentHelper::getParams('com_languages'); + $options['language'] = $params->get('site', $this->get('language', 'en-GB')); + } + + // One last check to make sure we have something + if (!LanguageHelper::exists($options['language'])) { + $lang = $this->config->get('language', 'en-GB'); + + if (LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } else { + // As a last ditch fail to english + $options['language'] = 'en-GB'; + } + } + + // Finish initialisation + parent::initialiseApp($options); + } + + /** + * Load the library language files for the application + * + * @return void + * + * @since 3.6.3 + */ + protected function loadLibraryLanguage() + { + /* + * Try the lib_joomla file in the current language (without allowing the loading of the file in the default language) + * Fallback to the default language if necessary + */ + $this->getLanguage()->load('lib_joomla', JPATH_SITE) + || $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR); + } + + /** + * Login authentication function + * + * @param array $credentials Array('username' => string, 'password' => string) + * @param array $options Array('remember' => boolean) + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function login($credentials, $options = array()) + { + // Set the application login entry point + if (!\array_key_exists('entry_url', $options)) { + $options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=user.login'; + } + + // Set the access control action to check. + $options['action'] = 'core.login.site'; + + return parent::login($credentials, $options); + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 3.2 + */ + protected function render() + { + switch ($this->document->getType()) { + case 'feed': + // No special processing for feeds + break; + + case 'html': + default: + $template = $this->getTemplate(true); + $file = $this->input->get('tmpl', 'index'); + + if ($file === 'offline' && !$this->get('offline')) { + $this->set('themeFile', 'index.php'); + } + + if ($this->get('offline') && !Factory::getUser()->authorise('core.login.offline')) { + $this->setUserState('users.login.form.data', array('return' => Uri::getInstance()->toString())); + $this->set('themeFile', 'offline.php'); + $this->setHeader('Status', '503 Service Temporarily Unavailable', 'true'); + } + + if (!is_dir(JPATH_THEMES . '/' . $template->template) && !$this->get('offline')) { + $this->set('themeFile', 'component.php'); + } + + // Ensure themeFile is set by now + if ($this->get('themeFile') == '') { + $this->set('themeFile', $file . '.php'); + } + + // Pass the parent template to the state + $this->set('themeInherits', $template->parent); + + break; + } + + parent::render(); + } + + /** + * Route the application. + * + * Routing is the process of examining the request environment to determine which + * component should receive the request. The component optional parameters + * are then set in the request object to be processed when the application is being + * dispatched. + * + * @return void + * + * @since 3.2 + */ + protected function route() + { + // Get the full request URI. + $uri = clone Uri::getInstance(); + + // It is not possible to inject the SiteRouter as it requires a SiteApplication + // and we would end in an infinite loop + $result = $this->getContainer()->get(SiteRouter::class)->parse($uri, true); + + $active = $this->getMenu()->getActive(); + + if ( + $active !== null + && $active->type === 'alias' + && $active->getParams()->get('alias_redirect') + && \in_array($this->input->getMethod(), ['GET', 'HEAD'], true) + ) { + $item = $this->getMenu()->getItem($active->getParams()->get('aliasoptions')); + + if ($item !== null) { + $oldUri = clone Uri::getInstance(); + + if ($oldUri->getVar('Itemid') == $active->id) { + $oldUri->setVar('Itemid', $item->id); + } + + $base = Uri::base(true); + $oldPath = StringHelper::strtolower(substr($oldUri->getPath(), \strlen($base) + 1)); + $activePathPrefix = StringHelper::strtolower($active->route); + + $position = strpos($oldPath, $activePathPrefix); + + if ($position !== false) { + $oldUri->setPath($base . '/' . substr_replace($oldPath, $item->route, $position, \strlen($activePathPrefix))); + + $this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true); + $this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); + $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false); + $this->sendHeaders(); + + $this->redirect((string) $oldUri, 301); + } + } + } + + foreach ($result as $key => $value) { + $this->input->def($key, $value); + } + + // Trigger the onAfterRoute event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterRoute'); + + $Itemid = $this->input->getInt('Itemid', null); + $this->authorise($Itemid); + } + + /** + * Set the current state of the detect browser option. + * + * @param boolean $state The new state of the detect browser option + * + * @return boolean The previous state + * + * @since 3.2 + */ + public function setDetectBrowser($state = false) + { + $old = $this->getDetectBrowser(); + $this->detect_browser = $state; + + return $old; + } + + /** + * Set the current state of the language filter. + * + * @param boolean $state The new state of the language filter + * + * @return boolean The previous state + * + * @since 3.2 + */ + public function setLanguageFilter($state = false) + { + $old = $this->getLanguageFilter(); + $this->language_filter = $state; + + return $old; + } + + /** + * Overrides the default template that would be used + * + * @param \stdClass|string $template The template name or definition + * @param mixed $styleParams The template style parameters + * + * @return void + * + * @since 3.2 + */ + public function setTemplate($template, $styleParams = null) + { + if (is_object($template)) { + $templateName = empty($template->template) + ? '' + : $template->template; + $templateInheritable = empty($template->inheritable) + ? 0 + : $template->inheritable; + $templateParent = empty($template->parent) + ? '' + : $template->parent; + $templateParams = empty($template->params) + ? $styleParams + : $template->params; + } else { + $templateName = $template; + $templateInheritable = 0; + $templateParent = ''; + $templateParams = $styleParams; + } + + if (is_dir(JPATH_THEMES . '/' . $templateName)) { + $this->template = new \stdClass(); + $this->template->template = $templateName; + + if ($templateParams instanceof Registry) { + $this->template->params = $templateParams; + } else { + $this->template->params = new Registry($templateParams); + } + + $this->template->inheritable = $templateInheritable; + $this->template->parent = $templateParent; + + // Store the template and its params to the config + $this->set('theme', $this->template->template); + $this->set('themeParams', $this->template->params); + } + } } diff --git a/code/libraries/src/Application/WebApplication.php b/code/libraries/src/Application/WebApplication.php index b2b9e588..ea01244a 100644 --- a/code/libraries/src/Application/WebApplication.php +++ b/code/libraries/src/Application/WebApplication.php @@ -1,4 +1,5 @@ set('execution.datetime', gmdate('Y-m-d H:i:s')); - $this->set('execution.timestamp', time()); - - // Set the system URIs. - $this->loadSystemUris(); - } - - /** - * Returns a reference to the global WebApplication object, only creating it if it doesn't already exist. - * - * This method must be invoked as: $web = WebApplication::getInstance(); - * - * @param string $name The name (optional) of the WebApplication class to instantiate. - * - * @return WebApplication - * - * @since 1.7.3 - * @throws \RuntimeException - * @deprecated 5.0 Use \Joomla\CMS\Factory::getContainer()->get($name) instead - */ - public static function getInstance($name = null) - { - // Only create the object if it doesn't exist. - if (empty(static::$instance)) - { - if (!is_subclass_of($name, '\\Joomla\\CMS\\Application\\WebApplication')) - { - throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500); - } - - static::$instance = new $name; - } - - return static::$instance; - } - - /** - * Execute the application. - * - * @return void - * - * @since 1.7.3 - */ - public function execute() - { - // Trigger the onBeforeExecute event. - $this->triggerEvent('onBeforeExecute'); - - // Perform application routines. - $this->doExecute(); - - // Trigger the onAfterExecute event. - $this->triggerEvent('onAfterExecute'); - - // If we have an application document object, render it. - if ($this->document instanceof Document) - { - // Trigger the onBeforeRender event. - $this->triggerEvent('onBeforeRender'); - - // Render the application output. - $this->render(); - - // Trigger the onAfterRender event. - $this->triggerEvent('onAfterRender'); - } - - // If gzip compression is enabled in configuration and the server is compliant, compress the output. - if ($this->get('gzip') && !ini_get('zlib.output_compression') && (ini_get('output_handler') !== 'ob_gzhandler')) - { - $this->compress(); - } - - // Trigger the onBeforeRespond event. - $this->triggerEvent('onBeforeRespond'); - - // Send the application response. - $this->respond(); - - // Trigger the onAfterRespond event. - $this->triggerEvent('onAfterRespond'); - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 1.7.3 - */ - protected function render() - { - // Setup the document options. - $options = array( - 'template' => $this->get('theme'), - 'file' => $this->get('themeFile', 'index.php'), - 'params' => $this->get('themeParams'), - 'templateInherits' => $this->get('themeInherits'), - ); - - if ($this->get('themes.base')) - { - $options['directory'] = $this->get('themes.base'); - } - // Fall back to constants. - else - { - $options['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes'; - } - - // Parse the document. - $this->document->parse($options); - - // Render the document. - $data = $this->document->render($this->get('cache_enabled'), $options); - - // Set the application output data. - $this->setBody($data); - } - - /** - * Method to get the application document object. - * - * @return Document The document object - * - * @since 1.7.3 - */ - public function getDocument() - { - return $this->document; - } - - /** - * Method to get the application language object. - * - * @return Language The language object - * - * @since 1.7.3 - */ - public function getLanguage() - { - return $this->language; - } - - /** - * Flush the media version to refresh versionable assets - * - * @return void - * - * @since 3.2 - */ - public function flushAssets() - { - (new Version)->refreshMediaVersion(); - } - - /** - * Allows the application to load a custom or default document. - * - * The logic and options for creating this object are adequately generic for default cases - * but for many applications it will make sense to override this method and create a document, - * if required, based on more specific needs. - * - * @param Document $document An optional document object. If omitted, the factory document is created. - * - * @return WebApplication This method is chainable. - * - * @since 1.7.3 - */ - public function loadDocument(Document $document = null) - { - $this->document = $document ?? Factory::getDocument(); - - return $this; - } - - /** - * Allows the application to load a custom or default language. - * - * The logic and options for creating this object are adequately generic for default cases - * but for many applications it will make sense to override this method and create a language, - * if required, based on more specific needs. - * - * @param Language $language An optional language object. If omitted, the factory language is created. - * - * @return WebApplication This method is chainable. - * - * @since 1.7.3 - */ - public function loadLanguage(Language $language = null) - { - $this->language = $language ?? Factory::getLanguage(); - - return $this; - } - - /** - * Allows the application to load a custom or default session. - * - * The logic and options for creating this object are adequately generic for default cases - * but for many applications it will make sense to override this method and create a session, - * if required, based on more specific needs. - * - * @param Session $session An optional session object. If omitted, the session is created. - * - * @return WebApplication This method is chainable. - * - * @since 1.7.3 - * @deprecated 5.0 The session should be injected as a service. - */ - public function loadSession(Session $session = null) - { - $this->getLogger()->warning(__METHOD__ . '() is deprecated. Inject the session as a service instead.', array('category' => 'deprecated')); - - return $this; - } - - /** - * After the session has been started we need to populate it with some default values. - * - * @param SessionEvent $event Session event being triggered - * - * @return void - * - * @since 3.0.1 - */ - public function afterSessionStart(SessionEvent $event) - { - $session = $event->getSession(); - - if ($session->isNew()) - { - $session->set('registry', new Registry); - $session->set('user', new User); - } - - // Ensure the identity is loaded - if (!$this->getIdentity()) - { - $this->loadIdentity($session->get('user')); - } - } - - /** - * Method to load the system URI strings for the application. - * - * @param string $requestUri An optional request URI to use instead of detecting one from the - * server environment variables. - * - * @return void - * - * @since 1.7.3 - */ - protected function loadSystemUris($requestUri = null) - { - // Set the request URI. - if (!empty($requestUri)) - { - $this->set('uri.request', $requestUri); - } - else - { - $this->set('uri.request', $this->detectRequestUri()); - } - - // Check to see if an explicit base URI has been set. - $siteUri = trim($this->get('site_uri', '')); - - if ($siteUri !== '') - { - $uri = Uri::getInstance($siteUri); - $path = $uri->toString(array('path')); - } - // No explicit base URI was set so we need to detect it. - else - { - // Start with the requested URI. - $uri = Uri::getInstance($this->get('uri.request')); - - // If we are working from a CGI SAPI with the 'cgi.fix_pathinfo' directive disabled we use PHP_SELF. - if (strpos(PHP_SAPI, 'cgi') !== false && !ini_get('cgi.fix_pathinfo') && !empty($_SERVER['REQUEST_URI'])) - { - // We aren't expecting PATH_INFO within PHP_SELF so this should work. - $path = \dirname($_SERVER['PHP_SELF']); - } - // Pretty much everything else should be handled with SCRIPT_NAME. - else - { - $path = \dirname($_SERVER['SCRIPT_NAME']); - } - } - - $host = $uri->toString(array('scheme', 'user', 'pass', 'host', 'port')); - - // Check if the path includes "index.php". - if (strpos($path, 'index.php') !== false) - { - // Remove the index.php portion of the path. - $path = substr_replace($path, '', strpos($path, 'index.php'), 9); - } - - $path = rtrim($path, '/\\'); - - // Set the base URI both as just a path and as the full URI. - $this->set('uri.base.full', $host . $path . '/'); - $this->set('uri.base.host', $host); - $this->set('uri.base.path', $path . '/'); - - // Set the extended (non-base) part of the request URI as the route. - if (stripos($this->get('uri.request'), $this->get('uri.base.full')) === 0) - { - $this->set('uri.route', substr_replace($this->get('uri.request'), '', 0, \strlen($this->get('uri.base.full')))); - } - - // Get an explicitly set media URI is present. - $mediaURI = trim($this->get('media_uri', '')); - - if ($mediaURI) - { - if (strpos($mediaURI, '://') !== false) - { - $this->set('uri.media.full', $mediaURI); - $this->set('uri.media.path', $mediaURI); - } - else - { - // Normalise slashes. - $mediaURI = trim($mediaURI, '/\\'); - $mediaURI = !empty($mediaURI) ? '/' . $mediaURI . '/' : '/'; - $this->set('uri.media.full', $this->get('uri.base.host') . $mediaURI); - $this->set('uri.media.path', $mediaURI); - } - } - // No explicit media URI was set, build it dynamically from the base uri. - else - { - $this->set('uri.media.full', $this->get('uri.base.full') . 'media/'); - $this->set('uri.media.path', $this->get('uri.base.path') . 'media/'); - } - } - - /** - * Retrieve the application configuration object. - * - * @return Registry - * - * @since 4.0.0 - */ - public function getConfig() - { - return $this->config; - } + use EventAware; + use IdentityAware; + + /** + * The application document object. + * + * @var Document + * @since 1.7.3 + */ + protected $document; + + /** + * The application language object. + * + * @var Language + * @since 1.7.3 + */ + protected $language; + + /** + * The application instance. + * + * @var static + * @since 1.7.3 + */ + protected static $instance; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's + * input object. If the argument is a JInput object that object will become + * the application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's + * client object. If the argument is a WebClient object that object will become + * the application's client object, otherwise a default client object is created. + * @param ResponseInterface $response An optional argument to provide dependency injection for the application's + * response object. If the argument is a ResponseInterface object that object + * will become the application's response object, otherwise a default response + * object is created. + * + * @since 1.7.3 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, ResponseInterface $response = null) + { + // Ensure we have a CMS Input object otherwise the DI for \Joomla\CMS\Session\Storage\JoomlaStorage fails + $input = $input ?: new Input(); + + parent::__construct($input, $config, $client, $response); + + // Set the execution datetime and timestamp; + $this->set('execution.datetime', gmdate('Y-m-d H:i:s')); + $this->set('execution.timestamp', time()); + + // Set the system URIs. + $this->loadSystemUris(); + } + + /** + * Returns a reference to the global WebApplication object, only creating it if it doesn't already exist. + * + * This method must be invoked as: $web = WebApplication::getInstance(); + * + * @param string $name The name (optional) of the WebApplication class to instantiate. + * + * @return WebApplication + * + * @since 1.7.3 + * @throws \RuntimeException + * @deprecated 5.0 Use \Joomla\CMS\Factory::getContainer()->get($name) instead + */ + public static function getInstance($name = null) + { + // Only create the object if it doesn't exist. + if (empty(static::$instance)) { + if (!is_subclass_of($name, '\\Joomla\\CMS\\Application\\WebApplication')) { + throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500); + } + + static::$instance = new $name(); + } + + return static::$instance; + } + + /** + * Execute the application. + * + * @return void + * + * @since 1.7.3 + */ + public function execute() + { + // Trigger the onBeforeExecute event. + $this->triggerEvent('onBeforeExecute'); + + // Perform application routines. + $this->doExecute(); + + // Trigger the onAfterExecute event. + $this->triggerEvent('onAfterExecute'); + + // If we have an application document object, render it. + if ($this->document instanceof Document) { + // Trigger the onBeforeRender event. + $this->triggerEvent('onBeforeRender'); + + // Render the application output. + $this->render(); + + // Trigger the onAfterRender event. + $this->triggerEvent('onAfterRender'); + } + + // If gzip compression is enabled in configuration and the server is compliant, compress the output. + if ($this->get('gzip') && !ini_get('zlib.output_compression') && (ini_get('output_handler') !== 'ob_gzhandler')) { + $this->compress(); + } + + // Trigger the onBeforeRespond event. + $this->triggerEvent('onBeforeRespond'); + + // Send the application response. + $this->respond(); + + // Trigger the onAfterRespond event. + $this->triggerEvent('onAfterRespond'); + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 1.7.3 + */ + protected function render() + { + // Setup the document options. + $options = array( + 'template' => $this->get('theme'), + 'file' => $this->get('themeFile', 'index.php'), + 'params' => $this->get('themeParams'), + 'templateInherits' => $this->get('themeInherits'), + ); + + if ($this->get('themes.base')) { + $options['directory'] = $this->get('themes.base'); + } else { + // Fall back to constants. + $options['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes'; + } + + // Parse the document. + $this->document->parse($options); + + // Render the document. + $data = $this->document->render($this->get('cache_enabled'), $options); + + // Set the application output data. + $this->setBody($data); + } + + /** + * Method to get the application document object. + * + * @return Document The document object + * + * @since 1.7.3 + */ + public function getDocument() + { + return $this->document; + } + + /** + * Method to get the application language object. + * + * @return Language The language object + * + * @since 1.7.3 + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Flush the media version to refresh versionable assets + * + * @return void + * + * @since 3.2 + */ + public function flushAssets() + { + (new Version())->refreshMediaVersion(); + } + + /** + * Allows the application to load a custom or default document. + * + * The logic and options for creating this object are adequately generic for default cases + * but for many applications it will make sense to override this method and create a document, + * if required, based on more specific needs. + * + * @param Document $document An optional document object. If omitted, the factory document is created. + * + * @return WebApplication This method is chainable. + * + * @since 1.7.3 + */ + public function loadDocument(Document $document = null) + { + $this->document = $document ?? Factory::getDocument(); + + return $this; + } + + /** + * Allows the application to load a custom or default language. + * + * The logic and options for creating this object are adequately generic for default cases + * but for many applications it will make sense to override this method and create a language, + * if required, based on more specific needs. + * + * @param Language $language An optional language object. If omitted, the factory language is created. + * + * @return WebApplication This method is chainable. + * + * @since 1.7.3 + */ + public function loadLanguage(Language $language = null) + { + $this->language = $language ?? Factory::getLanguage(); + + return $this; + } + + /** + * Allows the application to load a custom or default session. + * + * The logic and options for creating this object are adequately generic for default cases + * but for many applications it will make sense to override this method and create a session, + * if required, based on more specific needs. + * + * @param Session $session An optional session object. If omitted, the session is created. + * + * @return WebApplication This method is chainable. + * + * @since 1.7.3 + * @deprecated 5.0 The session should be injected as a service. + */ + public function loadSession(Session $session = null) + { + $this->getLogger()->warning(__METHOD__ . '() is deprecated. Inject the session as a service instead.', array('category' => 'deprecated')); + + return $this; + } + + /** + * After the session has been started we need to populate it with some default values. + * + * @param SessionEvent $event Session event being triggered + * + * @return void + * + * @since 3.0.1 + */ + public function afterSessionStart(SessionEvent $event) + { + $session = $event->getSession(); + + if ($session->isNew()) { + $session->set('registry', new Registry()); + $session->set('user', new User()); + } + + // Ensure the identity is loaded + if (!$this->getIdentity()) { + $this->loadIdentity($session->get('user')); + } + } + + /** + * Method to load the system URI strings for the application. + * + * @param string $requestUri An optional request URI to use instead of detecting one from the + * server environment variables. + * + * @return void + * + * @since 1.7.3 + */ + protected function loadSystemUris($requestUri = null) + { + // Set the request URI. + if (!empty($requestUri)) { + $this->set('uri.request', $requestUri); + } else { + $this->set('uri.request', $this->detectRequestUri()); + } + + // Check to see if an explicit base URI has been set. + $siteUri = trim($this->get('site_uri', '')); + + if ($siteUri !== '') { + $uri = Uri::getInstance($siteUri); + $path = $uri->toString(array('path')); + } else { + // No explicit base URI was set so we need to detect it. + // Start with the requested URI. + $uri = Uri::getInstance($this->get('uri.request')); + + // If we are working from a CGI SAPI with the 'cgi.fix_pathinfo' directive disabled we use PHP_SELF. + if (strpos(PHP_SAPI, 'cgi') !== false && !ini_get('cgi.fix_pathinfo') && !empty($_SERVER['REQUEST_URI'])) { + // We aren't expecting PATH_INFO within PHP_SELF so this should work. + $path = \dirname($_SERVER['PHP_SELF']); + } else { + // Pretty much everything else should be handled with SCRIPT_NAME. + $path = \dirname($_SERVER['SCRIPT_NAME']); + } + } + + $host = $uri->toString(array('scheme', 'user', 'pass', 'host', 'port')); + + // Check if the path includes "index.php". + if (strpos($path, 'index.php') !== false) { + // Remove the index.php portion of the path. + $path = substr_replace($path, '', strpos($path, 'index.php'), 9); + } + + $path = rtrim($path, '/\\'); + + // Set the base URI both as just a path and as the full URI. + $this->set('uri.base.full', $host . $path . '/'); + $this->set('uri.base.host', $host); + $this->set('uri.base.path', $path . '/'); + + // Set the extended (non-base) part of the request URI as the route. + if (stripos($this->get('uri.request'), $this->get('uri.base.full')) === 0) { + $this->set('uri.route', substr_replace($this->get('uri.request'), '', 0, \strlen($this->get('uri.base.full')))); + } + + // Get an explicitly set media URI is present. + $mediaURI = trim($this->get('media_uri', '')); + + if ($mediaURI) { + if (strpos($mediaURI, '://') !== false) { + $this->set('uri.media.full', $mediaURI); + $this->set('uri.media.path', $mediaURI); + } else { + // Normalise slashes. + $mediaURI = trim($mediaURI, '/\\'); + $mediaURI = !empty($mediaURI) ? '/' . $mediaURI . '/' : '/'; + $this->set('uri.media.full', $this->get('uri.base.host') . $mediaURI); + $this->set('uri.media.path', $mediaURI); + } + } else { + // No explicit media URI was set, build it dynamically from the base uri. + $this->set('uri.media.full', $this->get('uri.base.full') . 'media/'); + $this->set('uri.media.path', $this->get('uri.base.path') . 'media/'); + } + } + + /** + * Retrieve the application configuration object. + * + * @return Registry + * + * @since 4.0.0 + */ + public function getConfig() + { + return $this->config; + } } diff --git a/code/libraries/src/Association/AssociationExtensionHelper.php b/code/libraries/src/Association/AssociationExtensionHelper.php index 0815984d..b619629f 100644 --- a/code/libraries/src/Association/AssociationExtensionHelper.php +++ b/code/libraries/src/Association/AssociationExtensionHelper.php @@ -1,4 +1,5 @@ associationsSupport; - } - - /** - * Get the item types - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getItemTypes() - { - return $this->itemTypes; - } - - /** - * Get the associated items for an item - * - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return array - * - * @since 3.7.0 - */ - public function getAssociationList($typeName, $itemId) - { - $items = array(); - - $associations = $this->getAssociations($typeName, $itemId); - - foreach ($associations as $key => $association) - { - $items[$key] = ArrayHelper::fromObject($this->getItem($typeName, (int) $association->id), false); - } - - return $items; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } - - /** - * Get information about the fields the type provides - * - * @param string $typeName The item type - * - * @return array Array of support information - * - * @since 3.7.0 - */ - public function getTypeFields($typeName) - { - return $this->getTypeInformation($typeName, 'fields'); - } - - /** - * Get information about the fields the type provides - * - * @param string $typeName The item type - * - * @return array Array of support information - * - * @since 3.7.0 - */ - public function getTypeSupport($typeName) - { - return $this->getTypeInformation($typeName, 'support'); - } - - /** - * Get information about the tables the type use - * - * @param string $typeName The item type - * - * @return array Array of support information - * - * @since 3.7.0 - */ - public function getTypeTables($typeName) - { - return $this->getTypeInformation($typeName, 'tables'); - } - - /** - * Get information about the table joins for the type - * - * @param string $typeName The item type - * - * @return array Array of support information - * - * @since 3.7.0 - */ - public function getTypeJoins($typeName) - { - return $this->getTypeInformation($typeName, 'joins'); - } - - /** - * Get the type title - * - * @param string $typeName The item type - * - * @return string The type title - * - * @since 3.7.0 - */ - public function getTypeTitle($typeName) - { - $type = $this->getType($typeName); - - if (!\array_key_exists('title', $type)) - { - return ''; - } - - return $type['title']; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * @param string $part part of the information - * - * @return array Array of support information - * - * @since 3.7.0 - */ - private function getTypeInformation($typeName, $part = 'support') - { - $type = $this->getType($typeName); - - if (!\array_key_exists($part, $type)) - { - return array(); - } - - return $type[$part]; - } - - /** - * Get a table field name for a type - * - * @param string $typeName The item type - * @param string $fieldName The item type - * - * @return string - * - * @since 3.7.0 - */ - public function getTypeFieldName($typeName, $fieldName) - { - $fields = $this->getTypeFields($typeName); - - if (!\array_key_exists($fieldName, $fields)) - { - return ''; - } - - $tmp = $fields[$fieldName]; - $pos = strpos($tmp, '.'); - - if ($pos === false) - { - return $tmp; - } - - return substr($tmp, $pos + 1); - } - - /** - * Get default values for support array - * - * @return array - * - * @since 3.7.0 - */ - protected function getSupportTemplate() - { - return array( - 'state' => false, - 'acl' => false, - 'checkout' => false - ); - } - - /** - * Get default values for fields array - * - * @return array - * - * @since 3.7.0 - */ - protected function getFieldsTemplate() - { - return array( - 'id' => 'a.id', - 'title' => 'a.title', - 'alias' => 'a.alias', - 'ordering' => 'a.ordering', - 'menutype' => '', - 'level' => '', - 'catid' => 'a.catid', - 'language' => 'a.language', - 'access' => 'a.access', - 'state' => 'a.state', - 'created_user_id' => 'a.created_by', - 'checked_out' => 'a.checked_out', - 'checked_out_time' => 'a.checked_out_time' - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_??'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array(); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = false; + + /** + * Checks if the extension supports associations + * + * @return boolean Supports the extension associations + * + * @since 3.7.0 + */ + public function hasAssociationsSupport() + { + return $this->associationsSupport; + } + + /** + * Get the item types + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getItemTypes() + { + return $this->itemTypes; + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociationList($typeName, $itemId) + { + $items = array(); + + $associations = $this->getAssociations($typeName, $itemId); + + foreach ($associations as $key => $association) { + $items[$key] = ArrayHelper::fromObject($this->getItem($typeName, (int) $association->id), false); + } + + return $items; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } + + /** + * Get information about the fields the type provides + * + * @param string $typeName The item type + * + * @return array Array of support information + * + * @since 3.7.0 + */ + public function getTypeFields($typeName) + { + return $this->getTypeInformation($typeName, 'fields'); + } + + /** + * Get information about the fields the type provides + * + * @param string $typeName The item type + * + * @return array Array of support information + * + * @since 3.7.0 + */ + public function getTypeSupport($typeName) + { + return $this->getTypeInformation($typeName, 'support'); + } + + /** + * Get information about the tables the type use + * + * @param string $typeName The item type + * + * @return array Array of support information + * + * @since 3.7.0 + */ + public function getTypeTables($typeName) + { + return $this->getTypeInformation($typeName, 'tables'); + } + + /** + * Get information about the table joins for the type + * + * @param string $typeName The item type + * + * @return array Array of support information + * + * @since 3.7.0 + */ + public function getTypeJoins($typeName) + { + return $this->getTypeInformation($typeName, 'joins'); + } + + /** + * Get the type title + * + * @param string $typeName The item type + * + * @return string The type title + * + * @since 3.7.0 + */ + public function getTypeTitle($typeName) + { + $type = $this->getType($typeName); + + if (!\array_key_exists('title', $type)) { + return ''; + } + + return $type['title']; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * @param string $part part of the information + * + * @return array Array of support information + * + * @since 3.7.0 + */ + private function getTypeInformation($typeName, $part = 'support') + { + $type = $this->getType($typeName); + + if (!\array_key_exists($part, $type)) { + return array(); + } + + return $type[$part]; + } + + /** + * Get a table field name for a type + * + * @param string $typeName The item type + * @param string $fieldName The item type + * + * @return string + * + * @since 3.7.0 + */ + public function getTypeFieldName($typeName, $fieldName) + { + $fields = $this->getTypeFields($typeName); + + if (!\array_key_exists($fieldName, $fields)) { + return ''; + } + + $tmp = $fields[$fieldName]; + $pos = strpos($tmp, '.'); + + if ($pos === false) { + return $tmp; + } + + return substr($tmp, $pos + 1); + } + + /** + * Get default values for support array + * + * @return array + * + * @since 3.7.0 + */ + protected function getSupportTemplate() + { + return array( + 'state' => false, + 'acl' => false, + 'checkout' => false + ); + } + + /** + * Get default values for fields array + * + * @return array + * + * @since 3.7.0 + */ + protected function getFieldsTemplate() + { + return array( + 'id' => 'a.id', + 'title' => 'a.title', + 'alias' => 'a.alias', + 'ordering' => 'a.ordering', + 'menutype' => '', + 'level' => '', + 'catid' => 'a.catid', + 'language' => 'a.language', + 'access' => 'a.access', + 'state' => 'a.state', + 'created_user_id' => 'a.created_by', + 'checked_out' => 'a.checked_out', + 'checked_out_time' => 'a.checked_out_time' + ); + } } diff --git a/code/libraries/src/Association/AssociationExtensionInterface.php b/code/libraries/src/Association/AssociationExtensionInterface.php index 8d9d6121..ac04d065 100644 --- a/code/libraries/src/Association/AssociationExtensionInterface.php +++ b/code/libraries/src/Association/AssociationExtensionInterface.php @@ -1,4 +1,5 @@ associationExtension; - } + /** + * Returns the associations extension helper class. + * + * @return AssociationExtensionInterface + * + * @since 4.0.0 + */ + public function getAssociationsExtension(): AssociationExtensionInterface + { + return $this->associationExtension; + } - /** - * The association extension. - * - * @param AssociationExtensionInterface $associationExtension The extension - * - * @return void - * - * @since 4.0.0 - */ - public function setAssociationExtension(AssociationExtensionInterface $associationExtension) - { - $this->associationExtension = $associationExtension; - } + /** + * The association extension. + * + * @param AssociationExtensionInterface $associationExtension The extension + * + * @return void + * + * @since 4.0.0 + */ + public function setAssociationExtension(AssociationExtensionInterface $associationExtension) + { + $this->associationExtension = $associationExtension; + } } diff --git a/code/libraries/src/Authentication/Authentication.php b/code/libraries/src/Authentication/Authentication.php index 05f2b629..e7b70fee 100644 --- a/code/libraries/src/Authentication/Authentication.php +++ b/code/libraries/src/Authentication/Authentication.php @@ -1,4 +1,5 @@ get('dispatcher'); - } - - $this->setDispatcher($dispatcher); - $this->pluginType = $pluginType; - - $isLoaded = PluginHelper::importPlugin($this->pluginType); - - if (!$isLoaded) - { - Log::add(Text::_('JLIB_USER_ERROR_AUTHENTICATION_LIBRARIES'), Log::WARNING, 'jerror'); - } - } - - /** - * Returns the global authentication object, only creating it - * if it doesn't already exist. - * - * @param string $pluginType The plugin type to run authorisation and authentication on - * - * @return Authentication The global Authentication object - * - * @since 1.7.0 - */ - public static function getInstance(string $pluginType = 'authentication') - { - if (empty(self::$instance[$pluginType])) - { - self::$instance[$pluginType] = new static($pluginType); - } - - return self::$instance[$pluginType]; - } - - /** - * Finds out if a set of login credentials are valid by asking all observing - * objects to run their respective authentication routines. - * - * @param array $credentials Array holding the user credentials. - * @param array $options Array holding user options. - * - * @return AuthenticationResponse Response object with status variable filled in for last plugin or first successful plugin. - * - * @see AuthenticationResponse - * @since 1.7.0 - */ - public function authenticate($credentials, $options = array()) - { - // Get plugins - $plugins = PluginHelper::getPlugin($this->pluginType); - - // Create authentication response - $response = new AuthenticationResponse; - - /* - * Loop through the plugins and check if the credentials can be used to authenticate - * the user - * - * Any errors raised in the plugin should be returned via the AuthenticationResponse - * and handled appropriately. - */ - foreach ($plugins as $plugin) - { - $plugin = Factory::getApplication()->bootPlugin($plugin->name, $plugin->type); - - if (!method_exists($plugin, 'onUserAuthenticate')) - { - // Bail here if the plugin can't be created - Log::add(Text::sprintf('JLIB_USER_ERROR_AUTHENTICATION_FAILED_LOAD_PLUGIN', $plugin->name), Log::WARNING, 'jerror'); - continue; - } - - // Try to authenticate - $plugin->onUserAuthenticate($credentials, $options, $response); - - // If authentication is successful break out of the loop - if ($response->status === self::STATUS_SUCCESS) - { - if (empty($response->type)) - { - $response->type = $plugin->_name ?? $plugin->name; - } - - break; - } - } - - if (empty($response->username)) - { - $response->username = $credentials['username']; - } - - if (empty($response->fullname)) - { - $response->fullname = $credentials['username']; - } - - if (empty($response->password) && isset($credentials['password'])) - { - $response->password = $credentials['password']; - } - - return $response; - } - - /** - * Authorises that a particular user should be able to login - * - * @param AuthenticationResponse $response response including username of the user to authorise - * @param array $options list of options - * - * @return AuthenticationResponse[] Array of authentication response objects - * - * @since 1.7.0 - * @throws \Exception - */ - public function authorise($response, $options = array()) - { - // Get plugins in case they haven't been imported already - PluginHelper::importPlugin('user'); - $results = Factory::getApplication()->triggerEvent('onUserAuthorisation', array($response, $options)); - - return $results; - } + use DispatcherAwareTrait; + + /** + * This is the status code returned when the authentication is success (permit login) + * + * @var integer + * @since 1.7.0 + */ + public const STATUS_SUCCESS = 1; + + /** + * Status to indicate cancellation of authentication (unused) + * + * @var integer + * @since 1.7.0 + */ + public const STATUS_CANCEL = 2; + + /** + * This is the status code returned when the authentication failed (prevent login if no success) + * + * @var integer + * @since 1.7.0 + */ + public const STATUS_FAILURE = 4; + + /** + * This is the status code returned when the account has expired (prevent login) + * + * @var integer + * @since 1.7.0 + */ + public const STATUS_EXPIRED = 8; + + /** + * This is the status code returned when the account has been denied (prevent login) + * + * @var integer + * @since 1.7.0 + */ + public const STATUS_DENIED = 16; + + /** + * This is the status code returned when the account doesn't exist (not an error) + * + * @var integer + * @since 1.7.0 + */ + public const STATUS_UNKNOWN = 32; + + /** + * @var Authentication[] JAuthentication instances container. + * @since 1.7.3 + */ + protected static $instance = []; + + /** + * Plugin Type to run + * + * @var string + * @since 4.0.0 + */ + protected $pluginType; + + /** + * Constructor + * + * @param string $pluginType The plugin type to run authorisation and authentication on + * @param DispatcherInterface $dispatcher The event dispatcher we're going to use + * + * @since 1.7.0 + */ + public function __construct(string $pluginType = 'authentication', DispatcherInterface $dispatcher = null) + { + // Set the dispatcher + if (!\is_object($dispatcher)) { + $dispatcher = Factory::getContainer()->get('dispatcher'); + } + + $this->setDispatcher($dispatcher); + $this->pluginType = $pluginType; + + $isLoaded = PluginHelper::importPlugin($this->pluginType); + + if (!$isLoaded) { + Log::add(Text::_('JLIB_USER_ERROR_AUTHENTICATION_LIBRARIES'), Log::WARNING, 'jerror'); + } + } + + /** + * Returns the global authentication object, only creating it + * if it doesn't already exist. + * + * @param string $pluginType The plugin type to run authorisation and authentication on + * + * @return Authentication The global Authentication object + * + * @since 1.7.0 + */ + public static function getInstance(string $pluginType = 'authentication') + { + if (empty(self::$instance[$pluginType])) { + self::$instance[$pluginType] = new static($pluginType); + } + + return self::$instance[$pluginType]; + } + + /** + * Finds out if a set of login credentials are valid by asking all observing + * objects to run their respective authentication routines. + * + * @param array $credentials Array holding the user credentials. + * @param array $options Array holding user options. + * + * @return AuthenticationResponse Response object with status variable filled in for last plugin or first successful plugin. + * + * @see AuthenticationResponse + * @since 1.7.0 + */ + public function authenticate($credentials, $options = array()) + { + // Get plugins + $plugins = PluginHelper::getPlugin($this->pluginType); + + // Create authentication response + $response = new AuthenticationResponse(); + + /* + * Loop through the plugins and check if the credentials can be used to authenticate + * the user + * + * Any errors raised in the plugin should be returned via the AuthenticationResponse + * and handled appropriately. + */ + foreach ($plugins as $plugin) { + $plugin = Factory::getApplication()->bootPlugin($plugin->name, $plugin->type); + + if (!method_exists($plugin, 'onUserAuthenticate')) { + // Bail here if the plugin can't be created + Log::add(Text::sprintf('JLIB_USER_ERROR_AUTHENTICATION_FAILED_LOAD_PLUGIN', $plugin->name), Log::WARNING, 'jerror'); + continue; + } + + // Try to authenticate + $plugin->onUserAuthenticate($credentials, $options, $response); + + // If authentication is successful break out of the loop + if ($response->status === self::STATUS_SUCCESS) { + if (empty($response->type)) { + $response->type = $plugin->_name ?? $plugin->name; + } + + break; + } + } + + if (empty($response->username)) { + $response->username = $credentials['username']; + } + + if (empty($response->fullname)) { + $response->fullname = $credentials['username']; + } + + if (empty($response->password) && isset($credentials['password'])) { + $response->password = $credentials['password']; + } + + return $response; + } + + /** + * Authorises that a particular user should be able to login + * + * @param AuthenticationResponse $response response including username of the user to authorise + * @param array $options list of options + * + * @return AuthenticationResponse[] Array of authentication response objects + * + * @since 1.7.0 + * @throws \Exception + */ + public function authorise($response, $options = array()) + { + // Get plugins in case they haven't been imported already + PluginHelper::importPlugin('user'); + $results = Factory::getApplication()->triggerEvent('onUserAuthorisation', array($response, $options)); + + return $results; + } } diff --git a/code/libraries/src/Authentication/AuthenticationResponse.php b/code/libraries/src/Authentication/AuthenticationResponse.php index 8d4b7dba..e1f5a309 100644 --- a/code/libraries/src/Authentication/AuthenticationResponse.php +++ b/code/libraries/src/Authentication/AuthenticationResponse.php @@ -1,4 +1,5 @@ handlers[] = $handler; - } + /** + * Add a handler to the chain + * + * @param HandlerInterface $handler The password handler to add + * + * @return void + * + * @since 4.0.0 + */ + public function addHandler(HandlerInterface $handler) + { + $this->handlers[] = $handler; + } - /** - * Check if the password requires rehashing - * - * @param string $hash The password hash to check - * - * @return boolean - * - * @since 4.0.0 - */ - public function checkIfRehashNeeded(string $hash): bool - { - foreach ($this->handlers as $handler) - { - if ($handler instanceof CheckIfRehashNeededHandlerInterface && $handler->isSupported() && $handler->checkIfRehashNeeded($hash)) - { - return true; - } - } + /** + * Check if the password requires rehashing + * + * @param string $hash The password hash to check + * + * @return boolean + * + * @since 4.0.0 + */ + public function checkIfRehashNeeded(string $hash): bool + { + foreach ($this->handlers as $handler) { + if ($handler instanceof CheckIfRehashNeededHandlerInterface && $handler->isSupported() && $handler->checkIfRehashNeeded($hash)) { + return true; + } + } - return false; - } + return false; + } - /** - * Generate a hash for a plaintext password - * - * @param string $plaintext The plaintext password to validate - * @param array $options Options for the hashing operation - * - * @return void - * - * @since 4.0.0 - * @throws \RuntimeException - */ - public function hashPassword($plaintext, array $options = []) - { - throw new \RuntimeException('The chained password handler cannot be used to hash a password'); - } + /** + * Generate a hash for a plaintext password + * + * @param string $plaintext The plaintext password to validate + * @param array $options Options for the hashing operation + * + * @return void + * + * @since 4.0.0 + * @throws \RuntimeException + */ + public function hashPassword($plaintext, array $options = []) + { + throw new \RuntimeException('The chained password handler cannot be used to hash a password'); + } - /** - * Check that the password handler is supported in this environment - * - * @return boolean - * - * @since 4.0.0 - */ - public static function isSupported() - { - return true; - } + /** + * Check that the password handler is supported in this environment + * + * @return boolean + * + * @since 4.0.0 + */ + public static function isSupported() + { + return true; + } - /** - * Validate a password - * - * @param string $plaintext The plain text password to validate - * @param string $hashed The password hash to validate against - * - * @return boolean - * - * @since 4.0.0 - */ - public function validatePassword($plaintext, $hashed) - { - foreach ($this->handlers as $handler) - { - if ($handler->isSupported() && $handler->validatePassword($plaintext, $hashed)) - { - return true; - } - } + /** + * Validate a password + * + * @param string $plaintext The plain text password to validate + * @param string $hashed The password hash to validate against + * + * @return boolean + * + * @since 4.0.0 + */ + public function validatePassword($plaintext, $hashed) + { + foreach ($this->handlers as $handler) { + if ($handler->isSupported() && $handler->validatePassword($plaintext, $hashed)) { + return true; + } + } - return false; - } + return false; + } } diff --git a/code/libraries/src/Authentication/Password/CheckIfRehashNeededHandlerInterface.php b/code/libraries/src/Authentication/Password/CheckIfRehashNeededHandlerInterface.php index 9974a0b0..bf4d9bab 100644 --- a/code/libraries/src/Authentication/Password/CheckIfRehashNeededHandlerInterface.php +++ b/code/libraries/src/Authentication/Password/CheckIfRehashNeededHandlerInterface.php @@ -1,4 +1,5 @@ getPasswordHash()->HashPassword($plaintext); - } + /** + * Generate a hash for a plaintext password + * + * @param string $plaintext The plaintext password to validate + * @param array $options Options for the hashing operation + * + * @return string + * + * @since 4.0.0 + */ + public function hashPassword($plaintext, array $options = []) + { + return $this->getPasswordHash()->HashPassword($plaintext); + } - /** - * Check that the password handler is supported in this environment - * - * @return boolean - * - * @since 4.0.0 - */ - public static function isSupported() - { - return class_exists(\PasswordHash::class); - } + /** + * Check that the password handler is supported in this environment + * + * @return boolean + * + * @since 4.0.0 + */ + public static function isSupported() + { + return class_exists(\PasswordHash::class); + } - /** - * Validate a password - * - * @param string $plaintext The plain text password to validate - * @param string $hashed The password hash to validate against - * - * @return boolean - * - * @since 4.0.0 - */ - public function validatePassword($plaintext, $hashed) - { - return $this->getPasswordHash()->CheckPassword($plaintext, $hashed); - } + /** + * Validate a password + * + * @param string $plaintext The plain text password to validate + * @param string $hashed The password hash to validate against + * + * @return boolean + * + * @since 4.0.0 + */ + public function validatePassword($plaintext, $hashed) + { + return $this->getPasswordHash()->CheckPassword($plaintext, $hashed); + } - /** - * Get an instance of the PasswordHash class - * - * @return \PasswordHash - * - * @since 4.0.0 - */ - private function getPasswordHash(): \PasswordHash - { - return new \PasswordHash(10, true); - } + /** + * Get an instance of the PasswordHash class + * + * @return \PasswordHash + * + * @since 4.0.0 + */ + private function getPasswordHash(): \PasswordHash + { + return new \PasswordHash(10, true); + } } diff --git a/code/libraries/src/Authentication/ProviderAwareAuthenticationPluginInterface.php b/code/libraries/src/Authentication/ProviderAwareAuthenticationPluginInterface.php index 759eaa46..40f75e33 100644 --- a/code/libraries/src/Authentication/ProviderAwareAuthenticationPluginInterface.php +++ b/code/libraries/src/Authentication/ProviderAwareAuthenticationPluginInterface.php @@ -1,4 +1,5 @@ loader = $loader; - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * - * @return boolean|null True if loaded, null otherwise - * - * @since 3.4 - */ - public function loadClass($class) - { - if ($result = $this->loader->loadClass($class)) - { - \JLoader::applyAliasFor($class); - } - - return $result; - } + /** + * The Composer class loader + * + * @var ComposerClassLoader + * @since 3.4 + */ + private $loader; + + /** + * Constructor + * + * @param ComposerClassLoader $loader Composer autoloader + * + * @since 3.4 + */ + public function __construct(ComposerClassLoader $loader) + { + $this->loader = $loader; + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * + * @return boolean|null True if loaded, null otherwise + * + * @since 3.4 + */ + public function loadClass($class) + { + if ($result = $this->loader->loadClass($class)) { + \JLoader::applyAliasFor($class); + } + + return $result; + } } diff --git a/code/libraries/src/Button/ActionButton.php b/code/libraries/src/Button/ActionButton.php index d4da31ef..58714361 100644 --- a/code/libraries/src/Button/ActionButton.php +++ b/code/libraries/src/Button/ActionButton.php @@ -1,4 +1,5 @@ null, - 'task' => '', - 'icon' => 'question', - 'title' => 'Unknown state', - 'options' => [ - 'disabled' => false, - 'only_icon' => false, - 'tip' => true, - 'tip_title' => '', - 'task_prefix' => '', - 'checkbox_name' => 'cb', - ], - ]; - - /** - * Options of this button set. - * - * @var Registry - * - * @since 4.0.0 - */ - protected $options; - - /** - * The layout path to render. - * - * @var string - * - * @since 4.0.0 - */ - protected $layout = 'joomla.button.action-button'; - - /** - * ActionButton constructor. - * - * @param array $options The options for all buttons in this group. - * - * @since 4.0.0 - */ - public function __construct(array $options = []) - { - $this->options = new Registry($options); - - // Replace some dynamic values - $this->unknownState['title'] = Text::_('JLIB_HTML_UNKNOWN_STATE'); - - $this->preprocess(); - } - - /** - * Configure this object. - * - * @return void - * - * @since 4.0.0 - */ - protected function preprocess() - { - // Implement this method. - } - - /** - * Add a state profile. - * - * @param integer $value The value of this state. - * @param string $task The task you want to execute after click this button. - * @param string $icon The icon to display for user. - * @param string $title Title text will show if we enable tooltips. - * @param array $options The button options, will override group options. - * - * @return static Return self to support chaining. - * - * @since 4.0.0 - */ - public function addState(int $value, string $task, string $icon = 'ok', string $title = '', array $options = []): self - { - // Force type to prevent null data - $this->states[$value] = [ - 'value' => $value, - 'task' => $task, - 'icon' => $icon, - 'title' => $title, - 'options' => $options - ]; - - return $this; - } - - /** - * Get state profile by value name. - * - * @param integer $value The value name we want to get. - * - * @return array|null Return state profile or NULL. - * - * @since 4.0.0 - */ - public function getState(int $value): ?array - { - return $this->states[$value] ?? null; - } - - /** - * Remove a state by value name. - * - * @param integer $value Remove state by this value. - * - * @return static Return to support chaining. - * - * @since 4.0.0 - */ - public function removeState(int $value): self - { - if (isset($this->states[$value])) - { - unset($this->states[$value]); - } - - return $this; - } - - /** - * Render action button by item value. - * - * @param integer|null $value Current value of this item. - * @param integer|null $row The row number of this item. - * @param array $options The options to override group options. - * - * @return string Rendered HTML. - * - * @since 4.0.0 - * - * @throws \InvalidArgumentException - */ - public function render(?int $value = null, ?int $row = null, array $options = []): string - { - $data = $this->getState($value) ?? $this->unknownState; - - $data = ArrayHelper::mergeRecursive( - $this->unknownState, - $data, - [ - 'options' => $this->options->toArray() - ], - [ - 'options' => $options - ] - ); - - $data['row'] = $row; - $data['icon'] = $this->fetchIconClass($data['icon']); - - return LayoutHelper::render($this->layout, $data); - } - - /** - * Render to string. - * - * @return string - * - * @since 4.0.0 - */ - public function __toString(): string - { - try - { - return $this->render(); - } - catch (\Throwable $e) - { - return (string) $e; - } - } - - /** - * Method to get property layout. - * - * @return string - * - * @since 4.0.0 - */ - public function getLayout(): string - { - return $this->layout; - } - - /** - * Method to set property template. - * - * @param string $layout The layout path. - * - * @return static Return self to support chaining. - * - * @since 4.0.0 - */ - public function setLayout(string $layout): self - { - $this->layout = $layout; - - return $this; - } - - /** - * Method to get property options. - * - * @return array - * - * @since 4.0.0 - */ - public function getOptions(): array - { - return (array) $this->options->toArray(); - } - - /** - * Method to set property options. - * - * @param array $options The options of this button group. - * - * @return static Return self to support chaining. - * - * @since 4.0.0 - */ - public function setOptions(array $options): self - { - $this->options = new Registry($options); - - return $this; - } - - /** - * Get an option value. - * - * @param string $name The option name. - * @param mixed $default Default value if not exists. - * - * @return mixed Return option value or default value. - * - * @since 4.0.0 - */ - public function getOption(string $name, $default = null) - { - return $this->options->get($name, $default); - } - - /** - * Set option value. - * - * @param string $name The option name. - * @param mixed $value The option value. - * - * @return static Return self to support chaining. - * - * @since 4.0.0 - */ - public function setOption(string $name, $value): self - { - $this->options->set($name, $value); - - return $this; - } - - /** - * Method to get the CSS class name for an icon identifier. - * - * Can be redefined in the final class. - * - * @param string $identifier Icon identification string. - * - * @return string CSS class name. - * - * @since 4.0.0 - */ - public function fetchIconClass(string $identifier): string - { - // It's an ugly hack, but this allows templates to define the icon classes for the toolbar - $layout = new FileLayout('joomla.button.iconclass'); - - return $layout->render(array('icon' => $identifier)); - } + /** + * The button states profiles. + * + * @var array + * + * @since 4.0.0 + */ + protected $states = []; + + /** + * Default options for unknown state. + * + * @var array + * + * @since 4.0.0 + */ + protected $unknownState = [ + 'value' => null, + 'task' => '', + 'icon' => 'question', + 'title' => 'Unknown state', + 'options' => [ + 'disabled' => false, + 'only_icon' => false, + 'tip' => true, + 'tip_title' => '', + 'task_prefix' => '', + 'checkbox_name' => 'cb', + ], + ]; + + /** + * Options of this button set. + * + * @var Registry + * + * @since 4.0.0 + */ + protected $options; + + /** + * The layout path to render. + * + * @var string + * + * @since 4.0.0 + */ + protected $layout = 'joomla.button.action-button'; + + /** + * ActionButton constructor. + * + * @param array $options The options for all buttons in this group. + * + * @since 4.0.0 + */ + public function __construct(array $options = []) + { + $this->options = new Registry($options); + + // Replace some dynamic values + $this->unknownState['title'] = Text::_('JLIB_HTML_UNKNOWN_STATE'); + + $this->preprocess(); + } + + /** + * Configure this object. + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocess() + { + // Implement this method. + } + + /** + * Add a state profile. + * + * @param integer $value The value of this state. + * @param string $task The task you want to execute after click this button. + * @param string $icon The icon to display for user. + * @param string $title Title text will show if we enable tooltips. + * @param array $options The button options, will override group options. + * + * @return static Return self to support chaining. + * + * @since 4.0.0 + */ + public function addState(int $value, string $task, string $icon = 'ok', string $title = '', array $options = []): self + { + // Force type to prevent null data + $this->states[$value] = [ + 'value' => $value, + 'task' => $task, + 'icon' => $icon, + 'title' => $title, + 'options' => $options + ]; + + return $this; + } + + /** + * Get state profile by value name. + * + * @param integer $value The value name we want to get. + * + * @return array|null Return state profile or NULL. + * + * @since 4.0.0 + */ + public function getState(int $value): ?array + { + return $this->states[$value] ?? null; + } + + /** + * Remove a state by value name. + * + * @param integer $value Remove state by this value. + * + * @return static Return to support chaining. + * + * @since 4.0.0 + */ + public function removeState(int $value): self + { + if (isset($this->states[$value])) { + unset($this->states[$value]); + } + + return $this; + } + + /** + * Render action button by item value. + * + * @param integer|null $value Current value of this item. + * @param integer|null $row The row number of this item. + * @param array $options The options to override group options. + * + * @return string Rendered HTML. + * + * @since 4.0.0 + * + * @throws \InvalidArgumentException + */ + public function render(?int $value = null, ?int $row = null, array $options = []): string + { + $data = $this->getState($value) ?? $this->unknownState; + + $data = ArrayHelper::mergeRecursive( + $this->unknownState, + $data, + [ + 'options' => $this->options->toArray() + ], + [ + 'options' => $options + ] + ); + + $data['row'] = $row; + $data['icon'] = $this->fetchIconClass($data['icon']); + + return LayoutHelper::render($this->layout, $data); + } + + /** + * Render to string. + * + * @return string + * + * @since 4.0.0 + */ + public function __toString(): string + { + try { + return $this->render(); + } catch (\Throwable $e) { + return (string) $e; + } + } + + /** + * Method to get property layout. + * + * @return string + * + * @since 4.0.0 + */ + public function getLayout(): string + { + return $this->layout; + } + + /** + * Method to set property template. + * + * @param string $layout The layout path. + * + * @return static Return self to support chaining. + * + * @since 4.0.0 + */ + public function setLayout(string $layout): self + { + $this->layout = $layout; + + return $this; + } + + /** + * Method to get property options. + * + * @return array + * + * @since 4.0.0 + */ + public function getOptions(): array + { + return (array) $this->options->toArray(); + } + + /** + * Method to set property options. + * + * @param array $options The options of this button group. + * + * @return static Return self to support chaining. + * + * @since 4.0.0 + */ + public function setOptions(array $options): self + { + $this->options = new Registry($options); + + return $this; + } + + /** + * Get an option value. + * + * @param string $name The option name. + * @param mixed $default Default value if not exists. + * + * @return mixed Return option value or default value. + * + * @since 4.0.0 + */ + public function getOption(string $name, $default = null) + { + return $this->options->get($name, $default); + } + + /** + * Set option value. + * + * @param string $name The option name. + * @param mixed $value The option value. + * + * @return static Return self to support chaining. + * + * @since 4.0.0 + */ + public function setOption(string $name, $value): self + { + $this->options->set($name, $value); + + return $this; + } + + /** + * Method to get the CSS class name for an icon identifier. + * + * Can be redefined in the final class. + * + * @param string $identifier Icon identification string. + * + * @return string CSS class name. + * + * @since 4.0.0 + */ + public function fetchIconClass(string $identifier): string + { + // It's an ugly hack, but this allows templates to define the icon classes for the toolbar + $layout = new FileLayout('joomla.button.iconclass'); + + return $layout->render(array('icon' => $identifier)); + } } diff --git a/code/libraries/src/Button/FeaturedButton.php b/code/libraries/src/Button/FeaturedButton.php index 818f2b05..851da7ba 100644 --- a/code/libraries/src/Button/FeaturedButton.php +++ b/code/libraries/src/Button/FeaturedButton.php @@ -1,4 +1,5 @@ addState(0, 'featured', 'icon-unfeatured', - Text::_('JGLOBAL_TOGGLE_FEATURED'), ['tip_title' => Text::_('JUNFEATURED')] - ); - $this->addState(1, 'unfeatured', 'icon-color-featured icon-star', - Text::_('JGLOBAL_TOGGLE_FEATURED'), ['tip_title' => Text::_('JFEATURED')] - ); - } - - /** - * Render action button by item value. - * - * @param integer|null $value Current value of this item. - * @param integer|null $row The row number of this item. - * @param array $options The options to override group options. - * @param string|Date $featuredUp The date which item featured up. - * @param string|Date $featuredDown The date which item featured down. - * - * @return string Rendered HTML. - * - * @since 4.0.0 - */ - public function render(?int $value = null, ?int $row = null, array $options = [], $featuredUp = null, $featuredDown = null): string - { - if ($featuredUp || $featuredDown) - { - $bakState = $this->getState($value); - $default = $this->getState($value) ?? $this->unknownState; - - $nowDate = Factory::getDate()->toUnix(); - - $tz = Factory::getUser()->getTimezone(); - - if (!is_null($featuredUp)) - { - $featuredUp = Factory::getDate($featuredUp, 'UTC')->setTimezone($tz); - } - - if (!is_null($featuredDown)) - { - $featuredDown = Factory::getDate($featuredDown, 'UTC')->setTimezone($tz); - } - - // Add tips and special titles - // Create special titles for featured items - if ($value === 1) - { - // Create tip text, only we have featured up or down settings - $tips = []; - - if ($featuredUp) - { - $tips[] = Text::sprintf('JLIB_HTML_FEATURED_STARTED', HTMLHelper::_('date', $featuredUp, Text::_('DATE_FORMAT_LC5'), 'UTC')); - } - - if ($featuredDown) - { - $tips[] = Text::sprintf('JLIB_HTML_FEATURED_FINISHED', HTMLHelper::_('date', $featuredDown, Text::_('DATE_FORMAT_LC5'), 'UTC')); - } - - $tip = empty($tips) ? false : implode('
    ', $tips); - - $default['title'] = $tip; - - $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_ITEM'); - - if ($featuredUp && $nowDate < $featuredUp->toUnix()) - { - $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_PENDING_ITEM'); - $default['icon'] = 'pending'; - } - - if ($featuredDown && $nowDate > $featuredDown->toUnix()) - { - $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_EXPIRED_ITEM'); - $default['icon'] = 'expired'; - } - } - - $this->states[$value] = $default; - - $html = parent::render($value, $row, $options); - - $this->states[$value] = $bakState; - - return $html; - } - - return parent::render($value, $row, $options); - } + /** + * Configure this object. + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocess() + { + $this->addState( + 0, + 'featured', + 'icon-unfeatured', + Text::_('JGLOBAL_TOGGLE_FEATURED'), + ['tip_title' => Text::_('JUNFEATURED')] + ); + $this->addState( + 1, + 'unfeatured', + 'icon-color-featured icon-star', + Text::_('JGLOBAL_TOGGLE_FEATURED'), + ['tip_title' => Text::_('JFEATURED')] + ); + } + + /** + * Render action button by item value. + * + * @param integer|null $value Current value of this item. + * @param integer|null $row The row number of this item. + * @param array $options The options to override group options. + * @param string|Date $featuredUp The date which item featured up. + * @param string|Date $featuredDown The date which item featured down. + * + * @return string Rendered HTML. + * + * @since 4.0.0 + */ + public function render(?int $value = null, ?int $row = null, array $options = [], $featuredUp = null, $featuredDown = null): string + { + if ($featuredUp || $featuredDown) { + $bakState = $this->getState($value); + $default = $this->getState($value) ?? $this->unknownState; + + $nowDate = Factory::getDate()->toUnix(); + + $tz = Factory::getUser()->getTimezone(); + + if (!is_null($featuredUp)) { + $featuredUp = Factory::getDate($featuredUp, 'UTC')->setTimezone($tz); + } + + if (!is_null($featuredDown)) { + $featuredDown = Factory::getDate($featuredDown, 'UTC')->setTimezone($tz); + } + + // Add tips and special titles + // Create special titles for featured items + if ($value === 1) { + // Create tip text, only we have featured up or down settings + $tips = []; + + if ($featuredUp) { + $tips[] = Text::sprintf('JLIB_HTML_FEATURED_STARTED', HTMLHelper::_('date', $featuredUp, Text::_('DATE_FORMAT_LC5'), 'UTC')); + } + + if ($featuredDown) { + $tips[] = Text::sprintf('JLIB_HTML_FEATURED_FINISHED', HTMLHelper::_('date', $featuredDown, Text::_('DATE_FORMAT_LC5'), 'UTC')); + } + + $tip = empty($tips) ? false : implode('
    ', $tips); + + $default['title'] = $tip; + + $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_ITEM'); + + if ($featuredUp && $nowDate < $featuredUp->toUnix()) { + $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_PENDING_ITEM'); + $default['icon'] = 'pending'; + } + + if ($featuredDown && $nowDate > $featuredDown->toUnix()) { + $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_EXPIRED_ITEM'); + $default['icon'] = 'expired'; + } + } + + $this->states[$value] = $default; + + $html = parent::render($value, $row, $options); + + $this->states[$value] = $bakState; + + return $html; + } + + return parent::render($value, $row, $options); + } } diff --git a/code/libraries/src/Button/PublishedButton.php b/code/libraries/src/Button/PublishedButton.php index 00f0e1b3..a1f7b3f0 100644 --- a/code/libraries/src/Button/PublishedButton.php +++ b/code/libraries/src/Button/PublishedButton.php @@ -1,4 +1,5 @@ addState(1, 'unpublish', 'publish', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JPUBLISHED')]); - $this->addState(0, 'publish', 'unpublish', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JUNPUBLISHED')]); - $this->addState(2, 'unpublish', 'archive', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JARCHIVED')]); - $this->addState(-2, 'publish', 'trash', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JTRASHED')]); - } - - /** - * Render action button by item value. - * - * @param integer|null $value Current value of this item. - * @param integer|null $row The row number of this item. - * @param array $options The options to override group options. - * @param string|Date $publishUp The date which item publish up. - * @param string|Date $publishDown The date which item publish down. - * - * @return string Rendered HTML. - * - * @since 4.0.0 - */ - public function render(?int $value = null, ?int $row = null, array $options = [], $publishUp = null, $publishDown = null): string - { - if ($publishUp || $publishDown) - { - $bakState = $this->getState($value); - $default = $this->getState($value) ?? $this->unknownState; - - $nullDate = Factory::getDbo()->getNullDate(); - $nowDate = Factory::getDate()->toUnix(); - - $tz = Factory::getUser()->getTimezone(); - - $publishUp = ($publishUp !== null && $publishUp !== $nullDate) ? Factory::getDate($publishUp, 'UTC')->setTimezone($tz) : false; - $publishDown = ($publishDown !== null && $publishDown !== $nullDate) ? Factory::getDate($publishDown, 'UTC')->setTimezone($tz) : false; - - // Add tips and special titles - // Create special titles for published items - if ($value === 1) - { - // Create tip text, only we have publish up or down settings - $tips = array(); - - if ($publishUp) - { - $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_START', HTMLHelper::_('date', $publishUp, Text::_('DATE_FORMAT_LC5'), 'UTC')); - $tips[] = Text::_('JLIB_HTML_PUBLISHED_UNPUBLISH'); - } - - if ($publishDown) - { - $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_FINISHED', HTMLHelper::_('date', $publishDown, Text::_('DATE_FORMAT_LC5'), 'UTC')); - } - - $tip = empty($tips) ? false : implode('
    ', $tips); - - $default['title'] = $tip; - - $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_ITEM'); - - if ($publishUp && $nowDate < $publishUp->toUnix()) - { - $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_PENDING_ITEM'); - $default['icon'] = 'pending'; - } - - if ($publishDown && $nowDate > $publishDown->toUnix()) - { - $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_EXPIRED_ITEM'); - $default['icon'] = 'expired'; - } - } - - $this->states[$value] = $default; - - $html = parent::render($value, $row, $options); - - $this->states[$value] = $bakState; - - return $html; - } - - return parent::render($value, $row, $options); - } + /** + * Configure this object. + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocess() + { + $this->addState(1, 'unpublish', 'publish', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JPUBLISHED')]); + $this->addState(0, 'publish', 'unpublish', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JUNPUBLISHED')]); + $this->addState(2, 'unpublish', 'archive', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JARCHIVED')]); + $this->addState(-2, 'publish', 'trash', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JTRASHED')]); + } + + /** + * Render action button by item value. + * + * @param integer|null $value Current value of this item. + * @param integer|null $row The row number of this item. + * @param array $options The options to override group options. + * @param string|Date $publishUp The date which item publish up. + * @param string|Date $publishDown The date which item publish down. + * + * @return string Rendered HTML. + * + * @since 4.0.0 + */ + public function render(?int $value = null, ?int $row = null, array $options = [], $publishUp = null, $publishDown = null): string + { + if ($publishUp || $publishDown) { + $bakState = $this->getState($value); + $default = $this->getState($value) ?? $this->unknownState; + + $nullDate = Factory::getDbo()->getNullDate(); + $nowDate = Factory::getDate()->toUnix(); + + $tz = Factory::getUser()->getTimezone(); + + $publishUp = ($publishUp !== null && $publishUp !== $nullDate) ? Factory::getDate($publishUp, 'UTC')->setTimezone($tz) : false; + $publishDown = ($publishDown !== null && $publishDown !== $nullDate) ? Factory::getDate($publishDown, 'UTC')->setTimezone($tz) : false; + + // Add tips and special titles + // Create special titles for published items + if ($value === 1) { + // Create tip text, only we have publish up or down settings + $tips = array(); + + if ($publishUp) { + $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_START', HTMLHelper::_('date', $publishUp, Text::_('DATE_FORMAT_LC5'), 'UTC')); + $tips[] = Text::_('JLIB_HTML_PUBLISHED_UNPUBLISH'); + } + + if ($publishDown) { + $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_FINISHED', HTMLHelper::_('date', $publishDown, Text::_('DATE_FORMAT_LC5'), 'UTC')); + } + + $tip = empty($tips) ? false : implode('
    ', $tips); + + $default['title'] = $tip; + + $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_ITEM'); + + if ($publishUp && $nowDate < $publishUp->toUnix()) { + $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_PENDING_ITEM'); + $default['icon'] = 'pending'; + } + + if ($publishDown && $nowDate > $publishDown->toUnix()) { + $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_EXPIRED_ITEM'); + $default['icon'] = 'expired'; + } + + if (array_key_exists('category_published', $options)) { + $categoryPublished = $options['category_published']; + + if ($categoryPublished === 0) { + $options['tip_title'] = Text::_('JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_UNPUBLISHED'); + $default['icon'] = 'expired'; + } + + if ($categoryPublished === -2) { + $options['tip_title'] = Text::_('JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_TRASHED'); + $default['icon'] = 'expired'; + } + } + } + + $this->states[$value] = $default; + + $html = parent::render($value, $row, $options); + + $this->states[$value] = $bakState; + + return $html; + } + + return parent::render($value, $row, $options); + } } diff --git a/code/libraries/src/Button/TransitionButton.php b/code/libraries/src/Button/TransitionButton.php index 01dbbc3f..b4bec69d 100644 --- a/code/libraries/src/Button/TransitionButton.php +++ b/code/libraries/src/Button/TransitionButton.php @@ -1,4 +1,5 @@ unknownState['icon'] = 'shuffle'; - $this->unknownState['title'] = $options['title'] ?? Text::_('JLIB_HTML_UNKNOWN_STATE'); - $this->unknownState['tip_content'] = $options['tip_content'] ?? $this->unknownState['title']; - } + $this->unknownState['icon'] = 'shuffle'; + $this->unknownState['title'] = $options['title'] ?? Text::_('JLIB_HTML_UNKNOWN_STATE'); + $this->unknownState['tip_content'] = $options['tip_content'] ?? $this->unknownState['title']; + } - /** - * Render action button by item value. - * - * @param integer|null $value Current value of this item. - * @param integer|null $row The row number of this item. - * @param array $options The options to override group options. - * - * @return string Rendered HTML. - * - * @since 4.0.0 - */ - public function render(?int $value = null, ?int $row = null, array $options = []): string - { - $default = $this->unknownState; + /** + * Render action button by item value. + * + * @param integer|null $value Current value of this item. + * @param integer|null $row The row number of this item. + * @param array $options The options to override group options. + * + * @return string Rendered HTML. + * + * @since 4.0.0 + */ + public function render(?int $value = null, ?int $row = null, array $options = []): string + { + $default = $this->unknownState; - $options['tip_title'] = $options['tip_title'] ?? ($options['title'] ?? $default['title']); + $options['tip_title'] = $options['tip_title'] ?? ($options['title'] ?? $default['title']); - return parent::render($value, $row, $options); - } + return parent::render($value, $row, $options); + } } diff --git a/code/libraries/src/Cache/CacheControllerFactoryAwareInterface.php b/code/libraries/src/Cache/CacheControllerFactoryAwareInterface.php new file mode 100644 index 00000000..e3dd21ce --- /dev/null +++ b/code/libraries/src/Cache/CacheControllerFactoryAwareInterface.php @@ -0,0 +1,30 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Cache; + +\defined('_JEXEC') or die; + +/** + * Interface to be implemented by classes depending on a cache controller factory. + * + * @since 4.2.0 + */ +interface CacheControllerFactoryAwareInterface +{ + /** + * Set the cache controller factory to use. + * + * @param CacheControllerFactoryInterface $factory The cache controller factory to use. + * + * @return void + * + * @since 4.2.0 + */ + public function setCacheControllerFactory(CacheControllerFactoryInterface $factory): void; +} diff --git a/code/libraries/src/Cache/CacheControllerFactoryAwareTrait.php b/code/libraries/src/Cache/CacheControllerFactoryAwareTrait.php new file mode 100644 index 00000000..d40e946f --- /dev/null +++ b/code/libraries/src/Cache/CacheControllerFactoryAwareTrait.php @@ -0,0 +1,66 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Cache; + +\defined('_JEXEC') or die; + +use Joomla\CMS\Factory; + +/** + * Defines the trait for a CacheControllerFactoryInterface Aware Class. + * + * @since 4.2.0 + */ +trait CacheControllerFactoryAwareTrait +{ + /** + * CacheControllerFactoryInterface + * + * @var CacheControllerFactoryInterface + * + * @since 4.2.0 + */ + private $cacheControllerFactory; + + /** + * Get the CacheControllerFactoryInterface. + * + * @return CacheControllerFactoryInterface + * + * @since 4.2.0 + */ + protected function getCacheControllerFactory(): CacheControllerFactoryInterface + { + if ($this->cacheControllerFactory) + { + return $this->cacheControllerFactory; + } + + @trigger_error( + sprintf('A cache controller is needed in %s. An UnexpectedValueException will be thrown in 5.0.', __CLASS__), + E_USER_DEPRECATED + ); + + return Factory::getContainer()->get(CacheControllerFactoryInterface::class); + } + + /** + * Set the cache controller factory to use. + * + * @param CacheControllerFactoryInterface $cacheControllerFactory The cache controller factory to use. + * + * @return void + * + * @since 4.2.0 + */ + public function setCacheControllerFactory(CacheControllerFactoryInterface $cacheControllerFactory = null): void + { + $this->cacheControllerFactory = $cacheControllerFactory; + } +} diff --git a/code/libraries/src/Cache/Storage/WincacheStorage.php b/code/libraries/src/Cache/Storage/WincacheStorage.php index 9af3bf33..928ab4cf 100644 --- a/code/libraries/src/Cache/Storage/WincacheStorage.php +++ b/code/libraries/src/Cache/Storage/WincacheStorage.php @@ -15,8 +15,9 @@ /** * WinCache cache storage handler * - * @link https://www.php.net/manual/en/book.wincache.php - * @since 1.7.0 + * @link https://www.php.net/manual/en/book.wincache.php + * @since 1.7.0 + * @deprecated 5.0 WinCache is abandoned and not supported from PHP 8 onwards */ class WincacheStorage extends CacheStorage { @@ -28,7 +29,8 @@ class WincacheStorage extends CacheStorage * * @return boolean * - * @since 3.7.0 + * @since 3.7.0 + * @deprecated 5.0 */ public function contains($id, $group) { @@ -44,7 +46,8 @@ public function contains($id, $group) * * @return mixed Boolean false on failure or a cached data object * - * @since 1.7.0 + * @since 1.7.0 + * @deprecated 5.0 */ public function get($id, $group, $checkTime = true) { @@ -56,7 +59,8 @@ public function get($id, $group, $checkTime = true) * * @return mixed Boolean false on failure or a cached data object * - * @since 1.7.0 + * @since 1.7.0 + * @deprecated 5.0 */ public function getAll() { @@ -109,7 +113,8 @@ public function getAll() * * @return boolean * - * @since 1.7.0 + * @since 1.7.0 + * @deprecated 5.0 */ public function store($id, $group, $data) { @@ -124,7 +129,8 @@ public function store($id, $group, $data) * * @return boolean * - * @since 1.7.0 + * @since 1.7.0 + * @deprecated 5.0 */ public function remove($id, $group) { @@ -142,7 +148,8 @@ public function remove($id, $group) * * @return boolean * - * @since 1.7.0 + * @since 1.7.0 + * @deprecated 5.0 */ public function clean($group, $mode = null) { @@ -166,7 +173,8 @@ public function clean($group, $mode = null) * * @return boolean * - * @since 1.7.0 + * @since 1.7.0 + * @deprecated 5.0 */ public function gc() { @@ -190,7 +198,8 @@ public function gc() * * @return boolean * - * @since 3.0.0 + * @since 3.0.0 + * @deprecated 5.0 */ public static function isSupported() { diff --git a/code/libraries/src/Captcha/Captcha.php b/code/libraries/src/Captcha/Captcha.php index 651afceb..c55dcbd7 100644 --- a/code/libraries/src/Captcha/Captcha.php +++ b/code/libraries/src/Captcha/Captcha.php @@ -1,4 +1,5 @@ name = $captcha; - - if (!empty($options['dispatcher']) && $options['dispatcher'] instanceof DispatcherInterface) - { - $this->setDispatcher($options['dispatcher']); - } - else - { - $this->setDispatcher(Factory::getApplication()->getDispatcher()); - } - - $this->_load($options); - } - - /** - * Returns the global Captcha object, only creating it - * if it doesn't already exist. - * - * @param string $captcha The plugin to use. - * @param array $options Associative array of options. - * - * @return Captcha|null Instance of this class. - * - * @since 2.5 - * @throws \RuntimeException - */ - public static function getInstance($captcha, array $options = array()) - { - $signature = md5(serialize(array($captcha, $options))); - - if (empty(self::$instances[$signature])) - { - self::$instances[$signature] = new Captcha($captcha, $options); - } - - return self::$instances[$signature]; - } - - /** - * Fire the onInit event to initialise the captcha plugin. - * - * @param string $id The id of the field. - * - * @return boolean True on success - * - * @since 2.5 - * @throws \RuntimeException - */ - public function initialise($id) - { - $arg = ['id' => $id]; - - $this->update('onInit', $arg); - - return true; - } - - /** - * Get the HTML for the captcha. - * - * @param string $name The control name. - * @param string $id The id for the control. - * @param string $class Value for the HTML class attribute - * - * @return string The return value of the function "onDisplay" of the selected Plugin. - * - * @since 2.5 - * @throws \RuntimeException - */ - public function display($name, $id, $class = '') - { - // Check if captcha is already loaded. - if ($this->captcha === null) - { - return ''; - } - - // Initialise the Captcha. - if (!$this->initialise($id)) - { - return ''; - } - - $arg = [ - 'name' => $name, - 'id' => $id ?: $name, - 'class' => $class, - ]; - - $result = $this->update('onDisplay', $arg); - - return $result; - } - - /** - * Checks if the answer is correct. - * - * @param string $code The answer. - * - * @return bool Whether the provided answer was correct - * - * @since 2.5 - * @throws \RuntimeException - */ - public function checkAnswer($code) - { - // Check if captcha is already loaded - if ($this->captcha === null) - { - return false; - } - - $arg = ['code' => $code]; - - $result = $this->update('onCheckAnswer', $arg); - - return $result; - } - - /** - * Method to react on the setup of a captcha field. Gives the possibility - * to change the field and/or the XML element for the field. - * - * @param \Joomla\CMS\Form\Field\CaptchaField $field Captcha field instance - * @param \SimpleXMLElement $element XML form definition - * - * @return void - */ - public function setupField(\Joomla\CMS\Form\Field\CaptchaField $field, \SimpleXMLElement $element) - { - if ($this->captcha === null) - { - return; - } - - $arg = [ - 'field' => $field, - 'element' => $element, - ]; - - $result = $this->update('onSetupField', $arg); - - return $result; - } - - /** - * Method to call the captcha callback if it exist. - * - * @param string $name Callback name - * @param array &$args Arguments - * - * @return mixed - * - * @since 4.0.0 - */ - private function update($name, &$args) - { - if (method_exists($this->captcha, $name)) - { - return call_user_func_array(array($this->captcha, $name), array_values($args)); - } - - return null; - } - - /** - * Load the Captcha plugin. - * - * @param array $options Associative array of options. - * - * @return void - * - * @since 2.5 - * @throws \RuntimeException - */ - private function _load(array $options = array()) - { - // Build the path to the needed captcha plugin - $name = InputFilter::getInstance()->clean($this->name, 'cmd'); - $path = JPATH_PLUGINS . '/captcha/' . $name . '/' . $name . '.php'; - - if (!is_file($path)) - { - throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name)); - } - - // Require plugin file - require_once $path; - - // Get the plugin - $plugin = PluginHelper::getPlugin('captcha', $this->name); - - if (!$plugin) - { - throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name)); - } - - // Check for already loaded params - if (!($plugin->params instanceof Registry)) - { - $params = new Registry($plugin->params); - $plugin->params = $params; - } - - // Build captcha plugin classname - $name = 'PlgCaptcha' . $this->name; - $dispatcher = $this->getDispatcher(); - $this->captcha = new $name($dispatcher, (array) $plugin, $options); - } + use DispatcherAwareTrait; + + /** + * Captcha Plugin object + * + * @var CMSPlugin + * @since 2.5 + */ + private $captcha; + + /** + * Editor Plugin name + * + * @var string + * @since 2.5 + */ + private $name; + + /** + * Array of instances of this class. + * + * @var Captcha[] + * @since 2.5 + */ + private static $instances = array(); + + /** + * Class constructor. + * + * @param string $captcha The plugin to use. + * @param array $options Associative array of options. + * + * @since 2.5 + * @throws \RuntimeException + */ + public function __construct($captcha, $options) + { + $this->name = $captcha; + + if (!empty($options['dispatcher']) && $options['dispatcher'] instanceof DispatcherInterface) { + $this->setDispatcher($options['dispatcher']); + } else { + $this->setDispatcher(Factory::getApplication()->getDispatcher()); + } + + $this->_load($options); + } + + /** + * Returns the global Captcha object, only creating it + * if it doesn't already exist. + * + * @param string $captcha The plugin to use. + * @param array $options Associative array of options. + * + * @return Captcha|null Instance of this class. + * + * @since 2.5 + * @throws \RuntimeException + */ + public static function getInstance($captcha, array $options = array()) + { + $signature = md5(serialize(array($captcha, $options))); + + if (empty(self::$instances[$signature])) { + self::$instances[$signature] = new Captcha($captcha, $options); + } + + return self::$instances[$signature]; + } + + /** + * Fire the onInit event to initialise the captcha plugin. + * + * @param string $id The id of the field. + * + * @return boolean True on success + * + * @since 2.5 + * @throws \RuntimeException + */ + public function initialise($id) + { + $arg = ['id' => $id]; + + $this->update('onInit', $arg); + + return true; + } + + /** + * Get the HTML for the captcha. + * + * @param string $name The control name. + * @param string $id The id for the control. + * @param string $class Value for the HTML class attribute + * + * @return string The return value of the function "onDisplay" of the selected Plugin. + * + * @since 2.5 + * @throws \RuntimeException + */ + public function display($name, $id, $class = '') + { + // Check if captcha is already loaded. + if ($this->captcha === null) { + return ''; + } + + // Initialise the Captcha. + if (!$this->initialise($id)) { + return ''; + } + + $arg = [ + 'name' => $name, + 'id' => $id ?: $name, + 'class' => $class, + ]; + + $result = $this->update('onDisplay', $arg); + + return $result; + } + + /** + * Checks if the answer is correct. + * + * @param string $code The answer. + * + * @return bool Whether the provided answer was correct + * + * @since 2.5 + * @throws \RuntimeException + */ + public function checkAnswer($code) + { + // Check if captcha is already loaded + if ($this->captcha === null) { + return false; + } + + $arg = ['code' => $code]; + + $result = $this->update('onCheckAnswer', $arg); + + return $result; + } + + /** + * Method to react on the setup of a captcha field. Gives the possibility + * to change the field and/or the XML element for the field. + * + * @param \Joomla\CMS\Form\Field\CaptchaField $field Captcha field instance + * @param \SimpleXMLElement $element XML form definition + * + * @return void + */ + public function setupField(\Joomla\CMS\Form\Field\CaptchaField $field, \SimpleXMLElement $element) + { + if ($this->captcha === null) { + return; + } + + $arg = [ + 'field' => $field, + 'element' => $element, + ]; + + $result = $this->update('onSetupField', $arg); + + return $result; + } + + /** + * Method to call the captcha callback if it exist. + * + * @param string $name Callback name + * @param array &$args Arguments + * + * @return mixed + * + * @since 4.0.0 + */ + private function update($name, &$args) + { + if (method_exists($this->captcha, $name)) { + return call_user_func_array(array($this->captcha, $name), array_values($args)); + } + + return null; + } + + /** + * Load the Captcha plugin. + * + * @param array $options Associative array of options. + * + * @return void + * + * @since 2.5 + * @throws \RuntimeException + */ + private function _load(array $options = array()) + { + // Build the path to the needed captcha plugin + $name = InputFilter::getInstance()->clean($this->name, 'cmd'); + $path = JPATH_PLUGINS . '/captcha/' . $name . '/' . $name . '.php'; + + if (!is_file($path)) { + throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name)); + } + + // Require plugin file + require_once $path; + + // Get the plugin + $plugin = PluginHelper::getPlugin('captcha', $this->name); + + if (!$plugin) { + throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name)); + } + + // Check for already loaded params + if (!($plugin->params instanceof Registry)) { + $params = new Registry($plugin->params); + $plugin->params = $params; + } + + // Build captcha plugin classname + $name = 'PlgCaptcha' . $this->name; + $dispatcher = $this->getDispatcher(); + $this->captcha = new $name($dispatcher, (array) $plugin, $options); + } } diff --git a/code/libraries/src/Captcha/Google/HttpBridgePostRequestMethod.php b/code/libraries/src/Captcha/Google/HttpBridgePostRequestMethod.php index ff4a8f9c..cb134f89 100644 --- a/code/libraries/src/Captcha/Google/HttpBridgePostRequestMethod.php +++ b/code/libraries/src/Captcha/Google/HttpBridgePostRequestMethod.php @@ -1,4 +1,5 @@ http = $http ?: HttpFactory::getHttp(); - } + /** + * Class constructor. + * + * @param Http|null $http The HTTP adapter + * + * @since 3.9.0 + */ + public function __construct(Http $http = null) + { + $this->http = $http ?: HttpFactory::getHttp(); + } - /** - * Submit the request with the specified parameters. - * - * @param RequestParameters $params Request parameters - * - * @return string Body of the reCAPTCHA response - * - * @since 3.9.0 - */ - public function submit(RequestParameters $params) - { - try - { - $response = $this->http->post(self::SITE_VERIFY_URL, $params->toArray()); + /** + * Submit the request with the specified parameters. + * + * @param RequestParameters $params Request parameters + * + * @return string Body of the reCAPTCHA response + * + * @since 3.9.0 + */ + public function submit(RequestParameters $params) + { + try { + $response = $this->http->post(self::SITE_VERIFY_URL, $params->toArray()); - return (string) $response->getBody(); - } - catch (InvalidResponseCodeException $exception) - { - return ''; - } - } + return (string) $response->getBody(); + } catch (InvalidResponseCodeException $exception) { + return ''; + } + } } diff --git a/code/libraries/src/Categories/Categories.php b/code/libraries/src/Categories/Categories.php index 8071697c..e2b4eaaa 100644 --- a/code/libraries/src/Categories/Categories.php +++ b/code/libraries/src/Categories/Categories.php @@ -1,4 +1,5 @@ _extension = $options['extension']; - $this->_table = $options['table']; - $this->_field = isset($options['field']) && $options['field'] ? $options['field'] : 'catid'; - $this->_key = isset($options['key']) && $options['key'] ? $options['key'] : 'id'; - $this->_statefield = isset($options['statefield']) ? $options['statefield'] : 'state'; - - $options['access'] = isset($options['access']) ? $options['access'] : 'true'; - $options['published'] = isset($options['published']) ? $options['published'] : 1; - $options['countItems'] = isset($options['countItems']) ? $options['countItems'] : 0; - $options['currentlang'] = Multilanguage::isEnabled() ? Factory::getLanguage()->getTag() : 0; - - $this->_options = $options; - } - - /** - * Returns a reference to a Categories object - * - * @param string $extension Name of the categories extension - * @param array $options An array of options - * - * @return Categories|boolean Categories object on success, boolean false if an object does not exist - * - * @since 1.6 - * @deprecated 5.0 Use the ComponentInterface to get the categories - */ - public static function getInstance($extension, $options = array()) - { - $hash = md5(strtolower($extension) . serialize($options)); - - if (isset(self::$instances[$hash])) - { - return self::$instances[$hash]; - } - - $categories = null; - - try - { - $parts = explode('.', $extension, 2); - - $component = Factory::getApplication()->bootComponent($parts[0]); - - if ($component instanceof CategoryServiceInterface) - { - $categories = $component->getCategory($options, \count($parts) > 1 ? $parts[1] : ''); - } - } - catch (SectionNotFoundException $e) - { - $categories = null; - } - - self::$instances[$hash] = $categories; - - return self::$instances[$hash]; - } - - /** - * Loads a specific category and all its children in a CategoryNode object. - * - * @param mixed $id an optional id integer or equal to 'root' - * @param boolean $forceload True to force the _load method to execute - * - * @return CategoryNode|null CategoryNode object or null if $id is not valid - * - * @since 1.6 - */ - public function get($id = 'root', $forceload = false) - { - if ($id !== 'root') - { - $id = (int) $id; - - if ($id == 0) - { - $id = 'root'; - } - } - - // If this $id has not been processed yet, execute the _load method - if ((!isset($this->_nodes[$id]) && !isset($this->_checkedCategories[$id])) || $forceload) - { - $this->_load($id); - } - - // If we already have a value in _nodes for this $id, then use it. - if (isset($this->_nodes[$id])) - { - return $this->_nodes[$id]; - } - - return null; - } - - /** - * Returns the extension of the category. - * - * @return string The extension - * - * @since 3.9.0 - */ - public function getExtension() - { - return $this->_extension; - } - - /** - * Load method - * - * @param integer $id Id of category to load - * - * @return void - * - * @since 1.6 - */ - protected function _load($id) - { - /** @var \Joomla\Database\DatabaseDriver */ - $db = Factory::getDbo(); - $app = Factory::getApplication(); - $user = Factory::getUser(); - $extension = $this->_extension; - - if ($id !== 'root') - { - $id = (int) $id; - - if ($id === 0) - { - $id = 'root'; - } - } - - // Record that has this $id has been checked - $this->_checkedCategories[$id] = true; - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('c.id'), - $db->quoteName('c.asset_id'), - $db->quoteName('c.access'), - $db->quoteName('c.alias'), - $db->quoteName('c.checked_out'), - $db->quoteName('c.checked_out_time'), - $db->quoteName('c.created_time'), - $db->quoteName('c.created_user_id'), - $db->quoteName('c.description'), - $db->quoteName('c.extension'), - $db->quoteName('c.hits'), - $db->quoteName('c.language'), - $db->quoteName('c.level'), - $db->quoteName('c.lft'), - $db->quoteName('c.metadata'), - $db->quoteName('c.metadesc'), - $db->quoteName('c.metakey'), - $db->quoteName('c.modified_time'), - $db->quoteName('c.note'), - $db->quoteName('c.params'), - $db->quoteName('c.parent_id'), - $db->quoteName('c.path'), - $db->quoteName('c.published'), - $db->quoteName('c.rgt'), - $db->quoteName('c.title'), - $db->quoteName('c.modified_user_id'), - $db->quoteName('c.version'), - ] - ); - - $case_when = ' CASE WHEN '; - $case_when .= $query->charLength($db->quoteName('c.alias'), '!=', '0'); - $case_when .= ' THEN '; - $c_id = $query->castAsChar($db->quoteName('c.id')); - $case_when .= $query->concatenate(array($c_id, $db->quoteName('c.alias')), ':'); - $case_when .= ' ELSE '; - $case_when .= $c_id . ' END as ' . $db->quoteName('slug'); - - $query->select($case_when) - ->where('(' . $db->quoteName('c.extension') . ' = :extension OR ' . $db->quoteName('c.extension') . ' = ' . $db->quote('system') . ')') - ->bind(':extension', $extension); - - if ($this->_options['access']) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('c.access'), $groups); - } - - if ($this->_options['published'] == 1) - { - $query->where($db->quoteName('c.published') . ' = 1'); - } - - $query->order($db->quoteName('c.lft')); - - // Note: s for selected id - if ($id !== 'root') - { - // Get the selected category - $query->from($db->quoteName('#__categories', 's')) - ->where($db->quoteName('s.id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - if ($app->isClient('site') && Multilanguage::isEnabled()) - { - // For the most part, we use c.lft column, which index is properly used instead of c.rgt - $query->join( - 'INNER', - $db->quoteName('#__categories', 'c'), - '(' . $db->quoteName('s.lft') . ' < ' . $db->quoteName('c.lft') - . ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') - . ' AND ' . $db->quoteName('c.language') - . ' IN (' . implode(',', $query->bindArray([Factory::getLanguage()->getTag(), '*'], ParameterType::STRING)) . '))' - . ' OR (' . $db->quoteName('c.lft') . ' <= ' . $db->quoteName('s.lft') - . ' AND ' . $db->quoteName('s.rgt') . ' <= ' . $db->quoteName('c.rgt') . ')' - ); - } - else - { - $query->join( - 'INNER', - $db->quoteName('#__categories', 'c'), - '(' . $db->quoteName('s.lft') . ' <= ' . $db->quoteName('c.lft') - . ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') . ')' - . ' OR (' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.lft') - . ' AND ' . $db->quoteName('s.rgt') . ' < ' . $db->quoteName('c.rgt') . ')' - ); - } - } - else - { - $query->from($db->quoteName('#__categories', 'c')); - - if ($app->isClient('site') && Multilanguage::isEnabled()) - { - $query->whereIn($db->quoteName('c.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - } - - // Note: i for item - if ($this->_options['countItems'] == 1) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName($db->escape('i.' . $this->_key)) . ')') - ->from($db->quoteName($db->escape($this->_table), 'i')) - ->where($db->quoteName($db->escape('i.' . $this->_field)) . ' = ' . $db->quoteName('c.id')); - - if ($this->_options['published'] == 1) - { - $subQuery->where($db->quoteName($db->escape('i.' . $this->_statefield)) . ' = 1'); - } - - if ($this->_options['currentlang'] !== 0) - { - $subQuery->where( - $db->quoteName('i.language') - . ' IN (' . implode(',', $query->bindArray([$this->_options['currentlang'], '*'], ParameterType::STRING)) . ')' - ); - } - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('numitems')); - } - - // Get the results - $db->setQuery($query); - $results = $db->loadObjectList('id'); - $childrenLoaded = false; - - if (\count($results)) - { - // Foreach categories - foreach ($results as $result) - { - // Deal with root category - if ($result->id == 1) - { - $result->id = 'root'; - } - - // Deal with parent_id - if ($result->parent_id == 1) - { - $result->parent_id = 'root'; - } - - // Create the node - if (!isset($this->_nodes[$result->id])) - { - // Create the CategoryNode and add to _nodes - $this->_nodes[$result->id] = new CategoryNode($result, $this); - - // If this is not root and if the current node's parent is in the list or the current node parent is 0 - if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id == 1)) - { - // Compute relationship between node and its parent - set the parent in the _nodes field - $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]); - } - - // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0), - // then remove the node from the list - if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) - { - unset($this->_nodes[$result->id]); - continue; - } - - if ($result->id == $id || $childrenLoaded) - { - $this->_nodes[$result->id]->setAllLoaded(); - $childrenLoaded = true; - } - } - elseif ($result->id == $id || $childrenLoaded) - { - // Create the CategoryNode - $this->_nodes[$result->id] = new CategoryNode($result, $this); - - if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id)) - { - // Compute relationship between node and its parent - $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]); - } - - // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0), - // then remove the node from the list - if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) - { - unset($this->_nodes[$result->id]); - continue; - } - - if ($result->id == $id || $childrenLoaded) - { - $this->_nodes[$result->id]->setAllLoaded(); - $childrenLoaded = true; - } - } - } - } - else - { - $this->_nodes[$id] = null; - } - } + use DatabaseAwareTrait; + + /** + * Array to hold the object instances + * + * @var Categories[] + * @since 1.6 + */ + public static $instances = array(); + + /** + * Array of category nodes + * + * @var CategoryNode[] + * @since 1.6 + */ + protected $_nodes; + + /** + * Array of checked categories -- used to save values when _nodes are null + * + * @var boolean[] + * @since 1.6 + */ + protected $_checkedCategories; + + /** + * Name of the extension the categories belong to + * + * @var string + * @since 1.6 + */ + protected $_extension = null; + + /** + * Name of the linked content table to get category content count + * + * @var string + * @since 1.6 + */ + protected $_table = null; + + /** + * Name of the category field + * + * @var string + * @since 1.6 + */ + protected $_field = null; + + /** + * Name of the key field + * + * @var string + * @since 1.6 + */ + protected $_key = null; + + /** + * Name of the items state field + * + * @var string + * @since 1.6 + */ + protected $_statefield = null; + + /** + * Array of options + * + * @var array + * @since 1.6 + */ + protected $_options = []; + + /** + * Class constructor + * + * @param array $options Array of options + * + * @since 1.6 + */ + public function __construct($options) + { + $this->_extension = $options['extension']; + $this->_table = $options['table']; + $this->_field = isset($options['field']) && $options['field'] ? $options['field'] : 'catid'; + $this->_key = isset($options['key']) && $options['key'] ? $options['key'] : 'id'; + $this->_statefield = isset($options['statefield']) ? $options['statefield'] : 'state'; + + $options['access'] = isset($options['access']) ? $options['access'] : 'true'; + $options['published'] = isset($options['published']) ? $options['published'] : 1; + $options['countItems'] = isset($options['countItems']) ? $options['countItems'] : 0; + $options['currentlang'] = Multilanguage::isEnabled() ? Factory::getLanguage()->getTag() : 0; + + $this->_options = $options; + } + + /** + * Returns a reference to a Categories object + * + * @param string $extension Name of the categories extension + * @param array $options An array of options + * + * @return Categories|boolean Categories object on success, boolean false if an object does not exist + * + * @since 1.6 + * @deprecated 5.0 Use the ComponentInterface to get the categories + */ + public static function getInstance($extension, $options = array()) + { + $hash = md5(strtolower($extension) . serialize($options)); + + if (isset(self::$instances[$hash])) { + return self::$instances[$hash]; + } + + $categories = null; + + try { + $parts = explode('.', $extension, 2); + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof CategoryServiceInterface) { + $categories = $component->getCategory($options, \count($parts) > 1 ? $parts[1] : ''); + } + } catch (SectionNotFoundException $e) { + $categories = null; + } + + self::$instances[$hash] = $categories; + + return self::$instances[$hash]; + } + + /** + * Loads a specific category and all its children in a CategoryNode object. + * + * @param mixed $id an optional id integer or equal to 'root' + * @param boolean $forceload True to force the _load method to execute + * + * @return CategoryNode|null CategoryNode object or null if $id is not valid + * + * @since 1.6 + */ + public function get($id = 'root', $forceload = false) + { + if ($id !== 'root') { + $id = (int) $id; + + if ($id == 0) { + $id = 'root'; + } + } + + // If this $id has not been processed yet, execute the _load method + if ((!isset($this->_nodes[$id]) && !isset($this->_checkedCategories[$id])) || $forceload) { + $this->_load($id); + } + + // If we already have a value in _nodes for this $id, then use it. + if (isset($this->_nodes[$id])) { + return $this->_nodes[$id]; + } + + return null; + } + + /** + * Returns the extension of the category. + * + * @return string The extension + * + * @since 3.9.0 + */ + public function getExtension() + { + return $this->_extension; + } + + /** + * Load method + * + * @param integer $id Id of category to load + * + * @return void + * + * @since 1.6 + */ + protected function _load($id) + { + try { + $db = $this->getDatabase(); + } catch (DatabaseNotFoundException $e) { + @trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED); + $db = Factory::getContainer()->get(DatabaseInterface::class); + } + + $app = Factory::getApplication(); + $user = Factory::getUser(); + $extension = $this->_extension; + + if ($id !== 'root') { + $id = (int) $id; + + if ($id === 0) { + $id = 'root'; + } + } + + // Record that has this $id has been checked + $this->_checkedCategories[$id] = true; + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('c.id'), + $db->quoteName('c.asset_id'), + $db->quoteName('c.access'), + $db->quoteName('c.alias'), + $db->quoteName('c.checked_out'), + $db->quoteName('c.checked_out_time'), + $db->quoteName('c.created_time'), + $db->quoteName('c.created_user_id'), + $db->quoteName('c.description'), + $db->quoteName('c.extension'), + $db->quoteName('c.hits'), + $db->quoteName('c.language'), + $db->quoteName('c.level'), + $db->quoteName('c.lft'), + $db->quoteName('c.metadata'), + $db->quoteName('c.metadesc'), + $db->quoteName('c.metakey'), + $db->quoteName('c.modified_time'), + $db->quoteName('c.note'), + $db->quoteName('c.params'), + $db->quoteName('c.parent_id'), + $db->quoteName('c.path'), + $db->quoteName('c.published'), + $db->quoteName('c.rgt'), + $db->quoteName('c.title'), + $db->quoteName('c.modified_user_id'), + $db->quoteName('c.version'), + ] + ); + + $case_when = ' CASE WHEN '; + $case_when .= $query->charLength($db->quoteName('c.alias'), '!=', '0'); + $case_when .= ' THEN '; + $c_id = $query->castAsChar($db->quoteName('c.id')); + $case_when .= $query->concatenate(array($c_id, $db->quoteName('c.alias')), ':'); + $case_when .= ' ELSE '; + $case_when .= $c_id . ' END as ' . $db->quoteName('slug'); + + $query->select($case_when) + ->where('(' . $db->quoteName('c.extension') . ' = :extension OR ' . $db->quoteName('c.extension') . ' = ' . $db->quote('system') . ')') + ->bind(':extension', $extension); + + if ($this->_options['access']) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('c.access'), $groups); + } + + if ($this->_options['published'] == 1) { + $query->where($db->quoteName('c.published') . ' = 1'); + } + + $query->order($db->quoteName('c.lft')); + + // Note: s for selected id + if ($id !== 'root') { + // Get the selected category + $query->from($db->quoteName('#__categories', 's')) + ->where($db->quoteName('s.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + if ($app->isClient('site') && Multilanguage::isEnabled()) { + // For the most part, we use c.lft column, which index is properly used instead of c.rgt + $query->join( + 'INNER', + $db->quoteName('#__categories', 'c'), + '(' . $db->quoteName('s.lft') . ' < ' . $db->quoteName('c.lft') + . ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') + . ' AND ' . $db->quoteName('c.language') + . ' IN (' . implode(',', $query->bindArray([Factory::getLanguage()->getTag(), '*'], ParameterType::STRING)) . '))' + . ' OR (' . $db->quoteName('c.lft') . ' <= ' . $db->quoteName('s.lft') + . ' AND ' . $db->quoteName('s.rgt') . ' <= ' . $db->quoteName('c.rgt') . ')' + ); + } else { + $query->join( + 'INNER', + $db->quoteName('#__categories', 'c'), + '(' . $db->quoteName('s.lft') . ' <= ' . $db->quoteName('c.lft') + . ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') . ')' + . ' OR (' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.lft') + . ' AND ' . $db->quoteName('s.rgt') . ' < ' . $db->quoteName('c.rgt') . ')' + ); + } + } else { + $query->from($db->quoteName('#__categories', 'c')); + + if ($app->isClient('site') && Multilanguage::isEnabled()) { + $query->whereIn($db->quoteName('c.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); + } + } + + // Note: i for item + if ($this->_options['countItems'] == 1) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName($db->escape('i.' . $this->_key)) . ')') + ->from($db->quoteName($db->escape($this->_table), 'i')) + ->where($db->quoteName($db->escape('i.' . $this->_field)) . ' = ' . $db->quoteName('c.id')); + + if ($this->_options['published'] == 1) { + $subQuery->where($db->quoteName($db->escape('i.' . $this->_statefield)) . ' = 1'); + } + + if ($this->_options['currentlang'] !== 0) { + $subQuery->where( + $db->quoteName('i.language') + . ' IN (' . implode(',', $query->bindArray([$this->_options['currentlang'], '*'], ParameterType::STRING)) . ')' + ); + } + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('numitems')); + } + + // Get the results + $db->setQuery($query); + $results = $db->loadObjectList('id'); + $childrenLoaded = false; + + if (\count($results)) { + // Foreach categories + foreach ($results as $result) { + // Deal with root category + if ($result->id == 1) { + $result->id = 'root'; + } + + // Deal with parent_id + if ($result->parent_id == 1) { + $result->parent_id = 'root'; + } + + // Create the node + if (!isset($this->_nodes[$result->id])) { + // Create the CategoryNode and add to _nodes + $this->_nodes[$result->id] = new CategoryNode($result, $this); + + // If this is not root and if the current node's parent is in the list or the current node parent is 0 + if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id == 1)) { + // Compute relationship between node and its parent - set the parent in the _nodes field + $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]); + } + + // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0), + // then remove the node from the list + if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) { + unset($this->_nodes[$result->id]); + continue; + } + + if ($result->id == $id || $childrenLoaded) { + $this->_nodes[$result->id]->setAllLoaded(); + $childrenLoaded = true; + } + } elseif ($result->id == $id || $childrenLoaded) { + // Create the CategoryNode + $this->_nodes[$result->id] = new CategoryNode($result, $this); + + if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id)) { + // Compute relationship between node and its parent + $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]); + } + + // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0), + // then remove the node from the list + if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) { + unset($this->_nodes[$result->id]); + continue; + } + + if ($result->id == $id || $childrenLoaded) { + $this->_nodes[$result->id]->setAllLoaded(); + $childrenLoaded = true; + } + } + } + } else { + $this->_nodes[$id] = null; + } + } } diff --git a/code/libraries/src/Categories/CategoryFactory.php b/code/libraries/src/Categories/CategoryFactory.php index d4662502..76495d3c 100644 --- a/code/libraries/src/Categories/CategoryFactory.php +++ b/code/libraries/src/Categories/CategoryFactory.php @@ -1,4 +1,5 @@ namespace = $namespace; + } + + /** + * Creates a category. + * + * @param array $options The options + * @param string $section The section + * + * @return CategoryInterface + * + * @since 3.10.0 + * + * @throws SectionNotFoundException + */ + public function createCategory(array $options = [], string $section = ''): CategoryInterface + { + $className = trim($this->namespace, '\\') . '\\Site\\Service\\' . ucfirst($section) . 'Category'; - /** - * The namespace must be like: - * Joomla\Component\Content - * - * @param string $namespace The namespace - * - * @since 4.0.0 - */ - public function __construct($namespace) - { - $this->namespace = $namespace; - } + if (!class_exists($className)) { + throw new SectionNotFoundException(); + } - /** - * Creates a category. - * - * @param array $options The options - * @param string $section The section - * - * @return CategoryInterface - * - * @since 3.10.0 - * - * @throws SectionNotFoundException - */ - public function createCategory(array $options = [], string $section = ''): CategoryInterface - { - $className = trim($this->namespace, '\\') . '\\Site\\Service\\' . ucfirst($section) . 'Category'; + $category = new $className($options); - if (!class_exists($className)) - { - throw new SectionNotFoundException; - } + if ($category instanceof DatabaseAwareInterface) { + $category->setDatabase($this->getDatabase()); + } - return new $className($options); - } + return $category; + } } diff --git a/code/libraries/src/Categories/CategoryFactoryInterface.php b/code/libraries/src/Categories/CategoryFactoryInterface.php index c3b0dca0..14887fd0 100644 --- a/code/libraries/src/Categories/CategoryFactoryInterface.php +++ b/code/libraries/src/Categories/CategoryFactoryInterface.php @@ -1,4 +1,5 @@ setProperties($category); - - if ($constructor) - { - $this->_constructor = $constructor; - } - - return true; - } - - return false; - } - - /** - * Set the parent of this category - * - * If the category already has a parent, the link is unset - * - * @param CategoryNode|null $parent CategoryNode for the parent to be set or null - * - * @return void - * - * @since 1.6 - */ - public function setParent(NodeInterface $parent) - { - if (!\is_null($this->_parent)) - { - $key = array_search($this, $this->_parent->_children); - unset($this->_parent->_children[$key]); - } - - $this->_parent = $parent; - - $this->_parent->_children[] = & $this; - - if (\count($this->_parent->_children) > 1) - { - end($this->_parent->_children); - $this->_leftSibling = prev($this->_parent->_children); - $this->_leftSibling->_rightsibling = & $this; - } - - if ($this->parent_id != 1) - { - $this->_path = $parent->getPath(); - } - - $this->_path[$this->id] = $this->id . ':' . $this->alias; - } - - /** - * Get the children of this node - * - * @param boolean $recursive False by default - * - * @return CategoryNode[] The children - * - * @since 1.6 - */ - public function &getChildren($recursive = false) - { - if (!$this->_allChildrenloaded) - { - $temp = $this->_constructor->get($this->id, true); - - if ($temp) - { - $this->_children = $temp->getChildren(); - $this->_leftSibling = $temp->getSibling(false); - $this->_rightSibling = $temp->getSibling(true); - $this->setAllLoaded(); - } - } - - if ($recursive) - { - $items = array(); - - foreach ($this->_children as $child) - { - $items[] = $child; - $items = array_merge($items, $child->getChildren(true)); - } - - return $items; - } - - return $this->_children; - } - - /** - * Returns the right or left sibling of a category - * - * @param boolean $right If set to false, returns the left sibling - * - * @return CategoryNode|null CategoryNode object with the sibling information or null if there is no sibling on that side. - * - * @since 1.6 - */ - public function getSibling($right = true) - { - if (!$this->_allChildrenloaded) - { - $temp = $this->_constructor->get($this->id, true); - $this->_children = $temp->getChildren(); - $this->_leftSibling = $temp->getSibling(false); - $this->_rightSibling = $temp->getSibling(true); - $this->setAllLoaded(); - } - - if ($right) - { - return $this->_rightSibling; - } - else - { - return $this->_leftSibling; - } - } - - /** - * Returns the category parameters - * - * @return Registry - * - * @since 1.6 - */ - public function getParams() - { - if (!($this->params instanceof Registry)) - { - $this->params = new Registry($this->params); - } - - return $this->params; - } - - /** - * Returns the category metadata - * - * @return Registry A Registry object containing the metadata - * - * @since 1.6 - */ - public function getMetadata() - { - if (!($this->metadata instanceof Registry)) - { - $this->metadata = new Registry($this->metadata); - } - - return $this->metadata; - } - - /** - * Returns the category path to the root category - * - * @return array - * - * @since 1.6 - */ - public function getPath() - { - return $this->_path; - } - - /** - * Returns the user that created the category - * - * @param boolean $modifiedUser Returns the modified_user when set to true - * - * @return \Joomla\CMS\User\User A User object containing a userid - * - * @since 1.6 - */ - public function getAuthor($modifiedUser = false) - { - if ($modifiedUser) - { - return Factory::getUser($this->modified_user_id); - } - - return Factory::getUser($this->created_user_id); - } - - /** - * Set to load all children - * - * @return void - * - * @since 1.6 - */ - public function setAllLoaded() - { - $this->_allChildrenloaded = true; - - foreach ($this->_children as $child) - { - $child->setAllLoaded(); - } - } - - /** - * Returns the number of items. - * - * @param boolean $recursive If false number of children, if true number of descendants - * - * @return integer Number of children or descendants - * - * @since 1.6 - */ - public function getNumItems($recursive = false) - { - if ($recursive) - { - $count = $this->numitems; - - foreach ($this->getChildren() as $child) - { - $count = $count + $child->getNumItems(true); - } - - return $count; - } - - return $this->numitems; - } + use NodeTrait; + + /** + * Primary key + * + * @var integer + * @since 1.6 + */ + public $id = null; + + /** + * The id of the category in the asset table + * + * @var integer + * @since 1.6 + */ + public $asset_id = null; + + /** + * The id of the parent of category in the asset table, 0 for category root + * + * @var integer + * @since 1.6 + */ + public $parent_id = null; + + /** + * The lft value for this category in the category tree + * + * @var integer + * @since 1.6 + */ + public $lft = null; + + /** + * The rgt value for this category in the category tree + * + * @var integer + * @since 1.6 + */ + public $rgt = null; + + /** + * The depth of this category's position in the category tree + * + * @var integer + * @since 1.6 + */ + public $level = null; + + /** + * The extension this category is associated with + * + * @var integer + * @since 1.6 + */ + public $extension = null; + + /** + * The menu title for the category (a short name) + * + * @var string + * @since 1.6 + */ + public $title = null; + + /** + * The the alias for the category + * + * @var string + * @since 1.6 + */ + public $alias = null; + + /** + * Description of the category. + * + * @var string + * @since 1.6 + */ + public $description = null; + + /** + * The publication status of the category + * + * @var boolean + * @since 1.6 + */ + public $published = null; + + /** + * Whether the category is or is not checked out + * + * @var boolean + * @since 1.6 + */ + public $checked_out = null; + + /** + * The time at which the category was checked out + * + * @var string + * @since 1.6 + */ + public $checked_out_time = null; + + /** + * Access level for the category + * + * @var integer + * @since 1.6 + */ + public $access = null; + + /** + * JSON string of parameters + * + * @var string + * @since 1.6 + */ + public $params = null; + + /** + * Metadata description + * + * @var string + * @since 1.6 + */ + public $metadesc = null; + + /** + * Key words for metadata + * + * @var string + * @since 1.6 + */ + public $metakey = null; + + /** + * JSON string of other metadata + * + * @var string + * @since 1.6 + */ + public $metadata = null; + + /** + * The ID of the user who created the category + * + * @var integer + * @since 1.6 + */ + public $created_user_id = null; + + /** + * The time at which the category was created + * + * @var string + * @since 1.6 + */ + public $created_time = null; + + /** + * The ID of the user who last modified the category + * + * @var integer + * @since 1.6 + */ + public $modified_user_id = null; + + /** + * The time at which the category was modified + * + * @var string + * @since 1.6 + */ + public $modified_time = null; + + /** + * Number of times the category has been viewed + * + * @var integer + * @since 1.6 + */ + public $hits = null; + + /** + * The language for the category in xx-XX format + * + * @var string + * @since 1.6 + */ + public $language = null; + + /** + * Number of items in this category or descendants of this category + * + * @var integer + * @since 1.6 + */ + public $numitems = null; + + /** + * Number of children items + * + * @var integer + * @since 1.6 + */ + public $childrennumitems = null; + + /** + * Slug for the category (used in URL) + * + * @var string + * @since 1.6 + */ + public $slug = null; + + /** + * Array of assets + * + * @var array + * @since 1.6 + */ + public $assets = null; + + /** + * Path from root to this category + * + * @var array + * @since 1.6 + */ + protected $_path = array(); + + /** + * Flag if all children have been loaded + * + * @var boolean + * @since 1.6 + */ + protected $_allChildrenloaded = false; + + /** + * Constructor of this tree + * + * @var Categories + * @since 1.6 + */ + protected $_constructor = null; + + /** + * Class constructor + * + * @param array $category The category data. + * @param Categories $constructor The tree constructor. + * + * @since 1.6 + */ + public function __construct($category = null, $constructor = null) + { + if ($category) { + $this->setProperties($category); + + if ($constructor) { + $this->_constructor = $constructor; + } + + return true; + } + + return false; + } + + /** + * Set the parent of this category + * + * If the category already has a parent, the link is unset + * + * @param CategoryNode|null $parent CategoryNode for the parent to be set or null + * + * @return void + * + * @since 1.6 + */ + public function setParent(NodeInterface $parent) + { + if (!\is_null($this->_parent)) { + $key = array_search($this, $this->_parent->_children); + unset($this->_parent->_children[$key]); + } + + $this->_parent = $parent; + + $this->_parent->_children[] = & $this; + + if (\count($this->_parent->_children) > 1) { + end($this->_parent->_children); + $this->_leftSibling = prev($this->_parent->_children); + $this->_leftSibling->_rightsibling = & $this; + } + + if ($this->parent_id != 1) { + $this->_path = $parent->getPath(); + } + + $this->_path[$this->id] = $this->id . ':' . $this->alias; + } + + /** + * Get the children of this node + * + * @param boolean $recursive False by default + * + * @return CategoryNode[] The children + * + * @since 1.6 + */ + public function &getChildren($recursive = false) + { + if (!$this->_allChildrenloaded) { + $temp = $this->_constructor->get($this->id, true); + + if ($temp) { + $this->_children = $temp->getChildren(); + $this->_leftSibling = $temp->getSibling(false); + $this->_rightSibling = $temp->getSibling(true); + $this->setAllLoaded(); + } + } + + if ($recursive) { + $items = array(); + + foreach ($this->_children as $child) { + $items[] = $child; + $items = array_merge($items, $child->getChildren(true)); + } + + return $items; + } + + return $this->_children; + } + + /** + * Returns the right or left sibling of a category + * + * @param boolean $right If set to false, returns the left sibling + * + * @return CategoryNode|null CategoryNode object with the sibling information or null if there is no sibling on that side. + * + * @since 1.6 + */ + public function getSibling($right = true) + { + if (!$this->_allChildrenloaded) { + $temp = $this->_constructor->get($this->id, true); + $this->_children = $temp->getChildren(); + $this->_leftSibling = $temp->getSibling(false); + $this->_rightSibling = $temp->getSibling(true); + $this->setAllLoaded(); + } + + if ($right) { + return $this->_rightSibling; + } else { + return $this->_leftSibling; + } + } + + /** + * Returns the category parameters + * + * @return Registry + * + * @since 1.6 + */ + public function getParams() + { + if (!($this->params instanceof Registry)) { + $this->params = new Registry($this->params); + } + + return $this->params; + } + + /** + * Returns the category metadata + * + * @return Registry A Registry object containing the metadata + * + * @since 1.6 + */ + public function getMetadata() + { + if (!($this->metadata instanceof Registry)) { + $this->metadata = new Registry($this->metadata); + } + + return $this->metadata; + } + + /** + * Returns the category path to the root category + * + * @return array + * + * @since 1.6 + */ + public function getPath() + { + return $this->_path; + } + + /** + * Returns the user that created the category + * + * @param boolean $modifiedUser Returns the modified_user when set to true + * + * @return \Joomla\CMS\User\User A User object containing a userid + * + * @since 1.6 + */ + public function getAuthor($modifiedUser = false) + { + if ($modifiedUser) { + return Factory::getUser($this->modified_user_id); + } + + return Factory::getUser($this->created_user_id); + } + + /** + * Set to load all children + * + * @return void + * + * @since 1.6 + */ + public function setAllLoaded() + { + $this->_allChildrenloaded = true; + + foreach ($this->_children as $child) { + $child->setAllLoaded(); + } + } + + /** + * Returns the number of items. + * + * @param boolean $recursive If false number of children, if true number of descendants + * + * @return integer Number of children or descendants + * + * @since 1.6 + */ + public function getNumItems($recursive = false) + { + if ($recursive) { + $count = $this->numitems; + + foreach ($this->getChildren() as $child) { + $count = $count + $child->getNumItems(true); + } + + return $count; + } + + return $this->numitems; + } } diff --git a/code/libraries/src/Categories/CategoryServiceInterface.php b/code/libraries/src/Categories/CategoryServiceInterface.php index 29b313d1..00b20ecf 100644 --- a/code/libraries/src/Categories/CategoryServiceInterface.php +++ b/code/libraries/src/Categories/CategoryServiceInterface.php @@ -1,4 +1,5 @@ categoryFactory->createCategory($options, $section); - } + /** + * Returns the category service. + * + * @param array $options The options + * @param string $section The section + * + * @return CategoryInterface + * + * @since 4.0.0 + * @throws SectionNotFoundException + */ + public function getCategory(array $options = [], $section = ''): CategoryInterface + { + return $this->categoryFactory->createCategory($options, $section); + } - /** - * Sets the internal category factory. - * - * @param CategoryFactoryInterface $categoryFactory The categories factory - * - * @return void - * - * @since 4.0.0 - */ - public function setCategoryFactory(CategoryFactoryInterface $categoryFactory) - { - $this->categoryFactory = $categoryFactory; - } + /** + * Sets the internal category factory. + * + * @param CategoryFactoryInterface $categoryFactory The categories factory + * + * @return void + * + * @since 4.0.0 + */ + public function setCategoryFactory(CategoryFactoryInterface $categoryFactory) + { + $this->categoryFactory = $categoryFactory; + } - /** - * Adds Count Items for Category Manager. - * - * @param \stdClass[] $items The category objects - * @param string $section The section - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function countItems(array $items, string $section) - { - $config = (object) array( - 'related_tbl' => $this->getTableNameForSection($section), - 'state_col' => $this->getStateColumnForSection($section), - 'group_col' => 'catid', - 'relation_type' => 'category_or_group', - ); + /** + * Adds Count Items for Category Manager. + * + * @param \stdClass[] $items The category objects + * @param string $section The section + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function countItems(array $items, string $section) + { + $config = (object) array( + 'related_tbl' => $this->getTableNameForSection($section), + 'state_col' => $this->getStateColumnForSection($section), + 'group_col' => 'catid', + 'relation_type' => 'category_or_group', + ); - ContentHelper::countRelations($items, $config); - } + ContentHelper::countRelations($items, $config); + } - /** - * Prepares the category form - * - * @param Form $form The form to change - * @param array|object $data The form data - * - * @return void - */ - public function prepareForm(Form $form, $data) - { - } + /** + * Prepares the category form + * + * @param Form $form The form to change + * @param array|object $data The form data + * + * @return void + */ + public function prepareForm(Form $form, $data) + { + } - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return null; - } + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return null; + } - /** - * Returns the state column for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getStateColumnForSection(string $section = null) - { - return 'state'; - } + /** + * Returns the state column for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getStateColumnForSection(string $section = null) + { + return 'state'; + } } diff --git a/code/libraries/src/Categories/SectionNotFoundException.php b/code/libraries/src/Categories/SectionNotFoundException.php index 43910177..77183666 100644 --- a/code/libraries/src/Categories/SectionNotFoundException.php +++ b/code/libraries/src/Categories/SectionNotFoundException.php @@ -1,4 +1,5 @@ ` element - * - * @var string - * @since 4.0.0 - */ - protected $element; - - /** - * Update manifest `` element - * - * @var string - * @since 4.0.0 - */ - protected $type; - - /** - * Update manifest `` element - * - * @var string - * @since 4.0.0 - */ - protected $version; - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $security = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $fix = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $language = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $addition = array(); - - /** - * Update manifest `` elements - * - * @var array - * @since 4.0.0 - */ - protected $change = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $remove = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $note = array(); - - /** - * List of node items - * - * @var array - * @since 4.0.0 - */ - private $items = array(); - - /** - * Resource handle for the XML Parser - * - * @var resource - * @since 4.0.0 - */ - protected $xmlParser; - - /** - * Element call stack - * - * @var array - * @since 4.0.0 - */ - protected $stack = array('base'); - - /** - * Object containing the current update data - * - * @var \stdClass - * @since 4.0.0 - */ - protected $currentChangelog; - - /** - * The version to match the changelog - * - * @var string - * @since 4.0.0 - */ - private $matchVersion = ''; - - /** - * Object containing the latest changelog data - * - * @var \stdClass - * @since 4.0.0 - */ - protected $latest; - - /** - * Gets the reference to the current direct parent - * - * @return string - * - * @since 4.0.0 - */ - protected function getStackLocation() - { - return implode('->', $this->stack); - } - - /** - * Get the last position in stack count - * - * @return string - * - * @since 4.0.0 - */ - protected function getLastTag() - { - return $this->stack[\count($this->stack) - 1]; - } - - /** - * Set the version to match. - * - * @param string $version The version to match - * - * @return void - * - * @since 4.0.0 - */ - public function setVersion(string $version) - { - $this->matchVersion = $version; - } - - /** - * XML Start Element callback - * - * @param object $parser Parser object - * @param string $name Name of the tag found - * @param array $attrs Attributes of the tag - * - * @return void - * - * @note This is public because it is called externally - * @since 1.7.0 - */ - public function startElement($parser, $name, $attrs = array()) - { - $this->stack[] = $name; - $tag = $this->getStackLocation(); - - // Reset the data - if (isset($this->$tag)) - { - $this->$tag->data = ''; - } - - $name = strtolower($name); - - if (!isset($this->currentChangelog->$name)) - { - $this->currentChangelog->$name = new \stdClass; - } - - $this->currentChangelog->$name->data = ''; - - foreach ($attrs as $key => $data) - { - $key = strtolower($key); - $this->currentChangelog->$name->$key = $data; - } - } - - /** - * Callback for closing the element - * - * @param object $parser Parser object - * @param string $name Name of element that was closed - * - * @return void - * - * @note This is public because it is called externally - * @since 1.7.0 - */ - public function endElement($parser, $name) - { - array_pop($this->stack); - - switch ($name) - { - case 'SECURITY': - case 'FIX': - case 'LANGUAGE': - case 'ADDITION': - case 'CHANGE': - case 'REMOVE': - case 'NOTE': - $name = strtolower($name); - $this->currentChangelog->$name->data = $this->items; - $this->items = array(); - break; - case 'CHANGELOG': - if (version_compare($this->currentChangelog->version->data, $this->matchVersion, '==') === true) - { - $this->latest = $this->currentChangelog; - } - - // No version match, empty it - $this->currentChangelog = new \stdClass; - break; - case 'CHANGELOGS': - // If the latest item is set then we transfer it to where we want to - if (isset($this->latest)) - { - foreach (get_object_vars($this->latest) as $key => $val) - { - $this->$key = $val; - } - - unset($this->latest); - unset($this->currentChangelog); - } - elseif (isset($this->currentChangelog)) - { - // The update might be for an older version of j! - unset($this->currentChangelog); - } - break; - } - } - - /** - * Character Parser Function - * - * @param object $parser Parser object. - * @param object $data The data. - * - * @return void - * - * @note This is public because its called externally. - * @since 1.7.0 - */ - public function characterData($parser, $data) - { - $tag = $this->getLastTag(); - - switch ($tag) - { - case 'ITEM': - $this->items[] = $data; - break; - case 'SECURITY': - case 'FIX': - case 'LANGUAGE': - case 'ADDITION': - case 'CHANGE': - case 'REMOVE': - case 'NOTE': - break; - default: - // Throw the data for this item together - $tag = strtolower($tag); - - if (isset($this->currentChangelog->$tag)) - { - $this->currentChangelog->$tag->data .= $data; - } - break; - } - } - - /** - * Loads an XML file from a URL. - * - * @param string $url The URL. - * - * @return boolean True on success - * - * @since 4.0.0 - */ - public function loadFromXml($url) - { - $version = new Version; - $httpOption = new Registry; - $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); - - try - { - $http = HttpFactory::getHttp($httpOption); - $response = $http->get($url); - } - catch (RuntimeException $e) - { - $response = null; - } - - if ($response === null || $response->code !== 200) - { - // @todo: Add a 'mark bad' setting here somehow - Log::add(Text::sprintf('JLIB_UPDATER_ERROR_EXTENSION_OPEN_URL', $url), Log::WARNING, 'jerror'); - - return false; - } - - $this->currentChangelog = new \stdClass; - - $this->xmlParser = xml_parser_create(''); - xml_set_object($this->xmlParser, $this); - xml_set_element_handler($this->xmlParser, 'startElement', 'endElement'); - xml_set_character_data_handler($this->xmlParser, 'characterData'); - - if (!xml_parse($this->xmlParser, $response->body)) - { - Log::add( - sprintf( - 'XML error: %s at line %d', xml_error_string(xml_get_error_code($this->xmlParser)), - xml_get_current_line_number($this->xmlParser) - ), - Log::WARNING, 'updater' - ); - - return false; - } - - xml_parser_free($this->xmlParser); - - return true; - } + /** + * Update manifest `` element + * + * @var string + * @since 4.0.0 + */ + protected $element; + + /** + * Update manifest `` element + * + * @var string + * @since 4.0.0 + */ + protected $type; + + /** + * Update manifest `` element + * + * @var string + * @since 4.0.0 + */ + protected $version; + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $security = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $fix = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $language = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $addition = array(); + + /** + * Update manifest `` elements + * + * @var array + * @since 4.0.0 + */ + protected $change = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $remove = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $note = array(); + + /** + * List of node items + * + * @var array + * @since 4.0.0 + */ + private $items = array(); + + /** + * Resource handle for the XML Parser + * + * @var resource + * @since 4.0.0 + */ + protected $xmlParser; + + /** + * Element call stack + * + * @var array + * @since 4.0.0 + */ + protected $stack = array('base'); + + /** + * Object containing the current update data + * + * @var \stdClass + * @since 4.0.0 + */ + protected $currentChangelog; + + /** + * The version to match the changelog + * + * @var string + * @since 4.0.0 + */ + private $matchVersion = ''; + + /** + * Object containing the latest changelog data + * + * @var \stdClass + * @since 4.0.0 + */ + protected $latest; + + /** + * Gets the reference to the current direct parent + * + * @return string + * + * @since 4.0.0 + */ + protected function getStackLocation() + { + return implode('->', $this->stack); + } + + /** + * Get the last position in stack count + * + * @return string + * + * @since 4.0.0 + */ + protected function getLastTag() + { + return $this->stack[\count($this->stack) - 1]; + } + + /** + * Set the version to match. + * + * @param string $version The version to match + * + * @return void + * + * @since 4.0.0 + */ + public function setVersion(string $version) + { + $this->matchVersion = $version; + } + + /** + * XML Start Element callback + * + * @param object $parser Parser object + * @param string $name Name of the tag found + * @param array $attrs Attributes of the tag + * + * @return void + * + * @note This is public because it is called externally + * @since 1.7.0 + */ + public function startElement($parser, $name, $attrs = array()) + { + $this->stack[] = $name; + $tag = $this->getStackLocation(); + + // Reset the data + if (isset($this->$tag)) { + $this->$tag->data = ''; + } + + $name = strtolower($name); + + if (!isset($this->currentChangelog->$name)) { + $this->currentChangelog->$name = new \stdClass(); + } + + $this->currentChangelog->$name->data = ''; + + foreach ($attrs as $key => $data) { + $key = strtolower($key); + $this->currentChangelog->$name->$key = $data; + } + } + + /** + * Callback for closing the element + * + * @param object $parser Parser object + * @param string $name Name of element that was closed + * + * @return void + * + * @note This is public because it is called externally + * @since 1.7.0 + */ + public function endElement($parser, $name) + { + array_pop($this->stack); + + switch ($name) { + case 'SECURITY': + case 'FIX': + case 'LANGUAGE': + case 'ADDITION': + case 'CHANGE': + case 'REMOVE': + case 'NOTE': + $name = strtolower($name); + $this->currentChangelog->$name->data = $this->items; + $this->items = array(); + break; + case 'CHANGELOG': + if (version_compare($this->currentChangelog->version->data, $this->matchVersion, '==') === true) { + $this->latest = $this->currentChangelog; + } + + // No version match, empty it + $this->currentChangelog = new \stdClass(); + break; + case 'CHANGELOGS': + // If the latest item is set then we transfer it to where we want to + if (isset($this->latest)) { + foreach (get_object_vars($this->latest) as $key => $val) { + $this->$key = $val; + } + + unset($this->latest); + unset($this->currentChangelog); + } elseif (isset($this->currentChangelog)) { + // The update might be for an older version of j! + unset($this->currentChangelog); + } + break; + } + } + + /** + * Character Parser Function + * + * @param object $parser Parser object. + * @param object $data The data. + * + * @return void + * + * @note This is public because its called externally. + * @since 1.7.0 + */ + public function characterData($parser, $data) + { + $tag = $this->getLastTag(); + + switch ($tag) { + case 'ITEM': + $this->items[] = $data; + break; + case 'SECURITY': + case 'FIX': + case 'LANGUAGE': + case 'ADDITION': + case 'CHANGE': + case 'REMOVE': + case 'NOTE': + break; + default: + // Throw the data for this item together + $tag = strtolower($tag); + + if (isset($this->currentChangelog->$tag)) { + $this->currentChangelog->$tag->data .= $data; + } + break; + } + } + + /** + * Loads an XML file from a URL. + * + * @param string $url The URL. + * + * @return boolean True on success + * + * @since 4.0.0 + */ + public function loadFromXml($url) + { + $version = new Version(); + $httpOption = new Registry(); + $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); + + try { + $http = HttpFactory::getHttp($httpOption); + $response = $http->get($url); + } catch (RuntimeException $e) { + $response = null; + } + + if ($response === null || $response->code !== 200) { + // @todo: Add a 'mark bad' setting here somehow + Log::add(Text::sprintf('JLIB_UPDATER_ERROR_EXTENSION_OPEN_URL', $url), Log::WARNING, 'jerror'); + + return false; + } + + $this->currentChangelog = new \stdClass(); + + $this->xmlParser = xml_parser_create(''); + xml_set_object($this->xmlParser, $this); + xml_set_element_handler($this->xmlParser, 'startElement', 'endElement'); + xml_set_character_data_handler($this->xmlParser, 'characterData'); + + if (!xml_parse($this->xmlParser, $response->body)) { + Log::add( + sprintf( + 'XML error: %s at line %d', + xml_error_string(xml_get_error_code($this->xmlParser)), + xml_get_current_line_number($this->xmlParser) + ), + Log::WARNING, + 'updater' + ); + + return false; + } + + xml_parser_free($this->xmlParser); + + return true; + } } diff --git a/code/libraries/src/Client/ClientHelper.php b/code/libraries/src/Client/ClientHelper.php index 26ba9afe..d7eafa48 100644 --- a/code/libraries/src/Client/ClientHelper.php +++ b/code/libraries/src/Client/ClientHelper.php @@ -1,4 +1,5 @@ $app->get('ftp_enable'), - 'host' => $app->get('ftp_host'), - 'port' => $app->get('ftp_port'), - 'user' => $app->get('ftp_user'), - 'pass' => $app->get('ftp_pass'), - 'root' => $app->get('ftp_root'), - ); - break; - - default: - $options = array('enabled' => false, 'host' => '', 'port' => '', 'user' => '', 'pass' => '', 'root' => ''); - break; - } - - // If user and pass are not set in global config lets see if they are in the session - if ($options['enabled'] == true && ($options['user'] == '' || $options['pass'] == '')) - { - $session = Factory::getSession(); - $options['user'] = $session->get($client . '.user', null, 'JClientHelper'); - $options['pass'] = $session->get($client . '.pass', null, 'JClientHelper'); - } - - // If user or pass are missing, disable this client - if ($options['user'] == '' || $options['pass'] == '') - { - $options['enabled'] = false; - } - - // Save the credentials for later use - $credentials[$client] = $options; - } - - return $credentials[$client]; - } - - /** - * Method to set client login credentials - * - * @param string $client Client name, currently only 'ftp' is supported - * @param string $user Username - * @param string $pass Password - * - * @return boolean True if the given login credentials have been set and are valid - * - * @since 1.7.0 - */ - public static function setCredentials($client, $user, $pass) - { - $return = false; - $client = strtolower($client); - - // Test if the given credentials are valid - switch ($client) - { - case 'ftp': - $app = Factory::getApplication(); - $options = array('enabled' => $app->get('ftp_enable'), 'host' => $app->get('ftp_host'), 'port' => $app->get('ftp_port')); - - if ($options['enabled']) - { - $ftp = FtpClient::getInstance($options['host'], $options['port']); - - // Test the connection and try to log in - if ($ftp->isConnected()) - { - if ($ftp->login($user, $pass)) - { - $return = true; - } - - $ftp->quit(); - } - } - break; - - default: - break; - } - - if ($return) - { - // Save valid credentials to the session - $session = Factory::getSession(); - $session->set($client . '.user', $user, 'JClientHelper'); - $session->set($client . '.pass', $pass, 'JClientHelper'); - - // Force re-creation of the data saved within JClientHelper::getCredentials() - self::getCredentials($client, true); - } - - return $return; - } - - /** - * Method to determine if client login credentials are present - * - * @param string $client Client name, currently only 'ftp' is supported - * - * @return boolean True if login credentials are available - * - * @since 1.7.0 - */ - public static function hasCredentials($client) - { - $return = false; - $client = strtolower($client); - - // Get (unmodified) credentials for this client - switch ($client) - { - case 'ftp': - $app = Factory::getApplication(); - $options = array('enabled' => $app->get('ftp_enable'), 'user' => $app->get('ftp_user'), 'pass' => $app->get('ftp_pass')); - break; - - default: - $options = array('enabled' => false, 'user' => '', 'pass' => ''); - break; - } - - if ($options['enabled'] == false) - { - // The client is disabled in global config, so let's pretend we are OK - $return = true; - } - elseif ($options['user'] != '' && $options['pass'] != '') - { - // Login credentials are available in global config - $return = true; - } - else - { - // Check if login credentials are available in the session - $session = Factory::getSession(); - $user = $session->get($client . '.user', null, 'JClientHelper'); - $pass = $session->get($client . '.pass', null, 'JClientHelper'); - - if ($user != '' && $pass != '') - { - $return = true; - } - } - - return $return; - } - - /** - * Determine whether input fields for client settings need to be shown - * - * If valid credentials were passed along with the request, they are saved to the session. - * This functions returns an exception if invalid credentials have been given or if the - * connection to the server failed for some other reason. - * - * @param string $client The name of the client. - * - * @return boolean True if credentials are present - * - * @since 1.7.0 - * @throws \InvalidArgumentException if credentials invalid - */ - public static function setCredentialsFromRequest($client) - { - // Determine whether FTP credentials have been passed along with the current request - $input = Factory::getApplication()->input; - $user = $input->post->getString('username', null); - $pass = $input->post->getString('password', null); - - if ($user != '' && $pass != '') - { - // Add credentials to the session - if (!self::setCredentials($client, $user, $pass)) - { - throw new \InvalidArgumentException('Invalid user credentials'); - } - - $return = false; - } - else - { - // Just determine if the FTP input fields need to be shown - $return = !self::hasCredentials('ftp'); - } - - return $return; - } + /** + * Method to return the array of client layer configuration options + * + * @param string $client Client name, currently only 'ftp' is supported + * @param boolean $force Forces re-creation of the login credentials. Set this to + * true if login credentials in the session storage have changed + * + * @return array Client layer configuration options, consisting of at least + * these fields: enabled, host, port, user, pass, root + * + * @since 1.7.0 + */ + public static function getCredentials($client, $force = false) + { + static $credentials = array(); + + $client = strtolower($client); + + if (!isset($credentials[$client]) || $force) { + $app = Factory::getApplication(); + + // Fetch the client layer configuration options for the specific client + switch ($client) { + case 'ftp': + $options = array( + 'enabled' => $app->get('ftp_enable'), + 'host' => $app->get('ftp_host'), + 'port' => $app->get('ftp_port'), + 'user' => $app->get('ftp_user'), + 'pass' => $app->get('ftp_pass'), + 'root' => $app->get('ftp_root'), + ); + break; + + default: + $options = array('enabled' => false, 'host' => '', 'port' => '', 'user' => '', 'pass' => '', 'root' => ''); + break; + } + + // If user and pass are not set in global config lets see if they are in the session + if ($options['enabled'] == true && ($options['user'] == '' || $options['pass'] == '')) { + $session = Factory::getSession(); + $options['user'] = $session->get($client . '.user', null, 'JClientHelper'); + $options['pass'] = $session->get($client . '.pass', null, 'JClientHelper'); + } + + // If user or pass are missing, disable this client + if ($options['user'] == '' || $options['pass'] == '') { + $options['enabled'] = false; + } + + // Save the credentials for later use + $credentials[$client] = $options; + } + + return $credentials[$client]; + } + + /** + * Method to set client login credentials + * + * @param string $client Client name, currently only 'ftp' is supported + * @param string $user Username + * @param string $pass Password + * + * @return boolean True if the given login credentials have been set and are valid + * + * @since 1.7.0 + */ + public static function setCredentials($client, $user, $pass) + { + $return = false; + $client = strtolower($client); + + // Test if the given credentials are valid + switch ($client) { + case 'ftp': + $app = Factory::getApplication(); + $options = array('enabled' => $app->get('ftp_enable'), 'host' => $app->get('ftp_host'), 'port' => $app->get('ftp_port')); + + if ($options['enabled']) { + $ftp = FtpClient::getInstance($options['host'], $options['port']); + + // Test the connection and try to log in + if ($ftp->isConnected()) { + if ($ftp->login($user, $pass)) { + $return = true; + } + + $ftp->quit(); + } + } + break; + + default: + break; + } + + if ($return) { + // Save valid credentials to the session + $session = Factory::getSession(); + $session->set($client . '.user', $user, 'JClientHelper'); + $session->set($client . '.pass', $pass, 'JClientHelper'); + + // Force re-creation of the data saved within JClientHelper::getCredentials() + self::getCredentials($client, true); + } + + return $return; + } + + /** + * Method to determine if client login credentials are present + * + * @param string $client Client name, currently only 'ftp' is supported + * + * @return boolean True if login credentials are available + * + * @since 1.7.0 + */ + public static function hasCredentials($client) + { + $return = false; + $client = strtolower($client); + + // Get (unmodified) credentials for this client + switch ($client) { + case 'ftp': + $app = Factory::getApplication(); + $options = array('enabled' => $app->get('ftp_enable'), 'user' => $app->get('ftp_user'), 'pass' => $app->get('ftp_pass')); + break; + + default: + $options = array('enabled' => false, 'user' => '', 'pass' => ''); + break; + } + + if ($options['enabled'] == false) { + // The client is disabled in global config, so let's pretend we are OK + $return = true; + } elseif ($options['user'] != '' && $options['pass'] != '') { + // Login credentials are available in global config + $return = true; + } else { + // Check if login credentials are available in the session + $session = Factory::getSession(); + $user = $session->get($client . '.user', null, 'JClientHelper'); + $pass = $session->get($client . '.pass', null, 'JClientHelper'); + + if ($user != '' && $pass != '') { + $return = true; + } + } + + return $return; + } + + /** + * Determine whether input fields for client settings need to be shown + * + * If valid credentials were passed along with the request, they are saved to the session. + * This functions returns an exception if invalid credentials have been given or if the + * connection to the server failed for some other reason. + * + * @param string $client The name of the client. + * + * @return boolean True if credentials are present + * + * @since 1.7.0 + * @throws \InvalidArgumentException if credentials invalid + */ + public static function setCredentialsFromRequest($client) + { + // Determine whether FTP credentials have been passed along with the current request + $input = Factory::getApplication()->input; + $user = $input->post->getString('username', null); + $pass = $input->post->getString('password', null); + + if ($user != '' && $pass != '') { + // Add credentials to the session + if (!self::setCredentials($client, $user, $pass)) { + throw new \InvalidArgumentException('Invalid user credentials'); + } + + $return = false; + } else { + // Just determine if the FTP input fields need to be shown + $return = !self::hasCredentials('ftp'); + } + + return $return; + } } diff --git a/code/libraries/src/Client/FtpClient.php b/code/libraries/src/Client/FtpClient.php index f7d86fb5..4d65ead4 100644 --- a/code/libraries/src/Client/FtpClient.php +++ b/code/libraries/src/Client/FtpClient.php @@ -1,4 +1,5 @@ "\n", 'WIN' => "\r\n"); - - /** - * @var array FtpClient instances container. - * @since 2.5 - */ - protected static $instances = array(); - - /** - * FtpClient object constructor - * - * @param array $options Associative array of options to set - * - * @since 1.5 - */ - public function __construct(array $options = array()) - { - // If default transfer type is not set, set it to autoascii detect - if (!isset($options['type'])) - { - $options['type'] = FTP_BINARY; - } - - $this->setOptions($options); - - if (FTP_NATIVE) - { - BufferStreamHandler::stream_register(); - } - } - - /** - * FtpClient object destructor - * - * Closes an existing connection, if we have one - * - * @since 1.5 - */ - public function __destruct() - { - if (\is_resource($this->_conn)) - { - $this->quit(); - } - } - - /** - * Returns the global FTP connector object, only creating it - * if it doesn't already exist. - * - * You may optionally specify a username and password in the parameters. If you do so, - * you may not login() again with different credentials using the same object. - * If you do not use this option, you must quit() the current connection when you - * are done, to free it for use by others. - * - * @param string $host Host to connect to - * @param string $port Port to connect to - * @param array $options Array with any of these options: type=>[FTP_AUTOASCII|FTP_ASCII|FTP_BINARY], timeout=>(int) - * @param string $user Username to use for a connection - * @param string $pass Password to use for a connection - * - * @return FtpClient The FTP Client object. - * - * @since 1.5 - */ - public static function getInstance($host = '127.0.0.1', $port = '21', array $options = array(), $user = null, $pass = null) - { - $signature = $user . ':' . $pass . '@' . $host . ':' . $port; - - // Create a new instance, or set the options of an existing one - if (!isset(static::$instances[$signature]) || !\is_object(static::$instances[$signature])) - { - static::$instances[$signature] = new static($options); - } - else - { - static::$instances[$signature]->setOptions($options); - } - - // Connect to the server, and login, if requested - if (!static::$instances[$signature]->isConnected()) - { - $return = static::$instances[$signature]->connect($host, $port); - - if ($return && $user !== null && $pass !== null) - { - static::$instances[$signature]->login($user, $pass); - } - } - - return static::$instances[$signature]; - } - - /** - * Set client options - * - * @param array $options Associative array of options to set - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function setOptions(array $options) - { - if (isset($options['type'])) - { - $this->_type = $options['type']; - } - - if (isset($options['timeout'])) - { - $this->_timeout = $options['timeout']; - } - - return true; - } - - /** - * Method to connect to a FTP server - * - * @param string $host Host to connect to [Default: 127.0.0.1] - * @param int $port Port to connect on [Default: port 21] - * - * @return boolean True if successful - * - * @since 3.0.0 - */ - public function connect($host = '127.0.0.1', $port = 21) - { - $errno = null; - $err = null; - - // If already connected, return - if (\is_resource($this->_conn)) - { - return true; - } - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - $this->_conn = @ftp_connect($host, $port, $this->_timeout); - - if ($this->_conn === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__, $host, $port), Log::WARNING, 'jerror'); - - return false; - } - - // Set the timeout for this connection - ftp_set_option($this->_conn, FTP_TIMEOUT_SEC, $this->_timeout); - - return true; - } - - // Connect to the FTP server. - $this->_conn = @ fsockopen($host, $port, $errno, $err, $this->_timeout); - - if (!$this->_conn) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT_SOCKET', __METHOD__, $host, $port, $errno, $err), Log::WARNING, 'jerror'); - - return false; - } - - // Set the timeout for this connection - socket_set_timeout($this->_conn, $this->_timeout, 0); - - // Check for welcome response code - if (!$this->_verifyResponse(220)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 220), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to determine if the object is connected to an FTP server - * - * @return boolean True if connected - * - * @since 1.5 - */ - public function isConnected() - { - return \is_resource($this->_conn); - } - - /** - * Method to login to a server once connected - * - * @param string $user Username to login to the server - * @param string $pass Password to login to the server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function login($user = 'anonymous', $pass = 'jftp@joomla.org') - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_login($this->_conn, $user, $pass) === false) - { - Log::add('JFtp::login: Unable to login', Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send the username - if (!$this->_putCmd('USER ' . $user, array(331, 503))) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_USERNAME', __METHOD__, $this->_response, $user), Log::WARNING, 'jerror'); - - return false; - } - - // If we are already logged in, continue :) - if ($this->_responseCode == 503) - { - return true; - } - - // Send the password - if (!$this->_putCmd('PASS ' . $pass, 230)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_PASSWORD', __METHOD__, $this->_response, str_repeat('*', \strlen($pass))), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to quit and close the connection - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function quit() - { - // If native FTP support is enabled lets use it... - if (FTP_NATIVE) - { - @ftp_close($this->_conn); - - return true; - } - - // Logout and close connection - @fwrite($this->_conn, "QUIT\r\n"); - @fclose($this->_conn); - - return true; - } - - /** - * Method to retrieve the current working directory on the FTP server - * - * @return string Current working directory - * - * @since 1.5 - */ - public function pwd() - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (($ret = @ftp_pwd($this->_conn)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return $ret; - } - - $match = array(null); - - // Send print working directory command and verify success - if (!$this->_putCmd('PWD', 257)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 257), Log::WARNING, 'jerror'); - - return false; - } - - // Match just the path - preg_match('/"[^"\r\n]*"/', $this->_response, $match); - - // Return the cleaned path - return preg_replace("/\"/", '', $match[0]); - } - - /** - * Method to system string from the FTP server - * - * @return string System identifier string - * - * @since 1.5 - */ - public function syst() - { - // If native FTP support is enabled lets use it... - if (FTP_NATIVE) - { - if (($ret = @ftp_systype($this->_conn)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - } - else - { - // Send print working directory command and verify success - if (!$this->_putCmd('SYST', 215)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 215), Log::WARNING, 'jerror'); - - return false; - } - - $ret = $this->_response; - } - - // Match the system string to an OS - if (strpos(strtoupper($ret), 'MAC') !== false) - { - $ret = 'MAC'; - } - elseif (strpos(strtoupper($ret), 'WIN') !== false) - { - $ret = 'WIN'; - } - else - { - $ret = 'UNIX'; - } - - // Return the os type - return $ret; - } - - /** - * Method to change the current working directory on the FTP server - * - * @param string $path Path to change into on the server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function chdir($path) - { - // If native FTP support is enabled lets use it... - if (FTP_NATIVE) - { - if (@ftp_chdir($this->_conn, $path) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send change directory command and verify success - if (!$this->_putCmd('CWD ' . $path, 250)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 250, $path), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to reinitialise the server, ie. need to login again - * - * NOTE: This command not available on all servers - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function reinit() - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_site($this->_conn, 'REIN') === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send reinitialise command to the server - if (!$this->_putCmd('REIN', 220)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 220), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to rename a file/folder on the FTP server - * - * @param string $from Path to change file/folder from - * @param string $to Path to change file/folder to - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function rename($from, $to) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_rename($this->_conn, $from, $to) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send rename from command to the server - if (!$this->_putCmd('RNFR ' . $from, 350)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RENAME_BAD_RESPONSE_FROM', __METHOD__, $this->_response, $from), Log::WARNING, 'jerror'); - - return false; - } - - // Send rename to command to the server - if (!$this->_putCmd('RNTO ' . $to, 250)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RENAME_BAD_RESPONSE_TO', __METHOD__, $this->_response, $to), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to change mode for a path on the FTP server - * - * @param string $path Path to change mode on - * @param mixed $mode Octal value to change mode to, e.g. '0777', 0777 or 511 (string or integer) - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function chmod($path, $mode) - { - // If no filename is given, we assume the current directory is the target - if ($path == '') - { - $path = '.'; - } - - // Convert the mode to a string - if (\is_int($mode)) - { - $mode = decoct($mode); - } - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_site($this->_conn, 'CHMOD ' . $mode . ' ' . $path) === false) - { - if (!IS_WIN) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - } - - return false; - } - - return true; - } - - // Send change mode command and verify success [must convert mode from octal] - if (!$this->_putCmd('SITE CHMOD ' . $mode . ' ' . $path, array(200, 250))) - { - if (!IS_WIN) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_CHMOD_BAD_RESPONSE', __METHOD__, $this->_response, $path, $mode), Log::WARNING, 'jerror'); - } - - return false; - } - - return true; - } - - /** - * Method to delete a path [file/folder] on the FTP server - * - * @param string $path Path to delete - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function delete($path) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_delete($this->_conn, $path) === false) - { - if (@ftp_rmdir($this->_conn, $path) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - } - - return true; - } - - // Send delete file command and if that doesn't work, try to remove a directory - if (!$this->_putCmd('DELE ' . $path, 250)) - { - if (!$this->_putCmd('RMD ' . $path, 250)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 250, $path), Log::WARNING, 'jerror'); - - return false; - } - } - - return true; - } - - /** - * Method to create a directory on the FTP server - * - * @param string $path Directory to create - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function mkdir($path) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_mkdir($this->_conn, $path) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send change directory command and verify success - if (!$this->_putCmd('MKD ' . $path, 257)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 257, $path), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to restart data transfer at a given byte - * - * @param integer $point Byte to restart transfer at - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function restart($point) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_site($this->_conn, 'REST ' . $point) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send restart command and verify success - if (!$this->_putCmd('REST ' . $point, 350)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RESTART_BAD_RESPONSE', __METHOD__, $this->_response, $point), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to create an empty file on the FTP server - * - * @param string $path Path local file to store on the FTP server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function create($path) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $buffer = fopen('buffer://tmp', 'r'); - - if (@ftp_fput($this->_conn, $path, $buffer, FTP_ASCII) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - fclose($buffer); - - return false; - } - - fclose($buffer); - - return true; - } - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (!$this->_putCmd('STOR ' . $path, array(150, 125))) - { - @ fclose($this->_dataconn); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - // To create a zero byte upload close the data port connection - fclose($this->_dataconn); - - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to read a file from the FTP server's contents into a buffer - * - * @param string $remote Path to remote file to read on the FTP server - * @param string &$buffer Buffer variable to read file contents into - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function read($remote, &$buffer) - { - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $tmp = fopen('buffer://tmp', 'br+'); - - if (@ftp_fget($this->_conn, $tmp, $remote, $mode) === false) - { - fclose($tmp); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Read tmp buffer contents - rewind($tmp); - $buffer = ''; - - while (!feof($tmp)) - { - $buffer .= fread($tmp, 8192); - } - - fclose($tmp); - - return true; - } - - $this->_mode($mode); - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (!$this->_putCmd('RETR ' . $remote, array(150, 125))) - { - @ fclose($this->_dataconn); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - // Read data from data port connection and add to the buffer - $buffer = ''; - - while (!feof($this->_dataconn)) - { - $buffer .= fread($this->_dataconn, 4096); - } - - // Close the data port connection - fclose($this->_dataconn); - - // Let's try to cleanup some line endings if it is ascii - if ($mode == FTP_ASCII) - { - $os = 'UNIX'; - - if (IS_WIN) - { - $os = 'WIN'; - } - - $buffer = preg_replace('/' . CRLF . '/', $this->_lineEndings[$os], $buffer); - } - - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to get a file from the FTP server and save it to a local file - * - * @param string $local Local path to save remote file to - * @param string $remote Path to remote file to get on the FTP server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function get($local, $remote) - { - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (@ftp_get($this->_conn, $local, $remote, $mode) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - $this->_mode($mode); - - // Check to see if the local file can be opened for writing - $fp = fopen($local, 'wb'); - - if (!$fp) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_OPEN_WRITING', __METHOD__, $local), Log::WARNING, 'jerror'); - - return false; - } - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (!$this->_putCmd('RETR ' . $remote, array(150, 125))) - { - @ fclose($this->_dataconn); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - // Read data from data port connection and add to the buffer - while (!feof($this->_dataconn)) - { - $buffer = fread($this->_dataconn, 4096); - fwrite($fp, $buffer, 4096); - } - - // Close the data port connection and file pointer - fclose($this->_dataconn); - fclose($fp); - - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to store a file to the FTP server - * - * @param string $local Path to local file to store on the FTP server - * @param string $remote FTP path to file to create - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function store($local, $remote = null) - { - // If remote file is not given, use the filename of the local file in the current - // working directory. - if ($remote == null) - { - $remote = basename($local); - } - - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (@ftp_put($this->_conn, $remote, $local, $mode) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - $this->_mode($mode); - - // Check to see if the local file exists and if so open it for reading - if (@ file_exists($local)) - { - $fp = fopen($local, 'rb'); - - if (!$fp) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_OPEN_READING', __METHOD__, $local), Log::WARNING, 'jerror'); - - return false; - } - } - else - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_FIND', __METHOD__, $local), Log::WARNING, 'jerror'); - - return false; - } - - // Start passive mode - if (!$this->_passive()) - { - @ fclose($fp); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Send store command to the FTP server - if (!$this->_putCmd('STOR ' . $remote, array(150, 125))) - { - @ fclose($fp); - @ fclose($this->_dataconn); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - // Do actual file transfer, read local file and write to data port connection - while (!feof($fp)) - { - $line = fread($fp, 4096); - - do - { - if (($result = @ fwrite($this->_dataconn, $line)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $line = substr($line, $result); - } - while ($line != ''); - } - - fclose($fp); - fclose($this->_dataconn); - - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to write a string to the FTP server - * - * @param string $remote FTP path to file to write to - * @param string $buffer Contents to write to the FTP server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function write($remote, $buffer) - { - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $tmp = fopen('buffer://tmp', 'br+'); - fwrite($tmp, $buffer); - rewind($tmp); - - if (@ftp_fput($this->_conn, $remote, $tmp, $mode) === false) - { - fclose($tmp); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - fclose($tmp); - - return true; - } - - // First we need to set the transfer mode - $this->_mode($mode); - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Send store command to the FTP server - if (!$this->_putCmd('STOR ' . $remote, array(150, 125))) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - @ fclose($this->_dataconn); - - return false; - } - - // Write buffer to the data connection port - do - { - if (($result = @ fwrite($this->_dataconn, $buffer)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $buffer = substr($buffer, $result); - } - while ($buffer != ''); - - // Close the data connection port [Data transfer complete] - fclose($this->_dataconn); - - // Verify that the server received the transfer - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to append a string to the FTP server - * - * @param string $remote FTP path to file to append to - * @param string $buffer Contents to append to the FTP server - * - * @return boolean True if successful - * - * @since 3.6.0 - */ - public function append($remote, $buffer) - { - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); - } - - $tmp = fopen('buffer://tmp', 'bw+'); - fwrite($tmp, $buffer); - rewind($tmp); - - $size = $this->size($remote); - - if ($size === false) - { - } - - if (@ftp_fput($this->_conn, $remote, $tmp, $mode, $size) === false) - { - fclose($tmp); - - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), 35); - } - - fclose($tmp); - - return true; - } - - // First we need to set the transfer mode - $this->_mode($mode); - - // Start passive mode - if (!$this->_passive()) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); - } - - // Send store command to the FTP server - if (!$this->_putCmd('APPE ' . $remote, array(150, 125))) - { - @fclose($this->_dataconn); - - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), 35); - } - - // Write buffer to the data connection port - do - { - if (($result = @ fwrite($this->_dataconn, $buffer)) === false) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), 37); - } - - $buffer = substr($buffer, $result); - } - while ($buffer != ''); - - // Close the data connection port [Data transfer complete] - fclose($this->_dataconn); - - // Verify that the server received the transfer - if (!$this->_verifyResponse(226)) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), 37); - } - - return true; - } - - /** - * Get the size of the remote file. - * - * @param string $remote FTP path to file whose size to get - * - * @return mixed number of bytes or false on error - * - * @since 3.6.0 - */ - public function size($remote) - { - if (FTP_NATIVE) - { - $size = ftp_size($this->_conn, $remote); - - // In case ftp_size fails, try the SIZE command directly. - if ($size === -1) - { - $response = ftp_raw($this->_conn, 'SIZE ' . $remote); - $responseCode = substr($response[0], 0, 3); - $responseMessage = substr($response[0], 4); - - if ($responseCode != '213') - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), 35); - } - - $size = (int) $responseMessage; - } - - return $size; - } - - // Start passive mode - if (!$this->_passive()) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); - } - - // Send size command to the FTP server - if (!$this->_putCmd('SIZE ' . $remote, array(213))) - { - @fclose($this->_dataconn); - - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 213, $remote), 35); - } - - return (int) substr($this->_responseMsg, 4); - } - - /** - * Method to list the filenames of the contents of a directory on the FTP server - * - * Note: Some servers also return folder names. However, to be sure to list folders on all - * servers, you should use listDetails() instead if you also need to deal with folders - * - * @param string $path Path local file to store on the FTP server - * - * @return string Directory listing - * - * @since 1.5 - */ - public function listNames($path = null) - { - $data = null; - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (($list = @ftp_nlist($this->_conn, $path)) === false) - { - // Workaround for empty directories on some servers - if ($this->listDetails($path, 'files') === array()) - { - return array(); - } - - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $list = preg_replace('#^' . preg_quote($path, '#') . '[/\\\\]?#', '', $list); - - if ($keys = array_merge(array_keys($list, '.'), array_keys($list, '..'))) - { - foreach ($keys as $key) - { - unset($list[$key]); - } - } - - return $list; - } - - // If a path exists, prepend a space - if ($path != null) - { - $path = ' ' . $path; - } - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (!$this->_putCmd('NLST' . $path, array(150, 125))) - { - @ fclose($this->_dataconn); - - // Workaround for empty directories on some servers - if ($this->listDetails($path, 'files') === array()) - { - return array(); - } - - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - // Read in the file listing. - while (!feof($this->_dataconn)) - { - $data .= fread($this->_dataconn, 4096); - } - - fclose($this->_dataconn); - - // Everything go okay? - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - $data = preg_split('/[' . CRLF . ']+/', $data, -1, PREG_SPLIT_NO_EMPTY); - $data = preg_replace('#^' . preg_quote(substr($path, 1), '#') . '[/\\\\]?#', '', $data); - - if ($keys = array_merge(array_keys($data, '.'), array_keys($data, '..'))) - { - foreach ($keys as $key) - { - unset($data[$key]); - } - } - - return $data; - } - - /** - * Method to list the contents of a directory on the FTP server - * - * @param string $path Path to the local file to be stored on the FTP server - * @param string $type Return type [raw|all|folders|files] - * - * @return mixed If $type is raw: string Directory listing, otherwise array of string with file-names - * - * @since 1.5 - */ - public function listDetails($path = null, $type = 'all') - { - $dir_list = array(); - $data = null; - $regs = null; - - // @todo: Deal with recurse -- nightmare - // For now we will just set it to false - $recurse = false; - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (($contents = @ftp_rawlist($this->_conn, $path)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - } - else - { - // Non Native mode - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // If a path exists, prepend a space - if ($path != null) - { - $path = ' ' . $path; - } - - // Request the file listing - if (!$this->_putCmd(($recurse == true) ? 'LIST -R' : 'LIST' . $path, array(150, 125))) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - @ fclose($this->_dataconn); - - return false; - } - - // Read in the file listing. - while (!feof($this->_dataconn)) - { - $data .= fread($this->_dataconn, 4096); - } - - fclose($this->_dataconn); - - // Everything go okay? - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - $contents = explode(CRLF, $data); - } - - // If only raw output is requested we are done - if ($type === 'raw') - { - return $data; - } - - // If we received the listing of an empty directory, we are done as well - if (empty($contents[0])) - { - return $dir_list; - } - - // If the server returned the number of results in the first response, let's dump it - if (strtolower(substr($contents[0], 0, 6)) === 'total ') - { - array_shift($contents); - - if (!isset($contents[0]) || empty($contents[0])) - { - return $dir_list; - } - } - - // Regular expressions for the directory listing parsing. - $regexps = array( - 'UNIX' => '#([-dl][rwxstST-]+).* ([0-9]*) ([a-zA-Z0-9]+).* ([a-zA-Z0-9]+).* ([0-9]*)' - . ' ([a-zA-Z]+[0-9: ]*[0-9])[ ]+(([0-9]{1,2}:[0-9]{2})|[0-9]{4}) (.+)#', - 'MAC' => '#([-dl][rwxstST-]+).* ?([0-9 ]*)?([a-zA-Z0-9]+).* ([a-zA-Z0-9]+).* ([0-9]*)' - . ' ([a-zA-Z]+[0-9: ]*[0-9])[ ]+(([0-9]{2}:[0-9]{2})|[0-9]{4}) (.+)#', - 'WIN' => '#([0-9]{2})-([0-9]{2})-([0-9]{2}) +([0-9]{2}):([0-9]{2})(AM|PM) +([0-9]+|) +(.+)#', - ); - - // Find out the format of the directory listing by matching one of the regexps - $osType = null; - - foreach ($regexps as $k => $v) - { - if (@preg_match($v, $contents[0])) - { - $osType = $k; - $regexp = $v; - break; - } - } - - if (!$osType) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_UNRECOGNISED_FOLDER_LISTING_FORMATJLIB_CLIENT_ERROR_JFTP_LISTDETAILS_UNRECOGNISED', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Here is where it is going to get dirty.... - if ($osType === 'UNIX' || $osType === 'MAC') - { - foreach ($contents as $file) - { - $tmp_array = null; - - if (@preg_match($regexp, $file, $regs)) - { - $fType = (int) strpos('-dl', $regs[1][0]); - - // $tmp_array['line'] = $regs[0]; - $tmp_array['type'] = $fType; - $tmp_array['rights'] = $regs[1]; - - // $tmp_array['number'] = $regs[2]; - $tmp_array['user'] = $regs[3]; - $tmp_array['group'] = $regs[4]; - $tmp_array['size'] = $regs[5]; - $tmp_array['date'] = @date('m-d', strtotime($regs[6])); - $tmp_array['time'] = $regs[7]; - $tmp_array['name'] = $regs[9]; - } - - // If we just want files, do not add a folder - if ($type === 'files' && $tmp_array['type'] == 1) - { - continue; - } - - // If we just want folders, do not add a file - if ($type === 'folders' && $tmp_array['type'] == 0) - { - continue; - } - - if (\is_array($tmp_array) && $tmp_array['name'] != '.' && $tmp_array['name'] != '..') - { - $dir_list[] = $tmp_array; - } - } - } - else - { - foreach ($contents as $file) - { - $tmp_array = null; - - if (@preg_match($regexp, $file, $regs)) - { - $fType = (int) ($regs[7] === ''); - $timestamp = strtotime("$regs[3]-$regs[1]-$regs[2] $regs[4]:$regs[5]$regs[6]"); - - // $tmp_array['line'] = $regs[0]; - $tmp_array['type'] = $fType; - $tmp_array['rights'] = ''; - - // $tmp_array['number'] = 0; - $tmp_array['user'] = ''; - $tmp_array['group'] = ''; - $tmp_array['size'] = (int) $regs[7]; - $tmp_array['date'] = date('m-d', $timestamp); - $tmp_array['time'] = date('H:i', $timestamp); - $tmp_array['name'] = $regs[8]; - } - - // If we just want files, do not add a folder - if ($type === 'files' && $tmp_array['type'] == 1) - { - continue; - } - - // If we just want folders, do not add a file - if ($type === 'folders' && $tmp_array['type'] == 0) - { - continue; - } - - if (\is_array($tmp_array) && $tmp_array['name'] != '.' && $tmp_array['name'] != '..') - { - $dir_list[] = $tmp_array; - } - } - } - - return $dir_list; - } - - /** - * Send command to the FTP server and validate an expected response code - * - * @param string $cmd Command to send to the FTP server - * @param mixed $expectedResponse Integer response code or array of integer response codes - * - * @return boolean True if command executed successfully - * - * @since 1.5 - */ - protected function _putCmd($cmd, $expectedResponse) - { - // Make sure we have a connection to the server - if (!\is_resource($this->_conn)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PUTCMD_UNCONNECTED', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Send the command to the server - if (!fwrite($this->_conn, $cmd . "\r\n")) - { - Log::add(Text::sprintf('DDD', Text::sprintf('JLIB_CLIENT_ERROR_FTP_PUTCMD_SEND', __METHOD__, $cmd)), Log::WARNING, 'jerror'); - } - - return $this->_verifyResponse($expectedResponse); - } - - /** - * Verify the response code from the server and log response if flag is set - * - * @param mixed $expected Integer response code or array of integer response codes - * - * @return boolean True if response code from the server is expected - * - * @since 1.5 - */ - protected function _verifyResponse($expected) - { - $parts = null; - - // Wait for a response from the server, but timeout after the set time limit - $endTime = time() + $this->_timeout; - $this->_response = ''; - - do - { - $this->_response .= fgets($this->_conn, 4096); - } - while (!preg_match('/^([0-9]{3})(-(.*' . CRLF . ')+\1)? [^' . CRLF . ']+' . CRLF . "$/", $this->_response, $parts) && time() < $endTime); - - // Catch a timeout or bad response - if (!isset($parts[1])) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TIMEOUT', __METHOD__, $this->_response), Log::WARNING, 'jerror'); - - return false; - } - - // Separate the code from the message - $this->_responseCode = $parts[1]; - $this->_responseMsg = $parts[0]; - - // Did the server respond with the code we wanted? - if (\is_array($expected)) - { - if (\in_array($this->_responseCode, $expected)) - { - $retval = true; - } - else - { - $retval = false; - } - } - else - { - if ($this->_responseCode == $expected) - { - $retval = true; - } - else - { - $retval = false; - } - } - - return $retval; - } - - /** - * Set server to passive mode and open a data port connection - * - * @return boolean True if successful - * - * @since 1.5 - */ - protected function _passive() - { - $match = array(); - $parts = array(); - $errno = null; - $err = null; - - // Make sure we have a connection to the server - if (!\is_resource($this->_conn)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Request a passive connection - this means, we'll talk to you, you don't talk to us. - @ fwrite($this->_conn, "PASV\r\n"); - - // Wait for a response from the server, but timeout after the set time limit - $endTime = time() + $this->_timeout; - $this->_response = ''; - - do - { - $this->_response .= fgets($this->_conn, 4096); - } - while (!preg_match('/^([0-9]{3})(-(.*' . CRLF . ')+\1)? [^' . CRLF . ']+' . CRLF . "$/", $this->_response, $parts) && time() < $endTime); - - // Catch a timeout or bad response - if (!isset($parts[1])) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TIMEOUT', __METHOD__, $this->_response), Log::WARNING, 'jerror'); - - return false; - } - - // Separate the code from the message - $this->_responseCode = $parts[1]; - $this->_responseMsg = $parts[0]; - - // If it's not 227, we weren't given an IP and port, which means it failed. - if ($this->_responseCode != '227') - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE_IP_OBTAIN', __METHOD__, $this->_responseMsg), Log::WARNING, 'jerror'); - - return false; - } - - // Snatch the IP and port information, or die horribly trying... - if (preg_match('~\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))\)~', $this->_responseMsg, $match) == 0) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE_IP_VALID', __METHOD__, $this->_responseMsg), Log::WARNING, 'jerror'); - - return false; - } - - // This is pretty simple - store it for later use ;). - $this->_pasv = array('ip' => $match[1] . '.' . $match[2] . '.' . $match[3] . '.' . $match[4], 'port' => $match[5] * 256 + $match[6]); - - // Connect, assuming we've got a connection. - $this->_dataconn = @fsockopen($this->_pasv['ip'], $this->_pasv['port'], $errno, $err, $this->_timeout); - - if (!$this->_dataconn) - { - Log::add( - Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__, $this->_pasv['ip'], $this->_pasv['port'], $errno, $err), - Log::WARNING, - 'jerror' - ); - - return false; - } - - // Set the timeout for this connection - socket_set_timeout($this->_conn, $this->_timeout, 0); - - return true; - } - - /** - * Method to find out the correct transfer mode for a specific file - * - * @param string $fileName Name of the file - * - * @return integer Transfer-mode for this filetype [FTP_ASCII|FTP_BINARY] - * - * @since 1.5 - */ - protected function _findMode($fileName) - { - if ($this->_type == FTP_AUTOASCII) - { - $dot = strrpos($fileName, '.') + 1; - $ext = substr($fileName, $dot); - - if (\in_array($ext, $this->_autoAscii)) - { - $mode = FTP_ASCII; - } - else - { - $mode = FTP_BINARY; - } - } - elseif ($this->_type == FTP_ASCII) - { - $mode = FTP_ASCII; - } - else - { - $mode = FTP_BINARY; - } - - return $mode; - } - - /** - * Set transfer mode - * - * @param integer $mode Integer representation of data transfer mode [1:Binary|0:Ascii] - * Defined constants can also be used [FTP_BINARY|FTP_ASCII] - * - * @return boolean True if successful - * - * @since 1.5 - */ - protected function _mode($mode) - { - if ($mode == FTP_BINARY) - { - if (!$this->_putCmd('TYPE I', 200)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_MODE_BINARY', __METHOD__, $this->_response), Log::WARNING, 'jerror'); - - return false; - } - } - else - { - if (!$this->_putCmd('TYPE A', 200)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_MODE_ASCII', __METHOD__, $this->_response), Log::WARNING, 'jerror'); - - return false; - } - } - - return true; - } + /** + * @var resource Socket resource + * @since 1.5 + */ + protected $_conn = null; + + /** + * @var resource Data port connection resource + * @since 1.5 + */ + protected $_dataconn = null; + + /** + * @var array Passive connection information + * @since 1.5 + */ + protected $_pasv = null; + + /** + * @var string Response Message + * @since 1.5 + */ + protected $_response = null; + + /** + * @var integer Timeout limit + * @since 1.5 + */ + protected $_timeout = 15; + + /** + * @var integer Transfer Type + * @since 1.5 + */ + protected $_type = null; + + /** + * @var array Array to hold ascii format file extensions + * @since 1.5 + */ + protected $_autoAscii = array( + 'asp', + 'bat', + 'c', + 'cpp', + 'csv', + 'h', + 'htm', + 'html', + 'shtml', + 'ini', + 'inc', + 'log', + 'php', + 'php3', + 'pl', + 'perl', + 'sh', + 'sql', + 'txt', + 'xhtml', + 'xml', + ); + + /** + * Array to hold native line ending characters + * + * @var array + * @since 1.5 + */ + protected $_lineEndings = array('UNIX' => "\n", 'WIN' => "\r\n"); + + /** + * @var array FtpClient instances container. + * @since 2.5 + */ + protected static $instances = array(); + + /** + * FtpClient object constructor + * + * @param array $options Associative array of options to set + * + * @since 1.5 + */ + public function __construct(array $options = array()) + { + // If default transfer type is not set, set it to autoascii detect + if (!isset($options['type'])) { + $options['type'] = FTP_BINARY; + } + + $this->setOptions($options); + + if (FTP_NATIVE) { + BufferStreamHandler::stream_register(); + } + } + + /** + * FtpClient object destructor + * + * Closes an existing connection, if we have one + * + * @since 1.5 + */ + public function __destruct() + { + if (\is_resource($this->_conn)) { + $this->quit(); + } + } + + /** + * Returns the global FTP connector object, only creating it + * if it doesn't already exist. + * + * You may optionally specify a username and password in the parameters. If you do so, + * you may not login() again with different credentials using the same object. + * If you do not use this option, you must quit() the current connection when you + * are done, to free it for use by others. + * + * @param string $host Host to connect to + * @param string $port Port to connect to + * @param array $options Array with any of these options: type=>[FTP_AUTOASCII|FTP_ASCII|FTP_BINARY], timeout=>(int) + * @param string $user Username to use for a connection + * @param string $pass Password to use for a connection + * + * @return FtpClient The FTP Client object. + * + * @since 1.5 + */ + public static function getInstance($host = '127.0.0.1', $port = '21', array $options = array(), $user = null, $pass = null) + { + $signature = $user . ':' . $pass . '@' . $host . ':' . $port; + + // Create a new instance, or set the options of an existing one + if (!isset(static::$instances[$signature]) || !\is_object(static::$instances[$signature])) { + static::$instances[$signature] = new static($options); + } else { + static::$instances[$signature]->setOptions($options); + } + + // Connect to the server, and login, if requested + if (!static::$instances[$signature]->isConnected()) { + $return = static::$instances[$signature]->connect($host, $port); + + if ($return && $user !== null && $pass !== null) { + static::$instances[$signature]->login($user, $pass); + } + } + + return static::$instances[$signature]; + } + + /** + * Set client options + * + * @param array $options Associative array of options to set + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function setOptions(array $options) + { + if (isset($options['type'])) { + $this->_type = $options['type']; + } + + if (isset($options['timeout'])) { + $this->_timeout = $options['timeout']; + } + + return true; + } + + /** + * Method to connect to a FTP server + * + * @param string $host Host to connect to [Default: 127.0.0.1] + * @param int $port Port to connect on [Default: port 21] + * + * @return boolean True if successful + * + * @since 3.0.0 + */ + public function connect($host = '127.0.0.1', $port = 21) + { + $errno = null; + $err = null; + + // If already connected, return + if (\is_resource($this->_conn)) { + return true; + } + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + $this->_conn = @ftp_connect($host, $port, $this->_timeout); + + if ($this->_conn === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__, $host, $port), Log::WARNING, 'jerror'); + + return false; + } + + // Set the timeout for this connection + ftp_set_option($this->_conn, FTP_TIMEOUT_SEC, $this->_timeout); + + return true; + } + + // Connect to the FTP server. + $this->_conn = @ fsockopen($host, $port, $errno, $err, $this->_timeout); + + if (!$this->_conn) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT_SOCKET', __METHOD__, $host, $port, $errno, $err), Log::WARNING, 'jerror'); + + return false; + } + + // Set the timeout for this connection + socket_set_timeout($this->_conn, $this->_timeout, 0); + + // Check for welcome response code + if (!$this->_verifyResponse(220)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 220), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to determine if the object is connected to an FTP server + * + * @return boolean True if connected + * + * @since 1.5 + */ + public function isConnected() + { + return \is_resource($this->_conn); + } + + /** + * Method to login to a server once connected + * + * @param string $user Username to login to the server + * @param string $pass Password to login to the server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function login($user = 'anonymous', $pass = 'jftp@joomla.org') + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_login($this->_conn, $user, $pass) === false) { + Log::add('JFtp::login: Unable to login', Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send the username + if (!$this->_putCmd('USER ' . $user, array(331, 503))) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_USERNAME', __METHOD__, $this->_response, $user), Log::WARNING, 'jerror'); + + return false; + } + + // If we are already logged in, continue :) + if ($this->_responseCode == 503) { + return true; + } + + // Send the password + if (!$this->_putCmd('PASS ' . $pass, 230)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_PASSWORD', __METHOD__, $this->_response, str_repeat('*', \strlen($pass))), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to quit and close the connection + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function quit() + { + // If native FTP support is enabled lets use it... + if (FTP_NATIVE) { + @ftp_close($this->_conn); + + return true; + } + + // Logout and close connection + @fwrite($this->_conn, "QUIT\r\n"); + @fclose($this->_conn); + + return true; + } + + /** + * Method to retrieve the current working directory on the FTP server + * + * @return string Current working directory + * + * @since 1.5 + */ + public function pwd() + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (($ret = @ftp_pwd($this->_conn)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return $ret; + } + + $match = array(null); + + // Send print working directory command and verify success + if (!$this->_putCmd('PWD', 257)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 257), Log::WARNING, 'jerror'); + + return false; + } + + // Match just the path + preg_match('/"[^"\r\n]*"/', $this->_response, $match); + + // Return the cleaned path + return preg_replace("/\"/", '', $match[0]); + } + + /** + * Method to system string from the FTP server + * + * @return string System identifier string + * + * @since 1.5 + */ + public function syst() + { + // If native FTP support is enabled lets use it... + if (FTP_NATIVE) { + if (($ret = @ftp_systype($this->_conn)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + } else { + // Send print working directory command and verify success + if (!$this->_putCmd('SYST', 215)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 215), Log::WARNING, 'jerror'); + + return false; + } + + $ret = $this->_response; + } + + // Match the system string to an OS + if (strpos(strtoupper($ret), 'MAC') !== false) { + $ret = 'MAC'; + } elseif (strpos(strtoupper($ret), 'WIN') !== false) { + $ret = 'WIN'; + } else { + $ret = 'UNIX'; + } + + // Return the os type + return $ret; + } + + /** + * Method to change the current working directory on the FTP server + * + * @param string $path Path to change into on the server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function chdir($path) + { + // If native FTP support is enabled lets use it... + if (FTP_NATIVE) { + if (@ftp_chdir($this->_conn, $path) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send change directory command and verify success + if (!$this->_putCmd('CWD ' . $path, 250)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 250, $path), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to reinitialise the server, ie. need to login again + * + * NOTE: This command not available on all servers + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function reinit() + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_site($this->_conn, 'REIN') === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send reinitialise command to the server + if (!$this->_putCmd('REIN', 220)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 220), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to rename a file/folder on the FTP server + * + * @param string $from Path to change file/folder from + * @param string $to Path to change file/folder to + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function rename($from, $to) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_rename($this->_conn, $from, $to) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send rename from command to the server + if (!$this->_putCmd('RNFR ' . $from, 350)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RENAME_BAD_RESPONSE_FROM', __METHOD__, $this->_response, $from), Log::WARNING, 'jerror'); + + return false; + } + + // Send rename to command to the server + if (!$this->_putCmd('RNTO ' . $to, 250)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RENAME_BAD_RESPONSE_TO', __METHOD__, $this->_response, $to), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to change mode for a path on the FTP server + * + * @param string $path Path to change mode on + * @param mixed $mode Octal value to change mode to, e.g. '0777', 0777 or 511 (string or integer) + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function chmod($path, $mode) + { + // If no filename is given, we assume the current directory is the target + if ($path == '') { + $path = '.'; + } + + // Convert the mode to a string + if (\is_int($mode)) { + $mode = decoct($mode); + } + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_site($this->_conn, 'CHMOD ' . $mode . ' ' . $path) === false) { + if (!IS_WIN) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + } + + return false; + } + + return true; + } + + // Send change mode command and verify success [must convert mode from octal] + if (!$this->_putCmd('SITE CHMOD ' . $mode . ' ' . $path, array(200, 250))) { + if (!IS_WIN) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_CHMOD_BAD_RESPONSE', __METHOD__, $this->_response, $path, $mode), Log::WARNING, 'jerror'); + } + + return false; + } + + return true; + } + + /** + * Method to delete a path [file/folder] on the FTP server + * + * @param string $path Path to delete + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function delete($path) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_delete($this->_conn, $path) === false) { + if (@ftp_rmdir($this->_conn, $path) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + } + + return true; + } + + // Send delete file command and if that doesn't work, try to remove a directory + if (!$this->_putCmd('DELE ' . $path, 250)) { + if (!$this->_putCmd('RMD ' . $path, 250)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 250, $path), Log::WARNING, 'jerror'); + + return false; + } + } + + return true; + } + + /** + * Method to create a directory on the FTP server + * + * @param string $path Directory to create + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function mkdir($path) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_mkdir($this->_conn, $path) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send change directory command and verify success + if (!$this->_putCmd('MKD ' . $path, 257)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 257, $path), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to restart data transfer at a given byte + * + * @param integer $point Byte to restart transfer at + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function restart($point) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_site($this->_conn, 'REST ' . $point) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send restart command and verify success + if (!$this->_putCmd('REST ' . $point, 350)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RESTART_BAD_RESPONSE', __METHOD__, $this->_response, $point), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to create an empty file on the FTP server + * + * @param string $path Path local file to store on the FTP server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function create($path) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $buffer = fopen('buffer://tmp', 'r'); + + if (@ftp_fput($this->_conn, $path, $buffer, FTP_ASCII) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + fclose($buffer); + + return false; + } + + fclose($buffer); + + return true; + } + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (!$this->_putCmd('STOR ' . $path, array(150, 125))) { + @ fclose($this->_dataconn); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + // To create a zero byte upload close the data port connection + fclose($this->_dataconn); + + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to read a file from the FTP server's contents into a buffer + * + * @param string $remote Path to remote file to read on the FTP server + * @param string &$buffer Buffer variable to read file contents into + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function read($remote, &$buffer) + { + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $tmp = fopen('buffer://tmp', 'br+'); + + if (@ftp_fget($this->_conn, $tmp, $remote, $mode) === false) { + fclose($tmp); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Read tmp buffer contents + rewind($tmp); + $buffer = ''; + + while (!feof($tmp)) { + $buffer .= fread($tmp, 8192); + } + + fclose($tmp); + + return true; + } + + $this->_mode($mode); + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (!$this->_putCmd('RETR ' . $remote, array(150, 125))) { + @ fclose($this->_dataconn); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + // Read data from data port connection and add to the buffer + $buffer = ''; + + while (!feof($this->_dataconn)) { + $buffer .= fread($this->_dataconn, 4096); + } + + // Close the data port connection + fclose($this->_dataconn); + + // Let's try to cleanup some line endings if it is ascii + if ($mode == FTP_ASCII) { + $os = 'UNIX'; + + if (IS_WIN) { + $os = 'WIN'; + } + + $buffer = preg_replace('/' . CRLF . '/', $this->_lineEndings[$os], $buffer); + } + + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to get a file from the FTP server and save it to a local file + * + * @param string $local Local path to save remote file to + * @param string $remote Path to remote file to get on the FTP server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function get($local, $remote) + { + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (@ftp_get($this->_conn, $local, $remote, $mode) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + $this->_mode($mode); + + // Check to see if the local file can be opened for writing + $fp = fopen($local, 'wb'); + + if (!$fp) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_OPEN_WRITING', __METHOD__, $local), Log::WARNING, 'jerror'); + + return false; + } + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (!$this->_putCmd('RETR ' . $remote, array(150, 125))) { + @ fclose($this->_dataconn); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + // Read data from data port connection and add to the buffer + while (!feof($this->_dataconn)) { + $buffer = fread($this->_dataconn, 4096); + fwrite($fp, $buffer, 4096); + } + + // Close the data port connection and file pointer + fclose($this->_dataconn); + fclose($fp); + + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to store a file to the FTP server + * + * @param string $local Path to local file to store on the FTP server + * @param string $remote FTP path to file to create + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function store($local, $remote = null) + { + // If remote file is not given, use the filename of the local file in the current + // working directory. + if ($remote == null) { + $remote = basename($local); + } + + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (@ftp_put($this->_conn, $remote, $local, $mode) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + $this->_mode($mode); + + // Check to see if the local file exists and if so open it for reading + if (@ file_exists($local)) { + $fp = fopen($local, 'rb'); + + if (!$fp) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_OPEN_READING', __METHOD__, $local), Log::WARNING, 'jerror'); + + return false; + } + } else { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_FIND', __METHOD__, $local), Log::WARNING, 'jerror'); + + return false; + } + + // Start passive mode + if (!$this->_passive()) { + @ fclose($fp); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Send store command to the FTP server + if (!$this->_putCmd('STOR ' . $remote, array(150, 125))) { + @ fclose($fp); + @ fclose($this->_dataconn); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + // Do actual file transfer, read local file and write to data port connection + while (!feof($fp)) { + $line = fread($fp, 4096); + + do { + if (($result = @ fwrite($this->_dataconn, $line)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $line = substr($line, $result); + } while ($line != ''); + } + + fclose($fp); + fclose($this->_dataconn); + + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to write a string to the FTP server + * + * @param string $remote FTP path to file to write to + * @param string $buffer Contents to write to the FTP server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function write($remote, $buffer) + { + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $tmp = fopen('buffer://tmp', 'br+'); + fwrite($tmp, $buffer); + rewind($tmp); + + if (@ftp_fput($this->_conn, $remote, $tmp, $mode) === false) { + fclose($tmp); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + fclose($tmp); + + return true; + } + + // First we need to set the transfer mode + $this->_mode($mode); + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Send store command to the FTP server + if (!$this->_putCmd('STOR ' . $remote, array(150, 125))) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + @ fclose($this->_dataconn); + + return false; + } + + // Write buffer to the data connection port + do { + if (($result = @ fwrite($this->_dataconn, $buffer)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $buffer = substr($buffer, $result); + } while ($buffer != ''); + + // Close the data connection port [Data transfer complete] + fclose($this->_dataconn); + + // Verify that the server received the transfer + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to append a string to the FTP server + * + * @param string $remote FTP path to file to append to + * @param string $buffer Contents to append to the FTP server + * + * @return boolean True if successful + * + * @since 3.6.0 + */ + public function append($remote, $buffer) + { + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); + } + + $tmp = fopen('buffer://tmp', 'bw+'); + fwrite($tmp, $buffer); + rewind($tmp); + + $size = $this->size($remote); + + if ($size === false) { + } + + if (@ftp_fput($this->_conn, $remote, $tmp, $mode, $size) === false) { + fclose($tmp); + + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), 35); + } + + fclose($tmp); + + return true; + } + + // First we need to set the transfer mode + $this->_mode($mode); + + // Start passive mode + if (!$this->_passive()) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); + } + + // Send store command to the FTP server + if (!$this->_putCmd('APPE ' . $remote, array(150, 125))) { + @fclose($this->_dataconn); + + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), 35); + } + + // Write buffer to the data connection port + do { + if (($result = @ fwrite($this->_dataconn, $buffer)) === false) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), 37); + } + + $buffer = substr($buffer, $result); + } while ($buffer != ''); + + // Close the data connection port [Data transfer complete] + fclose($this->_dataconn); + + // Verify that the server received the transfer + if (!$this->_verifyResponse(226)) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), 37); + } + + return true; + } + + /** + * Get the size of the remote file. + * + * @param string $remote FTP path to file whose size to get + * + * @return mixed number of bytes or false on error + * + * @since 3.6.0 + */ + public function size($remote) + { + if (FTP_NATIVE) { + $size = ftp_size($this->_conn, $remote); + + // In case ftp_size fails, try the SIZE command directly. + if ($size === -1) { + $response = ftp_raw($this->_conn, 'SIZE ' . $remote); + $responseCode = substr($response[0], 0, 3); + $responseMessage = substr($response[0], 4); + + if ($responseCode != '213') { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), 35); + } + + $size = (int) $responseMessage; + } + + return $size; + } + + // Start passive mode + if (!$this->_passive()) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); + } + + // Send size command to the FTP server + if (!$this->_putCmd('SIZE ' . $remote, array(213))) { + @fclose($this->_dataconn); + + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 213, $remote), 35); + } + + return (int) substr($this->_responseMsg, 4); + } + + /** + * Method to list the filenames of the contents of a directory on the FTP server + * + * Note: Some servers also return folder names. However, to be sure to list folders on all + * servers, you should use listDetails() instead if you also need to deal with folders + * + * @param string $path Path local file to store on the FTP server + * + * @return string Directory listing + * + * @since 1.5 + */ + public function listNames($path = null) + { + $data = null; + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (($list = @ftp_nlist($this->_conn, $path)) === false) { + // Workaround for empty directories on some servers + if ($this->listDetails($path, 'files') === array()) { + return array(); + } + + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $list = preg_replace('#^' . preg_quote($path, '#') . '[/\\\\]?#', '', $list); + + if ($keys = array_merge(array_keys($list, '.'), array_keys($list, '..'))) { + foreach ($keys as $key) { + unset($list[$key]); + } + } + + return $list; + } + + // If a path exists, prepend a space + if ($path != null) { + $path = ' ' . $path; + } + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (!$this->_putCmd('NLST' . $path, array(150, 125))) { + @ fclose($this->_dataconn); + + // Workaround for empty directories on some servers + if ($this->listDetails($path, 'files') === array()) { + return array(); + } + + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + // Read in the file listing. + while (!feof($this->_dataconn)) { + $data .= fread($this->_dataconn, 4096); + } + + fclose($this->_dataconn); + + // Everything go okay? + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + $data = preg_split('/[' . CRLF . ']+/', $data, -1, PREG_SPLIT_NO_EMPTY); + $data = preg_replace('#^' . preg_quote(substr($path, 1), '#') . '[/\\\\]?#', '', $data); + + if ($keys = array_merge(array_keys($data, '.'), array_keys($data, '..'))) { + foreach ($keys as $key) { + unset($data[$key]); + } + } + + return $data; + } + + /** + * Method to list the contents of a directory on the FTP server + * + * @param string $path Path to the local file to be stored on the FTP server + * @param string $type Return type [raw|all|folders|files] + * + * @return mixed If $type is raw: string Directory listing, otherwise array of string with file-names + * + * @since 1.5 + */ + public function listDetails($path = null, $type = 'all') + { + $dir_list = array(); + $data = null; + $regs = null; + + // @todo: Deal with recurse -- nightmare + // For now we will just set it to false + $recurse = false; + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (($contents = @ftp_rawlist($this->_conn, $path)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + } else { + // Non Native mode + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // If a path exists, prepend a space + if ($path != null) { + $path = ' ' . $path; + } + + // Request the file listing + if (!$this->_putCmd(($recurse == true) ? 'LIST -R' : 'LIST' . $path, array(150, 125))) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + @ fclose($this->_dataconn); + + return false; + } + + // Read in the file listing. + while (!feof($this->_dataconn)) { + $data .= fread($this->_dataconn, 4096); + } + + fclose($this->_dataconn); + + // Everything go okay? + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + $contents = explode(CRLF, $data); + } + + // If only raw output is requested we are done + if ($type === 'raw') { + return $data; + } + + // If we received the listing of an empty directory, we are done as well + if (empty($contents[0])) { + return $dir_list; + } + + // If the server returned the number of results in the first response, let's dump it + if (strtolower(substr($contents[0], 0, 6)) === 'total ') { + array_shift($contents); + + if (!isset($contents[0]) || empty($contents[0])) { + return $dir_list; + } + } + + // Regular expressions for the directory listing parsing. + $regexps = array( + 'UNIX' => '#([-dl][rwxstST-]+).* ([0-9]*) ([a-zA-Z0-9]+).* ([a-zA-Z0-9]+).* ([0-9]*)' + . ' ([a-zA-Z]+[0-9: ]*[0-9])[ ]+(([0-9]{1,2}:[0-9]{2})|[0-9]{4}) (.+)#', + 'MAC' => '#([-dl][rwxstST-]+).* ?([0-9 ]*)?([a-zA-Z0-9]+).* ([a-zA-Z0-9]+).* ([0-9]*)' + . ' ([a-zA-Z]+[0-9: ]*[0-9])[ ]+(([0-9]{2}:[0-9]{2})|[0-9]{4}) (.+)#', + 'WIN' => '#([0-9]{2})-([0-9]{2})-([0-9]{2}) +([0-9]{2}):([0-9]{2})(AM|PM) +([0-9]+|) +(.+)#', + ); + + // Find out the format of the directory listing by matching one of the regexps + $osType = null; + + foreach ($regexps as $k => $v) { + if (@preg_match($v, $contents[0])) { + $osType = $k; + $regexp = $v; + break; + } + } + + if (!$osType) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_UNRECOGNISED_FOLDER_LISTING_FORMATJLIB_CLIENT_ERROR_JFTP_LISTDETAILS_UNRECOGNISED', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Here is where it is going to get dirty.... + if ($osType === 'UNIX' || $osType === 'MAC') { + foreach ($contents as $file) { + $tmp_array = null; + + if (@preg_match($regexp, $file, $regs)) { + $fType = (int) strpos('-dl', $regs[1][0]); + + // $tmp_array['line'] = $regs[0]; + $tmp_array['type'] = $fType; + $tmp_array['rights'] = $regs[1]; + + // $tmp_array['number'] = $regs[2]; + $tmp_array['user'] = $regs[3]; + $tmp_array['group'] = $regs[4]; + $tmp_array['size'] = $regs[5]; + $tmp_array['date'] = @date('m-d', strtotime($regs[6])); + $tmp_array['time'] = $regs[7]; + $tmp_array['name'] = $regs[9]; + } + + // If we just want files, do not add a folder + if ($type === 'files' && $tmp_array['type'] == 1) { + continue; + } + + // If we just want folders, do not add a file + if ($type === 'folders' && $tmp_array['type'] == 0) { + continue; + } + + if (\is_array($tmp_array) && $tmp_array['name'] != '.' && $tmp_array['name'] != '..') { + $dir_list[] = $tmp_array; + } + } + } else { + foreach ($contents as $file) { + $tmp_array = null; + + if (@preg_match($regexp, $file, $regs)) { + $fType = (int) ($regs[7] === ''); + $timestamp = strtotime("$regs[3]-$regs[1]-$regs[2] $regs[4]:$regs[5]$regs[6]"); + + // $tmp_array['line'] = $regs[0]; + $tmp_array['type'] = $fType; + $tmp_array['rights'] = ''; + + // $tmp_array['number'] = 0; + $tmp_array['user'] = ''; + $tmp_array['group'] = ''; + $tmp_array['size'] = (int) $regs[7]; + $tmp_array['date'] = date('m-d', $timestamp); + $tmp_array['time'] = date('H:i', $timestamp); + $tmp_array['name'] = $regs[8]; + } + + // If we just want files, do not add a folder + if ($type === 'files' && $tmp_array['type'] == 1) { + continue; + } + + // If we just want folders, do not add a file + if ($type === 'folders' && $tmp_array['type'] == 0) { + continue; + } + + if (\is_array($tmp_array) && $tmp_array['name'] != '.' && $tmp_array['name'] != '..') { + $dir_list[] = $tmp_array; + } + } + } + + return $dir_list; + } + + /** + * Send command to the FTP server and validate an expected response code + * + * @param string $cmd Command to send to the FTP server + * @param mixed $expectedResponse Integer response code or array of integer response codes + * + * @return boolean True if command executed successfully + * + * @since 1.5 + */ + protected function _putCmd($cmd, $expectedResponse) + { + // Make sure we have a connection to the server + if (!\is_resource($this->_conn)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PUTCMD_UNCONNECTED', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Send the command to the server + if (!fwrite($this->_conn, $cmd . "\r\n")) { + Log::add(Text::sprintf('DDD', Text::sprintf('JLIB_CLIENT_ERROR_FTP_PUTCMD_SEND', __METHOD__, $cmd)), Log::WARNING, 'jerror'); + } + + return $this->_verifyResponse($expectedResponse); + } + + /** + * Verify the response code from the server and log response if flag is set + * + * @param mixed $expected Integer response code or array of integer response codes + * + * @return boolean True if response code from the server is expected + * + * @since 1.5 + */ + protected function _verifyResponse($expected) + { + $parts = null; + + // Wait for a response from the server, but timeout after the set time limit + $endTime = time() + $this->_timeout; + $this->_response = ''; + + do { + $this->_response .= fgets($this->_conn, 4096); + } while (!preg_match('/^([0-9]{3})(-(.*' . CRLF . ')+\1)? [^' . CRLF . ']+' . CRLF . "$/", $this->_response, $parts) && time() < $endTime); + + // Catch a timeout or bad response + if (!isset($parts[1])) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TIMEOUT', __METHOD__, $this->_response), Log::WARNING, 'jerror'); + + return false; + } + + // Separate the code from the message + $this->_responseCode = $parts[1]; + $this->_responseMsg = $parts[0]; + + // Did the server respond with the code we wanted? + if (\is_array($expected)) { + if (\in_array($this->_responseCode, $expected)) { + $retval = true; + } else { + $retval = false; + } + } else { + if ($this->_responseCode == $expected) { + $retval = true; + } else { + $retval = false; + } + } + + return $retval; + } + + /** + * Set server to passive mode and open a data port connection + * + * @return boolean True if successful + * + * @since 1.5 + */ + protected function _passive() + { + $match = array(); + $parts = array(); + $errno = null; + $err = null; + + // Make sure we have a connection to the server + if (!\is_resource($this->_conn)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Request a passive connection - this means, we'll talk to you, you don't talk to us. + @ fwrite($this->_conn, "PASV\r\n"); + + // Wait for a response from the server, but timeout after the set time limit + $endTime = time() + $this->_timeout; + $this->_response = ''; + + do { + $this->_response .= fgets($this->_conn, 4096); + } while (!preg_match('/^([0-9]{3})(-(.*' . CRLF . ')+\1)? [^' . CRLF . ']+' . CRLF . "$/", $this->_response, $parts) && time() < $endTime); + + // Catch a timeout or bad response + if (!isset($parts[1])) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TIMEOUT', __METHOD__, $this->_response), Log::WARNING, 'jerror'); + + return false; + } + + // Separate the code from the message + $this->_responseCode = $parts[1]; + $this->_responseMsg = $parts[0]; + + // If it's not 227, we weren't given an IP and port, which means it failed. + if ($this->_responseCode != '227') { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE_IP_OBTAIN', __METHOD__, $this->_responseMsg), Log::WARNING, 'jerror'); + + return false; + } + + // Snatch the IP and port information, or die horribly trying... + if (preg_match('~\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))\)~', $this->_responseMsg, $match) == 0) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE_IP_VALID', __METHOD__, $this->_responseMsg), Log::WARNING, 'jerror'); + + return false; + } + + // This is pretty simple - store it for later use ;). + $this->_pasv = array('ip' => $match[1] . '.' . $match[2] . '.' . $match[3] . '.' . $match[4], 'port' => $match[5] * 256 + $match[6]); + + // Connect, assuming we've got a connection. + $this->_dataconn = @fsockopen($this->_pasv['ip'], $this->_pasv['port'], $errno, $err, $this->_timeout); + + if (!$this->_dataconn) { + Log::add( + Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__, $this->_pasv['ip'], $this->_pasv['port'], $errno, $err), + Log::WARNING, + 'jerror' + ); + + return false; + } + + // Set the timeout for this connection + socket_set_timeout($this->_conn, $this->_timeout, 0); + + return true; + } + + /** + * Method to find out the correct transfer mode for a specific file + * + * @param string $fileName Name of the file + * + * @return integer Transfer-mode for this filetype [FTP_ASCII|FTP_BINARY] + * + * @since 1.5 + */ + protected function _findMode($fileName) + { + if ($this->_type == FTP_AUTOASCII) { + $dot = strrpos($fileName, '.') + 1; + $ext = substr($fileName, $dot); + + if (\in_array($ext, $this->_autoAscii)) { + $mode = FTP_ASCII; + } else { + $mode = FTP_BINARY; + } + } elseif ($this->_type == FTP_ASCII) { + $mode = FTP_ASCII; + } else { + $mode = FTP_BINARY; + } + + return $mode; + } + + /** + * Set transfer mode + * + * @param integer $mode Integer representation of data transfer mode [1:Binary|0:Ascii] + * Defined constants can also be used [FTP_BINARY|FTP_ASCII] + * + * @return boolean True if successful + * + * @since 1.5 + */ + protected function _mode($mode) + { + if ($mode == FTP_BINARY) { + if (!$this->_putCmd('TYPE I', 200)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_MODE_BINARY', __METHOD__, $this->_response), Log::WARNING, 'jerror'); + + return false; + } + } else { + if (!$this->_putCmd('TYPE A', 200)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_MODE_ASCII', __METHOD__, $this->_response), Log::WARNING, 'jerror'); + + return false; + } + } + + return true; + } } diff --git a/code/libraries/src/Component/ComponentHelper.php b/code/libraries/src/Component/ComponentHelper.php index 9d8fcec6..433a1d88 100644 --- a/code/libraries/src/Component/ComponentHelper.php +++ b/code/libraries/src/Component/ComponentHelper.php @@ -1,4 +1,5 @@ enabled = $strict ? false : true; - $result->setParams(new Registry); - - return $result; - } - - /** - * Checks if the component is enabled - * - * @param string $option The component option. - * - * @return boolean - * - * @since 1.5 - */ - public static function isEnabled($option) - { - $components = static::getComponents(); - - return isset($components[$option]) && $components[$option]->enabled; - } - - /** - * Checks if a component is installed - * - * @param string $option The component option. - * - * @return integer - * - * @since 3.4 - */ - public static function isInstalled($option) - { - $components = static::getComponents(); - - return isset($components[$option]) ? 1 : 0; - } - - /** - * Gets the parameter object for the component - * - * @param string $option The option for the component. - * @param boolean $strict If set and the component does not exist, false will be returned - * - * @return Registry A Registry object. - * - * @see Registry - * @since 1.5 - */ - public static function getParams($option, $strict = false) - { - return static::getComponent($option, $strict)->getParams(); - } - - /** - * Applies the global text filters to arbitrary text as per settings for current user groups - * - * @param string $text The string to filter - * - * @return string The filtered string - * - * @since 2.5 - */ - public static function filterText($text) - { - // Punyencoding utf8 email addresses - $text = InputFilter::getInstance()->emailToPunycode($text); - - // Filter settings - $config = static::getParams('com_config'); - $user = Factory::getUser(); - $userGroups = Access::getGroupsByUser($user->get('id')); - - $filters = $config->get('filters'); - - $forbiddenListTags = array(); - $forbiddenListAttributes = array(); - - $customListTags = array(); - $customListAttributes = array(); - - $allowedListTags = array(); - $allowedListAttributes = array(); - - $allowedList = false; - $forbiddenList = false; - $customList = false; - $unfiltered = false; - - // Cycle through each of the user groups the user is in. - // Remember they are included in the Public group as well. - foreach ($userGroups as $groupId) - { - // May have added a group by not saved the filters. - if (!isset($filters->$groupId)) - { - continue; - } - - // Each group the user is in could have different filtering properties. - $filterData = $filters->$groupId; - $filterType = strtoupper($filterData->filter_type); - - if ($filterType === 'NH') - { - // Maximum HTML filtering. - } - elseif ($filterType === 'NONE') - { - // No HTML filtering. - $unfiltered = true; - } - else - { - // Forbidden list or allowed list. - // Preprocess the tags and attributes. - $tags = explode(',', $filterData->filter_tags); - $attributes = explode(',', $filterData->filter_attributes); - $tempTags = array(); - $tempAttributes = array(); - - foreach ($tags as $tag) - { - $tag = trim($tag); - - if ($tag) - { - $tempTags[] = $tag; - } - } - - foreach ($attributes as $attribute) - { - $attribute = trim($attribute); - - if ($attribute) - { - $tempAttributes[] = $attribute; - } - } - - // Collect the forbidden list or allowed list tags and attributes. - // Each list is cumulative. - if ($filterType === 'BL') - { - $forbiddenList = true; - $forbiddenListTags = array_merge($forbiddenListTags, $tempTags); - $forbiddenListAttributes = array_merge($forbiddenListAttributes, $tempAttributes); - } - elseif ($filterType === 'CBL') - { - // Only set to true if Tags or Attributes were added - if ($tempTags || $tempAttributes) - { - $customList = true; - $customListTags = array_merge($customListTags, $tempTags); - $customListAttributes = array_merge($customListAttributes, $tempAttributes); - } - } - elseif ($filterType === 'WL') - { - $allowedList = true; - $allowedListTags = array_merge($allowedListTags, $tempTags); - $allowedListAttributes = array_merge($allowedListAttributes, $tempAttributes); - } - } - } - - // Remove duplicates before processing (because the forbidden list uses both sets of arrays). - $forbiddenListTags = array_unique($forbiddenListTags); - $forbiddenListAttributes = array_unique($forbiddenListAttributes); - $customListTags = array_unique($customListTags); - $customListAttributes = array_unique($customListAttributes); - $allowedListTags = array_unique($allowedListTags); - $allowedListAttributes = array_unique($allowedListAttributes); - - if (!$unfiltered) - { - // Custom Forbidden list precedes Default forbidden list. - if ($customList) - { - $filter = InputFilter::getInstance(array(), array(), 1, 1); - - // Override filter's default forbidden tags and attributes - if ($customListTags) - { - $filter->blockedTags = $customListTags; - } - - if ($customListAttributes) - { - $filter->blockedAttributes = $customListAttributes; - } - } - // Forbidden list takes second precedence. - elseif ($forbiddenList) - { - // Remove the allowed tags and attributes from the forbidden list. - $forbiddenListTags = array_diff($forbiddenListTags, $allowedListTags); - $forbiddenListAttributes = array_diff($forbiddenListAttributes, $allowedListAttributes); - - $filter = InputFilter::getInstance( - $forbiddenListTags, - $forbiddenListAttributes, - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ); - - // Remove the allowed tags from filter's default forbidden list. - if ($allowedListTags) - { - $filter->blockedTags = array_diff($filter->blockedTags, $allowedListTags); - } - - // Remove the allowed attributes from filter's default forbidden list. - if ($allowedListAttributes) - { - $filter->blockedAttributes = array_diff($filter->blockedAttributes, $allowedListAttributes); - } - } - // Allowed lists take third precedence. - elseif ($allowedList) - { - // Turn off XSS auto clean - $filter = InputFilter::getInstance($allowedListTags, $allowedListAttributes, 0, 0, 0); - } - // No HTML takes last place. - else - { - $filter = InputFilter::getInstance(); - } - - $text = $filter->clean($text, 'html'); - } - - return $text; - } - - /** - * Render the component. - * - * @param string $option The component option. - * @param array $params The component parameters - * - * @return string - * - * @since 1.5 - * @throws MissingComponentException - */ - public static function renderComponent($option, $params = array()) - { - $app = Factory::getApplication(); - $lang = Factory::getLanguage(); - - if (!$app->isClient('api')) - { - // Load template language files. - $template = $app->getTemplate(true)->template; - $lang->load('tpl_' . $template, JPATH_BASE) - || $lang->load('tpl_' . $template, JPATH_THEMES . "/$template"); - } - - if (empty($option)) - { - throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); - } - - if (JDEBUG) - { - Profiler::getInstance('Application')->mark('beforeRenderComponent ' . $option); - } - - // Record the scope - $scope = $app->scope; - - // Set scope to component name - $app->scope = $option; - - // Build the component path. - $option = preg_replace('/[^A-Z0-9_\.-]/i', '', $option); - - // Define component path. - - if (!\defined('JPATH_COMPONENT')) - { - /** - * Defines the path to the active component for the request - * - * Note this constant is application aware and is different for each application (site/admin). - * - * @var string - * @since 1.5 - * @deprecated 5.0 without replacement - */ - \define('JPATH_COMPONENT', JPATH_BASE . '/components/' . $option); - } - - if (!\defined('JPATH_COMPONENT_SITE')) - { - /** - * Defines the path to the site element of the active component for the request - * - * @var string - * @since 1.5 - * @deprecated 5.0 without replacement - */ - \define('JPATH_COMPONENT_SITE', JPATH_SITE . '/components/' . $option); - } - - if (!\defined('JPATH_COMPONENT_ADMINISTRATOR')) - { - /** - * Defines the path to the admin element of the active component for the request - * - * @var string - * @since 1.5 - * @deprecated 5.0 without replacement - */ - \define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR . '/components/' . $option); - } - - // If component is disabled throw error - if (!static::isEnabled($option)) - { - throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); - } - - ob_start(); - $app->bootComponent($option)->getDispatcher($app)->dispatch(); - $contents = ob_get_clean(); - - // Revert the scope - $app->scope = $scope; - - if (JDEBUG) - { - Profiler::getInstance('Application')->mark('afterRenderComponent ' . $option); - } - - return $contents; - } - - /** - * Load the installed components into the components property. - * - * @return boolean True on success - * - * @since 3.2 - */ - protected static function load() - { - $loader = function () - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName(['extension_id', 'element', 'params', 'enabled'], ['id', 'option', null, null])) - ->from($db->quoteName('#__extensions')) - ->where( - [ - $db->quoteName('type') . ' = ' . $db->quote('component'), - $db->quoteName('state') . ' = 0', - $db->quoteName('enabled') . ' = 1', - ] - ); - - $components = []; - $db->setQuery($query); - - foreach ($db->getIterator() as $component) - { - $components[$component->option] = new ComponentRecord((array) $component); - } - - return $components; - }; - - /** @var CallbackController $cache */ - $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController('callback', ['defaultgroup' => '_system']); - - try - { - static::$components = $cache->get($loader, array(), __METHOD__); - } - catch (CacheExceptionInterface $e) - { - static::$components = $loader(); - } - - return true; - } - - /** - * Get installed components - * - * @return ComponentRecord[] The components property - * - * @since 3.6.3 - */ - public static function getComponents() - { - if (empty(static::$components)) - { - static::load(); - } - - return static::$components; - } - - /** - * Returns the component name (eg. com_content) for the given object based on the class name. - * If the object is not namespaced, then the alternative name is used. - * - * @param object $object The object controller or model - * @param string $alternativeName Mostly the value of getName() from the object - * - * @return string The name - * - * @since 4.0.0 - */ - public static function getComponentName($object, string $alternativeName): string - { - $reflect = new \ReflectionClass($object); - - if (!$reflect->getNamespaceName() || \get_class($object) === ComponentDispatcher::class || \get_class($object) === ApiDispatcher::class) - { - return 'com_' . strtolower($alternativeName); - } - - $from = strpos($reflect->getNamespaceName(), '\\Component'); - $to = strpos(substr($reflect->getNamespaceName(), $from + 11), '\\'); - - return 'com_' . strtolower(substr($reflect->getNamespaceName(), $from + 11, $to)); - } + /** + * The component list cache + * + * @var ComponentRecord[] + * @since 1.6 + */ + protected static $components = array(); + + /** + * Get the component information. + * + * @param string $option The component option. + * @param boolean $strict If set and the component does not exist, the enabled attribute will be set to false. + * + * @return ComponentRecord An object with the information for the component. + * + * @since 1.5 + */ + public static function getComponent($option, $strict = false) + { + $components = static::getComponents(); + + if (isset($components[$option])) { + return $components[$option]; + } + + $result = new ComponentRecord(); + $result->enabled = $strict ? false : true; + $result->setParams(new Registry()); + + return $result; + } + + /** + * Checks if the component is enabled + * + * @param string $option The component option. + * + * @return boolean + * + * @since 1.5 + */ + public static function isEnabled($option) + { + $components = static::getComponents(); + + return isset($components[$option]) && $components[$option]->enabled; + } + + /** + * Checks if a component is installed + * + * @param string $option The component option. + * + * @return integer + * + * @since 3.4 + */ + public static function isInstalled($option) + { + $components = static::getComponents(); + + return isset($components[$option]) ? 1 : 0; + } + + /** + * Gets the parameter object for the component + * + * @param string $option The option for the component. + * @param boolean $strict If set and the component does not exist, false will be returned + * + * @return Registry A Registry object. + * + * @see Registry + * @since 1.5 + */ + public static function getParams($option, $strict = false) + { + return static::getComponent($option, $strict)->getParams(); + } + + /** + * Applies the global text filters to arbitrary text as per settings for current user groups + * + * @param string $text The string to filter + * + * @return string The filtered string + * + * @since 2.5 + */ + public static function filterText($text) + { + // Punyencoding utf8 email addresses + $text = InputFilter::getInstance()->emailToPunycode($text); + + // Filter settings + $config = static::getParams('com_config'); + $user = Factory::getUser(); + $userGroups = Access::getGroupsByUser($user->get('id')); + + $filters = $config->get('filters'); + + $forbiddenListTags = array(); + $forbiddenListAttributes = array(); + + $customListTags = array(); + $customListAttributes = array(); + + $allowedListTags = array(); + $allowedListAttributes = array(); + + $allowedList = false; + $forbiddenList = false; + $customList = false; + $unfiltered = false; + + // Cycle through each of the user groups the user is in. + // Remember they are included in the Public group as well. + foreach ($userGroups as $groupId) { + // May have added a group by not saved the filters. + if (!isset($filters->$groupId)) { + continue; + } + + // Each group the user is in could have different filtering properties. + $filterData = $filters->$groupId; + $filterType = strtoupper($filterData->filter_type); + + if ($filterType === 'NH') { + // Maximum HTML filtering. + } elseif ($filterType === 'NONE') { + // No HTML filtering. + $unfiltered = true; + } else { + // Forbidden list or allowed list. + // Preprocess the tags and attributes. + $tags = explode(',', $filterData->filter_tags); + $attributes = explode(',', $filterData->filter_attributes); + $tempTags = array(); + $tempAttributes = array(); + + foreach ($tags as $tag) { + $tag = trim($tag); + + if ($tag) { + $tempTags[] = $tag; + } + } + + foreach ($attributes as $attribute) { + $attribute = trim($attribute); + + if ($attribute) { + $tempAttributes[] = $attribute; + } + } + + // Collect the forbidden list or allowed list tags and attributes. + // Each list is cumulative. + if ($filterType === 'BL') { + $forbiddenList = true; + $forbiddenListTags = array_merge($forbiddenListTags, $tempTags); + $forbiddenListAttributes = array_merge($forbiddenListAttributes, $tempAttributes); + } elseif ($filterType === 'CBL') { + // Only set to true if Tags or Attributes were added + if ($tempTags || $tempAttributes) { + $customList = true; + $customListTags = array_merge($customListTags, $tempTags); + $customListAttributes = array_merge($customListAttributes, $tempAttributes); + } + } elseif ($filterType === 'WL') { + $allowedList = true; + $allowedListTags = array_merge($allowedListTags, $tempTags); + $allowedListAttributes = array_merge($allowedListAttributes, $tempAttributes); + } + } + } + + // Remove duplicates before processing (because the forbidden list uses both sets of arrays). + $forbiddenListTags = array_unique($forbiddenListTags); + $forbiddenListAttributes = array_unique($forbiddenListAttributes); + $customListTags = array_unique($customListTags); + $customListAttributes = array_unique($customListAttributes); + $allowedListTags = array_unique($allowedListTags); + $allowedListAttributes = array_unique($allowedListAttributes); + + if (!$unfiltered) { + // Custom Forbidden list precedes Default forbidden list. + if ($customList) { + $filter = InputFilter::getInstance(array(), array(), 1, 1); + + // Override filter's default forbidden tags and attributes + if ($customListTags) { + $filter->blockedTags = $customListTags; + } + + if ($customListAttributes) { + $filter->blockedAttributes = $customListAttributes; + } + } elseif ($forbiddenList) { + // Forbidden list takes second precedence. + // Remove the allowed tags and attributes from the forbidden list. + $forbiddenListTags = array_diff($forbiddenListTags, $allowedListTags); + $forbiddenListAttributes = array_diff($forbiddenListAttributes, $allowedListAttributes); + + $filter = InputFilter::getInstance( + $forbiddenListTags, + $forbiddenListAttributes, + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ); + + // Remove the allowed tags from filter's default forbidden list. + if ($allowedListTags) { + $filter->blockedTags = array_diff($filter->blockedTags, $allowedListTags); + } + + // Remove the allowed attributes from filter's default forbidden list. + if ($allowedListAttributes) { + $filter->blockedAttributes = array_diff($filter->blockedAttributes, $allowedListAttributes); + } + } elseif ($allowedList) { + // Allowed lists take third precedence. + // Turn off XSS auto clean + $filter = InputFilter::getInstance($allowedListTags, $allowedListAttributes, 0, 0, 0); + } else { + // No HTML takes last place. + $filter = InputFilter::getInstance(); + } + + $text = $filter->clean($text, 'html'); + } + + return $text; + } + + /** + * Render the component. + * + * @param string $option The component option. + * @param array $params The component parameters + * + * @return string + * + * @since 1.5 + * @throws MissingComponentException + */ + public static function renderComponent($option, $params = array()) + { + $app = Factory::getApplication(); + $lang = Factory::getLanguage(); + + if (!$app->isClient('api')) { + // Load template language files. + $template = $app->getTemplate(true)->template; + $lang->load('tpl_' . $template, JPATH_BASE) + || $lang->load('tpl_' . $template, JPATH_THEMES . "/$template"); + } + + if (empty($option)) { + throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); + } + + if (JDEBUG) { + Profiler::getInstance('Application')->mark('beforeRenderComponent ' . $option); + } + + // Record the scope + $scope = $app->scope; + + // Set scope to component name + $app->scope = $option; + + // Build the component path. + $option = preg_replace('/[^A-Z0-9_\.-]/i', '', $option); + + // Define component path. + + if (!\defined('JPATH_COMPONENT')) { + /** + * Defines the path to the active component for the request + * + * Note this constant is application aware and is different for each application (site/admin). + * + * @var string + * @since 1.5 + * @deprecated 5.0 without replacement + */ + \define('JPATH_COMPONENT', JPATH_BASE . '/components/' . $option); + } + + if (!\defined('JPATH_COMPONENT_SITE')) { + /** + * Defines the path to the site element of the active component for the request + * + * @var string + * @since 1.5 + * @deprecated 5.0 without replacement + */ + \define('JPATH_COMPONENT_SITE', JPATH_SITE . '/components/' . $option); + } + + if (!\defined('JPATH_COMPONENT_ADMINISTRATOR')) { + /** + * Defines the path to the admin element of the active component for the request + * + * @var string + * @since 1.5 + * @deprecated 5.0 without replacement + */ + \define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR . '/components/' . $option); + } + + // If component is disabled throw error + if (!static::isEnabled($option)) { + throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); + } + + ob_start(); + $app->bootComponent($option)->getDispatcher($app)->dispatch(); + $contents = ob_get_clean(); + + // Revert the scope + $app->scope = $scope; + + if (JDEBUG) { + Profiler::getInstance('Application')->mark('afterRenderComponent ' . $option); + } + + return $contents; + } + + /** + * Load the installed components into the components property. + * + * @return boolean True on success + * + * @since 3.2 + */ + protected static function load() + { + $loader = function () { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName(['extension_id', 'element', 'params', 'enabled'], ['id', 'option', null, null])) + ->from($db->quoteName('#__extensions')) + ->where( + [ + $db->quoteName('type') . ' = ' . $db->quote('component'), + $db->quoteName('state') . ' = 0', + $db->quoteName('enabled') . ' = 1', + ] + ); + + $components = []; + $db->setQuery($query); + + foreach ($db->getIterator() as $component) { + $components[$component->option] = new ComponentRecord((array) $component); + } + + return $components; + }; + + /** @var CallbackController $cache */ + $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController('callback', ['defaultgroup' => '_system']); + + try { + static::$components = $cache->get($loader, array(), __METHOD__); + } catch (CacheExceptionInterface $e) { + static::$components = $loader(); + } + + return true; + } + + /** + * Get installed components + * + * @return ComponentRecord[] The components property + * + * @since 3.6.3 + */ + public static function getComponents() + { + if (empty(static::$components)) { + static::load(); + } + + return static::$components; + } + + /** + * Returns the component name (eg. com_content) for the given object based on the class name. + * If the object is not namespaced, then the alternative name is used. + * + * @param object $object The object controller or model + * @param string $alternativeName Mostly the value of getName() from the object + * + * @return string The name + * + * @since 4.0.0 + */ + public static function getComponentName($object, string $alternativeName): string + { + $reflect = new \ReflectionClass($object); + + if (!$reflect->getNamespaceName() || \get_class($object) === ComponentDispatcher::class || \get_class($object) === ApiDispatcher::class) { + return 'com_' . strtolower($alternativeName); + } + + $from = strpos($reflect->getNamespaceName(), '\\Component'); + $to = strpos(substr($reflect->getNamespaceName(), $from + 11), '\\'); + + return 'com_' . strtolower(substr($reflect->getNamespaceName(), $from + 11, $to)); + } } diff --git a/code/libraries/src/Component/ComponentRecord.php b/code/libraries/src/Component/ComponentRecord.php index b4389e00..a810088b 100644 --- a/code/libraries/src/Component/ComponentRecord.php +++ b/code/libraries/src/Component/ComponentRecord.php @@ -1,4 +1,5 @@ $value) - { - $this->$key = $value; - } - } - - /** - * Method to get certain otherwise inaccessible properties from the form field object. - * - * @param string $name The property name for which to get the value. - * - * @return mixed The property value or null. - * - * @since 3.7.0 - * @deprecated 5.0 Access the item parameters through the `getParams()` method - */ - public function __get($name) - { - if ($name === 'params') - { - return $this->getParams(); - } - - return $this->$name; - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 3.7.0 - * @deprecated 5.0 Set the item parameters through the `setParams()` method - */ - public function __set($name, $value) - { - if ($name === 'params') - { - $this->setParams($value); - - return; - } - - $this->$name = $value; - } - - /** - * Returns the menu item parameters - * - * @return Registry - * - * @since 3.7.0 - */ - public function getParams() - { - if (!($this->params instanceof Registry)) - { - $this->params = new Registry($this->params); - } - - return $this->params; - } - - /** - * Sets the menu item parameters - * - * @param Registry|string $params The data to be stored as the parameters - * - * @return void - * - * @since 3.7.0 - */ - public function setParams($params) - { - $this->params = $params; - } + /** + * Primary key + * + * @var integer + * @since 3.7.0 + */ + public $id; + + /** + * The component name + * + * @var integer + * @since 3.7.0 + */ + public $option; + + /** + * The component parameters + * + * @var string|Registry + * @since 3.7.0 + * @note This field is protected to require reading this field to proxy through the getter to convert the params to a Registry instance + */ + protected $params; + + /** + * The extension namespace + * + * @var string + * @since 4.0.0 + */ + public $namespace; + + /** + * Indicates if this component is enabled + * + * @var integer + * @since 3.7.0 + */ + public $enabled; + + /** + * Class constructor + * + * @param array $data The component record data to load + * + * @since 3.7.0 + */ + public function __construct($data = array()) + { + foreach ((array) $data as $key => $value) { + $this->$key = $value; + } + } + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 3.7.0 + * @deprecated 5.0 Access the item parameters through the `getParams()` method + */ + public function __get($name) + { + if ($name === 'params') { + return $this->getParams(); + } + + return $this->$name; + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 3.7.0 + * @deprecated 5.0 Set the item parameters through the `setParams()` method + */ + public function __set($name, $value) + { + if ($name === 'params') { + $this->setParams($value); + + return; + } + + $this->$name = $value; + } + + /** + * Returns the menu item parameters + * + * @return Registry + * + * @since 3.7.0 + */ + public function getParams() + { + if (!($this->params instanceof Registry)) { + $this->params = new Registry($this->params); + } + + return $this->params; + } + + /** + * Sets the menu item parameters + * + * @param Registry|string $params The data to be stored as the parameters + * + * @return void + * + * @since 3.7.0 + */ + public function setParams($params) + { + $this->params = $params; + } } diff --git a/code/libraries/src/Component/Exception/MissingComponentException.php b/code/libraries/src/Component/Exception/MissingComponentException.php index 67947b5b..238f00ff 100644 --- a/code/libraries/src/Component/Exception/MissingComponentException.php +++ b/code/libraries/src/Component/Exception/MissingComponentException.php @@ -1,4 +1,5 @@ app = $app; - } - else - { - $this->app = Factory::getApplication(); - } + /** + * Class constructor. + * + * @param \Joomla\CMS\Application\CMSApplication $app Application-object that the router should use + * @param \Joomla\CMS\Menu\AbstractMenu $menu Menu-object that the router should use + * + * @since 3.4 + */ + public function __construct($app = null, $menu = null) + { + if ($app) { + $this->app = $app; + } else { + $this->app = Factory::getApplication(); + } - if ($menu) - { - $this->menu = $menu; - } - else - { - $this->menu = $this->app->getMenu(); - } - } + if ($menu) { + $this->menu = $menu; + } else { + $this->menu = $this->app->getMenu(); + } + } - /** - * Generic method to preprocess a URL - * - * @param array $query An associative array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.3 - */ - public function preprocess($query) - { - return $query; - } + /** + * Generic method to preprocess a URL + * + * @param array $query An associative array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.3 + */ + public function preprocess($query) + { + return $query; + } } diff --git a/code/libraries/src/Component/Router/RouterFactory.php b/code/libraries/src/Component/Router/RouterFactory.php index eb82e1aa..bf8fa9bc 100644 --- a/code/libraries/src/Component/Router/RouterFactory.php +++ b/code/libraries/src/Component/Router/RouterFactory.php @@ -1,4 +1,5 @@ namespace = $namespace; - $this->categoryFactory = $categoryFactory; - $this->db = $db; - } + /** + * The namespace must be like: + * Joomla\Component\Content + * + * @param string $namespace The namespace + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + * + * @since 4.0.0 + */ + public function __construct($namespace, CategoryFactoryInterface $categoryFactory = null, DatabaseInterface $db = null) + { + $this->namespace = $namespace; + $this->categoryFactory = $categoryFactory; + $this->db = $db; + } - /** - * Creates a router. - * - * @param CMSApplicationInterface $application The application - * @param AbstractMenu $menu The menu object to work with - * - * @return RouterInterface - * - * @since 4.0.0 - */ - public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface - { - $className = trim($this->namespace, '\\') . '\\' . ucfirst($application->getName()) . '\\Service\\Router'; + /** + * Creates a router. + * + * @param CMSApplicationInterface $application The application + * @param AbstractMenu $menu The menu object to work with + * + * @return RouterInterface + * + * @since 4.0.0 + */ + public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface + { + $className = trim($this->namespace, '\\') . '\\' . ucfirst($application->getName()) . '\\Service\\Router'; - if (!class_exists($className)) - { - throw new \RuntimeException('No router available for this application.'); - } + if (!class_exists($className)) { + throw new \RuntimeException('No router available for this application.'); + } - return new $className($application, $menu, $this->categoryFactory, $this->db); - } + return new $className($application, $menu, $this->categoryFactory, $this->db); + } } diff --git a/code/libraries/src/Component/Router/RouterFactoryInterface.php b/code/libraries/src/Component/Router/RouterFactoryInterface.php index b1e6748a..69d98dc5 100644 --- a/code/libraries/src/Component/Router/RouterFactoryInterface.php +++ b/code/libraries/src/Component/Router/RouterFactoryInterface.php @@ -1,4 +1,5 @@ component = $component; - } - - /** - * Generic preprocess function for missing or legacy component router - * - * @param array $query An associative array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.3 - */ - public function preprocess($query) - { - return $query; - } - - /** - * Generic build function for missing or legacy component router - * - * @param array &$query An array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.3 - */ - public function build(&$query) - { - $function = $this->component . 'BuildRoute'; - - if (\function_exists($function)) - { - $segments = $function($query); - $total = \count($segments); - - for ($i = 0; $i < $total; $i++) - { - $segments[$i] = str_replace(':', '-', $segments[$i]); - } - - return $segments; - } - - return array(); - } - - /** - * Generic parse function for missing or legacy component router - * - * @param array &$segments The segments of the URL to parse. - * - * @return array The URL attributes to be used by the application. - * - * @since 3.3 - */ - public function parse(&$segments) - { - $function = $this->component . 'ParseRoute'; - - if (\function_exists($function)) - { - $total = \count($segments); - - for ($i = 0; $i < $total; $i++) - { - $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1); - } - - return $function($segments); - } - - return array(); - } + /** + * Name of the component + * + * @var string + * @since 3.3 + */ + protected $component; + + /** + * Constructor + * + * @param string $component Component name without the com_ prefix this router should react upon + * + * @since 3.3 + */ + public function __construct($component) + { + $this->component = $component; + } + + /** + * Generic preprocess function for missing or legacy component router + * + * @param array $query An associative array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.3 + */ + public function preprocess($query) + { + return $query; + } + + /** + * Generic build function for missing or legacy component router + * + * @param array &$query An array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.3 + */ + public function build(&$query) + { + $function = $this->component . 'BuildRoute'; + + if (\function_exists($function)) { + $segments = $function($query); + $total = \count($segments); + + for ($i = 0; $i < $total; $i++) { + $segments[$i] = str_replace(':', '-', $segments[$i]); + } + + return $segments; + } + + return array(); + } + + /** + * Generic parse function for missing or legacy component router + * + * @param array &$segments The segments of the URL to parse. + * + * @return array The URL attributes to be used by the application. + * + * @since 3.3 + */ + public function parse(&$segments) + { + $function = $this->component . 'ParseRoute'; + + if (\function_exists($function)) { + $total = \count($segments); + + for ($i = 0; $i < $total; $i++) { + $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1); + } + + return $function($segments); + } + + return array(); + } } diff --git a/code/libraries/src/Component/Router/RouterServiceInterface.php b/code/libraries/src/Component/Router/RouterServiceInterface.php index f949a235..2cb128fb 100644 --- a/code/libraries/src/Component/Router/RouterServiceInterface.php +++ b/code/libraries/src/Component/Router/RouterServiceInterface.php @@ -1,4 +1,5 @@ routerFactory->createRouter($application, $menu); - } + /** + * Returns the router. + * + * @param CMSApplicationInterface $application The application object + * @param AbstractMenu $menu The menu object to work with + * + * @return RouterInterface + * + * @since 4.0.0 + */ + public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface + { + return $this->routerFactory->createRouter($application, $menu); + } - /** - * The router factory. - * - * @param RouterFactoryInterface $routerFactory The router factory - * - * @return void - * - * @since 4.0.0 - */ - public function setRouterFactory(RouterFactoryInterface $routerFactory) - { - $this->routerFactory = $routerFactory; - } + /** + * The router factory. + * + * @param RouterFactoryInterface $routerFactory The router factory + * + * @return void + * + * @since 4.0.0 + */ + public function setRouterFactory(RouterFactoryInterface $routerFactory) + { + $this->routerFactory = $routerFactory; + } } diff --git a/code/libraries/src/Component/Router/RouterView.php b/code/libraries/src/Component/Router/RouterView.php index 60a22272..30b02355 100644 --- a/code/libraries/src/Component/Router/RouterView.php +++ b/code/libraries/src/Component/Router/RouterView.php @@ -1,4 +1,5 @@ views[$view->name] = $view; - } - - /** - * Return an array of registered view objects - * - * @return RouterViewConfiguration[] Array of registered view objects - * - * @since 3.5 - */ - public function getViews() - { - return $this->views; - } - - /** - * Get the path of views from target view to root view - * including content items of a nestable view - * - * @param array $query Array of query elements - * - * @return array List of views including IDs of content items - * - * @since 3.5 - */ - public function getPath($query) - { - $views = $this->getViews(); - $result = array(); - - // Get the right view object - if (isset($query['view']) && isset($views[$query['view']])) - { - $viewobj = $views[$query['view']]; - } - - // Get the path from the current item to the root view with all IDs - if (isset($viewobj)) - { - $path = array_reverse($viewobj->path); - $start = true; - $childkey = false; - - foreach ($path as $element) - { - $view = $views[$element]; - - if ($start) - { - $key = $view->key; - $start = false; - } - else - { - $key = $childkey; - } - - $childkey = $view->parent_key; - - if (($key || $view->key) && \is_callable(array($this, 'get' . ucfirst($view->name) . 'Segment'))) - { - if (isset($query[$key])) - { - $result[$view->name] = \call_user_func_array(array($this, 'get' . ucfirst($view->name) . 'Segment'), array($query[$key], $query)); - } - elseif (isset($query[$view->key])) - { - $result[$view->name] = \call_user_func_array(array($this, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); - } - else - { - $result[$view->name] = array(); - } - } - else - { - $result[$view->name] = true; - } - } - } - - return $result; - } - - /** - * Get all currently attached rules - * - * @return RulesInterface[] All currently attached rules in an array - * - * @since 3.5 - */ - public function getRules() - { - return $this->rules; - } - - /** - * Add a number of router rules to the object - * - * @param RulesInterface[] $rules Array of JComponentRouterRulesInterface objects - * - * @return void - * - * @since 3.5 - */ - public function attachRules($rules) - { - foreach ($rules as $rule) - { - $this->attachRule($rule); - } - } - - /** - * Attach a build rule - * - * @param RulesInterface $rule The function to be called. - * - * @return void - * - * @since 3.5 - */ - public function attachRule(RulesInterface $rule) - { - $this->rules[] = $rule; - } - - /** - * Remove a build rule - * - * @param RulesInterface $rule The rule to be removed. - * - * @return boolean Was a rule removed? - * - * @since 3.5 - */ - public function detachRule(RulesInterface $rule) - { - foreach ($this->rules as $id => $r) - { - if ($r == $rule) - { - unset($this->rules[$id]); - - return true; - } - } - - return false; - } - - /** - * Generic method to preprocess a URL - * - * @param array $query An associative array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.5 - */ - public function preprocess($query) - { - // Process the parsed variables based on custom defined rules - foreach ($this->rules as $rule) - { - $rule->preprocess($query); - } - - return $query; - } - - /** - * Build method for URLs - * - * @param array &$query Array of query elements - * - * @return array Array of URL segments - * - * @since 3.5 - */ - public function build(&$query) - { - $segments = array(); - - // Process the parsed variables based on custom defined rules - foreach ($this->rules as $rule) - { - $rule->build($query, $segments); - } - - return $segments; - } - - /** - * Parse method for URLs - * - * @param array &$segments Array of URL string-segments - * - * @return array Associative array of query values - * - * @since 3.5 - */ - public function parse(&$segments) - { - $vars = array(); - - // Process the parsed variables based on custom defined rules - foreach ($this->rules as $rule) - { - $rule->parse($segments, $vars); - } - - return $vars; - } - - /** - * Method to return the name of the router - * - * @return string Name of the router - * - * @since 3.5 - */ - public function getName() - { - if (empty($this->name)) - { - $r = null; - - if (!preg_match('/(.*)Router/i', \get_class($this), $r)) - { - throw new \Exception('JLIB_APPLICATION_ERROR_ROUTER_GET_NAME', 500); - } - - $this->name = str_replace('com_', '', ComponentHelper::getComponentName($this, strtolower($r[1]))); - } - - return $this->name; - } + /** + * Name of the router of the component + * + * @var string + * @since 3.5 + */ + protected $name; + + /** + * Array of rules + * + * @var RulesInterface[] + * @since 3.5 + */ + protected $rules = array(); + + /** + * Views of the component + * + * @var RouterViewConfiguration[] + * @since 3.5 + */ + protected $views = array(); + + /** + * Register the views of a component + * + * @param RouterViewConfiguration $view View configuration object + * + * @return void + * + * @since 3.5 + */ + public function registerView(RouterViewConfiguration $view) + { + $this->views[$view->name] = $view; + } + + /** + * Return an array of registered view objects + * + * @return RouterViewConfiguration[] Array of registered view objects + * + * @since 3.5 + */ + public function getViews() + { + return $this->views; + } + + /** + * Get the path of views from target view to root view + * including content items of a nestable view + * + * @param array $query Array of query elements + * + * @return array List of views including IDs of content items + * + * @since 3.5 + */ + public function getPath($query) + { + $views = $this->getViews(); + $result = array(); + + // Get the right view object + if (isset($query['view']) && isset($views[$query['view']])) { + $viewobj = $views[$query['view']]; + } + + // Get the path from the current item to the root view with all IDs + if (isset($viewobj)) { + $path = array_reverse($viewobj->path); + $start = true; + $childkey = false; + + foreach ($path as $element) { + $view = $views[$element]; + + if ($start) { + $key = $view->key; + $start = false; + } else { + $key = $childkey; + } + + $childkey = $view->parent_key; + + if (($key || $view->key) && \is_callable(array($this, 'get' . ucfirst($view->name) . 'Segment'))) { + if (isset($query[$key])) { + $result[$view->name] = \call_user_func_array(array($this, 'get' . ucfirst($view->name) . 'Segment'), array($query[$key], $query)); + } elseif (isset($query[$view->key])) { + $result[$view->name] = \call_user_func_array(array($this, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); + } else { + $result[$view->name] = array(); + } + } else { + $result[$view->name] = true; + } + } + } + + return $result; + } + + /** + * Get all currently attached rules + * + * @return RulesInterface[] All currently attached rules in an array + * + * @since 3.5 + */ + public function getRules() + { + return $this->rules; + } + + /** + * Add a number of router rules to the object + * + * @param RulesInterface[] $rules Array of JComponentRouterRulesInterface objects + * + * @return void + * + * @since 3.5 + */ + public function attachRules($rules) + { + foreach ($rules as $rule) { + $this->attachRule($rule); + } + } + + /** + * Attach a build rule + * + * @param RulesInterface $rule The function to be called. + * + * @return void + * + * @since 3.5 + */ + public function attachRule(RulesInterface $rule) + { + $this->rules[] = $rule; + } + + /** + * Remove a build rule + * + * @param RulesInterface $rule The rule to be removed. + * + * @return boolean Was a rule removed? + * + * @since 3.5 + */ + public function detachRule(RulesInterface $rule) + { + foreach ($this->rules as $id => $r) { + if ($r == $rule) { + unset($this->rules[$id]); + + return true; + } + } + + return false; + } + + /** + * Generic method to preprocess a URL + * + * @param array $query An associative array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.5 + */ + public function preprocess($query) + { + // Process the parsed variables based on custom defined rules + foreach ($this->rules as $rule) { + $rule->preprocess($query); + } + + return $query; + } + + /** + * Build method for URLs + * + * @param array &$query Array of query elements + * + * @return array Array of URL segments + * + * @since 3.5 + */ + public function build(&$query) + { + $segments = array(); + + // Process the parsed variables based on custom defined rules + foreach ($this->rules as $rule) { + $rule->build($query, $segments); + } + + return $segments; + } + + /** + * Parse method for URLs + * + * @param array &$segments Array of URL string-segments + * + * @return array Associative array of query values + * + * @since 3.5 + */ + public function parse(&$segments) + { + $vars = array(); + + // Process the parsed variables based on custom defined rules + foreach ($this->rules as $rule) { + $rule->parse($segments, $vars); + } + + return $vars; + } + + /** + * Method to return the name of the router + * + * @return string Name of the router + * + * @since 3.5 + */ + public function getName() + { + if (empty($this->name)) { + $r = null; + + if (!preg_match('/(.*)Router/i', \get_class($this), $r)) { + throw new \Exception('JLIB_APPLICATION_ERROR_ROUTER_GET_NAME', 500); + } + + $this->name = str_replace('com_', '', ComponentHelper::getComponentName($this, strtolower($r[1]))); + } + + return $this->name; + } } diff --git a/code/libraries/src/Component/Router/Rules/MenuRules.php b/code/libraries/src/Component/Router/Rules/MenuRules.php index cd5254bc..3390e21a 100644 --- a/code/libraries/src/Component/Router/Rules/MenuRules.php +++ b/code/libraries/src/Component/Router/Rules/MenuRules.php @@ -1,4 +1,5 @@ router = $router; - - $this->buildLookup(); - } - - /** - * Finds the right Itemid for this query - * - * @param array &$query The query array to process - * - * @return void - * - * @since 3.4 - */ - public function preprocess(&$query) - { - $active = $this->router->menu->getActive(); - - /** - * If the active item id is not the same as the supplied item id or we have a supplied item id and no active - * menu item then we just use the supplied menu item and continue - */ - if (isset($query['Itemid']) && ($active === null || $query['Itemid'] != $active->id)) - { - return; - } - - // Get query language - $language = isset($query['lang']) ? $query['lang'] : '*'; - - // Set the language to the current one when multilang is enabled and item is tagged to ALL - if (Multilanguage::isEnabled() && $language === '*') - { - $language = $this->router->app->get('language'); - } - - if (!isset($this->lookup[$language])) - { - $this->buildLookup($language); - } - - // Check if the active menu item matches the requested query - if ($active !== null && isset($query['Itemid'])) - { - // Check if active->query and supplied query are the same - $match = true; - - foreach ($active->query as $k => $v) - { - if (isset($query[$k]) && $v !== $query[$k]) - { - // Compare again without alias - if (\is_string($v) && $v == current(explode(':', $query[$k], 2))) - { - continue; - } - - $match = false; - break; - } - } - - if ($match) - { - // Just use the supplied menu item - return; - } - } - - $needles = $this->router->getPath($query); - - $layout = isset($query['layout']) && $query['layout'] !== 'default' ? ':' . $query['layout'] : ''; - - if ($needles) - { - foreach ($needles as $view => $ids) - { - $viewLayout = $view . $layout; - - if ($layout && isset($this->lookup[$language][$viewLayout])) - { - if (\is_bool($ids)) - { - $query['Itemid'] = $this->lookup[$language][$viewLayout]; - - return; - } - - foreach ($ids as $id => $segment) - { - if (isset($this->lookup[$language][$viewLayout][(int) $id])) - { - $query['Itemid'] = $this->lookup[$language][$viewLayout][(int) $id]; - - return; - } - } - } - - if (isset($this->lookup[$language][$view])) - { - if (\is_bool($ids)) - { - $query['Itemid'] = $this->lookup[$language][$view]; - - return; - } - - foreach ($ids as $id => $segment) - { - if (isset($this->lookup[$language][$view][(int) $id])) - { - $query['Itemid'] = $this->lookup[$language][$view][(int) $id]; - - return; - } - } - } - } - } - - // Check if the active menuitem matches the requested language - if ($active && $active->component === 'com_' . $this->router->getName() - && ($language === '*' || \in_array($active->language, array('*', $language)) || !Multilanguage::isEnabled())) - { - $query['Itemid'] = $active->id; - - return; - } - - // If not found, return language specific home link - $default = $this->router->menu->getDefault($language); - - if (!empty($default->id)) - { - $query['Itemid'] = $default->id; - } - } - - /** - * Method to build the lookup array - * - * @param string $language The language that the lookup should be built up for - * - * @return void - * - * @since 3.4 - */ - protected function buildLookup($language = '*') - { - // Prepare the reverse lookup array. - if (!isset($this->lookup[$language])) - { - $this->lookup[$language] = array(); - - $component = ComponentHelper::getComponent('com_' . $this->router->getName()); - $views = $this->router->getViews(); - - $attributes = array('component_id'); - $values = array((int) $component->id); - - $attributes[] = 'language'; - $values[] = array($language, '*'); - - $items = $this->router->menu->getItems($attributes, $values); - - foreach ($items as $item) - { - if (isset($item->query['view'], $views[$item->query['view']])) - { - $view = $item->query['view']; - - $layout = ''; - - if (isset($item->query['layout'])) - { - $layout = ':' . $item->query['layout']; - } - - if ($views[$view]->key) - { - if (!isset($this->lookup[$language][$view . $layout])) - { - $this->lookup[$language][$view . $layout] = array(); - } - - if (!isset($this->lookup[$language][$view])) - { - $this->lookup[$language][$view] = array(); - } - - // If menuitem has no key set, we assume 0. - if (!isset($item->query[$views[$view]->key])) - { - $item->query[$views[$view]->key] = 0; - } - - /** - * Here it will become a bit tricky - * language != * can override existing entries - * language == * cannot override existing entries - */ - if (!isset($this->lookup[$language][$view . $layout][$item->query[$views[$view]->key]]) || $item->language !== '*') - { - $this->lookup[$language][$view . $layout][$item->query[$views[$view]->key]] = $item->id; - $this->lookup[$language][$view][$item->query[$views[$view]->key]] = $item->id; - } - } - else - { - /** - * Here it will become a bit tricky - * language != * can override existing entries - * language == * cannot override existing entries - */ - if (!isset($this->lookup[$language][$view . $layout]) || $item->language !== '*') - { - $this->lookup[$language][$view . $layout] = $item->id; - } - } - } - } - } - } - - /** - * Dummy method to fulfil the interface requirements - * - * @param array &$segments The URL segments to parse - * @param array &$vars The vars that result from the segments - * - * @return void - * - * @since 3.4 - * @codeCoverageIgnore - */ - public function parse(&$segments, &$vars) - { - } - - /** - * Dummy method to fulfil the interface requirements - * - * @param array &$query The vars that should be converted - * @param array &$segments The URL segments to create - * - * @return void - * - * @since 3.4 - * @codeCoverageIgnore - */ - public function build(&$query, &$segments) - { - } + /** + * Router this rule belongs to + * + * @var RouterView + * @since 3.4 + */ + protected $router; + + /** + * Lookup array of the menu items + * + * @var array + * @since 3.4 + */ + protected $lookup = array(); + + /** + * Class constructor. + * + * @param RouterView $router Router this rule belongs to + * + * @since 3.4 + */ + public function __construct(RouterView $router) + { + $this->router = $router; + + $this->buildLookup(); + } + + /** + * Finds the right Itemid for this query + * + * @param array &$query The query array to process + * + * @return void + * + * @since 3.4 + */ + public function preprocess(&$query) + { + $active = $this->router->menu->getActive(); + + /** + * If the active item id is not the same as the supplied item id or we have a supplied item id and no active + * menu item then we just use the supplied menu item and continue + */ + if (isset($query['Itemid']) && ($active === null || $query['Itemid'] != $active->id)) { + return; + } + + // Get query language + $language = isset($query['lang']) ? $query['lang'] : '*'; + + // Set the language to the current one when multilang is enabled and item is tagged to ALL + if (Multilanguage::isEnabled() && $language === '*') { + $language = $this->router->app->get('language'); + } + + if (!isset($this->lookup[$language])) { + $this->buildLookup($language); + } + + // Check if the active menu item matches the requested query + if ($active !== null && isset($query['Itemid'])) { + // Check if active->query and supplied query are the same + $match = true; + + foreach ($active->query as $k => $v) { + if (isset($query[$k]) && $v !== $query[$k]) { + // Compare again without alias + if (\is_string($v) && $v == current(explode(':', $query[$k], 2))) { + continue; + } + + $match = false; + break; + } + } + + if ($match) { + // Just use the supplied menu item + return; + } + } + + $needles = $this->router->getPath($query); + + $layout = isset($query['layout']) && $query['layout'] !== 'default' ? ':' . $query['layout'] : ''; + + if ($needles) { + foreach ($needles as $view => $ids) { + $viewLayout = $view . $layout; + + if ($layout && isset($this->lookup[$language][$viewLayout])) { + if (\is_bool($ids)) { + $query['Itemid'] = $this->lookup[$language][$viewLayout]; + + return; + } + + foreach ($ids as $id => $segment) { + if (isset($this->lookup[$language][$viewLayout][(int) $id])) { + $query['Itemid'] = $this->lookup[$language][$viewLayout][(int) $id]; + + return; + } + } + } + + if (isset($this->lookup[$language][$view])) { + if (\is_bool($ids)) { + $query['Itemid'] = $this->lookup[$language][$view]; + + return; + } + + foreach ($ids as $id => $segment) { + if (isset($this->lookup[$language][$view][(int) $id])) { + $query['Itemid'] = $this->lookup[$language][$view][(int) $id]; + + return; + } + } + } + } + } + + // Check if the active menuitem matches the requested language + if ( + $active && $active->component === 'com_' . $this->router->getName() + && ($language === '*' || \in_array($active->language, array('*', $language)) || !Multilanguage::isEnabled()) + ) { + $query['Itemid'] = $active->id; + + return; + } + + // If not found, return language specific home link + $default = $this->router->menu->getDefault($language); + + if (!empty($default->id)) { + $query['Itemid'] = $default->id; + } + } + + /** + * Method to build the lookup array + * + * @param string $language The language that the lookup should be built up for + * + * @return void + * + * @since 3.4 + */ + protected function buildLookup($language = '*') + { + // Prepare the reverse lookup array. + if (!isset($this->lookup[$language])) { + $this->lookup[$language] = array(); + + $component = ComponentHelper::getComponent('com_' . $this->router->getName()); + $views = $this->router->getViews(); + + $attributes = array('component_id'); + $values = array((int) $component->id); + + $attributes[] = 'language'; + $values[] = array($language, '*'); + + $items = $this->router->menu->getItems($attributes, $values); + + foreach ($items as $item) { + if (isset($item->query['view'], $views[$item->query['view']])) { + $view = $item->query['view']; + + $layout = ''; + + if (isset($item->query['layout'])) { + $layout = ':' . $item->query['layout']; + } + + if ($views[$view]->key) { + if (!isset($this->lookup[$language][$view . $layout])) { + $this->lookup[$language][$view . $layout] = array(); + } + + if (!isset($this->lookup[$language][$view])) { + $this->lookup[$language][$view] = array(); + } + + // If menuitem has no key set, we assume 0. + if (!isset($item->query[$views[$view]->key])) { + $item->query[$views[$view]->key] = 0; + } + + /** + * Here it will become a bit tricky + * language != * can override existing entries + * language == * cannot override existing entries + */ + if (!isset($this->lookup[$language][$view . $layout][$item->query[$views[$view]->key]]) || $item->language !== '*') { + $this->lookup[$language][$view . $layout][$item->query[$views[$view]->key]] = $item->id; + $this->lookup[$language][$view][$item->query[$views[$view]->key]] = $item->id; + } + } else { + /** + * Here it will become a bit tricky + * language != * can override existing entries + * language == * cannot override existing entries + */ + if (!isset($this->lookup[$language][$view . $layout]) || $item->language !== '*') { + $this->lookup[$language][$view . $layout] = $item->id; + } + } + } + } + } + } + + /** + * Dummy method to fulfil the interface requirements + * + * @param array &$segments The URL segments to parse + * @param array &$vars The vars that result from the segments + * + * @return void + * + * @since 3.4 + * @codeCoverageIgnore + */ + public function parse(&$segments, &$vars) + { + } + + /** + * Dummy method to fulfil the interface requirements + * + * @param array &$query The vars that should be converted + * @param array &$segments The URL segments to create + * + * @return void + * + * @since 3.4 + * @codeCoverageIgnore + */ + public function build(&$query, &$segments) + { + } } diff --git a/code/libraries/src/Component/Router/Rules/NomenuRules.php b/code/libraries/src/Component/Router/Rules/NomenuRules.php index 84f08ad5..c4b96dc4 100644 --- a/code/libraries/src/Component/Router/Rules/NomenuRules.php +++ b/code/libraries/src/Component/Router/Rules/NomenuRules.php @@ -1,4 +1,5 @@ router = $router; - } - - /** - * Dummy method to fulfil the interface requirements - * - * @param array &$query The query array to process - * - * @return void - * - * @since 3.4 - * @codeCoverageIgnore - */ - public function preprocess(&$query) - { - } - - /** - * Parse a menu-less URL - * - * @param array &$segments The URL segments to parse - * @param array &$vars The vars that result from the segments - * - * @return void - * - * @since 3.4 - */ - public function parse(&$segments, &$vars) - { - $active = $this->router->menu->getActive(); - - if (!\is_object($active)) - { - $views = $this->router->getViews(); - - if (isset($views[$segments[0]])) - { - $vars['view'] = array_shift($segments); - $view = $views[$vars['view']]; - - if (isset($view->key) && isset($segments[0])) - { - if (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Id'))) - { - if ($view->parent_key && $this->router->app->input->get($view->parent_key)) - { - $vars[$view->parent->key] = $this->router->app->input->get($view->parent_key); - $vars[$view->parent_key] = $this->router->app->input->get($view->parent_key); - } - - if ($view->nestable) - { - $vars[$view->key] = 0; - - while (count($segments)) - { - $segment = array_shift($segments); - $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); - - if (!$result) - { - array_unshift($segments, $segment); - break; - } - - $vars[$view->key] = preg_replace('/-/', ':', $result, 1); - } - } - else - { - $segment = array_shift($segments); - $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); - - $vars[$view->key] = preg_replace('/-/', ':', $result, 1); - } - } - else - { - $vars[$view->key] = preg_replace('/-/', ':', array_shift($segments), 1); - } - } - } - } - } - - /** - * Build a menu-less URL - * - * @param array &$query The vars that should be converted - * @param array &$segments The URL segments to create - * - * @return void - * - * @since 3.4 - */ - public function build(&$query, &$segments) - { - $menu_found = false; - - if (isset($query['Itemid'])) - { - $item = $this->router->menu->getItem($query['Itemid']); - - if (!isset($query['option']) - || ($item && isset($item->query['option']) && $item->query['option'] === $query['option'])) - { - $menu_found = true; - } - } - - if (!$menu_found && isset($query['view'])) - { - $views = $this->router->getViews(); - - if (isset($views[$query['view']])) - { - $view = $views[$query['view']]; - $segments[] = $query['view']; - - if ($view->key && isset($query[$view->key])) - { - if (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Segment'))) - { - $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); - - if ($view->nestable) - { - array_pop($result); - - while (count($result)) - { - $segments[] = str_replace(':', '-', array_pop($result)); - } - } - else - { - $segments[] = str_replace(':', '-', array_pop($result)); - } - } - else - { - $segments[] = str_replace(':', '-', $query[$view->key]); - } - - unset($query[$views[$query['view']]->key]); - } - - unset($query['view']); - } - } - } + /** + * Router this rule belongs to + * + * @var RouterView + * @since 3.4 + */ + protected $router; + + /** + * Class constructor. + * + * @param RouterView $router Router this rule belongs to + * + * @since 3.4 + */ + public function __construct(RouterView $router) + { + $this->router = $router; + } + + /** + * Dummy method to fulfil the interface requirements + * + * @param array &$query The query array to process + * + * @return void + * + * @since 3.4 + * @codeCoverageIgnore + */ + public function preprocess(&$query) + { + } + + /** + * Parse a menu-less URL + * + * @param array &$segments The URL segments to parse + * @param array &$vars The vars that result from the segments + * + * @return void + * + * @since 3.4 + */ + public function parse(&$segments, &$vars) + { + $active = $this->router->menu->getActive(); + + if (!\is_object($active)) { + $views = $this->router->getViews(); + + if (isset($views[$segments[0]])) { + $vars['view'] = array_shift($segments); + $view = $views[$vars['view']]; + + if (isset($view->key) && isset($segments[0])) { + if (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Id'))) { + if ($view->parent_key && $this->router->app->input->get($view->parent_key)) { + $vars[$view->parent->key] = $this->router->app->input->get($view->parent_key); + $vars[$view->parent_key] = $this->router->app->input->get($view->parent_key); + } + + if ($view->nestable) { + $vars[$view->key] = 0; + + while (count($segments)) { + $segment = array_shift($segments); + $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); + + if (!$result) { + array_unshift($segments, $segment); + break; + } + + $vars[$view->key] = preg_replace('/-/', ':', $result, 1); + } + } else { + $segment = array_shift($segments); + $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); + + $vars[$view->key] = preg_replace('/-/', ':', $result, 1); + } + } else { + $vars[$view->key] = preg_replace('/-/', ':', array_shift($segments), 1); + } + } + } + } + } + + /** + * Build a menu-less URL + * + * @param array &$query The vars that should be converted + * @param array &$segments The URL segments to create + * + * @return void + * + * @since 3.4 + */ + public function build(&$query, &$segments) + { + $menu_found = false; + + if (isset($query['Itemid'])) { + $item = $this->router->menu->getItem($query['Itemid']); + + if ( + !isset($query['option']) + || ($item && isset($item->query['option']) && $item->query['option'] === $query['option']) + ) { + $menu_found = true; + } + } + + if (!$menu_found && isset($query['view'])) { + $views = $this->router->getViews(); + + if (isset($views[$query['view']])) { + $view = $views[$query['view']]; + $segments[] = $query['view']; + + if ($view->key && isset($query[$view->key])) { + if (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Segment'))) { + $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); + + if ($view->nestable) { + array_pop($result); + + while (count($result)) { + $segments[] = str_replace(':', '-', array_pop($result)); + } + } else { + $segments[] = str_replace(':', '-', array_pop($result)); + } + } else { + $segments[] = str_replace(':', '-', $query[$view->key]); + } + + unset($query[$views[$query['view']]->key]); + } + + unset($query['view']); + } + } + } } diff --git a/code/libraries/src/Component/Router/Rules/RulesInterface.php b/code/libraries/src/Component/Router/Rules/RulesInterface.php index d56015c2..2bb97032 100644 --- a/code/libraries/src/Component/Router/Rules/RulesInterface.php +++ b/code/libraries/src/Component/Router/Rules/RulesInterface.php @@ -1,4 +1,5 @@ router = $router; - } - - /** - * Dummy method to fulfil the interface requirements - * - * @param array &$query The query array to process - * - * @return void - * - * @since 3.4 - */ - public function preprocess(&$query) - { - } - - /** - * Parse the URL - * - * @param array &$segments The URL segments to parse - * @param array &$vars The vars that result from the segments - * - * @return void - * - * @since 3.4 - */ - public function parse(&$segments, &$vars) - { - // Get the views and the currently active query vars - $views = $this->router->getViews(); - $active = $this->router->menu->getActive(); - - if ($active) - { - $vars = array_merge($active->query, $vars); - } - - // We don't have a view or its not a view of this component! We stop here - if (!isset($vars['view']) || !isset($views[$vars['view']])) - { - return; - } - - // Copy the segments, so that we can iterate over all of them and at the same time modify the original segments - $tempSegments = $segments; - - // Iterate over the segments as long as a segment fits - foreach ($tempSegments as $segment) - { - // Our current view is nestable. We need to check first if the segment fits to that - if ($views[$vars['view']]->nestable) - { - if (\is_callable(array($this->router, 'get' . ucfirst($views[$vars['view']]->name) . 'Id'))) - { - $key = \call_user_func_array(array($this->router, 'get' . ucfirst($views[$vars['view']]->name) . 'Id'), array($segment, $vars)); - - // Did we get a proper key? If not, we need to look in the child-views - if ($key) - { - $vars[$views[$vars['view']]->key] = $key; - - array_shift($segments); - - continue; - } - } - else - { - // The router is not complete. The getId() method is missing. - return; - } - } - - // Lets find the right view that belongs to this segment - $found = false; - - foreach ($views[$vars['view']]->children as $view) - { - if (!$view->key) - { - if ($view->name === $segment) - { - // The segment is a view name - $parent = $views[$vars['view']]; - $vars['view'] = $view->name; - $found = true; - - if ($view->parent_key && isset($vars[$parent->key])) - { - $parent_key = $vars[$parent->key]; - $vars[$view->parent_key] = $parent_key; - - unset($vars[$parent->key]); - } - - break; - } - } - elseif (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Id'))) - { - // Hand the data over to the router specific method and see if there is a content item that fits - $key = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); - - if ($key) - { - // Found the right view and the right item - $parent = $views[$vars['view']]; - $vars['view'] = $view->name; - $found = true; - - if ($view->parent_key && isset($vars[$parent->key])) - { - $parent_key = $vars[$parent->key]; - $vars[$view->parent_key] = $parent_key; - - unset($vars[$parent->key]); - } - - $vars[$view->key] = $key; - - break; - } - } - } - - if (!$found) - { - return; - } - - array_shift($segments); - } - } - - /** - * Build a standard URL - * - * @param array &$query The vars that should be converted - * @param array &$segments The URL segments to create - * - * @return void - * - * @since 3.4 - */ - public function build(&$query, &$segments) - { - if (!isset($query['Itemid'], $query['view'])) - { - return; - } - - // Get the menu item belonging to the Itemid that has been found - $item = $this->router->menu->getItem($query['Itemid']); - - if ($item === null - || $item->component !== 'com_' . $this->router->getName() - || !isset($item->query['view'])) - { - return; - } - - // Get menu item layout - $mLayout = isset($item->query['layout']) ? $item->query['layout'] : null; - - // Get all views for this component - $views = $this->router->getViews(); - - // Return directly when the URL of the Itemid is identical with the URL to build - if ($item->query['view'] === $query['view']) - { - $view = $views[$query['view']]; - - if (!$view->key) - { - unset($query['view']); - - if (isset($query['layout']) && $mLayout === $query['layout']) - { - unset($query['layout']); - } - - return; - } - - if (isset($query[$view->key]) && $item->query[$view->key] == (int) $query[$view->key]) - { - unset($query[$view->key]); - - while ($view) - { - unset($query[$view->parent_key]); - - $view = $view->parent; - } - - unset($query['view']); - - if (isset($query['layout']) && $mLayout === $query['layout']) - { - unset($query['layout']); - } - - return; - } - } - - // Get the path from the view of the current URL and parse it to the menu item - $path = array_reverse($this->router->getPath($query), true); - $found = false; - - foreach ($path as $element => $ids) - { - $view = $views[$element]; - - if ($found === false && $item->query['view'] === $element) - { - if ($view->nestable) - { - $found = true; - } - elseif ($view->children) - { - $found = true; - - continue; - } - } - - if ($found === false) - { - // Jump to the next view - continue; - } - - if ($ids) - { - if ($view->nestable) - { - $found2 = false; - - foreach (array_reverse($ids, true) as $id => $segment) - { - if ($found2) - { - $segments[] = str_replace(':', '-', $segment); - } - elseif ((int) $item->query[$view->key] === (int) $id) - { - $found2 = true; - } - } - } - elseif ($ids === true) - { - $segments[] = $element; - } - else - { - $segments[] = str_replace(':', '-', current($ids)); - } - } - - if ($view->parent_key) - { - // Remove parent key from query - unset($query[$view->parent_key]); - } - } - - if ($found) - { - unset($query[$views[$query['view']]->key], $query['view']); - - if (isset($query['layout']) && $mLayout === $query['layout']) - { - unset($query['layout']); - } - } - } + /** + * Router this rule belongs to + * + * @var RouterView + * @since 3.4 + */ + protected $router; + + /** + * Class constructor. + * + * @param RouterView $router Router this rule belongs to + * + * @since 3.4 + */ + public function __construct(RouterView $router) + { + $this->router = $router; + } + + /** + * Dummy method to fulfil the interface requirements + * + * @param array &$query The query array to process + * + * @return void + * + * @since 3.4 + */ + public function preprocess(&$query) + { + } + + /** + * Parse the URL + * + * @param array &$segments The URL segments to parse + * @param array &$vars The vars that result from the segments + * + * @return void + * + * @since 3.4 + */ + public function parse(&$segments, &$vars) + { + // Get the views and the currently active query vars + $views = $this->router->getViews(); + $active = $this->router->menu->getActive(); + + if ($active) { + $vars = array_merge($active->query, $vars); + } + + // We don't have a view or its not a view of this component! We stop here + if (!isset($vars['view']) || !isset($views[$vars['view']])) { + return; + } + + // Copy the segments, so that we can iterate over all of them and at the same time modify the original segments + $tempSegments = $segments; + + // Iterate over the segments as long as a segment fits + foreach ($tempSegments as $segment) { + // Our current view is nestable. We need to check first if the segment fits to that + if ($views[$vars['view']]->nestable) { + if (\is_callable(array($this->router, 'get' . ucfirst($views[$vars['view']]->name) . 'Id'))) { + $key = \call_user_func_array(array($this->router, 'get' . ucfirst($views[$vars['view']]->name) . 'Id'), array($segment, $vars)); + + // Did we get a proper key? If not, we need to look in the child-views + if ($key) { + $vars[$views[$vars['view']]->key] = $key; + + array_shift($segments); + + continue; + } + } else { + // The router is not complete. The getId() method is missing. + return; + } + } + + // Lets find the right view that belongs to this segment + $found = false; + + foreach ($views[$vars['view']]->children as $view) { + if (!$view->key) { + if ($view->name === $segment) { + // The segment is a view name + $parent = $views[$vars['view']]; + $vars['view'] = $view->name; + $found = true; + + if ($view->parent_key && isset($vars[$parent->key])) { + $parent_key = $vars[$parent->key]; + $vars[$view->parent_key] = $parent_key; + + unset($vars[$parent->key]); + } + + break; + } + } elseif (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Id'))) { + // Hand the data over to the router specific method and see if there is a content item that fits + $key = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); + + if ($key) { + // Found the right view and the right item + $parent = $views[$vars['view']]; + $vars['view'] = $view->name; + $found = true; + + if ($view->parent_key && isset($vars[$parent->key])) { + $parent_key = $vars[$parent->key]; + $vars[$view->parent_key] = $parent_key; + + unset($vars[$parent->key]); + } + + $vars[$view->key] = $key; + + break; + } + } + } + + if (!$found) { + return; + } + + array_shift($segments); + } + } + + /** + * Build a standard URL + * + * @param array &$query The vars that should be converted + * @param array &$segments The URL segments to create + * + * @return void + * + * @since 3.4 + */ + public function build(&$query, &$segments) + { + if (!isset($query['Itemid'], $query['view'])) { + return; + } + + // Get the menu item belonging to the Itemid that has been found + $item = $this->router->menu->getItem($query['Itemid']); + + if ( + $item === null + || $item->component !== 'com_' . $this->router->getName() + || !isset($item->query['view']) + ) { + return; + } + + // Get menu item layout + $mLayout = isset($item->query['layout']) ? $item->query['layout'] : null; + + // Get all views for this component + $views = $this->router->getViews(); + + // Return directly when the URL of the Itemid is identical with the URL to build + if ($item->query['view'] === $query['view']) { + $view = $views[$query['view']]; + + if (!$view->key) { + unset($query['view']); + + if (isset($query['layout']) && $mLayout === $query['layout']) { + unset($query['layout']); + } + + return; + } + + if (isset($query[$view->key]) && $item->query[$view->key] == (int) $query[$view->key]) { + unset($query[$view->key]); + + while ($view) { + unset($query[$view->parent_key]); + + $view = $view->parent; + } + + unset($query['view']); + + if (isset($query['layout']) && $mLayout === $query['layout']) { + unset($query['layout']); + } + + return; + } + } + + // Get the path from the view of the current URL and parse it to the menu item + $path = array_reverse($this->router->getPath($query), true); + $found = false; + + foreach ($path as $element => $ids) { + $view = $views[$element]; + + if ($found === false && $item->query['view'] === $element) { + if ($view->nestable) { + $found = true; + } elseif ($view->children) { + $found = true; + + continue; + } + } + + if ($found === false) { + // Jump to the next view + continue; + } + + if ($ids) { + if ($view->nestable) { + $found2 = false; + + foreach (array_reverse($ids, true) as $id => $segment) { + if ($found2) { + $segments[] = str_replace(':', '-', $segment); + } elseif ((int) $item->query[$view->key] === (int) $id) { + $found2 = true; + } + } + } elseif ($ids === true) { + $segments[] = $element; + } else { + $segments[] = str_replace(':', '-', current($ids)); + } + } + + if ($view->parent_key) { + // Remove parent key from query + unset($query[$view->parent_key]); + } + } + + if ($found) { + unset($query[$views[$query['view']]->key], $query['view']); + + if (isset($query['layout']) && $mLayout === $query['layout']) { + unset($query['layout']); + } + } + } } diff --git a/code/libraries/src/Console/AddUserCommand.php b/code/libraries/src/Console/AddUserCommand.php index 08dfc0ce..cfdbb6df 100644 --- a/code/libraries/src/Console/AddUserCommand.php +++ b/code/libraries/src/Console/AddUserCommand.php @@ -1,4 +1,5 @@ configureIO($input, $output); - $this->ioStyle->title('Add user'); - $this->user = $this->getStringFromOption('username', 'Please enter a username'); - $this->name = $this->getStringFromOption('name', 'Please enter a name (full name of user)'); - $this->email = $this->getStringFromOption('email', 'Please enter an email address'); - $this->password = $this->getStringFromOption('password', 'Please enter a password'); - $this->userGroups = $this->getUserGroups(); - - if (\in_array("error", $this->userGroups)) - { - $this->ioStyle->error("'" . $this->userGroups[1] . "' user group doesn't exist!"); - - return Command::FAILURE; - } - - // Get filter to remove invalid characters - $filter = new InputFilter; - - $user['username'] = $filter->clean($this->user, 'USERNAME'); - $user['password'] = $this->password; - $user['name'] = $filter->clean($this->name, 'STRING'); - $user['email'] = $this->email; - $user['groups'] = $this->userGroups; - - $userObj = User::getInstance(); - $userObj->bind($user); - - if (!$userObj->save()) - { - switch ($userObj->getError()) - { - case "JLIB_DATABASE_ERROR_USERNAME_INUSE": - $this->ioStyle->error("The username already exists!"); - break; - case "JLIB_DATABASE_ERROR_EMAIL_INUSE": - $this->ioStyle->error("The email address already exists!"); - break; - case "JLIB_DATABASE_ERROR_VALID_MAIL": - $this->ioStyle->error("The email address is invalid!"); - break; - } - - return 1; - } - - $this->ioStyle->success("User created!"); - - return Command::SUCCESS; - } - - /** - * Method to get groupId by groupName - * - * @param string $groupName name of group - * - * @return integer - * - * @since 4.0.0 - */ - protected function getGroupId($groupName) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('title') . ' = :groupName') - ->bind(':groupName', $groupName); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to get a value from option - * - * @param string $option set the option name - * @param string $question set the question if user enters no value to option - * - * @return string - * - * @since 4.0.0 - */ - public function getStringFromOption($option, $question): string - { - $answer = (string) $this->cliInput->getOption($option); - - while (!$answer) - { - if ($option === 'password') - { - $answer = (string) $this->ioStyle->askHidden($question); - } - else - { - $answer = (string) $this->ioStyle->ask($question); - } - } - - return $answer; - } - - /** - * Method to get a value from option - * - * @return array - * - * @since 4.0.0 - */ - protected function getUserGroups(): array - { - $groups = $this->getApplication()->getConsoleInput()->getOption('usergroup'); - $db = Factory::getDbo(); - - $groupList = []; - - // Group names have been supplied as input arguments - if (!\is_null($groups) && $groups[0]) - { - $groups = explode(',', $groups); - - foreach ($groups as $group) - { - $groupId = $this->getGroupId($group); - - if (empty($groupId)) - { - $this->ioStyle->error("Invalid group name '" . $group . "'"); - throw new InvalidOptionException("Invalid group name " . $group); - } - - $groupList[] = $this->getGroupId($group); - } - - return $groupList; - } - - // Generate select list for user - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->order($db->quoteName('id') . 'ASC'); - $db->setQuery($query); - - $list = $db->loadColumn(); - - $choice = new ChoiceQuestion( - 'Please select a usergroup (separate multiple groups with a comma)', - $list - ); - $choice->setMultiselect(true); - - $answer = (array) $this->ioStyle->askQuestion($choice); - - foreach ($answer as $group) - { - $groupList[] = $this->getGroupId($group); - } - - return $groupList; - } - - /** - * Configure the IO. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return void - * - * @since 4.0.0 - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% will add a user + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'user:add'; + + /** + * SymfonyStyle Object + * @var object + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Stores the Input Object + * @var object + * @since 4.0.0 + */ + private $cliInput; + + /** + * The username + * + * @var string + * + * @since 4.0.0 + */ + private $user; + + /** + * The password + * + * @var string + * + * @since 4.0.0 + */ + private $password; + + /** + * The name + * + * @var string + * + * @since 4.0.0 + */ + private $name; + + /** + * The email address + * + * @var string + * + * @since 4.0.0 + */ + private $email; + + /** + * The usergroups + * + * @var array + * + * @since 4.0.0 + */ + private $userGroups = []; + + /** + * Command constructor. + * + * @param DatabaseInterface $db The database + * + * @since 4.2.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->ioStyle->title('Add User'); + $this->user = $this->getStringFromOption('username', 'Please enter a username'); + $this->name = $this->getStringFromOption('name', 'Please enter a name (full name of user)'); + $this->email = $this->getStringFromOption('email', 'Please enter an email address'); + $this->password = $this->getStringFromOption('password', 'Please enter a password'); + $this->userGroups = $this->getUserGroups(); + + if (\in_array("error", $this->userGroups)) { + $this->ioStyle->error("'" . $this->userGroups[1] . "' user group doesn't exist!"); + + return Command::FAILURE; + } + + // Get filter to remove invalid characters + $filter = new InputFilter(); + + $user['username'] = $filter->clean($this->user, 'USERNAME'); + $user['password'] = $this->password; + $user['name'] = $filter->clean($this->name, 'STRING'); + $user['email'] = $this->email; + $user['groups'] = $this->userGroups; + + $userObj = User::getInstance(); + $userObj->bind($user); + + if (!$userObj->save()) { + switch ($userObj->getError()) { + case "JLIB_DATABASE_ERROR_USERNAME_INUSE": + $this->ioStyle->error("The username already exists!"); + break; + case "JLIB_DATABASE_ERROR_EMAIL_INUSE": + $this->ioStyle->error("The email address already exists!"); + break; + case "JLIB_DATABASE_ERROR_VALID_MAIL": + $this->ioStyle->error("The email address is invalid!"); + break; + } + + return 1; + } + + $this->ioStyle->success("User created!"); + + return Command::SUCCESS; + } + + /** + * Method to get groupId by groupName + * + * @param string $groupName name of group + * + * @return integer + * + * @since 4.0.0 + */ + protected function getGroupId($groupName) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('title') . ' = :groupName') + ->bind(':groupName', $groupName); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to get a value from option + * + * @param string $option set the option name + * @param string $question set the question if user enters no value to option + * + * @return string + * + * @since 4.0.0 + */ + public function getStringFromOption($option, $question): string + { + $answer = (string) $this->cliInput->getOption($option); + + while (!$answer) { + if ($option === 'password') { + $answer = (string) $this->ioStyle->askHidden($question); + } else { + $answer = (string) $this->ioStyle->ask($question); + } + } + + return $answer; + } + + /** + * Method to get a value from option + * + * @return array + * + * @since 4.0.0 + */ + protected function getUserGroups(): array + { + $groups = $this->getApplication()->getConsoleInput()->getOption('usergroup'); + $db = $this->getDatabase(); + + $groupList = []; + + // Group names have been supplied as input arguments + if (!\is_null($groups) && $groups[0]) { + $groups = explode(',', $groups); + + foreach ($groups as $group) { + $groupId = $this->getGroupId($group); + + if (empty($groupId)) { + $this->ioStyle->error("Invalid group name '" . $group . "'"); + throw new InvalidOptionException("Invalid group name " . $group); + } + + $groupList[] = $this->getGroupId($group); + } + + return $groupList; + } + + // Generate select list for user + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups')) + ->order($db->quoteName('id') . 'ASC'); + $db->setQuery($query); + + $list = $db->loadColumn(); + + $choice = new ChoiceQuestion( + 'Please select a usergroup (separate multiple groups with a comma)', + $list + ); + $choice->setMultiselect(true); + + $answer = (array) $this->ioStyle->askQuestion($choice); + + foreach ($answer as $group) { + $groupList[] = $this->getGroupId($group); + } + + return $groupList; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since 4.0.0 + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% will add a user \nUsage: php %command.full_name%"; - $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); - $this->addOption('name', null, InputOption::VALUE_OPTIONAL, 'full name of user'); - $this->addOption('password', null, InputOption::VALUE_OPTIONAL, 'password'); - $this->addOption('email', null, InputOption::VALUE_OPTIONAL, 'email address'); - $this->addOption('usergroup', null, InputOption::VALUE_OPTIONAL, 'usergroup (separate multiple groups with comma ",")'); - $this->setDescription('Add a user'); - $this->setHelp($help); - } + $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); + $this->addOption('name', null, InputOption::VALUE_OPTIONAL, 'full name of user'); + $this->addOption('password', null, InputOption::VALUE_OPTIONAL, 'password'); + $this->addOption('email', null, InputOption::VALUE_OPTIONAL, 'email address'); + $this->addOption('usergroup', null, InputOption::VALUE_OPTIONAL, 'usergroup (separate multiple groups with comma ",")'); + $this->setDescription('Add a user'); + $this->setHelp($help); + } } diff --git a/code/libraries/src/Console/AddUserToGroupCommand.php b/code/libraries/src/Console/AddUserToGroupCommand.php index ac40ace3..a2e7263b 100644 --- a/code/libraries/src/Console/AddUserToGroupCommand.php +++ b/code/libraries/src/Console/AddUserToGroupCommand.php @@ -1,4 +1,5 @@ configureIO($input, $output); - $this->username = $this->getStringFromOption('username', 'Please enter a username'); - $this->ioStyle->title('Add user to group'); - - $userId = $this->getUserId($this->username); - - if (empty($userId)) - { - $this->ioStyle->error("The user " . $this->username . " does not exist!"); - - return Command::FAILURE; - } - - // Fetch user - $user = User::getInstance($userId); - - $this->userGroups = $this->getGroups($user); - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('id') . ' = :userGroup'); - - foreach ($this->userGroups as $userGroup) - { - $query->bind(':userGroup', $userGroup); - $db->setQuery($query); - - $result = $db->loadResult(); - - if (UserHelper::addUserToGroup($user->id, $userGroup)) - { - $this->ioStyle->success("Added '" . $user->username . "' to group '" . $result . "'!"); - } - else - { - $this->ioStyle->error("Can't add '" . $user->username . "' to group '" . $result . "'!"); - - return Command::FAILURE; - } - } - - return Command::SUCCESS; - } - - - /** - * Method to get a value from option - * - * @param User $user a UserInstance - * - * @return array - * - * @since 4.0.0 - */ - protected function getGroups($user): array - { - $groups = $this->getApplication()->getConsoleInput()->getOption('group'); - - $db = Factory::getDbo(); - - $groupList = []; - - // Group names have been supplied as input arguments - if ($groups) - { - $groups = explode(',', $groups); - - foreach ($groups as $group) - { - $groupId = $this->getGroupId($group); - - if (empty($groupId)) - { - $this->ioStyle->error("Invalid group name '" . $group . "'"); - throw new InvalidOptionException("Invalid group name " . $group); - } - - $groupList[] = $this->getGroupId($group); - } - - return $groupList; - } - - $userGroups = Access::getGroupsByUser($user->id, false); - - // Generate select list for user - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->whereNotIn($db->quoteName('id'), $userGroups) - ->order($db->quoteName('id') . ' ASC'); - $db->setQuery($query); - - $list = $db->loadColumn(); - - $choice = new ChoiceQuestion( - 'Please select a usergroup (separate multiple groups with a comma)', - $list - ); - $choice->setMultiselect(true); - - $answer = (array) $this->ioStyle->askQuestion($choice); - - foreach ($answer as $group) - { - $groupList[] = $this->getGroupId($group); - } - - return $groupList; - } - - /** - * Method to get groupId by groupName - * - * @param string $groupName name of group - * - * @return integer - * - * @since 4.0.0 - */ - protected function getGroupId($groupName) - { - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('title') . '= :groupName') - ->bind(':groupName', $groupName); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to get a user object - * - * @param string $username username - * - * @return object - * - * @since 4.0.0 - */ - protected function getUserId($username) - { - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('username') . '= :username') - ->bind(':username', $username); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to get a value from option - * - * @param string $option set the option name - * - * @param string $question set the question if user enters no value to option - * - * @return string - * - * @since 4.0.0 - */ - protected function getStringFromOption($option, $question): string - { - $answer = (string) $this->getApplication()->getConsoleInput()->getOption($option); - - while (!$answer) - { - $answer = (string) $this->ioStyle->ask($question); - } - - return $answer; - } - - /** - * Configure the IO. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return void - * - * @since 4.0.0 - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% adds a user to a group + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'user:addtogroup'; + + /** + * SymfonyStyle Object + * @var object + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Stores the Input Object + * @var object + * @since 4.0.0 + */ + private $cliInput; + + /** + * The username + * + * @var string + * + * @since 4.0.0 + */ + private $username; + + /** + * The usergroups + * + * @var array + * + * @since 4.0.0 + */ + private $userGroups = []; + + /** + * Command constructor. + * + * @param DatabaseInterface $db The database + * + * @since 4.2.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->ioStyle->title('Add User To Group'); + $this->username = $this->getStringFromOption('username', 'Please enter a username'); + + $userId = $this->getUserId($this->username); + + if (empty($userId)) { + $this->ioStyle->error("The user " . $this->username . " does not exist!"); + + return Command::FAILURE; + } + + // Fetch user + $user = User::getInstance($userId); + + $this->userGroups = $this->getGroups($user); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :userGroup'); + + foreach ($this->userGroups as $userGroup) { + $query->bind(':userGroup', $userGroup); + $db->setQuery($query); + + $result = $db->loadResult(); + + if (UserHelper::addUserToGroup($user->id, $userGroup)) { + $this->ioStyle->success("Added '" . $user->username . "' to group '" . $result . "'!"); + } else { + $this->ioStyle->error("Can't add '" . $user->username . "' to group '" . $result . "'!"); + + return Command::FAILURE; + } + } + + return Command::SUCCESS; + } + + + /** + * Method to get a value from option + * + * @param User $user a UserInstance + * + * @return array + * + * @since 4.0.0 + */ + protected function getGroups($user): array + { + $groups = $this->getApplication()->getConsoleInput()->getOption('group'); + + $db = $this->getDatabase(); + + $groupList = []; + + // Group names have been supplied as input arguments + if ($groups) { + $groups = explode(',', $groups); + + foreach ($groups as $group) { + $groupId = $this->getGroupId($group); + + if (empty($groupId)) { + $this->ioStyle->error("Invalid group name '" . $group . "'"); + throw new InvalidOptionException("Invalid group name " . $group); + } + + $groupList[] = $this->getGroupId($group); + } + + return $groupList; + } + + $userGroups = Access::getGroupsByUser($user->id, false); + + // Generate select list for user + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups')) + ->whereNotIn($db->quoteName('id'), $userGroups) + ->order($db->quoteName('id') . ' ASC'); + $db->setQuery($query); + + $list = $db->loadColumn(); + + $choice = new ChoiceQuestion( + 'Please select a usergroup (separate multiple groups with a comma)', + $list + ); + $choice->setMultiselect(true); + + $answer = (array) $this->ioStyle->askQuestion($choice); + + foreach ($answer as $group) { + $groupList[] = $this->getGroupId($group); + } + + return $groupList; + } + + /** + * Method to get groupId by groupName + * + * @param string $groupName name of group + * + * @return integer + * + * @since 4.0.0 + */ + protected function getGroupId($groupName) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('title') . '= :groupName') + ->bind(':groupName', $groupName); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to get a user object + * + * @param string $username username + * + * @return object + * + * @since 4.0.0 + */ + protected function getUserId($username) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('username') . '= :username') + ->bind(':username', $username); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to get a value from option + * + * @param string $option set the option name + * + * @param string $question set the question if user enters no value to option + * + * @return string + * + * @since 4.0.0 + */ + protected function getStringFromOption($option, $question): string + { + $answer = (string) $this->getApplication()->getConsoleInput()->getOption($option); + + while (!$answer) { + $answer = (string) $this->ioStyle->ask($question); + } + + return $answer; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since 4.0.0 + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% adds a user to a group \nUsage: php %command.full_name%"; - $this->setDescription('Add a user to a group'); - $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); - $this->addOption('group', null, InputOption::VALUE_OPTIONAL, 'group'); - $this->setHelp($help); - } + $this->setDescription('Add a user to a group'); + $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); + $this->addOption('group', null, InputOption::VALUE_OPTIONAL, 'group'); + $this->setHelp($help); + } } diff --git a/code/libraries/src/Console/ChangeUserPasswordCommand.php b/code/libraries/src/Console/ChangeUserPasswordCommand.php index f5569324..fe9a7dde 100644 --- a/code/libraries/src/Console/ChangeUserPasswordCommand.php +++ b/code/libraries/src/Console/ChangeUserPasswordCommand.php @@ -1,4 +1,5 @@ configureIO($input, $output); - $this->username = $this->getStringFromOption('username', 'Please enter a username'); - $this->ioStyle->title('Change password'); - - $userId = UserHelper::getUserId($this->username); - - if (empty($userId)) - { - $this->ioStyle->error("The user " . $this->username . " does not exist!"); - - return Command::FAILURE; - } - - $user = User::getInstance($userId); - $this->password = $this->getStringFromOption('password', 'Please enter a new password'); - - $user->password = UserHelper::hashPassword($this->password); - - if (!$user->save(true)) - { - $this->ioStyle->error($user->getError()); - - return Command::FAILURE; - } - - $this->ioStyle->success("Password changed!"); - - return Command::SUCCESS; - } - - /** - * Method to get a value from option - * - * @param string $option set the option name - * - * @param string $question set the question if user enters no value to option - * - * @return string - * - * @since 4.0.0 - */ - protected function getStringFromOption($option, $question): string - { - $answer = (string) $this->cliInput->getOption($option); - - while (!$answer) - { - if ($option === 'password') - { - $answer = (string) $this->ioStyle->askHidden($question); - } - else - { - $answer = (string) $this->ioStyle->ask($question); - } - } - - return $answer; - } - - /** - * Configure the IO. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return void - * - * @since 4.0.0 - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% will change a user's password + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'user:reset-password'; + + /** + * SymfonyStyle Object + * @var object + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Stores the Input Object + * @var object + * @since 4.0.0 + */ + private $cliInput; + + /** + * The username + * + * @var string + * + * @since 4.0.0 + */ + private $username; + + /** + * The password + * + * @var string + * + * @since 4.0.0 + */ + private $password; + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->ioStyle->title('Change Password'); + $this->username = $this->getStringFromOption('username', 'Please enter a username'); + + $userId = UserHelper::getUserId($this->username); + + if (empty($userId)) { + $this->ioStyle->error("The user " . $this->username . " does not exist!"); + + return Command::FAILURE; + } + + $user = User::getInstance($userId); + $this->password = $this->getStringFromOption('password', 'Please enter a new password'); + + $user->password = UserHelper::hashPassword($this->password); + + if (!$user->save(true)) { + $this->ioStyle->error($user->getError()); + + return Command::FAILURE; + } + + $this->ioStyle->success("Password changed!"); + + return Command::SUCCESS; + } + + /** + * Method to get a value from option + * + * @param string $option set the option name + * + * @param string $question set the question if user enters no value to option + * + * @return string + * + * @since 4.0.0 + */ + protected function getStringFromOption($option, $question): string + { + $answer = (string) $this->cliInput->getOption($option); + + while (!$answer) { + if ($option === 'password') { + $answer = (string) $this->ioStyle->askHidden($question); + } else { + $answer = (string) $this->ioStyle->ask($question); + } + } + + return $answer; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since 4.0.0 + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% will change a user's password \nUsage: php %command.full_name%"; - $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); - $this->addOption('password', null, InputOption::VALUE_OPTIONAL, 'password'); - $this->setDescription("Change a user's password"); - $this->setHelp($help); - } + $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); + $this->addOption('password', null, InputOption::VALUE_OPTIONAL, 'password'); + $this->setDescription("Change a user's password"); + $this->setHelp($help); + } } diff --git a/code/libraries/src/Console/CheckJoomlaUpdatesCommand.php b/code/libraries/src/Console/CheckJoomlaUpdatesCommand.php index cc1661a9..1ef67635 100644 --- a/code/libraries/src/Console/CheckJoomlaUpdatesCommand.php +++ b/code/libraries/src/Console/CheckJoomlaUpdatesCommand.php @@ -1,4 +1,5 @@ %command.name% will check for Joomla updates + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'core:check-updates'; + + /** + * Stores the Update Information + * + * @var UpdateModel + * @since 4.0.0 + */ + private $updateInfo; + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% will check for Joomla updates \nUsage: php %command.full_name%"; - $this->setDescription('Check for Joomla updates'); - $this->setHelp($help); - } - - /** - * Retrieves Update Information - * - * @return mixed - * - * @since 4.0.0 - */ - private function getUpdateInformationFromModel() - { - $app = $this->getApplication(); - $updatemodel = $app->bootComponent('com_joomlaupdate')->getMVCFactory($app)->createModel('Update', 'Administrator'); - $updatemodel->purge(); - $updatemodel->refreshUpdates(true); - - return $updatemodel; - } - - /** - * Gets the Update Information - * - * @return mixed - * - * @since 4.0.0 - */ - public function getUpdateInfo() - { - if (!$this->updateInfo) - { - $this->setUpdateInfo(); - } - - return $this->updateInfo; - } - - /** - * Sets the Update Information - * - * @param null $info stores update Information - * - * @return void - * - * @since 4.0.0 - */ - public function setUpdateInfo($info = null): void - { - if (!$info) - { - $this->updateInfo = $this->getUpdateInformationFromModel(); - } - else - { - $this->updateInfo = $info; - } - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $symfonyStyle = new SymfonyStyle($input, $output); - - $model = $this->getUpdateInfo(); - $data = $model->getUpdateInformation(); - $symfonyStyle->title('Joomla! Updates'); - - if (!$data['hasUpdate']) - { - $symfonyStyle->success('You already have the latest Joomla version ' . $data['latest']); - - return Command::SUCCESS; - } - - $symfonyStyle->note('New Joomla Version ' . $data['latest'] . ' is available.'); - - if (!isset($data['object']->downloadurl->_data)) - { - $symfonyStyle->warning('We cannot find an update URL'); - } - - return Command::SUCCESS; - } + $this->setDescription('Check for Joomla updates'); + $this->setHelp($help); + } + + /** + * Retrieves Update Information + * + * @return mixed + * + * @since 4.0.0 + */ + private function getUpdateInformationFromModel() + { + $app = $this->getApplication(); + $updatemodel = $app->bootComponent('com_joomlaupdate')->getMVCFactory($app)->createModel('Update', 'Administrator'); + $updatemodel->purge(); + $updatemodel->refreshUpdates(true); + + return $updatemodel; + } + + /** + * Gets the Update Information + * + * @return mixed + * + * @since 4.0.0 + */ + public function getUpdateInfo() + { + if (!$this->updateInfo) { + $this->setUpdateInfo(); + } + + return $this->updateInfo; + } + + /** + * Sets the Update Information + * + * @param null $info stores update Information + * + * @return void + * + * @since 4.0.0 + */ + public function setUpdateInfo($info = null): void + { + if (!$info) { + $this->updateInfo = $this->getUpdateInformationFromModel(); + } else { + $this->updateInfo = $info; + } + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $symfonyStyle = new SymfonyStyle($input, $output); + + $model = $this->getUpdateInfo(); + $data = $model->getUpdateInformation(); + $symfonyStyle->title('Joomla! Updates'); + + if (!$data['hasUpdate']) { + $symfonyStyle->success('You already have the latest Joomla version ' . $data['latest']); + + return Command::SUCCESS; + } + + $symfonyStyle->note('New Joomla Version ' . $data['latest'] . ' is available.'); + + if (!isset($data['object']->downloadurl->_data)) { + $symfonyStyle->warning('We cannot find an update URL'); + } + + return Command::SUCCESS; + } } diff --git a/code/libraries/src/Console/CheckUpdatesCommand.php b/code/libraries/src/Console/CheckUpdatesCommand.php index a1948642..2861a91a 100644 --- a/code/libraries/src/Console/CheckUpdatesCommand.php +++ b/code/libraries/src/Console/CheckUpdatesCommand.php @@ -1,4 +1,5 @@ title('Fetching Extension Updates'); + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'update:extensions:check'; - // Get the update cache time - $component = ComponentHelper::getComponent('com_installer'); + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $symfonyStyle = new SymfonyStyle($input, $output); - $cache_timeout = 3600 * (int) $component->getParams()->get('cachetimeout', 6); + $symfonyStyle->title('Fetching Extension Updates'); - // Find all updates - $ret = Updater::getInstance()->findUpdates(0, $cache_timeout); + // Find all updates + $ret = Updater::getInstance()->findUpdates(); - if ($ret) - { - $symfonyStyle->note('There are available updates to apply'); - $symfonyStyle->success('Check complete.'); - } - else - { - $symfonyStyle->success('There are no available updates'); - } + if ($ret) { + $symfonyStyle->note('There are available updates to apply'); + $symfonyStyle->success('Check complete.'); + } else { + $symfonyStyle->success('There are no available updates'); + } - return Command::SUCCESS; - } + return Command::SUCCESS; + } - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% command checks for pending extension updates + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% command checks for pending extension updates \nUsage: php %command.full_name%"; - $this->setDescription('Check for pending extension updates'); - $this->setHelp($help); - } + $this->setDescription('Check for pending extension updates'); + $this->setHelp($help); + } } diff --git a/code/libraries/src/Console/CleanCacheCommand.php b/code/libraries/src/Console/CleanCacheCommand.php index 53364956..d9454503 100644 --- a/code/libraries/src/Console/CleanCacheCommand.php +++ b/code/libraries/src/Console/CleanCacheCommand.php @@ -1,4 +1,5 @@ title('Cleaning System Cache'); - - Factory::getCache()->gc(); - - $symfonyStyle->success('Cache cleaned'); - - return Command::SUCCESS; - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% will clear expired entries from the system cache + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'cache:clean'; + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $symfonyStyle = new SymfonyStyle($input, $output); + + $symfonyStyle->title('Cleaning System Cache'); + + $cache = $this->getApplication()->bootComponent('com_cache')->getMVCFactory(); + /** @var Joomla\Component\Cache\Administrator\Model\CacheModel $model */ + $model = $cache->createModel('Cache', 'Administrator', ['ignore_request' => true]); + + if ($input->getArgument('expired')) { + if (!$model->purge()) { + $symfonyStyle->error('Expired Cache not cleaned'); + + return Command::FAILURE; + } + + $symfonyStyle->success('Expired Cache cleaned'); + + return Command::SUCCESS; + } + + if (!$model->clean()) { + $symfonyStyle->error('Cache not cleaned'); + + return Command::FAILURE; + } + + $symfonyStyle->success('Cache cleaned'); + + return Command::SUCCESS; + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% will clear entries from the system cache \nUsage: php %command.full_name%"; - $this->setDescription('Clean expired cache entries'); - $this->setHelp($help); - } + $this->addArgument('expired', InputArgument::OPTIONAL, 'will clear expired entries from the system cache'); + $this->setDescription('Clean cache entries'); + $this->setHelp($help); + } } diff --git a/code/libraries/src/Console/DeleteUserCommand.php b/code/libraries/src/Console/DeleteUserCommand.php index 856db5d8..748acaa4 100644 --- a/code/libraries/src/Console/DeleteUserCommand.php +++ b/code/libraries/src/Console/DeleteUserCommand.php @@ -1,4 +1,5 @@ configureIO($input, $output); - - $this->ioStyle->title('Delete users'); - - $this->username = $this->getStringFromOption('username', 'Please enter a username'); - - $userId = UserHelper::getUserId($this->username); - $db = Factory::getDbo(); - - if (empty($userId)) - { - $this->ioStyle->error($this->username . ' does not exist!'); - - return Command::FAILURE; - } - - if ($input->isInteractive() && !$this->ioStyle->confirm('Are you sure you want to delete this user?', false)) - { - $this->ioStyle->note('User not deleted'); - - return Command::SUCCESS; - } - - $groups = UserHelper::getUserGroups($userId); - $user = User::getInstance($userId); - - if ($user->block == 0) - { - foreach ($groups as $groupId) - { - if (Access::checkGroup($groupId, 'core.admin')) - { - $queryUser = $db->getQuery(true); - $queryUser->select('COUNT(*)') - ->from($db->quoteName('#__users', 'u')) - ->leftJoin( - $db->quoteName('#__user_usergroup_map', 'g'), - '(' . $db->quoteName('u.id') . ' = ' . $db->quoteName('g.user_id') . ')' - ) - ->where($db->quoteName('g.group_id') . " = :groupId") - ->where($db->quoteName('u.block') . " = 0") - ->bind(':groupId', $groupId, ParameterType::INTEGER); - - $db->setQuery($queryUser); - $activeSuperUser = $db->loadResult(); - - if ($activeSuperUser < 2) - { - $this->ioStyle->error("You can't delete the last active Super User"); - - return Command::FAILURE; - } - } - } - } - - // Trigger delete of user - $result = $user->delete(); - - if (!$result) - { - $this->ioStyle->error("Can't remove " . $this->username . ' from usertable'); - - return Command::FAILURE; - } - - $this->ioStyle->success('User ' . $this->username . ' deleted!'); - - return Command::SUCCESS; - } - - /** - * Method to get a value from option - * - * @param string $option set the option name - * - * @param string $question set the question if user enters no value to option - * - * @return string - * - * @since 4.0.0 - */ - protected function getStringFromOption($option, $question): string - { - $answer = (string) $this->getApplication()->getConsoleInput()->getOption($option); - - while (!$answer) - { - $answer = (string) $this->ioStyle->ask($question); - } - - return $answer; - } - - /** - * Configure the IO. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return void - * - * @since 4.0.0 - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% deletes a user + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'user:delete'; + + /** + * SymfonyStyle Object + * @var object + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Stores the Input Object + * @var object + * @since 4.0.0 + */ + private $cliInput; + + /** + * The username + * + * @var string + * + * @since 4.0.0 + */ + private $username; + + /** + * Command constructor. + * + * @param DatabaseInterface $db The database + * + * @since 4.2.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + + $this->ioStyle->title('Delete User'); + + $this->username = $this->getStringFromOption('username', 'Please enter a username'); + + $userId = UserHelper::getUserId($this->username); + $db = $this->getDatabase(); + + if (empty($userId)) { + $this->ioStyle->error($this->username . ' does not exist!'); + + return Command::FAILURE; + } + + if ($input->isInteractive() && !$this->ioStyle->confirm('Are you sure you want to delete this user?', false)) { + $this->ioStyle->note('User not deleted'); + + return Command::SUCCESS; + } + + $groups = UserHelper::getUserGroups($userId); + $user = User::getInstance($userId); + + if ($user->block == 0) { + foreach ($groups as $groupId) { + if (Access::checkGroup($groupId, 'core.admin')) { + $queryUser = $db->getQuery(true); + $queryUser->select('COUNT(*)') + ->from($db->quoteName('#__users', 'u')) + ->leftJoin( + $db->quoteName('#__user_usergroup_map', 'g'), + '(' . $db->quoteName('u.id') . ' = ' . $db->quoteName('g.user_id') . ')' + ) + ->where($db->quoteName('g.group_id') . " = :groupId") + ->where($db->quoteName('u.block') . " = 0") + ->bind(':groupId', $groupId, ParameterType::INTEGER); + + $db->setQuery($queryUser); + $activeSuperUser = $db->loadResult(); + + if ($activeSuperUser < 2) { + $this->ioStyle->error("You can't delete the last active Super User"); + + return Command::FAILURE; + } + } + } + } + + // Trigger delete of user + $result = $user->delete(); + + if (!$result) { + $this->ioStyle->error("Can't remove " . $this->username . ' from usertable'); + + return Command::FAILURE; + } + + $this->ioStyle->success('User ' . $this->username . ' deleted!'); + + return Command::SUCCESS; + } + + /** + * Method to get a value from option + * + * @param string $option set the option name + * + * @param string $question set the question if user enters no value to option + * + * @return string + * + * @since 4.0.0 + */ + protected function getStringFromOption($option, $question): string + { + $answer = (string) $this->getApplication()->getConsoleInput()->getOption($option); + + while (!$answer) { + $answer = (string) $this->ioStyle->ask($question); + } + + return $answer; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since 4.0.0 + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% deletes a user \nUsage: php %command.full_name%"; - $this->setDescription('Delete a user'); - $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); - $this->setHelp($help); - } + $this->setDescription('Delete a user'); + $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); + $this->setHelp($help); + } } diff --git a/code/libraries/src/Console/ExtensionDiscoverCommand.php b/code/libraries/src/Console/ExtensionDiscoverCommand.php index 86f8fa26..296b0859 100644 --- a/code/libraries/src/Console/ExtensionDiscoverCommand.php +++ b/code/libraries/src/Console/ExtensionDiscoverCommand.php @@ -1,4 +1,5 @@ cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% is used to discover extensions + /** + * The default command name + * + * @var string + * + * @since 4.0.0 + */ + protected static $defaultName = 'extension:discover'; + + /** + * Stores the Input Object + * + * @var InputInterface + * + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * + * @var SymfonyStyle + * + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% is used to discover extensions \nUsage: \n php %command.full_name%"; - $this->setDescription('Discover extensions'); - $this->setHelp($help); - } - - /** - * Used for discovering extensions - * - * @return integer The count of discovered extensions - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function processDiscover(): int - { - $app = $this->getApplication(); - - $mvcFactory = $app->bootComponent('com_installer')->getMVCFactory(); - - $model = $mvcFactory->createModel('Discover', 'Administrator'); - - return $model->discover(); - } - - /** - * Used for finding the text for the note - * - * @param int $count The count of installed Extensions - * - * @return string The text for the note - * - * @since 4.0.0 - */ - public function getNote(int $count): string - { - if ($count < 1) - { - return 'No extensions were discovered.'; - } - elseif ($count === 1) - { - return $count . ' extension has been discovered.'; - } - else - { - return $count . ' extensions have been discovered.'; - } - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - $count = $this->processDiscover(); - - $this->ioStyle->note($this->getNote($count)); - - return Command::SUCCESS; - } + $this->setDescription('Discover extensions'); + $this->setHelp($help); + } + + /** + * Used for discovering extensions + * + * @return integer The count of discovered extensions + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function processDiscover(): int + { + $app = $this->getApplication(); + + $mvcFactory = $app->bootComponent('com_installer')->getMVCFactory(); + + $model = $mvcFactory->createModel('Discover', 'Administrator'); + + return $model->discover(); + } + + /** + * Used for finding the text for the note + * + * @param int $count The count of installed Extensions + * + * @return string The text for the note + * + * @since 4.0.0 + */ + public function getNote(int $count): string + { + if ($count < 1) { + return 'No extensions were discovered.'; + } elseif ($count === 1) { + return $count . ' extension has been discovered.'; + } else { + return $count . ' extensions have been discovered.'; + } + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + + $count = $this->processDiscover(); + $this->ioStyle->title('Discover Extensions'); + $this->ioStyle->note($this->getNote($count)); + + return Command::SUCCESS; + } } diff --git a/code/libraries/src/Console/ExtensionDiscoverInstallCommand.php b/code/libraries/src/Console/ExtensionDiscoverInstallCommand.php index 4b85e70b..ee3aefd6 100644 --- a/code/libraries/src/Console/ExtensionDiscoverInstallCommand.php +++ b/code/libraries/src/Console/ExtensionDiscoverInstallCommand.php @@ -1,4 +1,5 @@ db = $db; - parent::__construct(); - } - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - private function configureIO(InputInterface $input, OutputInterface $output): void - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $this->addOption('eid', null, InputOption::VALUE_REQUIRED, 'The ID of the extension to discover'); - - $help = "%command.name% is used to discover extensions + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'extension:discover:install'; + + /** + * Stores the Input Object + * + * @var InputInterface + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Instantiate the command. + * + * @param DatabaseInterface $db Database connector + * + * @since 4.0.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addOption('eid', null, InputOption::VALUE_REQUIRED, 'The ID of the extension to discover'); + + $help = "%command.name% is used to discover extensions \nYou can provide the following option to the command: \n --eid: The ID of the extension \n If you do not provide a ID all discovered extensions are installed. \nUsage: \n php %command.full_name% --eid="; - $this->setDescription('Install discovered extensions'); - $this->setHelp($help); - } - - /** - * Used for discovering extensions - * - * @param string $eid Id of the extension - * - * @return integer The count of installed extensions - * - * @throws \Exception - * @since 4.0.0 - */ - public function processDiscover($eid): int - { - $jInstaller = new Installer; - $count = 0; - - if ($eid === -1) - { - $db = $this->db; - $query = $db->getQuery(true) - ->select($db->quoteName(['extension_id'])) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('state') . ' = -1'); - $db->setQuery($query); - $eidsToDiscover = $db->loadObjectList(); - - foreach ($eidsToDiscover as $eidToDiscover) - { - if (!$jInstaller->discover_install($eidToDiscover->extension_id)) - { - return -1; - } - - $count++; - } - - if (empty($eidsToDiscover)) - { - return 0; - } - } - else - { - if ($jInstaller->discover_install($eid)) - { - return 1; - } - else - { - return -1; - } - } - - return $count; - } - - /** - * Used for finding the text for the note - * - * @param int $count Number of extensions to install - * @param int $eid ID of the extension or -1 if no special - * - * @return string The text for the note - * - * @since 4.0.0 - */ - public function getNote(int $count, int $eid): string - { - if ($count < 0 && $eid >= 0) - { - return 'Unable to install the extension with ID ' . $eid; - } - elseif ($count < 0 && $eid < 0) - { - return 'Unable to install discovered extensions.'; - } - elseif ($count === 0) - { - return 'There are no pending discovered extensions for install. Perhaps you need to run extension:discover first?'; - } - elseif ($count === 1 && $eid > 0) - { - return 'Extension with ID ' . $eid . ' installed successfully.'; - } - elseif ($count === 1 && $eid < 0) - { - return $count . ' discovered extension has been installed.'; - } - elseif ($count > 1 && $eid < 0) - { - return $count . ' discovered extensions have been installed.'; - } - else - { - return 'The return value is not possible and has to be checked.'; - } - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - if ($eid = $this->cliInput->getOption('eid')) - { - $result = $this->processDiscover($eid); - - if ($result === -1) - { - $this->ioStyle->error($this->getNote($result, $eid)); - - return Command::FAILURE; - } - else - { - $this->ioStyle->success($this->getNote($result, $eid)); - - return Command::SUCCESS; - } - } - else - { - $result = $this->processDiscover(-1); - - if ($result < 0) - { - $this->ioStyle->error($this->getNote($result, -1)); - - return Command::FAILURE; - } - elseif ($result === 0) - { - $this->ioStyle->note($this->getNote($result, -1)); - - return Command::SUCCESS; - } - - else - { - $this->ioStyle->note($this->getNote($result, -1)); - - return Command::SUCCESS; - } - } - } + $this->setDescription('Install discovered extensions'); + $this->setHelp($help); + } + + /** + * Used for discovering extensions + * + * @param string $eid Id of the extension + * + * @return integer The count of installed extensions + * + * @throws \Exception + * @since 4.0.0 + */ + public function processDiscover($eid): int + { + $jInstaller = new Installer(); + $jInstaller->setDatabase($this->getDatabase()); + $count = 0; + + if ($eid === -1) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['extension_id'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('state') . ' = -1'); + $db->setQuery($query); + $eidsToDiscover = $db->loadObjectList(); + + foreach ($eidsToDiscover as $eidToDiscover) { + if (!$jInstaller->discover_install($eidToDiscover->extension_id)) { + return -1; + } + + $count++; + } + + if (empty($eidsToDiscover)) { + return 0; + } + } else { + if ($jInstaller->discover_install($eid)) { + return 1; + } else { + return -1; + } + } + + return $count; + } + + /** + * Used for finding the text for the note + * + * @param int $count Number of extensions to install + * @param int $eid ID of the extension or -1 if no special + * + * @return string The text for the note + * + * @since 4.0.0 + */ + public function getNote(int $count, int $eid): string + { + if ($count < 0 && $eid >= 0) { + return 'Unable to install the extension with ID ' . $eid; + } elseif ($count < 0 && $eid < 0) { + return 'Unable to install discovered extensions.'; + } elseif ($count === 0) { + return 'There are no pending discovered extensions for install. Perhaps you need to run extension:discover first?'; + } elseif ($count === 1 && $eid > 0) { + return 'Extension with ID ' . $eid . ' installed successfully.'; + } elseif ($count === 1 && $eid < 0) { + return $count . ' discovered extension has been installed.'; + } elseif ($count > 1 && $eid < 0) { + return $count . ' discovered extensions have been installed.'; + } else { + return 'The return value is not possible and has to be checked.'; + } + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->ioStyle->title('Install Discovered Extensions'); + + if ($eid = $this->cliInput->getOption('eid')) { + $result = $this->processDiscover($eid); + + if ($result === -1) { + $this->ioStyle->error($this->getNote($result, $eid)); + + return Command::FAILURE; + } else { + $this->ioStyle->success($this->getNote($result, $eid)); + + return Command::SUCCESS; + } + } else { + $result = $this->processDiscover(-1); + + if ($result < 0) { + $this->ioStyle->error($this->getNote($result, -1)); + + return Command::FAILURE; + } elseif ($result === 0) { + $this->ioStyle->note($this->getNote($result, -1)); + + return Command::SUCCESS; + } else { + $this->ioStyle->note($this->getNote($result, -1)); + + return Command::SUCCESS; + } + } + } } diff --git a/code/libraries/src/Console/ExtensionDiscoverListCommand.php b/code/libraries/src/Console/ExtensionDiscoverListCommand.php index 9c5edaec..be723ffc 100644 --- a/code/libraries/src/Console/ExtensionDiscoverListCommand.php +++ b/code/libraries/src/Console/ExtensionDiscoverListCommand.php @@ -1,4 +1,5 @@ %command.name% is used to list all extensions that could be installed via discoverinstall + /** + * The default command name + * + * @var string + * + * @since 4.0.0 + */ + protected static $defaultName = 'extension:discover:list'; + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% is used to list all extensions that could be installed via discoverinstall \nUsage: \n php %command.full_name%"; - $this->setDescription('List discovered extensions'); - $this->setHelp($help); - } - - /** - * Filters the extension state - * - * @param array $extensions The Extensions - * @param string $state The Extension state - * - * @return array - * - * @since 4.0.0 - */ - public function filterExtensionsBasedOnState($extensions, $state): array - { - $filteredExtensions = []; - - foreach ($extensions as $key => $extension) - { - if ($extension['state'] === $state) - { - $filteredExtensions[] = $extension; - } - } - - return $filteredExtensions; - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - $extensions = $this->getExtensions(); - $state = -1; - - $discovered_extensions = $this->filterExtensionsBasedOnState($extensions, $state); - - if (empty($discovered_extensions)) - { - $this->ioStyle->note("There are no pending discovered extensions to install. Perhaps you need to run extension:discover first?"); - - return Command::SUCCESS; - } - - $discovered_extensions = $this->getExtensionsNameAndId($discovered_extensions); - - $this->ioStyle->title('Discovered extensions.'); - $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Active'], $discovered_extensions); - - return Command::SUCCESS; - } + $this->setDescription('List discovered extensions'); + $this->setHelp($help); + } + + /** + * Filters the extension state + * + * @param array $extensions The Extensions + * @param string $state The Extension state + * + * @return array + * + * @since 4.0.0 + */ + public function filterExtensionsBasedOnState($extensions, $state): array + { + $filteredExtensions = []; + + foreach ($extensions as $key => $extension) { + if ($extension['state'] === $state) { + $filteredExtensions[] = $extension; + } + } + + return $filteredExtensions; + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->ioStyle->title('Discovered Extensions'); + + $extensions = $this->getExtensions(); + $state = -1; + + $discovered_extensions = $this->filterExtensionsBasedOnState($extensions, $state); + + if (empty($discovered_extensions)) { + $this->ioStyle->note("There are no pending discovered extensions to install. Perhaps you need to run extension:discover first?"); + + return Command::SUCCESS; + } + + $discovered_extensions = $this->getExtensionsNameAndId($discovered_extensions); + + $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Enabled'], $discovered_extensions); + + return Command::SUCCESS; + } } diff --git a/code/libraries/src/Console/ExtensionInstallCommand.php b/code/libraries/src/Console/ExtensionInstallCommand.php index 7ece2734..e2ed3563 100644 --- a/code/libraries/src/Console/ExtensionInstallCommand.php +++ b/code/libraries/src/Console/ExtensionInstallCommand.php @@ -1,4 +1,5 @@ cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $this->addOption('path', null, InputOption::VALUE_REQUIRED, 'The path to the extension'); - $this->addOption('url', null, InputOption::VALUE_REQUIRED, 'The url to the extension'); - - $help = "%command.name% is used to install extensions + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'extension:install'; + + /** + * Stores the Input Object + * @var InputInterface + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Exit Code For installation failure + * @since 4.0.0 + */ + public const INSTALLATION_FAILED = 1; + + /** + * Exit Code For installation Success + * @since 4.0.0 + */ + public const INSTALLATION_SUCCESSFUL = 0; + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addOption('path', null, InputOption::VALUE_REQUIRED, 'The path to the extension'); + $this->addOption('url', null, InputOption::VALUE_REQUIRED, 'The url to the extension'); + + $help = "%command.name% is used to install extensions \nYou must provide one of the following options to the command: \n --path: The path on your local filesystem to the install package \n --url: The URL from where the install package should be downloaded @@ -96,127 +99,120 @@ protected function configure(): void \n php %command.full_name% --path= \n php %command.full_name% --url="; - $this->setDescription('Install an extension from a URL or from a path'); - $this->setHelp($help); - } - - /** - * Used for installing extension from a path - * - * @param string $path Path to the extension zip file - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception - */ - public function processPathInstallation($path): bool - { - if (!file_exists($path)) - { - $this->ioStyle->warning('The file path specified does not exist.'); - - return false; - } - - $tmpPath = $this->getApplication()->get('tmp_path'); - $tmpPath = $tmpPath . '/' . basename($path); - $package = InstallerHelper::unpack($path, true); - - if ($package['type'] === false) - { - return false; - } - - $jInstaller = Installer::getInstance(); - $result = $jInstaller->install($package['extractdir']); - InstallerHelper::cleanupInstall($tmpPath, $package['extractdir']); - - return $result; - } - - - /** - * Used for installing extension from a URL - * - * @param string $url URL to the extension zip file - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception - */ - public function processUrlInstallation($url): bool - { - $filename = InstallerHelper::downloadPackage($url); - - $tmpPath = $this->getApplication()->get('tmp_path'); - - $path = $tmpPath . '/' . basename($filename); - $package = InstallerHelper::unpack($path, true); - - if ($package['type'] === false) - { - return false; - } - - $jInstaller = new Installer; - $result = $jInstaller->install($package['extractdir']); - InstallerHelper::cleanupInstall($path, $package['extractdir']); - - return $result; - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @throws \Exception - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - if ($path = $this->cliInput->getOption('path')) - { - $result = $this->processPathInstallation($path); - - if (!$result) - { - $this->ioStyle->error('Unable to install extension'); - - return self::INSTALLATION_FAILED; - } - - $this->ioStyle->success('Extension installed successfully.'); - - return self::INSTALLATION_SUCCESSFUL; - } - elseif ($url = $this->cliInput->getOption('url')) - { - $result = $this->processUrlInstallation($url); - - if (!$result) - { - $this->ioStyle->error('Unable to install extension'); - - return self::INSTALLATION_FAILED; - } - - $this->ioStyle->success('Extension installed successfully.'); - - return self::INSTALLATION_SUCCESSFUL; - } - - $this->ioStyle->error('Invalid argument supplied for command.'); - - return self::INSTALLATION_FAILED; - } + $this->setDescription('Install an extension from a URL or from a path'); + $this->setHelp($help); + } + + /** + * Used for installing extension from a path + * + * @param string $path Path to the extension zip file + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception + */ + public function processPathInstallation($path): bool + { + if (!file_exists($path)) { + $this->ioStyle->warning('The file path specified does not exist.'); + + return false; + } + + $tmpPath = $this->getApplication()->get('tmp_path'); + $tmpPath = $tmpPath . '/' . basename($path); + $package = InstallerHelper::unpack($path, true); + + if ($package['type'] === false) { + return false; + } + + $jInstaller = Installer::getInstance(); + $result = $jInstaller->install($package['extractdir']); + InstallerHelper::cleanupInstall($tmpPath, $package['extractdir']); + + return $result; + } + + + /** + * Used for installing extension from a URL + * + * @param string $url URL to the extension zip file + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception + */ + public function processUrlInstallation($url): bool + { + $filename = InstallerHelper::downloadPackage($url); + + $tmpPath = $this->getApplication()->get('tmp_path'); + + $path = $tmpPath . '/' . basename($filename); + $package = InstallerHelper::unpack($path, true); + + if ($package['type'] === false) { + return false; + } + + $jInstaller = new Installer(); + $result = $jInstaller->install($package['extractdir']); + InstallerHelper::cleanupInstall($path, $package['extractdir']); + + return $result; + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @throws \Exception + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->ioStyle->title('Install Extension'); + + if ($path = $this->cliInput->getOption('path')) { + $result = $this->processPathInstallation($path); + + if (!$result) { + $this->ioStyle->error('Unable to install extension'); + + return self::INSTALLATION_FAILED; + } + + $this->ioStyle->success('Extension installed successfully.'); + + return self::INSTALLATION_SUCCESSFUL; + } elseif ($url = $this->cliInput->getOption('url')) { + $result = $this->processUrlInstallation($url); + + if (!$result) { + $this->ioStyle->error('Unable to install extension'); + + return self::INSTALLATION_FAILED; + } + + $this->ioStyle->success('Extension installed successfully.'); + + return self::INSTALLATION_SUCCESSFUL; + } + + $this->ioStyle->error('Invalid argument supplied for command.'); + + return self::INSTALLATION_FAILED; + } } diff --git a/code/libraries/src/Console/ExtensionRemoveCommand.php b/code/libraries/src/Console/ExtensionRemoveCommand.php index 24456795..d5999d5c 100644 --- a/code/libraries/src/Console/ExtensionRemoveCommand.php +++ b/code/libraries/src/Console/ExtensionRemoveCommand.php @@ -1,4 +1,5 @@ cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - $language = Factory::getLanguage(); - $language->load('', JPATH_ADMINISTRATOR, null, false, false) || - $language->load('', JPATH_ADMINISTRATOR, null, true); - $language->load('com_installer', JPATH_ADMINISTRATOR, null, false, false)|| - $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $this->addArgument( - 'extensionId', - InputArgument::REQUIRED, - 'ID of extension to be removed (run extension:list command to check)' - ); - - $help = "%command.name% is used to uninstall extensions. + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'extension:remove'; + + /** + * @var InputInterface + * @since 4.0.0 + */ + private $cliInput; + + /** + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Exit Code for extensions remove abort + * @since 4.0.0 + */ + public const REMOVE_ABORT = 3; + + /** + * Exit Code for extensions remove failure + * @since 4.0.0 + */ + public const REMOVE_FAILED = 1; + + /** + * Exit Code for invalid response + * @since 4.0.0 + */ + public const REMOVE_INVALID_RESPONSE = 5; + + /** + * Exit Code for invalid type + * @since 4.0.0 + */ + public const REMOVE_INVALID_TYPE = 6; + + /** + * Exit Code for extensions locked remove failure + * @since 4.0.0 + */ + public const REMOVE_LOCKED = 4; + + /** + * Exit Code for extensions not found + * @since 4.0.0 + */ + public const REMOVE_NOT_FOUND = 2; + + /** + * Exit Code for extensions remove success + * @since 4.0.0 + */ + public const REMOVE_SUCCESSFUL = 0; + + /** + * Command constructor. + * + * @param DatabaseInterface $db The database + * + * @since 4.2.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + $language = Factory::getLanguage(); + $language->load('', JPATH_ADMINISTRATOR, null, false, false) || + $language->load('', JPATH_ADMINISTRATOR, null, true); + $language->load('com_installer', JPATH_ADMINISTRATOR, null, false, false) || + $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addArgument( + 'extensionId', + InputArgument::REQUIRED, + 'ID of extension to be removed (run extension:list command to check)' + ); + + $help = "%command.name% is used to uninstall extensions. \nThe command requires one argument, the ID of the extension to uninstall. \nYou may find this ID by running the extension:list command. \nUsage: php %command.full_name% "; - $this->setDescription('Remove an extension'); - $this->setHelp($help); - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - $extensionId = $this->cliInput->getArgument('extensionId'); - - $response = $this->ioStyle->ask('Are you sure you want to remove this extension?', 'yes/no'); - - if (strtolower($response) === 'yes') - { - // Get an installer object for the extension type - $installer = Installer::getInstance(); - $row = new \Joomla\CMS\Table\Extension(Factory::getDbo()); - - if ((int) $extensionId === 0 || !$row->load($extensionId)) - { - $this->ioStyle->error("Extension with ID of $extensionId not found."); - - return self::REMOVE_NOT_FOUND; - } - - // Do not allow to uninstall locked extensions. - if ((int) $row->locked === 1) - { - $this->ioStyle->error(Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $extensionId)); - - return self::REMOVE_LOCKED; - } - - if ($row->type) - { - if (!$installer->uninstall($row->type, $extensionId)) - { - $this->ioStyle->error('Extension not removed.'); - - return self::REMOVE_FAILED; - } - - $this->ioStyle->success('Extension removed!'); - - return self::REMOVE_SUCCESSFUL; - } - - return self::REMOVE_INVALID_TYPE; - } - elseif (strtolower($response) === 'no') - { - $this->ioStyle->note('Extension not removed.'); - - return self::REMOVE_ABORT; - } - - $this->ioStyle->warning('Invalid response'); - - return self::REMOVE_INVALID_RESPONSE; - } + $this->setDescription('Remove an extension'); + $this->setHelp($help); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->ioStyle->title('Remove Extension'); + + $extensionId = $this->cliInput->getArgument('extensionId'); + + $response = $this->ioStyle->ask('Are you sure you want to remove this extension?', 'yes/no'); + + if (strtolower($response) === 'yes') { + // Get an installer object for the extension type + $installer = Installer::getInstance(); + $row = new Extension($this->getDatabase()); + + if ((int) $extensionId === 0 || !$row->load($extensionId)) { + $this->ioStyle->error("Extension with ID of $extensionId not found."); + + return self::REMOVE_NOT_FOUND; + } + + // Do not allow to uninstall locked extensions. + if ((int) $row->locked === 1) { + $this->ioStyle->error(Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $extensionId)); + + return self::REMOVE_LOCKED; + } + + if ($row->type) { + if (!$installer->uninstall($row->type, $extensionId)) { + $this->ioStyle->error('Extension not removed.'); + + return self::REMOVE_FAILED; + } + + $this->ioStyle->success('Extension removed!'); + + return self::REMOVE_SUCCESSFUL; + } + + return self::REMOVE_INVALID_TYPE; + } elseif (strtolower($response) === 'no') { + $this->ioStyle->note('Extension not removed.'); + + return self::REMOVE_ABORT; + } + + $this->ioStyle->warning('Invalid response'); + + return self::REMOVE_INVALID_RESPONSE; + } } diff --git a/code/libraries/src/Console/ExtensionsListCommand.php b/code/libraries/src/Console/ExtensionsListCommand.php index c7bfd228..fd89eb20 100644 --- a/code/libraries/src/Console/ExtensionsListCommand.php +++ b/code/libraries/src/Console/ExtensionsListCommand.php @@ -1,4 +1,5 @@ db = $db; - parent::__construct(); - } - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - protected function configureIO(InputInterface $input, OutputInterface $output): void - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - - $this->addOption('type', null, InputOption::VALUE_REQUIRED, 'Type of the extension'); - - $help = "%command.name% lists all installed extensions + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'extension:list'; + + /** + * Stores the installed Extensions + * @var array + * @since 4.0.0 + */ + protected $extensions; + + /** + * Stores the Input Object + * @var InputInterface + * @since 4.0.0 + */ + protected $cliInput; + + /** + * SymfonyStyle Object + * @var SymfonyStyle + * @since 4.0.0 + */ + protected $ioStyle; + + /** + * Instantiate the command. + * + * @param DatabaseInterface $db Database connector + * + * @since 4.0.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + protected function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + + $this->addOption('type', null, InputOption::VALUE_REQUIRED, 'Type of the extension'); + + $help = "%command.name% lists all installed extensions \nUsage: php %command.full_name% \nYou may filter on the type of extension (component, module, plugin, etc.) using the --type option: \n php %command.full_name% --type="; - $this->setDescription('List installed extensions'); - $this->setHelp($help); - } - - /** - * Retrieves all extensions - * - * @return mixed - * - * @since 4.0.0 - */ - public function getExtensions() - { - if (!$this->extensions) - { - $this->setExtensions(); - } - - return $this->extensions; - } - - /** - * Retrieves the extension from the model and sets the class variable - * - * @param null $extensions Array of extensions - * - * @return void - * - * @since 4.0.0 - */ - public function setExtensions($extensions = null): void - { - if (!$extensions) - { - $this->extensions = $this->getAllExtensionsFromDB(); - } - else - { - $this->extensions = $extensions; - } - } - - /** - * Retrieves extension list from DB - * - * @return array - * - * @since 4.0.0 - */ - private function getAllExtensionsFromDB(): array - { - $db = $this->db; - $query = $db->getQuery(true); - $query->select('*') - ->from('#__extensions'); - $db->setQuery($query); - $extensions = $db->loadAssocList('extension_id'); - - return $extensions; - } - - /** - * Transforms extension arrays into required form - * - * @param array $extensions Array of extensions - * - * @return array - * - * @since 4.0.0 - */ - protected function getExtensionsNameAndId($extensions): array - { - $extInfo = []; - - foreach ($extensions as $key => $extension) - { - $manifest = json_decode($extension['manifest_cache']); - $extInfo[] = [ - $extension['name'], - $extension['extension_id'], - $manifest ? $manifest->version : '--', - $extension['type'], - $extension['enabled'] == 1 ? 'Yes' : 'No', - ]; - } - - return $extInfo; - } - - /** - * Filters the extension type - * - * @param string $type Extension type - * - * @return array - * - * @since 4.0.0 - */ - private function filterExtensionsBasedOn($type): array - { - $extensions = []; - - foreach ($this->extensions as $key => $extension) - { - if ($extension['type'] == $type) - { - $extensions[] = $extension; - } - } - - return $extensions; - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - $extensions = $this->getExtensions(); - $type = $this->cliInput->getOption('type'); - - if ($type) - { - $extensions = $this->filterExtensionsBasedOn($type); - } - - if (empty($extensions)) - { - $this->ioStyle->error("Cannot find extensions of the type '$type' specified."); - - return Command::SUCCESS; - } - - $extensions = $this->getExtensionsNameAndId($extensions); - - $this->ioStyle->title('Installed extensions.'); - $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Active'], $extensions); - - return Command::SUCCESS; - } + $this->setDescription('List installed extensions'); + $this->setHelp($help); + } + + /** + * Retrieves all extensions + * + * @return mixed + * + * @since 4.0.0 + */ + public function getExtensions() + { + if (!$this->extensions) { + $this->setExtensions(); + } + + return $this->extensions; + } + + /** + * Retrieves the extension from the model and sets the class variable + * + * @param null $extensions Array of extensions + * + * @return void + * + * @since 4.0.0 + */ + public function setExtensions($extensions = null): void + { + if (!$extensions) { + $this->extensions = $this->getAllExtensionsFromDB(); + } else { + $this->extensions = $extensions; + } + } + + /** + * Retrieves extension list from DB + * + * @return array + * + * @since 4.0.0 + */ + private function getAllExtensionsFromDB(): array + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select('*') + ->from('#__extensions'); + $db->setQuery($query); + $extensions = $db->loadAssocList('extension_id'); + + return $extensions; + } + + /** + * Transforms extension arrays into required form + * + * @param array $extensions Array of extensions + * + * @return array + * + * @since 4.0.0 + */ + protected function getExtensionsNameAndId($extensions): array + { + $extInfo = []; + + foreach ($extensions as $key => $extension) { + $manifest = json_decode($extension['manifest_cache']); + $extInfo[] = [ + $extension['name'], + $extension['extension_id'], + $manifest ? $manifest->version : '--', + $extension['type'], + $extension['enabled'] == 1 ? 'Yes' : 'No', + ]; + } + + return $extInfo; + } + + /** + * Filters the extension type + * + * @param string $type Extension type + * + * @return array + * + * @since 4.0.0 + */ + private function filterExtensionsBasedOn($type): array + { + $extensions = []; + + foreach ($this->extensions as $key => $extension) { + if ($extension['type'] == $type) { + $extensions[] = $extension; + } + } + + return $extensions; + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $extensions = $this->getExtensions(); + $type = $this->cliInput->getOption('type'); + + if ($type) { + $extensions = $this->filterExtensionsBasedOn($type); + } + + if (empty($extensions)) { + $this->ioStyle->error("Cannot find extensions of the type '$type' specified."); + + return Command::SUCCESS; + } + + $extensions = $this->getExtensionsNameAndId($extensions); + + $this->ioStyle->title('Installed Extensions'); + $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Enabled'], $extensions); + + return Command::SUCCESS; + } } diff --git a/code/libraries/src/Console/FinderIndexCommand.php b/code/libraries/src/Console/FinderIndexCommand.php index c1510328..88bd4e3d 100644 --- a/code/libraries/src/Console/FinderIndexCommand.php +++ b/code/libraries/src/Console/FinderIndexCommand.php @@ -1,4 +1,5 @@ db = $db; - parent::__construct(); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $this->addArgument('purge', InputArgument::OPTIONAL, 'Purge the index and rebuilds'); - $this->addOption('minproctime', null, InputOption::VALUE_REQUIRED, 'Minimum processing time in seconds, in order to apply a pause', 1); - $this->addOption('pause', null, InputOption::VALUE_REQUIRED, 'Pausing type or defined pause time in seconds', 'division'); - $this->addOption('divisor', null, InputOption::VALUE_REQUIRED, 'The divisor of the division: batch-processing time / divisor', 5); - $help = <<<'EOF' + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'finder:index'; + + /** + * Stores the Input Object + * + * @var InputInterface + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Database connector + * + * @var DatabaseInterface + * @since 4.0.0 + */ + private $db; + + /** + * Start time for the index process + * + * @var string + * @since 2.5 + */ + private $time; + + /** + * Start time for each batch + * + * @var string + * @since 2.5 + */ + private $qtime; + + /** + * Static filters information. + * + * @var array + * @since 3.3 + */ + private $filters = array(); + + /** + * Pausing type or defined pause time in seconds. + * One pausing type is implemented: 'division' for dynamic calculation of pauses + * + * Defaults to 'division' + * + * @var string|integer + * @since 3.9.12 + */ + private $pause = 'division'; + + /** + * The divisor of the division: batch-processing time / divisor. + * This is used together with --pause=division in order to pause dynamically + * in relation to the processing time + * Defaults to 5 + * + * @var integer + * @since 3.9.12 + */ + private $divisor = 5; + + /** + * Minimum processing time in seconds, in order to apply a pause + * Defaults to 1 + * + * @var integer + * @since 3.9.12 + */ + private $minimumBatchProcessingTime = 1; + + /** + * Instantiate the command. + * + * @param DatabaseInterface $db Database connector + * + * @since 4.0.0 + */ + public function __construct(DatabaseInterface $db) + { + $this->db = $db; + parent::__construct(); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addArgument('purge', InputArgument::OPTIONAL, 'Purge the index and rebuilds'); + $this->addOption('minproctime', null, InputOption::VALUE_REQUIRED, 'Minimum processing time in seconds, in order to apply a pause', 1); + $this->addOption('pause', null, InputOption::VALUE_REQUIRED, 'Pausing type or defined pause time in seconds', 'division'); + $this->addOption('divisor', null, InputOption::VALUE_REQUIRED, 'The divisor of the division: batch-processing time / divisor', 5); + $help = <<<'EOF' The %command.name% Purges and rebuilds the index (search filters are preserved). php %command.full_name% EOF; - $this->setDescription('Purges and rebuild the index'); - $this->setHelp($help); - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - - // Initialize the time value. - $this->time = microtime(true); - $this->configureIO($input, $output); - - $this->ioStyle->writeln( - [ - 'Finder Indexer', - '==========================', - '', - ] - ); - - if ($this->cliInput->getOption('minproctime')) - { - $this->minimumBatchProcessingTime = $this->cliInput->getOption('minproctime'); - } - - if ($this->cliInput->getOption('pause')) - { - $this->pause = $this->cliInput->getOption('pause'); - } - - if ($this->cliInput->getOption('divisor')) - { - $this->divisor = $this->cliInput->getOption('divisor'); - } - - if ($this->cliInput->getArgument('purge')) - { - // Taxonomy ids will change following a purge/index, so save filter information first. - $this->getFilters(); - - // Purge the index. - $this->purge(); - - // Run the indexer. - $this->index(); - - // Restore the filters again. - $this->putFilters(); - } - else - { - $this->index(); - } - - $this->ioStyle->newLine(1); - - // Total reporting. - $this->ioStyle->writeln( - [ - '' . Text::sprintf('FINDER_CLI_PROCESS_COMPLETE', round(microtime(true) - $this->time, 3)) . '', - '' . Text::sprintf('FINDER_CLI_PEAK_MEMORY_USAGE', number_format(memory_get_peak_usage(true))) . '', - ] - ); - - $this->ioStyle->newLine(1); - - return Command::SUCCESS; - } - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - private function configureIO(InputInterface $input, OutputInterface $output): void - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - $language = Factory::getLanguage(); - $language->load('', JPATH_ADMINISTRATOR, null, false, false) || - $language->load('', JPATH_ADMINISTRATOR, null, true); - $language->load('finder_cli', JPATH_SITE, null, false, false)|| - $language->load('finder_cli', JPATH_SITE, null, true); - } - - /** - * Save static filters. - * - * Since a purge/index cycle will cause all the taxonomy ids to change, - * the static filters need to be updated with the new taxonomy ids. - * The static filter information is saved prior to the purge/index - * so that it can later be used to update the filters with new ids. - * - * @return void - * - * @since 4.0.0 - */ - private function getFilters(): void - { - $this->ioStyle->text(Text::_('FINDER_CLI_SAVE_FILTERS')); - - // Get the taxonomy ids used by the filters. - $db = $this->db; - $query = $db->getQuery(true); - $query - ->select('filter_id, title, data') - ->from($db->quoteName('#__finder_filters')); - $filters = $db->setQuery($query)->loadObjectList(); - - // Get the name of each taxonomy and the name of its parent. - foreach ($filters as $filter) - { - // Skip empty filters. - if ($filter->data === '') - { - continue; - } - - // Get taxonomy records. - $query = $db->getQuery(true); - $query - ->select('t.title, p.title AS parent') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->leftJoin($db->quoteName('#__finder_taxonomy') . ' AS p ON p.id = t.parent_id') - ->where($db->quoteName('t.id') . ' IN (' . $filter->data . ')'); - $taxonomies = $db->setQuery($query)->loadObjectList(); - - // Construct a temporary data structure to hold the filter information. - foreach ($taxonomies as $taxonomy) - { - $this->filters[$filter->filter_id][] = array( - 'filter' => $filter->title, - 'title' => $taxonomy->title, - 'parent' => $taxonomy->parent, - ); - } - } - - $this->ioStyle->text(Text::sprintf('FINDER_CLI_SAVE_FILTER_COMPLETED', count($filters))); - } - - /** - * Purge the index. - * - * @return void - * - * @since 3.3 - */ - private function purge() - { - $this->ioStyle->text(Text::_('FINDER_CLI_INDEX_PURGE')); - - // Load the model. - $app = $this->getApplication(); - $model = $app->bootComponent('com_finder')->getMVCFactory($app)->createModel('Index', 'Administrator'); - - // Attempt to purge the index. - $return = $model->purge(); - - // If unsuccessful then abort. - if (!$return) - { - $message = Text::_('FINDER_CLI_INDEX_PURGE_FAILED', $model->getError()); - $this->ioStyle->error($message); - exit(); - } - - $this->ioStyle->text(Text::_('FINDER_CLI_INDEX_PURGE_SUCCESS')); - } - - /** - * Run the indexer. - * - * @return void - * - * @since 2.5 - */ - private function index() - { - - // Disable caching. - $app = $this->getApplication(); - $app->set('caching', 0); - $app->set('cache_handler', 'file'); - - // Reset the indexer state. - Indexer::resetState(); - - // Import the plugins. - PluginHelper::importPlugin('system'); - PluginHelper::importPlugin('finder'); - - // Starting Indexer. - $this->ioStyle->text(Text::_('FINDER_CLI_STARTING_INDEXER')); - - // Trigger the onStartIndex event. - $app->triggerEvent('onStartIndex'); - - // Remove the script time limit. - @set_time_limit(0); - - // Get the indexer state. - $state = Indexer::getState(); - - // Setting up plugins. - $this->ioStyle->text(Text::_('FINDER_CLI_SETTING_UP_PLUGINS')); - - // Trigger the onBeforeIndex event. - $app->triggerEvent('onBeforeIndex'); - - // Startup reporting. - $this->ioStyle->text(Text::sprintf('FINDER_CLI_SETUP_ITEMS', $state->totalItems, round(microtime(true) - $this->time, 3))); - - // Get the number of batches. - $t = (int) $state->totalItems; - $c = (int) ceil($t / $state->batchSize); - $c = $c === 0 ? 1 : $c; - - try - { - // Process the batches. - for ($i = 0; $i < $c; $i++) - { - // Set the batch start time. - $this->qtime = microtime(true); - - // Reset the batch offset. - $state->batchOffset = 0; - - // Trigger the onBuildIndex event. - Factory::getApplication()->triggerEvent('onBuildIndex'); - - // Batch reporting. - $text = Text::sprintf('FINDER_CLI_BATCH_COMPLETE', $i + 1, $processingTime = round(microtime(true) - $this->qtime, 3)); - $this->ioStyle->text($text); - - if ($this->pause !== 0) - { - // Pausing Section - $skip = !($processingTime >= $this->minimumBatchProcessingTime); - $pause = 0; - - if ($this->pause === 'division' && $this->divisor > 0) - { - if (!$skip) - { - $pause = round($processingTime / $this->divisor); - } - else - { - $pause = 1; - } - } - elseif ($this->pause > 0) - { - $pause = $this->pause; - } - - if ($pause > 0 && !$skip) - { - $this->ioStyle->text(Text::sprintf('FINDER_CLI_BATCH_PAUSING', $pause)); - sleep($pause); - $this->ioStyle->text(Text::_('FINDER_CLI_BATCH_CONTINUING')); - } - - if ($skip) - { - $this->ioStyle->text( - Text::sprintf( - 'FINDER_CLI_SKIPPING_PAUSE_LOW_BATCH_PROCESSING_TIME', - $processingTime, - $this->minimumBatchProcessingTime - ) - ); - } - - // End of Pausing Section - } - } - } - catch (Exception $e) - { - // Display the error - $this->ioStyle->error($e->getMessage()); - - // Reset the indexer state. - Indexer::resetState(); - - // Close the app - $app->close($e->getCode()); - } - - // Reset the indexer state. - Indexer::resetState(); - } - - /** - * Restore static filters. - * - * Using the saved filter information, update the filter records - * with the new taxonomy ids. - * - * @return void - * - * @since 3.3 - */ - private function putFilters() - { - $this->ioStyle->text(Text::_('FINDER_CLI_RESTORE_FILTERS')); - - $db = $this->db; - - // Use the temporary filter information to update the filter taxonomy ids. - foreach ($this->filters as $filter_id => $filter) - { - $tids = array(); - - foreach ($filter as $element) - { - // Look for the old taxonomy in the new taxonomy table. - $query = $db->getQuery(true); - $query - ->select('t.id') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->leftJoin($db->quoteName('#__finder_taxonomy') . ' AS p ON p.id = t.parent_id') - ->where($db->quoteName('t.title') . ' = ' . $db->quote($element['title'])) - ->where($db->quoteName('p.title') . ' = ' . $db->quote($element['parent'])); - $taxonomy = $db->setQuery($query)->loadResult(); - - // If we found it then add it to the list. - if ($taxonomy) - { - $tids[] = $taxonomy; - } - else - { - $text = Text::sprintf('FINDER_CLI_FILTER_RESTORE_WARNING', $element['parent'], $element['title'], $element['filter']); - $this->ioStyle->text($text); - } - } - - // Construct a comma-separated string from the taxonomy ids. - $taxonomyIds = empty($tids) ? '' : implode(',', $tids); - - // Update the filter with the new taxonomy ids. - $query = $db->getQuery(true); - $query - ->update($db->quoteName('#__finder_filters')) - ->set($db->quoteName('data') . ' = ' . $db->quote($taxonomyIds)) - ->where($db->quoteName('filter_id') . ' = ' . (int) $filter_id); - $db->setQuery($query)->execute(); - } - - $this->ioStyle->text(Text::sprintf('FINDER_CLI_RESTORE_FILTER_COMPLETED', count($this->filters))); - } - + $this->setDescription('Purges and rebuild the index'); + $this->setHelp($help); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + + // Initialize the time value. + $this->time = microtime(true); + $this->configureIO($input, $output); + + $this->ioStyle->writeln( + [ + 'Finder Indexer', + '==========================', + '', + ] + ); + + if ($this->cliInput->getOption('minproctime')) { + $this->minimumBatchProcessingTime = $this->cliInput->getOption('minproctime'); + } + + if ($this->cliInput->getOption('pause')) { + $this->pause = $this->cliInput->getOption('pause'); + } + + if ($this->cliInput->getOption('divisor')) { + $this->divisor = $this->cliInput->getOption('divisor'); + } + + if ($this->cliInput->getArgument('purge')) { + // Taxonomy ids will change following a purge/index, so save filter information first. + $this->getFilters(); + + // Purge the index. + $this->purge(); + + // Run the indexer. + $this->index(); + + // Restore the filters again. + $this->putFilters(); + } else { + $this->index(); + } + + $this->ioStyle->newLine(1); + + // Total reporting. + $this->ioStyle->writeln( + [ + '' . Text::sprintf('FINDER_CLI_PROCESS_COMPLETE', round(microtime(true) - $this->time, 3)) . '', + '' . Text::sprintf('FINDER_CLI_PEAK_MEMORY_USAGE', number_format(memory_get_peak_usage(true))) . '', + ] + ); + + $this->ioStyle->newLine(1); + + return Command::SUCCESS; + } + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + $language = Factory::getLanguage(); + $language->load('', JPATH_ADMINISTRATOR, null, false, false) || + $language->load('', JPATH_ADMINISTRATOR, null, true); + $language->load('finder_cli', JPATH_SITE, null, false, false) || + $language->load('finder_cli', JPATH_SITE, null, true); + } + + /** + * Save static filters. + * + * Since a purge/index cycle will cause all the taxonomy ids to change, + * the static filters need to be updated with the new taxonomy ids. + * The static filter information is saved prior to the purge/index + * so that it can later be used to update the filters with new ids. + * + * @return void + * + * @since 4.0.0 + */ + private function getFilters(): void + { + $this->ioStyle->text(Text::_('FINDER_CLI_SAVE_FILTERS')); + + // Get the taxonomy ids used by the filters. + $db = $this->db; + $query = $db->getQuery(true); + $query + ->select('filter_id, title, data') + ->from($db->quoteName('#__finder_filters')); + $filters = $db->setQuery($query)->loadObjectList(); + + // Get the name of each taxonomy and the name of its parent. + foreach ($filters as $filter) { + // Skip empty filters. + if ($filter->data === '') { + continue; + } + + // Get taxonomy records. + $query = $db->getQuery(true); + $query + ->select('t.title, p.title AS parent') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->leftJoin($db->quoteName('#__finder_taxonomy') . ' AS p ON p.id = t.parent_id') + ->where($db->quoteName('t.id') . ' IN (' . $filter->data . ')'); + $taxonomies = $db->setQuery($query)->loadObjectList(); + + // Construct a temporary data structure to hold the filter information. + foreach ($taxonomies as $taxonomy) { + $this->filters[$filter->filter_id][] = array( + 'filter' => $filter->title, + 'title' => $taxonomy->title, + 'parent' => $taxonomy->parent, + ); + } + } + + $this->ioStyle->text(Text::sprintf('FINDER_CLI_SAVE_FILTER_COMPLETED', count($filters))); + } + + /** + * Purge the index. + * + * @return void + * + * @since 3.3 + */ + private function purge() + { + $this->ioStyle->text(Text::_('FINDER_CLI_INDEX_PURGE')); + + // Load the model. + $app = $this->getApplication(); + $model = $app->bootComponent('com_finder')->getMVCFactory($app)->createModel('Index', 'Administrator'); + + // Attempt to purge the index. + $return = $model->purge(); + + // If unsuccessful then abort. + if (!$return) { + $message = Text::_('FINDER_CLI_INDEX_PURGE_FAILED', $model->getError()); + $this->ioStyle->error($message); + exit(); + } + + $this->ioStyle->text(Text::_('FINDER_CLI_INDEX_PURGE_SUCCESS')); + } + + /** + * Run the indexer. + * + * @return void + * + * @since 2.5 + */ + private function index() + { + + // Disable caching. + $app = $this->getApplication(); + $app->set('caching', 0); + $app->set('cache_handler', 'file'); + + // Reset the indexer state. + Indexer::resetState(); + + // Import the plugins. + PluginHelper::importPlugin('system'); + PluginHelper::importPlugin('finder'); + + // Starting Indexer. + $this->ioStyle->text(Text::_('FINDER_CLI_STARTING_INDEXER')); + + // Trigger the onStartIndex event. + $app->triggerEvent('onStartIndex'); + + // Remove the script time limit. + @set_time_limit(0); + + // Get the indexer state. + $state = Indexer::getState(); + + // Setting up plugins. + $this->ioStyle->text(Text::_('FINDER_CLI_SETTING_UP_PLUGINS')); + + // Trigger the onBeforeIndex event. + $app->triggerEvent('onBeforeIndex'); + + // Startup reporting. + $this->ioStyle->text(Text::sprintf('FINDER_CLI_SETUP_ITEMS', $state->totalItems, round(microtime(true) - $this->time, 3))); + + // Get the number of batches. + $t = (int) $state->totalItems; + $c = (int) ceil($t / $state->batchSize); + $c = $c === 0 ? 1 : $c; + + try { + // Process the batches. + for ($i = 0; $i < $c; $i++) { + // Set the batch start time. + $this->qtime = microtime(true); + + // Reset the batch offset. + $state->batchOffset = 0; + + // Trigger the onBuildIndex event. + Factory::getApplication()->triggerEvent('onBuildIndex'); + + // Batch reporting. + $text = Text::sprintf('FINDER_CLI_BATCH_COMPLETE', $i + 1, $processingTime = round(microtime(true) - $this->qtime, 3)); + $this->ioStyle->text($text); + + if ($this->pause !== 0) { + // Pausing Section + $skip = !($processingTime >= $this->minimumBatchProcessingTime); + $pause = 0; + + if ($this->pause === 'division' && $this->divisor > 0) { + if (!$skip) { + $pause = round($processingTime / $this->divisor); + } else { + $pause = 1; + } + } elseif ($this->pause > 0) { + $pause = $this->pause; + } + + if ($pause > 0 && !$skip) { + $this->ioStyle->text(Text::sprintf('FINDER_CLI_BATCH_PAUSING', $pause)); + sleep($pause); + $this->ioStyle->text(Text::_('FINDER_CLI_BATCH_CONTINUING')); + } + + if ($skip) { + $this->ioStyle->text( + Text::sprintf( + 'FINDER_CLI_SKIPPING_PAUSE_LOW_BATCH_PROCESSING_TIME', + $processingTime, + $this->minimumBatchProcessingTime + ) + ); + } + + // End of Pausing Section + } + } + } catch (Exception $e) { + // Display the error + $this->ioStyle->error($e->getMessage()); + + // Reset the indexer state. + Indexer::resetState(); + + // Close the app + $app->close($e->getCode()); + } + + // Reset the indexer state. + Indexer::resetState(); + } + + /** + * Restore static filters. + * + * Using the saved filter information, update the filter records + * with the new taxonomy ids. + * + * @return void + * + * @since 3.3 + */ + private function putFilters() + { + $this->ioStyle->text(Text::_('FINDER_CLI_RESTORE_FILTERS')); + + $db = $this->db; + + // Use the temporary filter information to update the filter taxonomy ids. + foreach ($this->filters as $filter_id => $filter) { + $tids = array(); + + foreach ($filter as $element) { + // Look for the old taxonomy in the new taxonomy table. + $query = $db->getQuery(true); + $query + ->select('t.id') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->leftJoin($db->quoteName('#__finder_taxonomy') . ' AS p ON p.id = t.parent_id') + ->where($db->quoteName('t.title') . ' = ' . $db->quote($element['title'])) + ->where($db->quoteName('p.title') . ' = ' . $db->quote($element['parent'])); + $taxonomy = $db->setQuery($query)->loadResult(); + + // If we found it then add it to the list. + if ($taxonomy) { + $tids[] = $taxonomy; + } else { + $text = Text::sprintf('FINDER_CLI_FILTER_RESTORE_WARNING', $element['parent'], $element['title'], $element['filter']); + $this->ioStyle->text($text); + } + } + + // Construct a comma-separated string from the taxonomy ids. + $taxonomyIds = empty($tids) ? '' : implode(',', $tids); + + // Update the filter with the new taxonomy ids. + $query = $db->getQuery(true); + $query + ->update($db->quoteName('#__finder_filters')) + ->set($db->quoteName('data') . ' = ' . $db->quote($taxonomyIds)) + ->where($db->quoteName('filter_id') . ' = ' . (int) $filter_id); + $db->setQuery($query)->execute(); + } + + $this->ioStyle->text(Text::sprintf('FINDER_CLI_RESTORE_FILTER_COMPLETED', count($this->filters))); + } } diff --git a/code/libraries/src/Console/GetConfigurationCommand.php b/code/libraries/src/Console/GetConfigurationCommand.php index 9711f3f9..7b6d679f 100644 --- a/code/libraries/src/Console/GetConfigurationCommand.php +++ b/code/libraries/src/Console/GetConfigurationCommand.php @@ -1,4 +1,5 @@ 'db', - 'options' => [ - 'dbtype', - 'host', - 'user', - 'password', - 'dbprefix', - 'db', - 'dbencryption', - 'dbsslverifyservercert', - 'dbsslkey', - 'dbsslcert', - 'dbsslca', - 'dbsslcipher' - ] - ]; - - /** - * Constant defining the Session option group - * @var array - * @since 4.0.0 - */ - public const SESSION_GROUP = [ - 'name' => 'session', - 'options' => [ - 'session_handler', - 'shared_session', - 'session_metadata' - ] - ]; - - /** - * Constant defining the Mail option group - * @var array - * @since 4.0.0 - */ - public const MAIL_GROUP = [ - 'name' => 'mail', - 'options' => [ - 'mailonline', - 'mailer', - 'mailfrom', - 'fromname', - 'sendmail', - 'smtpauth', - 'smtpuser', - 'smtppass', - 'smtphost', - 'smtpsecure', - 'smtpport' - ] - ]; - - /** - * Return code if configuration is get successfully - * @since 4.0.0 - */ - public const CONFIG_GET_SUCCESSFUL = 0; - - /** - * Return code if configuration group option is not found - * @since 4.0.0 - */ - public const CONFIG_GET_GROUP_NOT_FOUND = 1; - - /** - * Return code if configuration option is not found - * @since 4.0.0 - */ - public const CONFIG_GET_OPTION_NOT_FOUND = 2; - - /** - * Return code if the command has been invoked with wrong options - * @since 4.0.0 - */ - public const CONFIG_GET_OPTION_FAILED = 3; - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - - /** - * Displays logically grouped options - * - * @param string $group The group to be processed - * - * @return integer - * - * @since 4.0.0 - */ - public function processGroupOptions($group): int - { - $configs = $this->getApplication()->getConfig()->toArray(); - $configs = $this->formatConfig($configs); - - $groups = $this->getGroups(); - - $foundGroup = false; - - foreach ($groups as $key => $value) - { - if ($value['name'] === $group) - { - $foundGroup = true; - $options = []; - - foreach ($value['options'] as $option) - { - $options[] = [$option, $configs[$option]]; - } - - $this->ioStyle->table(['Option', 'Value'], $options); - } - } - - if (!$foundGroup) - { - $this->ioStyle->error("Group *$group* not found"); - - return self::CONFIG_GET_GROUP_NOT_FOUND; - } - - return self::CONFIG_GET_SUCCESSFUL; - } - - /** - * Gets the defined option groups - * - * @return array - * - * @since 4.0.0 - */ - public function getGroups() - { - return [ - self::DB_GROUP, - self::MAIL_GROUP, - self::SESSION_GROUP - ]; - } - - /** - * Formats the configuration array into desired format - * - * @param array $configs Array of the configurations - * - * @return array - * - * @since 4.0.0 - */ - public function formatConfig(Array $configs): array - { - $newConfig = []; - - foreach ($configs as $key => $config) - { - $config = $config === false ? "false" : $config; - $config = $config === true ? "true" : $config; - - if (!in_array($key, ['cwd', 'execution'])) - { - $newConfig[$key] = $config; - } - } - - return $newConfig; - } - - /** - * Handles the command when a single option is requested - * - * @param string $option The option we want to get its value - * - * @return integer - * - * @since 4.0.0 - */ - public function processSingleOption($option): int - { - $configs = $this->getApplication()->getConfig()->toArray(); - - if (!array_key_exists($option, $configs)) - { - $this->ioStyle->error("Can't find option *$option* in configuration list"); - - return self::CONFIG_GET_OPTION_NOT_FOUND; - } - - $value = $this->formatConfigValue($this->getApplication()->get($option)); - - $this->ioStyle->table(['Option', 'Value'], [[$option, $value]]); - - return self::CONFIG_GET_SUCCESSFUL; - } - - /** - * Formats the Configuration value - * - * @param mixed $value Value to be formatted - * - * @return string - * - * @since version - */ - protected function formatConfigValue($value): string - { - if ($value === false) - { - return 'false'; - } - elseif ($value === true) - { - return 'true'; - } - elseif ($value === null) - { - return 'Not Set'; - } - elseif (\is_array($value)) - { - return \json_encode($value); - } - elseif (\is_object($value)) - { - return \json_encode(\get_object_vars($value)); - } - else - { - return $value; - } - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $groups = $this->getGroups(); - - foreach ($groups as $key => $group) - { - $groupNames[] = $group['name']; - } - - $groupNames = implode(', ', $groupNames); - - $this->addArgument('option', null, 'Name of the option'); - $this->addOption('group', 'g', InputOption::VALUE_REQUIRED, 'Name of the option'); - - $help = "%command.name% displays the current value of a configuration option + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'config:get'; + + /** + * Stores the Input Object + * @var Input + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Constant defining the Database option group + * @var array + * @since 4.0.0 + */ + public const DB_GROUP = [ + 'name' => 'db', + 'options' => [ + 'dbtype', + 'host', + 'user', + 'password', + 'dbprefix', + 'db', + 'dbencryption', + 'dbsslverifyservercert', + 'dbsslkey', + 'dbsslcert', + 'dbsslca', + 'dbsslcipher' + ] + ]; + + /** + * Constant defining the Session option group + * @var array + * @since 4.0.0 + */ + public const SESSION_GROUP = [ + 'name' => 'session', + 'options' => [ + 'session_handler', + 'shared_session', + 'session_metadata' + ] + ]; + + /** + * Constant defining the Mail option group + * @var array + * @since 4.0.0 + */ + public const MAIL_GROUP = [ + 'name' => 'mail', + 'options' => [ + 'mailonline', + 'mailer', + 'mailfrom', + 'fromname', + 'sendmail', + 'smtpauth', + 'smtpuser', + 'smtppass', + 'smtphost', + 'smtpsecure', + 'smtpport' + ] + ]; + + /** + * Return code if configuration is get successfully + * @since 4.0.0 + */ + public const CONFIG_GET_SUCCESSFUL = 0; + + /** + * Return code if configuration group option is not found + * @since 4.0.0 + */ + public const CONFIG_GET_GROUP_NOT_FOUND = 1; + + /** + * Return code if configuration option is not found + * @since 4.0.0 + */ + public const CONFIG_GET_OPTION_NOT_FOUND = 2; + + /** + * Return code if the command has been invoked with wrong options + * @since 4.0.0 + */ + public const CONFIG_GET_OPTION_FAILED = 3; + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + + /** + * Displays logically grouped options + * + * @param string $group The group to be processed + * + * @return integer + * + * @since 4.0.0 + */ + public function processGroupOptions($group): int + { + $configs = $this->getApplication()->getConfig()->toArray(); + $configs = $this->formatConfig($configs); + + $groups = $this->getGroups(); + + $foundGroup = false; + + foreach ($groups as $key => $value) { + if ($value['name'] === $group) { + $foundGroup = true; + $options = []; + + foreach ($value['options'] as $option) { + $options[] = [$option, $configs[$option]]; + } + + $this->ioStyle->table(['Option', 'Value'], $options); + } + } + + if (!$foundGroup) { + $this->ioStyle->error("Group *$group* not found"); + + return self::CONFIG_GET_GROUP_NOT_FOUND; + } + + return self::CONFIG_GET_SUCCESSFUL; + } + + /** + * Gets the defined option groups + * + * @return array + * + * @since 4.0.0 + */ + public function getGroups() + { + return [ + self::DB_GROUP, + self::MAIL_GROUP, + self::SESSION_GROUP + ]; + } + + /** + * Formats the configuration array into desired format + * + * @param array $configs Array of the configurations + * + * @return array + * + * @since 4.0.0 + */ + public function formatConfig(array $configs): array + { + $newConfig = []; + + foreach ($configs as $key => $config) { + $config = $config === false ? "false" : $config; + $config = $config === true ? "true" : $config; + + if (!in_array($key, ['cwd', 'execution'])) { + $newConfig[$key] = $config; + } + } + + return $newConfig; + } + + /** + * Handles the command when a single option is requested + * + * @param string $option The option we want to get its value + * + * @return integer + * + * @since 4.0.0 + */ + public function processSingleOption($option): int + { + $configs = $this->getApplication()->getConfig()->toArray(); + + if (!array_key_exists($option, $configs)) { + $this->ioStyle->error("Can't find option *$option* in configuration list"); + + return self::CONFIG_GET_OPTION_NOT_FOUND; + } + + $value = $this->formatConfigValue($this->getApplication()->get($option)); + + $this->ioStyle->table(['Option', 'Value'], [[$option, $value]]); + + return self::CONFIG_GET_SUCCESSFUL; + } + + /** + * Formats the Configuration value + * + * @param mixed $value Value to be formatted + * + * @return string + * + * @since 4.0.0 + */ + protected function formatConfigValue($value): string + { + if ($value === false) { + return 'false'; + } elseif ($value === true) { + return 'true'; + } elseif ($value === null) { + return 'Not Set'; + } elseif (\is_array($value)) { + return \json_encode($value); + } elseif (\is_object($value)) { + return \json_encode(\get_object_vars($value)); + } else { + return $value; + } + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $groups = $this->getGroups(); + + foreach ($groups as $key => $group) { + $groupNames[] = $group['name']; + } + + $groupNames = implode(', ', $groupNames); + + $this->addArgument('option', null, 'Name of the option'); + $this->addOption('group', 'g', InputOption::VALUE_REQUIRED, 'Name of the option'); + + $help = "%command.name% displays the current value of a configuration option \nUsage: php %command.full_name%